Skip to content

Commit 64e4f2d

Browse files
authored
feat: agent visual indicator (#59)
1 parent 584e6d6 commit 64e4f2d

File tree

8 files changed

+67
-14
lines changed

8 files changed

+67
-14
lines changed

chat/src/app/header.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"use client";
22

3-
import { useChat } from "@/components/chat-provider";
4-
import { ModeToggle } from "../components/mode-toggle";
3+
import {AgentType, useChat} from "@/components/chat-provider";
4+
import {ModeToggle} from "@/components/mode-toggle";
55

66
export function Header() {
7-
const { serverStatus } = useChat();
7+
const {serverStatus, agentType} = useChat();
88

99
return (
1010
<header className="p-4 flex items-center justify-between border-b">
@@ -24,7 +24,13 @@ export function Header() {
2424
<span className="first-letter:uppercase">{serverStatus}</span>
2525
</div>
2626
)}
27-
<ModeToggle />
27+
28+
{agentType !== "unknown" && (
29+
<div className="flex items-center gap-2 text-sm font-medium">
30+
<span>{AgentType[agentType].displayName}</span>
31+
</div>
32+
)}
33+
<ModeToggle/>
2834
</div>
2935
</header>
3036
);

chat/src/components/chat-provider.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
PropsWithChildren,
1010
useContext,
1111
} from "react";
12-
import { toast } from "sonner";
12+
import {toast} from "sonner";
1313
import {getErrorMessage} from "@/lib/error-utils";
1414

1515
interface Message {
@@ -33,6 +33,7 @@ interface MessageUpdateEvent {
3333

3434
interface StatusChangeEvent {
3535
status: string;
36+
agent_type: string;
3637
}
3738

3839
interface APIErrorDetail {
@@ -64,12 +65,35 @@ export interface FileUploadResponse {
6465
filePath?: string;
6566
}
6667

68+
export type AgentType = "claude" | "goose" | "aider" | "gemini" | "amp" | "codex" | "cursor" | "cursor-agent" | "copilot" | "auggie" | "amazonq" | "opencode" | "custom" | "unknown";
69+
70+
export type AgentColorDisplayNamePair = {
71+
displayName: string;
72+
}
73+
74+
export const AgentType: Record<Exclude<AgentType, "unknown">, AgentColorDisplayNamePair> = {
75+
claude: {displayName: "Claude Code"},
76+
goose: {displayName: "Goose"},
77+
aider: {displayName: "Aider"},
78+
gemini: { displayName: "Gemini"},
79+
amp: {displayName: "Amp"},
80+
codex: {displayName: "Codex"},
81+
cursor: { displayName: "Cursor Agent"},
82+
"cursor-agent": { displayName: "Cursor Agent"},
83+
copilot: {displayName: "Copilot"},
84+
auggie: {displayName: "Auggie"},
85+
amazonq: {displayName: "Amazon Q"},
86+
opencode: {displayName: "Opencode"},
87+
custom: { displayName: "Custom"}
88+
}
89+
6790
interface ChatContextValue {
6891
messages: (Message | DraftMessage)[];
6992
loading: boolean;
7093
serverStatus: ServerStatus;
7194
sendMessage: (message: string, type?: MessageType) => void;
7295
uploadFiles: (formData: FormData) => Promise<FileUploadResponse>;
96+
agentType: AgentType;
7397
}
7498

7599
const ChatContext = createContext<ChatContextValue | undefined>(undefined);
@@ -113,6 +137,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
113137
const [messages, setMessages] = useState<(Message | DraftMessage)[]>([]);
114138
const [loading, setLoading] = useState<boolean>(false);
115139
const [serverStatus, setServerStatus] = useState<ServerStatus>("unknown");
140+
const [agentType, setAgentType] = useState<AgentType>("custom");
116141
const eventSourceRef = useRef<EventSource | null>(null);
117142
const agentAPIUrl = useAgentAPIUrl();
118143

@@ -185,6 +210,9 @@ export function ChatProvider({ children }: PropsWithChildren) {
185210
} else {
186211
setServerStatus("unknown");
187212
}
213+
214+
// Set agent type
215+
setAgentType(data.agent_type === "" ? "unknown" : data.agent_type as AgentType);
188216
});
189217

190218
// Handle connection open (server is online)
@@ -311,7 +339,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
311339
} else {
312340
result = (await response.json()) as FileUploadResponse;
313341
}
314-
342+
315343
} catch (error) {
316344
result.ok = false;
317345
console.error("Error uploading files:", error);
@@ -332,6 +360,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
332360
sendMessage,
333361
serverStatus,
334362
uploadFiles,
363+
agentType,
335364
}}
336365
>
337366
{children}

lib/httpapi/events.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ type MessageUpdateBody struct {
4444
}
4545

4646
type StatusChangeBody struct {
47-
Status AgentStatus `json:"status" doc:"Agent status"`
47+
Status AgentStatus `json:"status" doc:"Agent status"`
48+
AgentType mf.AgentType `json:"agent_type" doc:"Type of the agent being used by the server."`
4849
}
4950

