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:
co-authored by
Claude
Happy
parent
4f2c2fc14a
commit
cf977968c6
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user