Based on the changes, I can see this is a significant feature addition that adds the ability to create agents and communicate with them through both text and audio, with a modal UI for agent creation and mode switching.

```
feat: add agent creation modal and agent-specific messaging

- Add CreateAgentModal component for creating new agents with mode selection
- Implement agent-specific text and audio message routing
- Add mode selector modal with visual mode switching UI
- Introduce unified stream reducer pattern for agent activity
- Refactor AgentStreamView to use StreamItem[] instead of raw notifications
- Update ActiveProcesses to show orchestrator pill and support mode switching
- Add message types: send_agent_message, send_agent_audio, create_agent_request, set_agent_mode
- Implement agent audio transcription and message forwarding in session handler
- Add mode badge UI showing current agent mode
- Update tmux capture-pane to allowed commands

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
```
This commit is contained in:
Mohamed Boudra
2025-10-22 22:06:00 +02:00
parent 2c72d02cc7
commit b86e5fbb3f
9 changed files with 1568 additions and 299 deletions
@@ -180,13 +180,15 @@ export class AgentManager {
});
// Create a new session
const sessionResponse = await agent.connection.newSession({
const sessionParams = {
cwd,
mcpServers: [],
...(options.initialMode ? { initialMode: options.initialMode } : {}),
});
};
console.log(`[Agent ${agentId}] newSession params:`, sessionParams);
const sessionResponse = await agent.connection.newSession(sessionParams);
agent.sessionId = sessionResponse.sessionId;
console.log(`[Agent ${agentId}] newSession response:`, JSON.stringify(sessionResponse, null, 2));
// Store session modes from response
if (sessionResponse.modes) {
@@ -200,6 +202,12 @@ export class AgentManager {
console.log(`[Agent ${agentId}] Session created:`, sessionResponse.sessionId);
}
// Set initial mode if requested (must be done after session creation)
if (options.initialMode && agent.sessionId) {
console.log(`[Agent ${agentId}] Setting initial mode to: ${options.initialMode}`);
await this.setSessionMode(agentId, options.initialMode);
}
agent.status = "ready";
// If an initial prompt was provided, send it
+41 -4
View File
@@ -7,11 +7,10 @@ import { z } from "zod";
export const UserTextMessageSchema = z.object({
type: z.literal("user_text"),
text: z.string(),
disableTTS: z.boolean().optional(),
});
export const AudioChunkMessageSchema = z.object({
type: z.literal("audio_chunk"),
export const RealtimeAudioChunkMessageSchema = z.object({
type: z.literal("realtime_audio_chunk"),
audio: z.string(), // base64 encoded
format: z.string(),
isLast: z.boolean(),
@@ -45,15 +44,45 @@ export const SetRealtimeModeMessageSchema = z.object({
enabled: z.boolean(),
});
export const SendAgentMessageSchema = z.object({
type: z.literal("send_agent_message"),
agentId: z.string(),
text: z.string(),
});
export const SendAgentAudioSchema = z.object({
type: z.literal("send_agent_audio"),
agentId: z.string(),
audio: z.string(), // base64 encoded
format: z.string(),
isLast: z.boolean(),
});
export const CreateAgentRequestMessageSchema = z.object({
type: z.literal("create_agent_request"),
cwd: z.string(),
initialMode: z.string().optional(),
});
export const SetAgentModeMessageSchema = z.object({
type: z.literal("set_agent_mode"),
agentId: z.string(),
modeId: z.string(),
});
export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
UserTextMessageSchema,
AudioChunkMessageSchema,
RealtimeAudioChunkMessageSchema,
AbortRequestMessageSchema,
AudioPlayedMessageSchema,
LoadConversationRequestMessageSchema,
ListConversationsRequestMessageSchema,
DeleteConversationRequestMessageSchema,
SetRealtimeModeMessageSchema,
SendAgentMessageSchema,
SendAgentAudioSchema,
CreateAgentRequestMessageSchema,
SetAgentModeMessageSchema,
]);
export type SessionInboundMessage = z.infer<typeof SessionInboundMessageSchema>;
@@ -274,6 +303,14 @@ export type DeleteConversationResponseMessage = z.infer<typeof DeleteConversatio
// Type exports for payload types
export type ActivityLogPayload = z.infer<typeof ActivityLogPayloadSchema>;
// Type exports for inbound message types
export type UserTextMessage = z.infer<typeof UserTextMessageSchema>;
export type RealtimeAudioChunkMessage = z.infer<typeof RealtimeAudioChunkMessageSchema>;
export type SendAgentMessage = z.infer<typeof SendAgentMessageSchema>;
export type SendAgentAudio = z.infer<typeof SendAgentAudioSchema>;
export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessageSchema>;
export type SetAgentModeMessage = z.infer<typeof SetAgentModeMessageSchema>;
// ============================================================================
// WebSocket Level Messages (wraps session messages)
// ============================================================================
+236 -2
View File
@@ -286,7 +286,7 @@ export class Session {
await this.handleUserText(msg.text);
break;
case "audio_chunk":
case "realtime_audio_chunk":
await this.handleAudioChunk(msg);
break;
@@ -313,6 +313,22 @@ export class Session {
case "set_realtime_mode":
this.handleSetRealtimeMode(msg.enabled);
break;
case "send_agent_message":
await this.handleSendAgentMessage(msg.agentId, msg.text);
break;
case "send_agent_audio":
await this.handleSendAgentAudio(msg);
break;
case "create_agent_request":
await this.handleCreateAgentRequest(msg.cwd, msg.initialMode);
break;
case "set_agent_mode":
await this.handleSetAgentMode(msg.agentId, msg.modeId);
break;
}
} catch (error: any) {
console.error(
@@ -423,6 +439,219 @@ export class Session {
);
}
/**
* Handle text message to agent
*/
private async handleSendAgentMessage(agentId: string, text: string): Promise<void> {
console.log(
`[Session ${this.clientId}] Sending text to agent ${agentId}: ${text.substring(0, 50)}...`
);
try {
// Emit user message notification before sending to agent
// (Claude Code ACP doesn't echo user messages, so we do it manually)
this.emit({
type: "agent_update",
payload: {
agentId,
timestamp: new Date().toISOString(),
notification: {
type: "sessionUpdate",
update: {
sessionUpdate: "user_message_chunk",
content: {
type: "text",
text: text,
},
},
} as any,
},
});
await this.agentManager.sendPrompt(agentId, text);
console.log(`[Session ${this.clientId}] Sent text to agent ${agentId}`);
} catch (error: any) {
console.error(
`[Session ${this.clientId}] Failed to send text to agent ${agentId}:`,
error
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to send message to agent: ${error.message}`,
},
});
throw error;
}
}
/**
* Handle audio message to agent (transcribe then send)
*/
private async handleSendAgentAudio(
msg: Extract<SessionInboundMessage, { type: "send_agent_audio" }>
): Promise<void> {
const { agentId, audio, format, isLast } = msg;
// Decode base64
const audioBuffer = Buffer.from(audio, "base64");
// For now, we'll process each audio segment immediately
// In the future, we might want to buffer chunks similar to realtime audio
if (!isLast) {
console.log(
`[Session ${this.clientId}] Buffering agent audio chunk for agent ${agentId}`
);
// TODO: Implement buffering if needed
return;
}
console.log(
`[Session ${this.clientId}] Transcribing audio for agent ${agentId}`
);
try {
// Transcribe the audio
const result = await this.sttManager.transcribe(audioBuffer, format);
const transcriptText = result.text.trim();
if (!transcriptText) {
console.log(
`[Session ${this.clientId}] Empty transcription for agent ${agentId}, ignoring`
);
return;
}
console.log(
`[Session ${this.clientId}] Transcribed audio for agent ${agentId}: ${transcriptText}`
);
// Send transcribed text to agent
await this.agentManager.sendPrompt(agentId, transcriptText);
console.log(`[Session ${this.clientId}] Sent transcribed text to agent ${agentId}`);
} catch (error: any) {
console.error(
`[Session ${this.clientId}] Failed to process audio for agent ${agentId}:`,
error
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to process audio for agent: ${error.message}`,
},
});
throw error;
}
}
/**
* Handle create agent request
*/
private async handleCreateAgentRequest(cwd: string, initialMode?: string): Promise<void> {
console.log(
`[Session ${this.clientId}] Creating agent in ${cwd} with mode ${initialMode || "default"}`
);
try {
const agentId = await this.agentManager.createAgent({
cwd,
initialMode,
});
console.log(`[Session ${this.clientId}] Created agent ${agentId}`);
// Get agent info
const agentInfo = this.agentManager.listAgents().find(a => a.id === agentId);
console.log(`[Session ${this.clientId}] Agent info:`, {
currentModeId: agentInfo?.currentModeId,
availableModes: agentInfo?.availableModes
});
// Subscribe to agent updates
this.subscribeToAgent(agentId);
// Emit agent_created message
this.emit({
type: "agent_created",
payload: {
agentId,
status: agentInfo?.status || "initializing",
type: "claude",
currentModeId: agentInfo?.currentModeId,
availableModes: agentInfo?.availableModes,
},
});
console.log(`[Session ${this.clientId}] Emitted agent_created with currentModeId:`, agentInfo?.currentModeId);
} catch (error: any) {
console.error(
`[Session ${this.clientId}] Failed to create agent:`,
error
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to create agent: ${error.message}`,
},
});
throw error;
}
}
/**
* Handle set agent mode request
*/
private async handleSetAgentMode(agentId: string, modeId: string): Promise<void> {
console.log(`[Session ${this.clientId}] Setting agent ${agentId} mode to ${modeId}`);
try {
await this.agentManager.setSessionMode(agentId, modeId);
console.log(`[Session ${this.clientId}] Agent ${agentId} mode set to ${modeId}`);
// Emit agent_status to notify client of mode change
const info = this.agentManager.listAgents().find(a => a.id === agentId);
if (info) {
this.emit({
type: "agent_status",
payload: {
agentId,
status: info.status,
info: {
id: info.id,
status: info.status,
createdAt: info.createdAt,
type: info.type,
sessionId: info.sessionId ?? undefined,
error: info.error ?? undefined,
currentModeId: info.currentModeId ?? undefined,
availableModes: info.availableModes ?? undefined,
},
},
});
}
} catch (error: any) {
console.error(`[Session ${this.clientId}] Failed to set agent mode:`, error);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to set agent mode: ${error.message}`,
},
});
throw error;
}
}
/**
* Send current session state (live agents and commands) to client
*/
@@ -431,6 +660,11 @@ export class Session {
// Get live agents with session modes
const agents = this.agentManager?.listAgents() || [];
// Subscribe to all existing agents (in case of reconnection)
for (const agent of agents) {
this.subscribeToAgent(agent.id);
}
// Get live commands from terminal manager
let commands: any[] = [];
if (this.terminalManager) {
@@ -503,7 +737,7 @@ export class Session {
* Handle audio chunk for buffering and transcription
*/
private async handleAudioChunk(
msg: Extract<SessionInboundMessage, { type: "audio_chunk" }>
msg: Extract<SessionInboundMessage, { type: "realtime_audio_chunk" }>
): Promise<void> {
// Decode base64
const audioBuffer = Buffer.from(msg.audio, "base64");