5051
type ScreenUpdateBody struct {
@@ -60,6 +61,7 @@ type EventEmitter struct {
6061
mu sync.Mutex
6162
messages []st.ConversationMessage
6263
status AgentStatus
64+
agentType mf.AgentType
6365
chans map[int]chan Event
6466
chanIdx int
6567
subscriptionBufSize int
@@ -147,7 +149,7 @@ func (e *EventEmitter) UpdateMessagesAndEmitChanges(newMessages []st.Conversatio
147149
e.messages = newMessages
148150
}
149151

150-
func (e *EventEmitter) UpdateStatusAndEmitChanges(newStatus st.ConversationStatus) {
152+
func (e *EventEmitter) UpdateStatusAndEmitChanges(newStatus st.ConversationStatus, agentType mf.AgentType) {
151153
e.mu.Lock()
152154
defer e.mu.Unlock()
153155

@@ -156,8 +158,9 @@ func (e *EventEmitter) UpdateStatusAndEmitChanges(newStatus st.ConversationStatu
156158
return
157159
}
158160

159-
e.notifyChannels(EventTypeStatusChange, StatusChangeBody{Status: newAgentStatus})
161+
e.notifyChannels(EventTypeStatusChange, StatusChangeBody{Status: newAgentStatus, AgentType: agentType})
160162
e.status = newAgentStatus
163+
e.agentType = agentType
161164
}
162165

163166
func (e *EventEmitter) UpdateScreenAndEmitChanges(newScreen string) {
@@ -183,7 +186,7 @@ func (e *EventEmitter) currentStateAsEvents() []Event {
183186
}
184187
events = append(events, Event{
185188
Type: EventTypeStatusChange,
186-
Payload: StatusChangeBody{Status: e.status},
189+
Payload: StatusChangeBody{Status: e.status, AgentType: e.agentType},
187190
})
188191
events = append(events, Event{
189192
Type: EventTypeScreenUpdate,

lib/httpapi/events_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66
"time"
77

8+
mf "github.com/coder/agentapi/lib/msgfmt"
89
st "github.com/coder/agentapi/lib/screentracker"
910
"github.com/stretchr/testify/assert"
1011
)
@@ -51,11 +52,11 @@ func TestEventEmitter(t *testing.T) {
5152
Payload: MessageUpdateBody{Id: 2, Message: "What's up?", Role: st.ConversationRoleAgent, Time: now},
5253
}, newEvent)
5354

54-
emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable)
55+
emitter.UpdateStatusAndEmitChanges(st.ConversationStatusStable, mf.AgentTypeAider)
5556
newEvent = <-ch
5657
assert.Equal(t, Event{
5758
Type: EventTypeStatusChange,
58-
Payload: StatusChangeBody{Status: AgentStatusStable},
59+
Payload: StatusChangeBody{Status: AgentStatusStable, AgentType: mf.AgentTypeAider},
5960
}, newEvent)
6061
})
6162

lib/httpapi/models.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package httpapi
33
import (
44
"time"
55

6+
mf "github.com/coder/agentapi/lib/msgfmt"
67
st "github.com/coder/agentapi/lib/screentracker"
78
"github.com/coder/agentapi/lib/util"
89
"github.com/danielgtaylor/huma/v2"
@@ -35,7 +36,8 @@ type Message struct {
3536
// StatusResponse represents the server status
3637
type StatusResponse struct {
3738
Body struct {
38-
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."`
39+
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."`
40+
AgentType mf.AgentType `json:"agent_type" doc:"Type of the agent being used by the server."`
3941
}
4042
}
4143

lib/httpapi/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) {
334334
s.logger.Info("Initial prompt sent successfully")
335335
}
336336
}
337-
s.emitter.UpdateStatusAndEmitChanges(currentStatus)
337+
s.emitter.UpdateStatusAndEmitChanges(currentStatus, s.agentType)
338338
s.emitter.UpdateMessagesAndEmitChanges(s.conversation.Messages())
339339
s.emitter.UpdateScreenAndEmitChanges(s.conversation.Screen())
340340
time.Sleep(snapshotInterval)
@@ -404,6 +404,7 @@ func (s *Server) getStatus(ctx context.Context, input *struct{}) (*StatusRespons
404404

405405
resp := &StatusResponse{}
406406
resp.Body.Status = agentStatus
407+
resp.Body.AgentType = s.agentType
407408

408409
return resp, nil
409410
}

lib/msgfmt/msgfmt.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ func trimEmptyLines(message string) string {
231231

232232
type AgentType string
233233

234+
// Remember to add the display name to the agentapi/chat/src/components/chat-provider.tsx
234235
const (
235236
AgentTypeClaude AgentType = "claude"
236237
AgentTypeGoose AgentType = "goose"

openapi.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,17 @@
270270
"StatusChangeBody": {
271271
"additionalProperties": false,
272272
"properties": {
273+
"agent_type": {
274+
"description": "Type of the agent being used by the server.",
275+
"type": "string"
276+
},
273277
"status": {
274278
"$ref": "#/components/schemas/AgentStatus",
275279
"description": "Agent status"
276280
}
277281
},
278282
"required": [
283+
"agent_type",
279284
"status"
280285
],
281286
"type": "object"
@@ -292,12 +297,17 @@
292297
"readOnly": true,
293298
"type": "string"
294299
},
300+
"agent_type": {
301+
"description": "Type of the agent being used by the server.",
302+
"type": "string"
303+
},
295304
"status": {
296305
"$ref": "#/components/schemas/AgentStatus",
297306
"description": "Current agent status. 'running' means that the agent is processing a message, 'stable' means that the agent is idle and waiting for input."
298307
}
299308
},
300309
"required": [
310+
"agent_type",
301311
"status"
302312
],
303313
"type": "object"

0 commit comments

Comments
 (0)