Skip to content
Merged
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
14 changes: 10 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 {AgentType, 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,13 @@ 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>{AgentType[agentType].displayName}</span>
</div>
)}
<ModeToggle/>
</div>
</header>
);
Expand Down
33 changes: 31 additions & 2 deletions chat/src/components/chat-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PropsWithChildren,
useContext,
} from "react";
import { toast } from "sonner";
import {toast} from "sonner";
import {getErrorMessage} from "@/lib/error-utils";

interface Message {
Expand All @@ -33,6 +33,7 @@ interface MessageUpdateEvent {

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

interface APIErrorDetail {
Expand Down Expand Up @@ -64,12 +65,35 @@ export interface FileUploadResponse {
filePath?: string;
}

export type AgentType = "claude" | "goose" | "aider" | "gemini" | "amp" | "codex" | "cursor" | "cursor-agent" | "copilot" | "auggie" | "amazonq" | "opencode" | "custom" | "unknown";

export type AgentColorDisplayNamePair = {
displayName: string;
}

export const AgentType: Record<Exclude<AgentType, "unknown">, AgentColorDisplayNamePair> = {
claude: {displayName: "Claude Code"},
goose: {displayName: "Goose"},
aider: {displayName: "Aider"},
gemini: { displayName: "Gemini"},
amp: {displayName: "Amp"},
codex: {displayName: "Codex"},
cursor: { displayName: "Cursor Agent"},
"cursor-agent": { displayName: "Cursor Agent"},
copilot: {displayName: "Copilot"},
auggie: {displayName: "Auggie"},
amazonq: {displayName: "Amazon Q"},
opencode: {displayName: "Opencode"},
custom: { displayName: "Custom"}
}

interface ChatContextValue {
messages: (Message | DraftMessage)[];
loading: boolean;
serverStatus: ServerStatus;
sendMessage: (message: string, type?: MessageType) => void;
uploadFiles: (formData: FormData) => Promise<FileUploadResponse>;
agentType: AgentType;
}

const ChatContext = createContext<ChatContextValue | undefined>(undefined);
Expand Down Expand Up @@ -113,6 +137,7 @@ 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 @@ -185,6 +210,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 @@ -311,7 +339,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
} else {
result = (await response.json()) as FileUploadResponse;
}

} catch (error) {
result.ok = false;
console.error("Error uploading files:", error);
Expand All @@ -332,6 +360,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
sendMessage,
serverStatus,
uploadFiles,
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 @@ -334,7 +334,7 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) {
s.logger.Info("Initial prompt sent successfully")
}
}
s.emitter.UpdateStatusAndEmitChanges(currentStatus)
s.emitter.UpdateStatusAndEmitChanges(currentStatus, s.agentType)
s.emitter.UpdateMessagesAndEmitChanges(s.conversation.Messages())
s.emitter.UpdateScreenAndEmitChanges(s.conversation.Screen())
time.Sleep(snapshotInterval)
Expand Down Expand Up @@ -404,6 +404,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
1 change: 1 addition & 0 deletions lib/msgfmt/msgfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ func trimEmptyLines(message string) string {

type AgentType string

// Remember to add the display name to the agentapi/chat/src/components/chat-provider.tsx
const (
AgentTypeClaude AgentType = "claude"
AgentTypeGoose AgentType = "goose"
Expand Down
10 changes: 10 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,17 @@
"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": [
"agent_type",
"status"
],
"type": "object"
Expand All @@ -292,12 +297,17 @@
"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": [
"agent_type",
"status"
],
"type": "object"
Expand Down