diff --git a/chat/src/app/header.tsx b/chat/src/app/header.tsx index 85c2836..615d2a3 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 {AgentTypeColorCoding, 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,18 @@ export function Header() { {serverStatus} )} - + + {agentType !== "unknown" && ( +
+ + + {AgentTypeColorCoding[agentType].displayName} +
+ )} + +
); diff --git a/chat/src/components/chat-provider.tsx b/chat/src/components/chat-provider.tsx index d57c3c8..b6051cc 100644 --- a/chat/src/components/chat-provider.tsx +++ b/chat/src/components/chat-provider.tsx @@ -1,6 +1,6 @@ "use client"; -import { useSearchParams } from "next/navigation"; +import {useSearchParams} from "next/navigation"; import { useState, useEffect, @@ -9,7 +9,7 @@ import { PropsWithChildren, useContext, } from "react"; -import { toast } from "sonner"; +import {toast} from "sonner"; interface Message { id: number; @@ -32,6 +32,7 @@ interface MessageUpdateEvent { interface StatusChangeEvent { status: string; + agent_type: string; } function isDraftMessage(message: Message | DraftMessage): boolean { @@ -42,11 +43,31 @@ type MessageType = "user" | "raw"; export type ServerStatus = "stable" | "running" | "offline" | "unknown"; +export type AgentType = "claude" | "goose" | "aider" | "gemini" | "amp" | "codex" | "cursor" | "cursor-agent" | "custom" | "unknown"; + +export type AgentColorDisplayNamePair = { + displayName: string; + color: string; +} + +export const AgentTypeColorCoding: Record, AgentColorDisplayNamePair> = { + claude: {color: "bg-blue-300 ring-blue-300/35", displayName: "Claude Code"}, + goose: {color: "bg-green-300 ring-green-300/35", displayName: "Goose"}, + aider: {color: "bg-yellow-300 ring-yellow-300/35", displayName: "Aider"}, + gemini: {color: "bg-purple-300 ring-purple-300/35", displayName: "Gemini"}, + amp: {color: "bg-pink-300 ring-pink-300/35", displayName: "Amp"}, + codex: {color: "bg-orange-300 ring-orange-300/35", displayName: "Codex"}, + cursor: {color: "bg-lime-300 ring-lime-300/35", displayName: "Cursor Agent"}, + "cursor-agent": {color: "bg-lime-300 ring-lime-300/35", displayName: "Cursor Agent"}, + custom: {color: "bg-gray-300 ring-gray-300/35", displayName: "Custom"} +} + interface ChatContextValue { messages: (Message | DraftMessage)[]; loading: boolean; serverStatus: ServerStatus; sendMessage: (message: string, type?: MessageType) => void; + agentType: AgentType; } const ChatContext = createContext(undefined); @@ -86,10 +107,11 @@ const useAgentAPIUrl = (): string => { return agentAPIURL; }; -export function ChatProvider({ children }: PropsWithChildren) { +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(); @@ -162,6 +184,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) @@ -211,7 +236,7 @@ export function ChatProvider({ children }: PropsWithChildren) { if (type === "user") { setMessages((prevMessages) => [ ...prevMessages, - { role: "user", content }, + {role: "user", content}, ]); setLoading(true); } @@ -235,7 +260,7 @@ export function ChatProvider({ children }: PropsWithChildren) { const messages = "errors" in errorData ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorData.errors.map((e: any) => e.message).join(", ") + errorData.errors.map((e: any) => e.message).join(", ") : ""; const fullDetail = `${detail}: ${messages}`; @@ -250,7 +275,7 @@ export function ChatProvider({ children }: PropsWithChildren) { const messages = "errors" in error ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - error.errors.map((e: any) => e.message).join("\n") + error.errors.map((e: any) => e.message).join("\n") : ""; const fullDetail = `${detail}: ${messages}`; @@ -275,6 +300,7 @@ export function ChatProvider({ children }: PropsWithChildren) { loading, sendMessage, serverStatus, + agentType, }} > {children} diff --git a/lib/httpapi/events.go b/lib/httpapi/events.go index 1e6281d..73eff07 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 23a1d36..46ccea5 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 8f969f9..4787426 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 986d071..a469b62 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -265,7 +265,7 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) { s.conversation.StartSnapshotLoop(ctx) go func() { for { - s.emitter.UpdateStatusAndEmitChanges(s.conversation.Status()) + s.emitter.UpdateStatusAndEmitChanges(s.conversation.Status(), s.agentType) s.emitter.UpdateMessagesAndEmitChanges(s.conversation.Messages()) s.emitter.UpdateScreenAndEmitChanges(s.conversation.Screen()) time.Sleep(snapshotInterval) @@ -329,6 +329,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/openapi.json b/openapi.json index b79f9c9..6aa2776 100644 --- a/openapi.json +++ b/openapi.json @@ -270,13 +270,18 @@ "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": [ - "status" + "status", + "agent_type" ], "type": "object" }, @@ -292,13 +297,18 @@ "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": [ - "status" + "status", + "agent_type" ], "type": "object" } @@ -326,10 +336,10 @@ { "properties": { "data": { - "$ref": "#/components/schemas/MessageUpdateBody" + "$ref": "#/components/schemas/StatusChangeBody" }, "event": { - "const": "message_update", + "const": "status_change", "description": "The event name.", "type": "string" }, @@ -346,16 +356,16 @@ "data", "event" ], - "title": "Event message_update", + "title": "Event status_change", "type": "object" }, { "properties": { "data": { - "$ref": "#/components/schemas/StatusChangeBody" + "$ref": "#/components/schemas/MessageUpdateBody" }, "event": { - "const": "status_change", + "const": "message_update", "description": "The event name.", "type": "string" }, @@ -372,7 +382,7 @@ "data", "event" ], - "title": "Event status_change", + "title": "Event message_update", "type": "object" } ] @@ -497,4 +507,4 @@ } } } -} \ No newline at end of file +}