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