Skip to content

Commit ed54a3e

Browse files
committed
feat: add running agent indicator
1 parent 1b6c85b commit ed54a3e

File tree

7 files changed

+80
-27
lines changed

7 files changed

+80
-27
lines changed

chat/src/app/header.tsx

Lines changed: 15 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 {AgentTypeColorCoding, 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,18 @@ 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
31+
className={`text-secondary w-2 h-2 rounded-full ${AgentTypeColorCoding[agentType].color} ring-2 flex items-center justify-center`}
32+
>
33+
</span>
34+
<span>{AgentTypeColorCoding[agentType].displayName}</span>
35+
</div>
36+
)}
37+
38+
<ModeToggle/>
2839
</div>
2940
</header>
3041
);

chat/src/components/chat-provider.tsx

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

3-
import { useSearchParams } from "next/navigation";
3+
import {useSearchParams} from "next/navigation";
44
import {
55
useState,
66
useEffect,
@@ -9,7 +9,7 @@ import {
99
PropsWithChildren,
1010
useContext,
1111
} from "react";
12-
import { toast } from "sonner";
12+
import {toast} from "sonner";
1313

1414
interface Message {
1515
id: number;
@@ -32,6 +32,7 @@ interface MessageUpdateEvent {
3232

3333
interface StatusChangeEvent {
3434
status: string;
35+
agent_type: string;
3536
}
3637

3738
function isDraftMessage(message: Message | DraftMessage): boolean {
@@ -42,11 +43,29 @@ type MessageType = "user" | "raw";
4243

4344
export type ServerStatus = "stable" | "running" | "offline" | "unknown";
4445

46+
export type AgentType = "claude" | "goose" | "aider" | "gemini" | "amp" | "codex" | "custom" | "unknown";
47+
48+
export type ColorAbbreviatePair = {
49+
displayName: string;
50+
color: string;
51+
}
52+
53+
export const AgentTypeColorCoding: Record<Exclude<AgentType, "unknown">, ColorAbbreviatePair> = {
54+
claude: {color: "bg-blue-300 ring-blue-300/35", displayName: "Claude Code"},
55+
goose: {color: "bg-green-300 ring-green-300/35", displayName: "Goose"},
56+
aider: {color: "bg-yellow-300 ring-yellow-300/35", displayName: "Aider"},
57+
gemini: {color: "bg-purple-300 ring-purple-300/35", displayName: "Gemini"},
58+
amp: {color: "bg-pink-300 ring-pink-300/35", displayName: "Amp"},
59+
codex: {color: "bg-orange-300 ring-orange-300/35", displayName: "Codex"},
60+
custom: {color: "bg-gray-300 ring-gray-300/35", displayName: "Custom"}
61+
}
62+
4563
interface ChatContextValue {
4664
messages: (Message | DraftMessage)[];
4765
loading: boolean;
4866
serverStatus: ServerStatus;
4967
sendMessage: (message: string, type?: MessageType) => void;
68+
agentType: AgentType;
5069
}
5170

5271
const ChatContext = createContext<ChatContextValue | undefined>(undefined);
@@ -86,10 +105,11 @@ const useAgentAPIUrl = (): string => {
86105
return agentAPIURL;
87106
};
88107

89-
export function ChatProvider({ children }: PropsWithChildren) {
108+
export function ChatProvider({children}: PropsWithChildren) {
90109
const [messages, setMessages] = useState<(Message | DraftMessage)[]>([]);
91110
const [loading, setLoading] = useState<boolean>(false);
92111
const [serverStatus, setServerStatus] = useState<ServerStatus>("unknown");
112+
const [agentType, setAgentType] = useState<AgentType>("custom");
93113
const eventSourceRef = useRef<EventSource | null>(null);
94114
const agentAPIUrl = useAgentAPIUrl();
95115

@@ -155,13 +175,17 @@ export function ChatProvider({ children }: PropsWithChildren) {
155175
// Handle status changes
156176
eventSource.addEventListener("status_change", (event) => {
157177
const data: StatusChangeEvent = JSON.parse(event.data);
178+
console.log(data)
158179
if (data.status === "stable") {
159180
setServerStatus("stable");
160181
} else if (data.status === "running") {
161182
setServerStatus("running");
162183
} else {
163184
setServerStatus("unknown");
164185
}
186+
187+
// Set agent type
188+
setAgentType(data.agent_type === "" ? "unknown" : data.agent_type as AgentType);
165189
});
166190

167191
// Handle connection open (server is online)
@@ -211,7 +235,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
211235
if (type === "user") {
212236
setMessages((prevMessages) => [
213237
...prevMessages,
214-
{ role: "user", content },
238+
{role: "user", content},
215239
]);
216240
setLoading(true);
217241
}
@@ -235,7 +259,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
235259
const messages =
236260
"errors" in errorData
237261
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
238-
errorData.errors.map((e: any) => e.message).join(", ")
262+
errorData.errors.map((e: any) => e.message).join(", ")
239263
: "";
240264

241265
const fullDetail = `${detail}: ${messages}`;
@@ -250,7 +274,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
250274
const messages =
251275
"errors" in error
252276
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
253-
error.errors.map((e: any) => e.message).join("\n")
277+
error.errors.map((e: any) => e.message).join("\n")
254278
: "";
255279

256280
const fullDetail = `${detail}: ${messages}`;
@@ -275,6 +299,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
275299
loading,
276300
sendMessage,
277301
serverStatus,
302+
agentType,
278303
}}
279304
>
280305
{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
@@ -265,7 +265,7 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) {
265265
s.conversation.StartSnapshotLoop(ctx)
266266
go func() {
267267
for {
268-
s.emitter.UpdateStatusAndEmitChanges(s.conversation.Status())
268+
s.emitter.UpdateStatusAndEmitChanges(s.conversation.Status(), s.agentType)
269269
s.emitter.UpdateMessagesAndEmitChanges(s.conversation.Messages())
270270
s.emitter.UpdateScreenAndEmitChanges(s.conversation.Screen())
271271
time.Sleep(snapshotInterval)
@@ -329,6 +329,7 @@ func (s *Server) getStatus(ctx context.Context, input *struct{}) (*StatusRespons
329329

330330
resp := &StatusResponse{}
331331
resp.Body.Status = agentStatus
332+
resp.Body.AgentType = s.agentType
332333

333334
return resp, nil
334335
}

openapi.json

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,18 @@
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": [
279-
"status"
283+
"status",
284+
"agent_type"
280285
],
281286
"type": "object"
282287
},
@@ -292,13 +297,18 @@
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": [
301-
"status"
310+
"status",
311+
"agent_type"
302312
],
303313
"type": "object"
304314
}
@@ -326,10 +336,10 @@
326336
{
327337
"properties": {
328338
"data": {
329-
"$ref": "#/components/schemas/MessageUpdateBody"
339+
"$ref": "#/components/schemas/StatusChangeBody"
330340
},
331341
"event": {
332-
"const": "message_update",
342+
"const": "status_change",
333343
"description": "The event name.",
334344
"type": "string"
335345
},
@@ -346,16 +356,16 @@
346356
"data",
347357
"event"
348358
],
349-
"title": "Event message_update",
359+
"title": "Event status_change",
350360
"type": "object"
351361
},
352362
{
353363
"properties": {
354364
"data": {
355-
"$ref": "#/components/schemas/StatusChangeBody"
365+
"$ref": "#/components/schemas/MessageUpdateBody"
356366
},
357367
"event": {
358-
"const": "status_change",
368+
"const": "message_update",
359369
"description": "The event name.",
360370
"type": "string"
361371
},
@@ -372,7 +382,7 @@
372382
"data",
373383
"event"
374384
],
375-
"title": "Event status_change",
385+
"title": "Event message_update",
376386
"type": "object"
377387
}
378388
]
@@ -497,4 +507,4 @@
497507
}
498508
}
499509
}
500-
}
510+
}

0 commit comments

Comments
 (0)