Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions chat/src/app/header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className="p-4 flex items-center justify-between border-b">
Expand All @@ -24,7 +24,18 @@ export function Header() {
<span className="first-letter:uppercase">{serverStatus}</span>
</div>
)}
<ModeToggle />

{agentType !== "unknown" && (
<div className="flex items-center gap-2 text-sm font-medium">
<span
className={`text-secondary w-2 h-2 rounded-full ${AgentTypeColorCoding[agentType].color} ring-2 flex items-center justify-center`}
>
</span>
<span>{AgentTypeColorCoding[agentType].displayName}</span>
</div>
)}

<ModeToggle/>
</div>
</header>
);
Expand Down
38 changes: 32 additions & 6 deletions chat/src/components/chat-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useSearchParams } from "next/navigation";
import {useSearchParams} from "next/navigation";
import {
useState,
useEffect,
Expand All @@ -9,7 +9,7 @@ import {
PropsWithChildren,
useContext,
} from "react";
import { toast } from "sonner";
import {toast} from "sonner";

interface Message {
id: number;
Expand All @@ -32,6 +32,7 @@ interface MessageUpdateEvent {

interface StatusChangeEvent {
status: string;
agent_type: string;
}

function isDraftMessage(message: Message | DraftMessage): boolean {
Expand All @@ -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<Exclude<AgentType, "unknown">, 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<ChatContextValue | undefined>(undefined);
Expand Down Expand Up @@ -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<boolean>(false);
const [serverStatus, setServerStatus] = useState<ServerStatus>("unknown");
const [agentType, setAgentType] = useState<AgentType>("custom");
const eventSourceRef = useRef<EventSource | null>(null);
const agentAPIUrl = useAgentAPIUrl();

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -211,7 +236,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
if (type === "user") {
setMessages((prevMessages) => [
...prevMessages,
{ role: "user", content },
{role: "user", content},
]);
setLoading(true);
}
Expand All @@ -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}`;
Expand All @@ -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}`;
Expand All @@ -275,6 +300,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
loading,
sendMessage,
serverStatus,
agentType,
}}
>
{children}
Expand Down
11 changes: 7 additions & 4 deletions lib/httpapi/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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) {
Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions lib/httpapi/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
})

Expand Down
4 changes: 3 additions & 1 deletion lib/httpapi/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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."`
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/httpapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
28 changes: 19 additions & 9 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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"
}
Expand Down Expand Up @@ -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"
},
Expand All @@ -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"
},
Expand All @@ -372,7 +382,7 @@
"data",
"event"
],
"title": "Event status_change",
"title": "Event message_update",
"type": "object"
}
]
Expand Down Expand Up @@ -497,4 +507,4 @@
}
}
}
}
}