diff --git a/chat/src/app/header.tsx b/chat/src/app/header.tsx index 85c2836d..47b370fe 100644 --- a/chat/src/app/header.tsx +++ b/chat/src/app/header.tsx @@ -1,10 +1,10 @@ "use client"; -import { useChat } from "@/components/chat-provider"; -import { ModeToggle } from "../components/mode-toggle"; +import {AgentType, useChat} from "@/components/chat-provider"; +import {ModeToggle} from "@/components/mode-toggle"; export function Header() { - const { serverStatus } = useChat(); + const {serverStatus, agentType} = useChat(); return (
@@ -24,7 +24,13 @@ export function Header() { {serverStatus} )} - + + {agentType !== "unknown" && ( +
+ {AgentType[agentType].displayName} +
+ )} +
); diff --git a/chat/src/components/chat-provider.tsx b/chat/src/components/chat-provider.tsx index baaf2e87..21a2ee3f 100644 --- a/chat/src/components/chat-provider.tsx +++ b/chat/src/components/chat-provider.tsx @@ -9,7 +9,7 @@ import { PropsWithChildren, useContext, } from "react"; -import { toast } from "sonner"; +import {toast} from "sonner"; import {getErrorMessage} from "@/lib/error-utils"; interface Message { @@ -33,6 +33,7 @@ interface MessageUpdateEvent { interface StatusChangeEvent { status: string; + agent_type: string; } interface APIErrorDetail { @@ -64,12 +65,35 @@ export interface FileUploadResponse { filePath?: string; } +export type AgentType = "claude" | "goose" | "aider" | "gemini" | "amp" | "codex" | "cursor" | "cursor-agent" | "copilot" | "auggie" | "amazonq" | "opencode" | "custom" | "unknown"; + +export type AgentColorDisplayNamePair = { + displayName: string; +} + +export const AgentType: Record, AgentColorDisplayNamePair> = { + claude: {displayName: "Claude Code"}, + goose: {displayName: "Goose"}, + aider: {displayName: "Aider"}, + gemini: { displayName: "Gemini"}, + amp: {displayName: "Amp"}, + codex: {displayName: "Codex"}, + cursor: { displayName: "Cursor Agent"}, + "cursor-agent": { displayName: "Cursor Agent"}, + copilot: {displayName: "Copilot"}, + auggie: {displayName: "Auggie"}, + amazonq: {displayName: "Amazon Q"}, + opencode: {displayName: "Opencode"}, + custom: { displayName: "Custom"} +} + interface ChatContextValue { messages: (Message | DraftMessage)[]; loading: boolean; serverStatus: ServerStatus; sendMessage: (message: string, type?: MessageType) => void; uploadFiles: (formData: FormData) => Promise; + agentType: AgentType; } const ChatContext = createContext(undefined); @@ -113,6 +137,7 @@ export function ChatProvider({ children }: PropsWithChildren) { const [messages, setMessages] = useState<(Message | DraftMessage)[]>([]); const [loading, setLoading] = useState(false); const [serverStatus, setServerStatus] = useState("unknown"); + const [agentType, setAgentType] = useState("custom"); const eventSourceRef = useRef(null); const agentAPIUrl = useAgentAPIUrl(); @@ -185,6 +210,9 @@ export function ChatProvider({ children }: PropsWithChildren) { } else { setServerStatus("unknown"); } + + // Set agent type + setAgentType(data.agent_type === "" ? "unknown" : data.agent_type as AgentType); }); // Handle connection open (server is online) @@ -311,7 +339,7 @@ export function ChatProvider({ children }: PropsWithChildren) { } else { result = (await response.json()) as FileUploadResponse; } - + } catch (error) { result.ok = false; console.error("Error uploading files:", error); @@ -332,6 +360,7 @@ export function ChatProvider({ children }: PropsWithChildren) { sendMessage, serverStatus, uploadFiles, + agentType, }} > {children} diff --git a/lib/httpapi/events.go b/lib/httpapi/events.go index 1e6281d7..73eff07b 100644 --- a/lib/httpapi/events.go +++ b/lib/httpapi/events.go @@ -44,7 +44,8 @@ type MessageUpdateBody struct { } type StatusChangeBody struct { - Status AgentStatus `json:"status" doc:"Agent status"` + Status AgentStatus `json:"status" doc:"Agent status"` + AgentType mf.AgentType `json:"agent_type" doc:"Type of the agent being used by the server."` } type ScreenUpdateBody struct { @@ -60,6 +61,7 @@ type EventEmitter struct { mu sync.Mutex messages []st.ConversationMessage status AgentStatus + agentType mf.AgentType chans map[int]chan Event chanIdx int subscriptionBufSize int @@ -147,7 +149,7 @@ func (e *EventEmitter) UpdateMessagesAndEmitChanges(newMessages []st.Conversatio e.messages = newMessages } -func (e *EventEmitter) UpdateStatusAndEmitChanges(newStatus st.ConversationStatus) { +func (e *EventEmitter) UpdateStatusAndEmitChanges(newStatus st.ConversationStatus, agentType mf.AgentType) { e.mu.Lock() defer e.mu.Unlock() @@ -156,8 +158,9 @@ func (e *EventEmitter) UpdateStatusAndEmitChanges(newStatus st.ConversationStatu return } - e.notifyChannels(EventTypeStatusChange, StatusChangeBody{Status: newAgentStatus}) + e.notifyChannels(EventTypeStatusChange, StatusChangeBody{Status: newAgentStatus, AgentType: agentType}) e.status = newAgentStatus + e.agentType = agentType } func (e *EventEmitter) UpdateScreenAndEmitChanges(newScreen string) { @@ -183,7 +186,7 @@ func (e *EventEmitter) currentStateAsEvents() []Event { } events = append(events, Event{ Type: EventTypeStatusChange, - Payload: StatusChangeBody{Status: e.status}, + Payload: StatusChangeBody{Status: e.status, AgentType: e.agentType}, }) events = append(events, Event{ Type: EventTypeScreenUpdate, diff --git a/lib/httpapi/events_test.go b/lib/httpapi/events_test.go index 23a1d365..46ccea56 100644 --- a/lib/httpapi/events_test.go +++ b/lib/httpapi/events_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + mf "github.com/coder/agentapi/lib/msgfmt" st "github.com/coder/agentapi/lib/screentracker" "github.com/stretchr/testify/assert" ) @@ -51,11 +52,11 @@ func TestEventEmitter(t *testing.T) { Payload: MessageUpdateBody{Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now}, }, newEvent) - emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable) + emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable, mf.AgentTypeAider) newEvent = <-ch assert.Equal(t, Event{ Type: EventTypeStatusChange, - Payload: StatusChangeBody{Status: AgentStatusStable}, + Payload: StatusChangeBody{Status: AgentStatusStable, AgentType: mf.AgentTypeAider}, }, newEvent) }) diff --git a/lib/httpapi/models.go b/lib/httpapi/models.go index 7d86e81b..7ed52c43 100644 --- a/lib/httpapi/models.go +++ b/lib/httpapi/models.go @@ -3,6 +3,7 @@ package httpapi import ( "time" + mf "github.com/coder/agentapi/lib/msgfmt" st "github.com/coder/agentapi/lib/screentracker" "github.com/coder/agentapi/lib/util" "github.com/danielgtaylor/huma/v2" @@ -35,7 +36,8 @@ type Message struct { // StatusResponse represents the server status type StatusResponse struct { Body struct { - Status AgentStatus `json:"status" doc:"Current agent status. 'running' means that the agent is processing a message, 'stable' means that the agent is idle and waiting for input."` + Status AgentStatus `json:"status" doc:"Current agent status. 'running' means that the agent is processing a message, 'stable' means that the agent is idle and waiting for input."` + AgentType mf.AgentType `json:"agent_type" doc:"Type of the agent being used by the server."` } } diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index d0917669..75cc6dc3 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -334,7 +334,7 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) { s.logger.Info("Initial prompt sent successfully") } } - s.emitter.UpdateStatusAndEmitChanges(currentStatus) + s.emitter.UpdateStatusAndEmitChanges(currentStatus, s.agentType) s.emitter.UpdateMessagesAndEmitChanges(s.conversation.Messages()) s.emitter.UpdateScreenAndEmitChanges(s.conversation.Screen()) time.Sleep(snapshotInterval) @@ -404,6 +404,7 @@ func (s *Server) getStatus(ctx context.Context, input *struct{}) (*StatusRespons resp := &StatusResponse{} resp.Body.Status = agentStatus + resp.Body.AgentType = s.agentType return resp, nil } diff --git a/lib/msgfmt/msgfmt.go b/lib/msgfmt/msgfmt.go index dbe2e65b..0cf1ca8e 100644 --- a/lib/msgfmt/msgfmt.go +++ b/lib/msgfmt/msgfmt.go @@ -231,6 +231,7 @@ func trimEmptyLines(message string) string { type AgentType string +// Remember to add the display name to the agentapi/chat/src/components/chat-provider.tsx const ( AgentTypeClaude AgentType = "claude" AgentTypeGoose AgentType = "goose" diff --git a/openapi.json b/openapi.json index a36154ed..4c387f3e 100644 --- a/openapi.json +++ b/openapi.json @@ -270,12 +270,17 @@ "StatusChangeBody": { "additionalProperties": false, "properties": { + "agent_type": { + "description": "Type of the agent being used by the server.", + "type": "string" + }, "status": { "$ref": "#/components/schemas/AgentStatus", "description": "Agent status" } }, "required": [ + "agent_type", "status" ], "type": "object" @@ -292,12 +297,17 @@ "readOnly": true, "type": "string" }, + "agent_type": { + "description": "Type of the agent being used by the server.", + "type": "string" + }, "status": { "$ref": "#/components/schemas/AgentStatus", "description": "Current agent status. 'running' means that the agent is processing a message, 'stable' means that the agent is idle and waiting for input." } }, "required": [ + "agent_type", "status" ], "type": "object"