Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /