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
co-authored by Claude Happy
parent 4f2c2fc14a
commit cf977968c6
9 changed files with 393 additions and 120 deletions
@@ -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 {