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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
// ============================================================================
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user