From cf977968c62ed6d85398aacceabd2e682a521a10 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 21 Oct 2025 15:49:55 +0200 Subject: [PATCH] refactor: implement proper ACP session modes and fix type safety Replace client-side permission mode hack with proper ACP session modes: - Add typed Client interface implementation to ACPClient - Import proper types from ACP SDK (SessionNotification, RequestPermissionRequest, etc.) - Fix critical sessionUpdate bug where params.update was undefined - Replace PermissionMode with SessionMode from ACP spec - Add getCurrentMode(), getAvailableModes(), setSessionMode() to AgentManager - Update MCP tools: replace set_agent_permission_mode with set_agent_mode - Add session_state message type to send live agents/commands to client - Update UI to display session mode badges (ask/code/architect) - Remove all permissionsMode references from tests Agents now have ONE mode (session mode) that controls their behavior, system prompts, and tool availability - not separate permission/session modes. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- .../src/server/acp/agent-manager.test.ts | 6 - .../src/server/acp/agent-manager.ts | 160 +++++++++++------- .../src/server/acp/mcp-server.ts | 64 ++++--- .../src/server/acp/test-comprehensive.ts | 6 - .../voice-assistant/src/server/acp/types.ts | 15 +- .../voice-assistant/src/server/messages.ts | 40 +++++ .../voice-assistant/src/server/session.ts | 54 ++++++ packages/voice-assistant/src/ui/App.tsx | 56 +++++- .../src/ui/components/ActiveProcesses.tsx | 112 +++++++++--- 9 files changed, 393 insertions(+), 120 deletions(-) diff --git a/packages/voice-assistant/src/server/acp/agent-manager.test.ts b/packages/voice-assistant/src/server/acp/agent-manager.test.ts index 75310bb92..00592c170 100644 --- a/packages/voice-assistant/src/server/acp/agent-manager.test.ts +++ b/packages/voice-assistant/src/server/acp/agent-manager.test.ts @@ -175,7 +175,6 @@ describe("AgentManager", () => { // Create agent without initial prompt first, so we can subscribe const agentId = await manager.createAgent({ cwd: tmpDir, - permissionsMode: "auto_approve", }); expect(agentId).toBeDefined(); @@ -229,7 +228,6 @@ describe("AgentManager", () => { const agentId = await manager.createAgent({ cwd: tmpDir, - permissionsMode: "auto_approve", }); expect(agentId).toBeDefined(); @@ -275,7 +273,6 @@ describe("AgentManager", () => { const agentId = await manager.createAgent({ cwd: tmpDir, - permissionsMode: "reject_all", }); expect(agentId).toBeDefined(); @@ -338,7 +335,6 @@ describe("AgentManager", () => { const agentId = await manager.createAgent({ cwd: tmpDir, - permissionsMode: "auto_approve", }); expect(agentId).toBeDefined(); @@ -393,7 +389,6 @@ describe("AgentManager", () => { const agentId = await manager.createAgent({ cwd: tmpDir, - permissionsMode: "auto_approve", }); expect(agentId).toBeDefined(); @@ -439,7 +434,6 @@ describe("AgentManager", () => { const agentId = await manager.createAgent({ cwd: tmpDir, - permissionsMode: "auto_approve", }); expect(agentId).toBeDefined(); diff --git a/packages/voice-assistant/src/server/acp/agent-manager.ts b/packages/voice-assistant/src/server/acp/agent-manager.ts index e8660f4f2..5d24d3a18 100644 --- a/packages/voice-assistant/src/server/acp/agent-manager.ts +++ b/packages/voice-assistant/src/server/acp/agent-manager.ts @@ -1,6 +1,18 @@ import { spawn, ChildProcess } from "child_process"; import { Writable, Readable } from "stream"; -import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk"; +import { + ClientSideConnection, + ndJsonStream, + PROTOCOL_VERSION, + type Client, + type SessionNotification, + type RequestPermissionRequest, + type RequestPermissionResponse, + type ReadTextFileRequest, + type ReadTextFileResponse, + type WriteTextFileRequest, + type WriteTextFileResponse, +} from "@agentclientprotocol/sdk"; import { v4 as uuidv4 } from "uuid"; import type { AgentStatus, @@ -8,7 +20,7 @@ import type { AgentUpdate, CreateAgentOptions, AgentUpdateCallback, - PermissionMode, + SessionMode, } from "./types.js"; interface ManagedAgent { @@ -19,7 +31,8 @@ interface ManagedAgent { connection: ClientSideConnection; sessionId?: string; error?: string; - permissionsMode: PermissionMode; + currentModeId?: string; + availableModes?: SessionMode[]; subscribers: Set; updates: AgentUpdate[]; } @@ -27,53 +40,30 @@ interface ManagedAgent { /** * Client implementation for ACP callbacks */ -class ACPClient { +class ACPClient implements Client { constructor( private agentId: string, - private onUpdate: (agentId: string, update: any) => void, - private getPermissionMode: () => PermissionMode + private onUpdate: (agentId: string, update: SessionNotification) => void ) {} - async requestPermission(params: any) { - const mode = this.getPermissionMode(); - console.log(`[Agent ${this.agentId}] Permission requested (mode: ${mode}):`, params); + async requestPermission(params: RequestPermissionRequest): Promise { + // TODO: Forward permission requests to the UI for user approval + // For now, auto-approve all permissions + console.log(`[Agent ${this.agentId}] Permission requested (auto-approving):`, params); - if (mode === "auto_approve") { - // Auto-approve all permissions - return { - outcome: { - outcome: "selected" as const, - optionId: params.options[0]?.optionId || "", - }, - }; - } else if (mode === "reject_all") { - // Reject all permissions - const rejectOption = params.options.find((opt: any) => - opt.kind === "reject_once" || opt.kind === "reject_all" - ); - return { - outcome: { - outcome: "selected" as const, - optionId: rejectOption?.optionId || params.options[params.options.length - 1]?.optionId || "", - }, - }; - } else { - // ask_user mode - for now, auto-approve (TODO: implement user prompts) - console.log(`[Agent ${this.agentId}] User permission prompt not yet implemented, auto-approving`); - return { - outcome: { - outcome: "selected" as const, - optionId: params.options[0]?.optionId || "", - }, - }; - } + return { + outcome: { + outcome: "selected" as const, + optionId: params.options[0]?.optionId || "", + }, + }; } - async sessionUpdate(params: any) { - this.onUpdate(this.agentId, params.update); + async sessionUpdate(params: SessionNotification): Promise { + this.onUpdate(this.agentId, params); } - async readTextFile(params: any) { + async readTextFile(params: ReadTextFileRequest): Promise { console.log(`[Agent ${this.agentId}] Read text file:`, params.path); const fs = await import("fs/promises"); try { @@ -85,7 +75,7 @@ class ACPClient { } } - async writeTextFile(params: any) { + async writeTextFile(params: WriteTextFileRequest): Promise { console.log(`[Agent ${this.agentId}] Write text file:`, params.path); const fs = await import("fs/promises"); try { @@ -111,7 +101,6 @@ export class AgentManager { async createAgent(options: CreateAgentOptions): Promise { const agentId = uuidv4(); const cwd = options.cwd; - const permissionsMode = options.permissionsMode || "auto_approve"; // Spawn the ACP process const agentProcess = spawn("npx", ["@zed-industries/claude-code-acp"], { @@ -129,10 +118,6 @@ export class AgentManager { agentId, (id, update) => { this.handleSessionNotification(id, update); - }, - () => { - const agent = this.agents.get(agentId); - return agent?.permissionsMode || "auto_approve"; } ); const connection = new ClientSideConnection(() => client, stream); @@ -144,7 +129,6 @@ export class AgentManager { createdAt: new Date(), process: agentProcess, connection, - permissionsMode, subscribers: new Set(), updates: [], }; @@ -189,11 +173,24 @@ export class AgentManager { const sessionResponse = await agent.connection.newSession({ cwd, mcpServers: [], + ...(options.initialMode ? { initialMode: options.initialMode } : {}), }); agent.sessionId = sessionResponse.sessionId; + + // Store session modes from response + if (sessionResponse.modes) { + agent.currentModeId = sessionResponse.modes.currentModeId; + agent.availableModes = sessionResponse.modes.availableModes; + console.log( + `[Agent ${agentId}] Session created with mode: ${agent.currentModeId}`, + `Available modes:`, agent.availableModes?.map(m => m.id).join(', ') + ); + } else { + console.log(`[Agent ${agentId}] Session created:`, sessionResponse.sessionId); + } + agent.status = "ready"; - console.log(`[Agent ${agentId}] Session created:`, sessionResponse.sessionId); // If an initial prompt was provided, send it if (options.initialPrompt) { @@ -320,6 +317,8 @@ export class AgentManager { type: "claude" as const, sessionId: agent.sessionId, error: agent.error, + currentModeId: agent.currentModeId, + availableModes: agent.availableModes, })); } @@ -355,32 +354,69 @@ export class AgentManager { } /** - * Set the permission mode for an agent + * Get the current session mode for an agent */ - setPermissionMode(agentId: string, mode: PermissionMode): void { + getCurrentMode(agentId: string): string | undefined { const agent = this.agents.get(agentId); if (!agent) { throw new Error(`Agent ${agentId} not found`); } - agent.permissionsMode = mode; - console.log(`[Agent ${agentId}] Permission mode set to: ${mode}`); + return agent.currentModeId; } /** - * Get the permission mode for an agent + * Get available session modes for an agent */ - getPermissionMode(agentId: string): PermissionMode { + getAvailableModes(agentId: string): SessionMode[] { const agent = this.agents.get(agentId); if (!agent) { throw new Error(`Agent ${agentId} not found`); } - return agent.permissionsMode; + return agent.availableModes || []; + } + + /** + * Set the session mode for an agent + * Validates that the mode is available before setting + */ + async setSessionMode(agentId: string, modeId: string): Promise { + const agent = this.agents.get(agentId); + if (!agent) { + throw new Error(`Agent ${agentId} not found`); + } + + if (!agent.sessionId) { + throw new Error(`Agent ${agentId} has no active session`); + } + + // Validate mode is available + const availableModes = agent.availableModes || []; + const mode = availableModes.find((m) => m.id === modeId); + if (!mode && availableModes.length > 0) { + const availableIds = availableModes.map((m) => m.id).join(", "); + throw new Error( + `Mode '${modeId}' not available for agent ${agentId}. Available modes: ${availableIds}` + ); + } + + try { + await agent.connection.setSessionMode({ + sessionId: agent.sessionId, + modeId, + }); + + agent.currentModeId = modeId; + console.log(`[Agent ${agentId}] Session mode changed to: ${modeId}`); + } catch (error) { + console.error(`[Agent ${agentId}] Failed to set mode:`, error); + throw error; + } } /** * Handle session notifications from the ACP connection */ - private handleSessionNotification(agentId: string, update: any): void { + private handleSessionNotification(agentId: string, update: SessionNotification): void { const agent = this.agents.get(agentId); if (!agent) return; @@ -393,10 +429,16 @@ export class AgentManager { // Store the update in history agent.updates.push(agentUpdate); + // Handle mode change notifications + if ((update as any).type === "currentModeUpdate" && (update as any).currentModeId) { + agent.currentModeId = (update as any).currentModeId; + console.log(`[Agent ${agentId}] Mode changed to: ${agent.currentModeId}`); + } + // Track completion based on final message state // ACP protocol indicates completion when the last agent_message_chunk arrives // We detect this by tracking a completion timer that fires if no more updates arrive - const updateType = update.sessionUpdate?.sessionUpdate; + const updateType = update.update.sessionUpdate; // Clear and reset completion timer on any update during processing if (agent.status === "processing") { @@ -418,7 +460,7 @@ export class AgentManager { // Log update for debugging console.log( `[Agent ${agentId}] Session update:`, - updateType || update.sessionUpdate || update + updateType ); // Notify all subscribers diff --git a/packages/voice-assistant/src/server/acp/mcp-server.ts b/packages/voice-assistant/src/server/acp/mcp-server.ts index b9a7781e7..6f32ea78b 100644 --- a/packages/voice-assistant/src/server/acp/mcp-server.ts +++ b/packages/voice-assistant/src/server/acp/mcp-server.ts @@ -59,11 +59,11 @@ export async function createAgentMcpServer( .describe( "Optional initial task or prompt for the agent to start working on immediately after creation. If provided, agent will begin processing this task right away." ), - permissionsMode: z - .enum(["auto_approve", "ask_user", "reject_all"]) + initialMode: z + .string() .optional() .describe( - "Permission mode for file operations. 'auto_approve' (default): automatically allow all operations. 'ask_user': prompt user for each permission (not yet implemented, falls back to auto_approve). 'reject_all': reject all permission requests." + "Initial session mode for the agent (e.g., 'ask', 'code', 'architect'). If not specified, the agent will use its default mode. The available modes depend on the specific agent implementation." ), }, outputSchema: { @@ -74,26 +74,34 @@ export async function createAgentMcpServer( "Current agent status: 'initializing', 'ready', 'processing', etc." ), cwd: z.string().describe("The resolved absolute working directory the agent is running in"), - permissionsMode: z.string().describe("The permission mode the agent is using"), + currentModeId: z.string().optional().describe("The agent's current session mode"), + availableModes: z.array(z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + })).optional().describe("Available session modes for this agent"), }, }, - async ({ cwd, initialPrompt, permissionsMode }) => { + async ({ cwd, initialPrompt, initialMode }) => { // Expand and resolve the working directory const resolvedCwd = expandPath(cwd); const agentId = await agentManager.createAgent({ cwd: resolvedCwd, initialPrompt, - permissionsMode: permissionsMode || "auto_approve", + initialMode, }); const status = agentManager.getAgentStatus(agentId); + const currentModeId = agentManager.getCurrentMode(agentId); + const availableModes = agentManager.getAvailableModes(agentId); const result = { agentId, status, cwd: resolvedCwd, - permissionsMode: permissionsMode || "auto_approve", + currentModeId, + availableModes, }; return { @@ -156,6 +164,12 @@ export async function createAgentMcpServer( type: z.literal("claude"), sessionId: z.string().optional(), error: z.string().optional(), + currentModeId: z.string().optional(), + availableModes: z.array(z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + })).optional(), }) .describe("Detailed agent information"), }, @@ -198,6 +212,12 @@ export async function createAgentMcpServer( type: z.literal("claude"), sessionId: z.string().optional(), error: z.string().optional(), + currentModeId: z.string().optional(), + availableModes: z.array(z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + })).optional(), }) ), }, @@ -325,35 +345,35 @@ export async function createAgentMcpServer( } ); - // Tool: set_agent_permission_mode + // Tool: set_agent_mode server.registerTool( - "set_agent_permission_mode", + "set_agent_mode", { - title: "Set Agent Permission Mode", + title: "Set Agent Session Mode", description: - "Change how the agent handles file operation permission requests. This is a client-side setting that controls how we respond to the agent's permission requests. 'auto_approve' automatically allows all operations (use with caution). 'reject_all' blocks all file operations. 'ask_user' prompts the user for each permission (not yet implemented, currently falls back to auto_approve).", + "Change the agent's session mode (e.g., from 'ask' to 'code', or 'architect' to 'code'). Each mode affects the agent's behavior - 'ask' mode requests permission before changes, 'code' mode writes code directly, 'architect' mode plans without implementation. The available modes depend on the specific agent. Use get_agent_status or list_agents to see available modes for each agent.", inputSchema: { agentId: z.string().describe("Agent ID to configure"), - mode: z - .enum(["auto_approve", "ask_user", "reject_all"]) + modeId: z + .string() .describe( - "Permission mode: 'auto_approve' (allow all), 'ask_user' (prompt user), 'reject_all' (deny all)" + "The session mode to set (e.g., 'ask', 'code', 'architect'). Must be one of the agent's available modes." ), }, outputSchema: { - agentId: z.string(), - previousMode: z.string().describe("The previous permission mode"), - newMode: z.string().describe("The new permission mode"), + success: z.boolean().describe("Whether the mode change succeeded"), + previousMode: z.string().optional().describe("The previous session mode"), + newMode: z.string().describe("The new session mode"), }, }, - async ({ agentId, mode }) => { - const previousMode = agentManager.getPermissionMode(agentId); - agentManager.setPermissionMode(agentId, mode); + async ({ agentId, modeId }) => { + const previousMode = agentManager.getCurrentMode(agentId); + await agentManager.setSessionMode(agentId, modeId); const result = { - agentId, + success: true, previousMode, - newMode: mode, + newMode: modeId, }; return { diff --git a/packages/voice-assistant/src/server/acp/test-comprehensive.ts b/packages/voice-assistant/src/server/acp/test-comprehensive.ts index 8e518dad0..d3857f64a 100755 --- a/packages/voice-assistant/src/server/acp/test-comprehensive.ts +++ b/packages/voice-assistant/src/server/acp/test-comprehensive.ts @@ -84,7 +84,6 @@ async function testDirectoryAndInitialPrompt(ctx: TestContext) { // Create agent without initial prompt first, so we can subscribe const agentId = await ctx.manager.createAgent({ cwd: ctx.tmpDir, - permissionsMode: "auto_approve", }); console.log(`✓ Agent created: ${agentId}`); @@ -151,7 +150,6 @@ async function testPermissionAutoApprove(ctx: TestContext) { const agentId = await ctx.manager.createAgent({ cwd: ctx.tmpDir, - permissionsMode: "auto_approve", }); console.log(`✓ Agent created: ${agentId}`); @@ -208,7 +206,6 @@ async function testPermissionRejectAll(ctx: TestContext) { const agentId = await ctx.manager.createAgent({ cwd: ctx.tmpDir, - permissionsMode: "reject_all", }); console.log(`✓ Agent created: ${agentId}`); @@ -282,7 +279,6 @@ async function testMultiplePrompts(ctx: TestContext) { const agentId = await ctx.manager.createAgent({ cwd: ctx.tmpDir, - permissionsMode: "auto_approve", }); console.log(`✓ Agent created: ${agentId}`); @@ -341,7 +337,6 @@ async function testUpdateStreaming(ctx: TestContext) { const agentId = await ctx.manager.createAgent({ cwd: ctx.tmpDir, - permissionsMode: "auto_approve", }); console.log(`✓ Agent created: ${agentId}`); @@ -408,7 +403,6 @@ async function testStateManagement(ctx: TestContext) { const agentId = await ctx.manager.createAgent({ cwd: ctx.tmpDir, - permissionsMode: "auto_approve", }); console.log(`✓ Agent created: ${agentId}`); diff --git a/packages/voice-assistant/src/server/acp/types.ts b/packages/voice-assistant/src/server/acp/types.ts index 1822c95a3..afee4fcb6 100644 --- a/packages/voice-assistant/src/server/acp/types.ts +++ b/packages/voice-assistant/src/server/acp/types.ts @@ -21,6 +21,8 @@ export interface AgentInfo { type: "claude"; sessionId?: string; error?: string; + currentModeId?: string; + availableModes?: SessionMode[]; } /** @@ -34,12 +36,13 @@ export interface AgentUpdate { } /** - * Permission modes for agent file operations + * Session mode definition from ACP */ -export type PermissionMode = - | "auto_approve" // Automatically approve all permissions - | "ask_user" // Prompt user for each permission - | "reject_all"; // Reject all permission requests +export interface SessionMode { + id: string; + name: string; + description?: string | null; +} /** * Options for creating an agent @@ -47,7 +50,7 @@ export type PermissionMode = export interface CreateAgentOptions { cwd: string; initialPrompt?: string; - permissionsMode?: PermissionMode; + initialMode?: string; } /** diff --git a/packages/voice-assistant/src/server/messages.ts b/packages/voice-assistant/src/server/messages.ts index 8f31d2fa1..77fc89d7e 100644 --- a/packages/voice-assistant/src/server/messages.ts +++ b/packages/voice-assistant/src/server/messages.ts @@ -123,6 +123,12 @@ export const AgentCreatedMessageSchema = z.object({ agentId: z.string(), status: z.string(), type: z.literal("claude"), + currentModeId: z.string().optional(), + availableModes: z.array(z.object({ + id: z.string(), + name: z.string(), + description: z.string().nullable().optional(), + })).optional(), }), }); @@ -151,6 +157,38 @@ export const AgentStatusMessageSchema = z.object({ }), }); +export const SessionStateMessageSchema = z.object({ + type: z.literal("session_state"), + payload: z.object({ + agents: z.array( + z.object({ + id: z.string(), + status: z.string(), + createdAt: z.date(), + type: z.literal("claude"), + sessionId: z.string().optional(), + error: z.string().optional(), + currentModeId: z.string().optional(), + availableModes: z.array(z.object({ + id: z.string(), + name: z.string(), + description: z.string().nullable().optional(), + })).optional(), + }) + ), + commands: z.array( + z.object({ + id: z.string(), + name: z.string(), + workingDirectory: z.string(), + currentCommand: z.string(), + isDead: z.boolean(), + exitCode: z.number().nullable(), + }) + ), + }), +}); + export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ ActivityLogMessageSchema, AssistantChunkMessageSchema, @@ -162,6 +200,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ AgentCreatedMessageSchema, AgentUpdateMessageSchema, AgentStatusMessageSchema, + SessionStateMessageSchema, ]); export type SessionOutboundMessage = z.infer< @@ -179,6 +218,7 @@ export type ConversationLoadedMessage = z.infer; export type AgentUpdateMessage = z.infer; export type AgentStatusMessage = z.infer; +export type SessionStateMessage = z.infer; // Type exports for payload types export type ActivityLogPayload = z.infer; diff --git a/packages/voice-assistant/src/server/session.ts b/packages/voice-assistant/src/server/session.ts index 6c13cbc66..387442850 100644 --- a/packages/voice-assistant/src/server/session.ts +++ b/packages/voice-assistant/src/server/session.ts @@ -70,6 +70,7 @@ export class Session { // Per-session MCP client and tools private terminalMcpClient: Awaited> | null = null; private terminalTools: Record | null = null; + private terminalManager: any | null = null; private agentMcpClient: Awaited> | null = null; private agentTools: Record | null = null; private agentManager: AgentManager | null = null; @@ -156,6 +157,11 @@ export class Session { */ private async initializeTerminalMcp(): Promise { try { + // Create Terminal Manager directly + const { TerminalManager } = await import("./terminal-mcp/terminal-manager.js"); + this.terminalManager = new TerminalManager(this.conversationId); + await this.terminalManager.initialize(); + // Create Terminal MCP server with conversation-specific session const server = await createTerminalMcpServer({ sessionName: this.conversationId @@ -281,6 +287,50 @@ export class Session { messageCount: this.messages.length, }, }); + + // Send current session state (live agents and commands) + await this.sendSessionState(); + } + + /** + * Send current session state (live agents and commands) to client + */ + private async sendSessionState(): Promise { + try { + // Get live agents with session modes + const agents = this.agentManager?.listAgents() || []; + + // Get live commands from terminal manager + let commands: any[] = []; + if (this.terminalManager) { + try { + commands = await this.terminalManager.listCommands(); + } catch (error) { + console.error( + `[Session ${this.clientId}] Failed to list commands:`, + error + ); + } + } + + // Emit session state + this.emit({ + type: "session_state", + payload: { + agents, + commands, + }, + }); + + console.log( + `[Session ${this.clientId}] Sent session state: ${agents.length} agents, ${commands.length} commands` + ); + } catch (error) { + console.error( + `[Session ${this.clientId}] Failed to send session state:`, + error + ); + } } /** @@ -637,6 +687,8 @@ export class Session { if (result.structuredContent?.agentId) { const agentId = result.structuredContent.agentId; const status = result.structuredContent.status; + const currentModeId = result.structuredContent.currentModeId; + const availableModes = result.structuredContent.availableModes; // Subscribe to agent updates this.subscribeToAgent(agentId); @@ -648,6 +700,8 @@ export class Session { agentId, status, type: "claude", + currentModeId, + availableModes, }, }); } diff --git a/packages/voice-assistant/src/ui/App.tsx b/packages/voice-assistant/src/ui/App.tsx index 4ef37df88..9a8c267c4 100644 --- a/packages/voice-assistant/src/ui/App.tsx +++ b/packages/voice-assistant/src/ui/App.tsx @@ -74,9 +74,26 @@ function App() { const [agents, setAgents] = useState< Map< string, - { id: string; status: AgentStatus; createdAt: Date; type: "claude" } + { + id: string; + status: AgentStatus; + createdAt: Date; + type: "claude"; + currentModeId?: string; + availableModes?: Array<{ id: string; name: string; description?: string | null }>; + } > >(new Map()); + const [commands, setCommands] = useState< + Array<{ + id: string; + name: string; + workingDirectory: string; + currentCommand: string; + isDead: boolean; + exitCode: number | null; + }> + >([]); const [agentUpdates, setAgentUpdates] = useState< Map> >(new Map()); @@ -345,10 +362,39 @@ function App() { } }); + // Listen for session state + const unsubSessionState = ws.on("session_state", (message) => { + if (message.type !== "session_state") return; + const { agents: liveAgents, commands: liveCommands } = message.payload; + + // Update agents with session modes + setAgents((prev) => { + const newMap = new Map(prev); + liveAgents.forEach((agent) => { + newMap.set(agent.id, { + id: agent.id, + status: agent.status as AgentStatus, + createdAt: new Date(agent.createdAt), + type: agent.type, + currentModeId: agent.currentModeId, + availableModes: agent.availableModes, + }); + }); + return newMap; + }); + + // Update commands + setCommands(liveCommands); + + console.log( + `[App] Session state loaded: ${liveAgents.length} agents, ${liveCommands.length} commands` + ); + }); + // Listen for agent created const unsubAgentCreated = ws.on("agent_created", (message) => { if (message.type !== "agent_created") return; - const { agentId, status, type } = message.payload; + const { agentId, status, type, currentModeId, availableModes } = message.payload; setAgents((prev) => { const newMap = new Map(prev); @@ -357,6 +403,8 @@ function App() { status: status as AgentStatus, createdAt: new Date(), type, + currentModeId, + availableModes, }); return newMap; }); @@ -411,6 +459,7 @@ function App() { unsubConversationLoaded(); unsubArtifact(); unsubAudioOutput(); + unsubSessionState(); unsubAgentCreated(); unsubAgentUpdate(); unsubAgentStatus(); @@ -599,6 +648,8 @@ function App() { setCurrentAssistantMessage(""); setArtifacts(new Map()); setCurrentArtifact(null); + setAgents(new Map()); + setCommands([]); }; const handleSelectProcess = (id: string, type: "terminal" | "agent") => { @@ -868,6 +919,7 @@ function App() { ; - agents: Array<{ id: string; status: AgentStatus; type: "claude" }>; + agents: Array<{ + id: string; + status: AgentStatus; + type: "claude"; + currentModeId?: string; + availableModes?: Array<{ id: string; name: string; description?: string | null }>; + }>; + commands: Array<{ + id: string; + name: string; + workingDirectory: string; + currentCommand: string; + isDead: boolean; + exitCode: number | null; + }>; activeProcessId: string | null; activeProcessType: "terminal" | "agent" | null; onSelectProcess: (id: string, type: "terminal" | "agent") => void; @@ -33,9 +47,27 @@ function getAgentStatusLabel(status: AgentStatus): string { return status; } +function getModeName(modeId?: string, availableModes?: Array<{ id: string; name: string }>): string { + if (!modeId) return "unknown"; + const mode = availableModes?.find(m => m.id === modeId); + return mode?.name || modeId; +} + +function getModeColor(modeId?: string): string { + if (!modeId) return "#9ca3af"; // gray + + // Color based on common mode types + if (modeId.includes("ask")) return "#f59e0b"; // orange - asks permission + if (modeId.includes("code")) return "#22c55e"; // green - writes code + if (modeId.includes("architect") || modeId.includes("plan")) return "#3b82f6"; // blue - plans + + return "#9ca3af"; // gray - unknown +} + export function ActiveProcesses({ terminals, agents, + commands, activeProcessId, activeProcessType, onSelectProcess, @@ -73,28 +105,70 @@ export function ActiveProcesses({ ))} - {agents.map((agent) => ( - + ); + })} + + {commands.map((command) => ( +
- + - {agent.id.substring(0, 8)} + {command.name || command.currentCommand.substring(0, 20)} - +
))}