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 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Mohamed Boudra
2025-10-21 15:49:55 +02:00
parent 4f2c2fc14a
commit cf977968c6
9 changed files with 393 additions and 120 deletions

View File

@@ -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();

View File

@@ -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<AgentUpdateCallback>;
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<RequestPermissionResponse> {
// 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<void> {
this.onUpdate(this.agentId, params);
}
async readTextFile(params: any) {
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
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<WriteTextFileResponse> {
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<string> {
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<void> {
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

View File

@@ -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 {

View File

@@ -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}`);

View File

@@ -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;
}
/**

View File

@@ -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<typeof ConversationLoadedMessage
export type AgentCreatedMessage = z.infer<typeof AgentCreatedMessageSchema>;
export type AgentUpdateMessage = z.infer<typeof AgentUpdateMessageSchema>;
export type AgentStatusMessage = z.infer<typeof AgentStatusMessageSchema>;
export type SessionStateMessage = z.infer<typeof SessionStateMessageSchema>;
// Type exports for payload types
export type ActivityLogPayload = z.infer<typeof ActivityLogPayloadSchema>;

View File

@@ -70,6 +70,7 @@ export class Session {
// Per-session MCP client and tools
private terminalMcpClient: Awaited<ReturnType<typeof experimental_createMCPClient>> | null = null;
private terminalTools: Record<string, any> | null = null;
private terminalManager: any | null = null;
private agentMcpClient: Awaited<ReturnType<typeof experimental_createMCPClient>> | null = null;
private agentTools: Record<string, any> | null = null;
private agentManager: AgentManager | null = null;
@@ -156,6 +157,11 @@ export class Session {
*/
private async initializeTerminalMcp(): Promise<void> {
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<void> {
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,
},
});
}

View File

@@ -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<string, Array<{ timestamp: Date; notification: SessionNotification }>>
>(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() {
<ActiveProcesses
terminals={[]}
agents={Array.from(agents.values())}
commands={commands}
activeProcessId={activeProcessId}
activeProcessType={activeView === "orchestrator" ? null : activeView}
onSelectProcess={handleSelectProcess}

View File

@@ -1,9 +1,23 @@
import { Terminal, Bot } from "lucide-react";
import { Terminal, Bot, Code } from "lucide-react";
import type { AgentStatus } from "../../server/acp/types.js";
export interface ActiveProcessesProps {
terminals: Array<{ id: string; name: string }>;
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({
</button>
))}
{agents.map((agent) => (
<button
key={`agent-${agent.id}`}
className={`process-pill agent ${
activeProcessType === "agent" && activeProcessId === agent.id
? "active"
: ""
}`}
onClick={() => onSelectProcess(agent.id, "agent")}
type="button"
data-status={agent.status}
{agents.map((agent) => {
const modeName = getModeName(agent.currentModeId, agent.availableModes);
const modesList = agent.availableModes?.map(m => m.name).join(", ") || "unknown";
return (
<button
key={`agent-${agent.id}`}
className={`process-pill agent ${
activeProcessType === "agent" && activeProcessId === agent.id
? "active"
: ""
}`}
onClick={() => onSelectProcess(agent.id, "agent")}
type="button"
data-status={agent.status}
title={`Status: ${getAgentStatusLabel(agent.status)} | Mode: ${modeName} | Available: ${modesList}`}
>
<Bot size={14} />
<span className="process-name">
{agent.id.substring(0, 8)}
</span>
<span
className={`process-status-indicator ${agent.status}`}
style={{ backgroundColor: getAgentStatusColor(agent.status) }}
title={getAgentStatusLabel(agent.status)}
/>
<span
className="session-mode-badge"
style={{
backgroundColor: getModeColor(agent.currentModeId),
opacity: 0.3,
width: "6px",
height: "6px",
borderRadius: "50%",
marginLeft: "4px",
}}
title={`Mode: ${modeName}`}
/>
</button>
);
})}
{commands.map((command) => (
<div
key={`command-${command.id}`}
className="process-pill command"
title={`${command.currentCommand} | ${command.workingDirectory}${command.isDead ? ` | Exit code: ${command.exitCode}` : ""}`}
>
<Bot size={14} />
<Code size={14} />
<span className="process-name">
{agent.id.substring(0, 8)}
{command.name || command.currentCommand.substring(0, 20)}
</span>
<span
className={`process-status-indicator ${agent.status}`}
style={{ backgroundColor: getAgentStatusColor(agent.status) }}
title={getAgentStatusLabel(agent.status)}
className={`process-status-indicator ${command.isDead ? "dead" : "running"}`}
style={{
backgroundColor: command.isDead
? command.exitCode === 0
? "#22c55e"
: "#ef4444"
: "#3b82f6",
}}
title={command.isDead ? `Exited: ${command.exitCode}` : "Running"}
/>
</button>
</div>
))}
</div>
</div>