mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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