diff --git a/packages/core/src/agent/events.ts b/packages/core/src/agent/events.ts new file mode 100644 index 0000000..cf2f666 --- /dev/null +++ b/packages/core/src/agent/events.ts @@ -0,0 +1,165 @@ +/** + * Agent 事件系统 + * 用于在 Core 模块和 Server 模块之间传递子 Agent 执行事件 + */ + +/** + * 子 Agent 事件类型 + */ +export type SubagentEventType = + | 'subagent:start' + | 'subagent:end' + | 'subagent:stream' + | 'subagent:tool_start' + | 'subagent:tool_end'; + +/** + * 子 Agent 开始事件 + */ +export interface SubagentStartEvent { + type: 'subagent:start'; + sessionId: string; + agentId: string; + agentName: string; + description: string; + parentToolCallId?: string; +} + +/** + * 子 Agent 结束事件 + */ +export interface SubagentEndEvent { + type: 'subagent:end'; + sessionId: string; + agentId: string; + agentName: string; + success: boolean; + duration: number; + error?: string; +} + +/** + * 子 Agent 流式输出事件 + */ +export interface SubagentStreamEvent { + type: 'subagent:stream'; + sessionId: string; + agentId: string; + content: string; +} + +/** + * 子 Agent 工具调用开始事件 + */ +export interface SubagentToolStartEvent { + type: 'subagent:tool_start'; + sessionId: string; + agentId: string; + toolCallId: string; + toolName: string; + args: Record; +} + +/** + * 子 Agent 工具调用结束事件 + */ +export interface SubagentToolEndEvent { + type: 'subagent:tool_end'; + sessionId: string; + agentId: string; + toolCallId: string; + status: 'completed' | 'error'; + result?: unknown; + error?: string; + duration?: number; +} + +/** + * 子 Agent 事件联合类型 + */ +export type SubagentEvent = + | SubagentStartEvent + | SubagentEndEvent + | SubagentStreamEvent + | SubagentToolStartEvent + | SubagentToolEndEvent; + +/** + * 事件监听器类型 + */ +export type SubagentEventListener = (event: SubagentEvent) => void; + +/** + * Agent 事件发射器 + * 按 sessionId 隔离事件监听,支持跨模块事件传递 + */ +export class AgentEventEmitter { + private listeners = new Map>(); + + /** + * 订阅指定会话的事件 + * @param sessionId 会话 ID + * @param listener 事件监听器 + * @returns 取消订阅函数 + */ + on(sessionId: string, listener: SubagentEventListener): () => void { + let sessionListeners = this.listeners.get(sessionId); + if (!sessionListeners) { + sessionListeners = new Set(); + this.listeners.set(sessionId, sessionListeners); + } + sessionListeners.add(listener); + + // 返回取消订阅函数 + return () => this.off(sessionId, listener); + } + + /** + * 取消订阅 + */ + off(sessionId: string, listener: SubagentEventListener): void { + const sessionListeners = this.listeners.get(sessionId); + if (sessionListeners) { + sessionListeners.delete(listener); + if (sessionListeners.size === 0) { + this.listeners.delete(sessionId); + } + } + } + + /** + * 发射事件 + */ + emit(event: SubagentEvent): void { + const sessionListeners = this.listeners.get(event.sessionId); + if (sessionListeners) { + for (const listener of sessionListeners) { + try { + listener(event); + } catch (error) { + console.error('[AgentEventEmitter] Listener error:', error); + } + } + } + } + + /** + * 清理指定会话的所有监听器 + */ + clear(sessionId: string): void { + this.listeners.delete(sessionId); + } + + /** + * 检查是否有监听器 + */ + hasListeners(sessionId: string): boolean { + const listeners = this.listeners.get(sessionId); + return !!listeners && listeners.size > 0; + } +} + +/** + * 全局事件发射器单例 + */ +export const agentEventEmitter = new AgentEventEmitter(); diff --git a/packages/core/src/agent/executor.ts b/packages/core/src/agent/executor.ts index f890b21..ac729ad 100644 --- a/packages/core/src/agent/executor.ts +++ b/packages/core/src/agent/executor.ts @@ -18,6 +18,7 @@ import type { import { checkBashPermission, isPathInAllowedWritePaths } from './permission-merger.js'; import { getProviderRegistry } from '../provider/index.js'; import { renderPromptTemplate, createPlanContext } from '../template/index.js'; +import { agentEventEmitter } from './events.js'; /** * Agent 执行器 @@ -54,7 +55,10 @@ export class AgentExecutor { prompt: string, context: AgentExecutionContext ): Promise { - const { onStream, onToolCall, onToolResult, images } = context; + const { onStream, onToolCall, onToolResult, images, sessionId, agentId, emitEvents } = context; + + // 是否发射子 Agent 事件 + const shouldEmitEvents = emitEvents && sessionId && agentId; // 获取过滤后的工具 const tools = this.getFilteredTools(); @@ -82,9 +86,12 @@ export class AgentExecutor { let fullResponse = ''; let steps = 0; + // 工具调用时间追踪(用于计算持续时间) + const toolStartTimes = new Map(); + try { - if (onStream) { - // 流式模式 + if (onStream || shouldEmitEvents) { + // 流式模式(或需要发射事件时使用流式模式) const result = streamText({ model: this.getModel(modelName), system: systemPrompt, @@ -96,23 +103,72 @@ export class AgentExecutor { if (chunk.type === 'tool-call') { steps++; const toolArgs = 'input' in chunk ? chunk.input : {}; + const toolCallId = `tool-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; + + // 记录工具开始时间 + toolStartTimes.set(chunk.toolName, Date.now()); + onToolCall?.(chunk.toolName, toolArgs as Record); - onStream(`\n[调用工具: ${chunk.toolName}]\n`); + onStream?.(`\n[调用工具: ${chunk.toolName}]\n`); + + // 发射子 Agent 工具开始事件 + if (shouldEmitEvents) { + agentEventEmitter.emit({ + type: 'subagent:tool_start', + sessionId: sessionId!, + agentId: agentId!, + toolCallId, + toolName: chunk.toolName, + args: toolArgs as Record, + }); + } } else if (chunk.type === 'tool-result') { const output = (chunk as { output?: ToolResult }).output; - onToolResult?.( - (chunk as { toolName?: string }).toolName ?? 'unknown', - output - ); + const toolName = (chunk as { toolName?: string }).toolName ?? 'unknown'; + const toolCallId = `tool-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; + + // 计算工具执行时间 + const startTime = toolStartTimes.get(toolName); + const duration = startTime ? Date.now() - startTime : undefined; + toolStartTimes.delete(toolName); + + onToolResult?.(toolName, output); + if (output && typeof output === 'object') { if (output.success) { const displayOutput = output.output.length > 500 ? output.output.substring(0, 500) + '...(截断)' : output.output; - onStream(`[结果: ${displayOutput}]\n`); + onStream?.(`[结果: ${displayOutput}]\n`); + + // 发射子 Agent 工具完成事件 + if (shouldEmitEvents) { + agentEventEmitter.emit({ + type: 'subagent:tool_end', + sessionId: sessionId!, + agentId: agentId!, + toolCallId, + status: 'completed', + result: output.output, + duration, + }); + } } else { - onStream(`[错误: ${output.error}]\n`); + onStream?.(`[错误: ${output.error}]\n`); + + // 发射子 Agent 工具错误事件 + if (shouldEmitEvents) { + agentEventEmitter.emit({ + type: 'subagent:tool_end', + sessionId: sessionId!, + agentId: agentId!, + toolCallId, + status: 'error', + error: output.error, + duration, + }); + } } } } @@ -121,7 +177,17 @@ export class AgentExecutor { for await (const chunk of result.textStream) { fullResponse += chunk; - onStream(chunk); + onStream?.(chunk); + + // 发射子 Agent 流式输出事件 + if (shouldEmitEvents && chunk) { + agentEventEmitter.emit({ + type: 'subagent:stream', + sessionId: sessionId!, + agentId: agentId!, + content: chunk, + }); + } } await result.response; diff --git a/packages/core/src/agent/index.ts b/packages/core/src/agent/index.ts index 0711a0d..edef7fe 100644 --- a/packages/core/src/agent/index.ts +++ b/packages/core/src/agent/index.ts @@ -61,6 +61,20 @@ export { // System Prompt export { SystemPrompt } from './system-prompt.js'; +// Events +export { + AgentEventEmitter, + agentEventEmitter, + type SubagentEventType, + type SubagentStartEvent, + type SubagentEndEvent, + type SubagentStreamEvent, + type SubagentToolStartEvent, + type SubagentToolEndEvent, + type SubagentEvent, + type SubagentEventListener, +} from './events.js'; + // Prompt Template (re-export from ../template/) export { renderTemplate, diff --git a/packages/core/src/agent/manager.ts b/packages/core/src/agent/manager.ts index 37f821a..e20fae8 100644 --- a/packages/core/src/agent/manager.ts +++ b/packages/core/src/agent/manager.ts @@ -3,6 +3,7 @@ import type { AgentConfig } from '../types/index.js'; import type { AgentExecutionContext, AgentInfo } from './types.js'; import { AgentExecutor } from './executor.js'; import { ToolRegistry } from '../tools/registry.js'; +import { agentEventEmitter } from './events.js'; /** * 后台 Agent 状态 @@ -67,7 +68,7 @@ export class AgentManager { }); // 异步执行,不等待结果 - this.executeAsync(agentId, agentInfo, prompt, baseConfig, toolRegistry, context); + this.executeAsync(agentId, agentInfo, description, prompt, baseConfig, toolRegistry, context); return agentId; } @@ -78,16 +79,31 @@ export class AgentManager { private async executeAsync( agentId: string, agentInfo: AgentInfo, + description: string, prompt: string, baseConfig: AgentConfig, toolRegistry: ToolRegistry, context: AgentExecutionContext ): Promise { + const sessionId = context.parentSessionId || 'standalone'; + const startTime = Date.now(); + + // 发射子 Agent 开始事件 + agentEventEmitter.emit({ + type: 'subagent:start', + sessionId, + agentId, + agentName: agentInfo.name, + description, + }); + try { const executor = new AgentExecutor(agentInfo, baseConfig, toolRegistry); const result = await executor.execute(prompt, { ...context, - onStream: undefined, // 后台运行不使用流式输出 + sessionId, + agentId, + emitEvents: true, // 启用子 Agent 事件发射 }); // 更新状态为完成 @@ -101,14 +117,37 @@ export class AgentManager { agent.error = result.error; } } + + // 发射子 Agent 结束事件 + agentEventEmitter.emit({ + type: 'subagent:end', + sessionId, + agentId, + agentName: agentInfo.name, + success: result.success, + duration: Date.now() - startTime, + error: result.error, + }); } catch (error) { // 更新状态为失败 const agent = this.backgroundAgents.get(agentId); + const errorMessage = error instanceof Error ? error.message : String(error); if (agent) { agent.status = 'failed'; agent.completedAt = new Date(); - agent.error = error instanceof Error ? error.message : String(error); + agent.error = errorMessage; } + + // 发射子 Agent 错误事件 + agentEventEmitter.emit({ + type: 'subagent:end', + sessionId, + agentId, + agentName: agentInfo.name, + success: false, + duration: Date.now() - startTime, + error: errorMessage, + }); } // 触发等待回调 diff --git a/packages/core/src/agent/types.ts b/packages/core/src/agent/types.ts index aece14d..8db3223 100644 --- a/packages/core/src/agent/types.ts +++ b/packages/core/src/agent/types.ts @@ -174,6 +174,12 @@ export interface AgentExecutionContext { onToolCall?: (toolName: string, params: Record) => void; /** 回调:工具结果 */ onToolResult?: (toolName: string, result: unknown) => void; + /** 会话 ID(用于事件发射) */ + sessionId?: string; + /** 子 Agent 实例 ID(用于事件发射) */ + agentId?: string; + /** 是否发射子 Agent 事件(默认 false) */ + emitEvents?: boolean; } /** diff --git a/packages/core/src/tools/task/task.ts b/packages/core/src/tools/task/task.ts index 1fb6685..4deaef5 100644 --- a/packages/core/src/tools/task/task.ts +++ b/packages/core/src/tools/task/task.ts @@ -1,12 +1,19 @@ import type { ToolWithMetadata } from '../types.js'; import type { AgentConfig } from '../../types/index.js'; import type { ImageData } from '../../agent/types.js'; -import { agentRegistry, AgentExecutor } from '../../agent/index.js'; +import { agentRegistry, AgentExecutor, agentEventEmitter } from '../../agent/index.js'; import { toolRegistry } from '../registry.js'; import { SessionManager } from '../../session/index.js'; import { getAgentManager } from '../../agent/manager.js'; import { loadVisionConfig } from '../../utils/config.js'; +/** + * 生成短 ID(8 字符) + */ +function generateShortId(): string { + return Math.random().toString(36).slice(2, 10); +} + /** * 模型预设映射 */ @@ -231,15 +238,41 @@ export const taskTool: ToolWithMetadata = { `${description} (@${agent.name})` ); + // 生成子 Agent 实例 ID + const agentId = generateShortId(); + const startTime = Date.now(); + + // 发射子 Agent 开始事件 + agentEventEmitter.emit({ + type: 'subagent:start', + sessionId: parentSessionId, + agentId, + agentName: agent.name, + description, + }); + // 创建执行器 const executor = new AgentExecutor(agent, effectiveConfig, toolRegistry); - // 执行任务 + // 执行任务(启用事件发射) const result = await executor.execute(prompt, { parentSessionId, workdir: process.cwd(), images, - onStream: undefined, // 子任务不使用流式输出 + sessionId: parentSessionId, + agentId, + emitEvents: true, // 启用子 Agent 事件发射 + }); + + // 发射子 Agent 结束事件 + agentEventEmitter.emit({ + type: 'subagent:end', + sessionId: parentSessionId, + agentId, + agentName: agent.name, + success: result.success, + duration: Date.now() - startTime, + error: result.error, }); // 保存子会话 @@ -255,6 +288,7 @@ export const taskTool: ToolWithMetadata = { output: result.text, metadata: { agent: agent.name, + agentId, sessionId: childSession.id, steps: result.steps, mode: 'sync', @@ -267,6 +301,7 @@ export const taskTool: ToolWithMetadata = { error: result.error || '子任务执行失败', metadata: { agent: agent.name, + agentId, sessionId: childSession.id, steps: result.steps, mode: 'sync', diff --git a/packages/server/src/agent/adapter.ts b/packages/server/src/agent/adapter.ts index 7910d5d..4a3518c 100644 --- a/packages/server/src/agent/adapter.ts +++ b/packages/server/src/agent/adapter.ts @@ -168,6 +168,25 @@ interface AgentRegistryInterface { isInitialized(): boolean; } +/** + * 子 Agent 事件类型 + */ +interface SubagentEvent { + type: 'subagent:start' | 'subagent:end' | 'subagent:stream' | 'subagent:tool_start' | 'subagent:tool_end'; + sessionId: string; + agentId: string; + [key: string]: unknown; +} + +/** + * Agent Event Emitter 接口 + */ +interface AgentEventEmitterInterface { + on(sessionId: string, listener: (event: SubagentEvent) => void): () => void; + off(sessionId: string, listener: (event: SubagentEvent) => void): void; + clear(sessionId: string): void; +} + /** * Core 模块接口 */ @@ -180,6 +199,7 @@ interface CoreModule { getPermissionManager: (projectRoot?: string) => PermissionManager; getProviderRegistry: () => ProviderRegistryInterface; agentRegistry: AgentRegistryInterface; + agentEventEmitter?: AgentEventEmitterInterface; } // ============================================================================ @@ -446,6 +466,22 @@ export async function processMessage( setSessionAutoApprove(sessionId, null); } + // 订阅子 Agent 事件(如果可用) + let unsubscribeSubagentEvents: (() => void) | null = null; + if (coreModule?.agentEventEmitter) { + unsubscribeSubagentEvents = coreModule.agentEventEmitter.on(sessionId, (event: SubagentEvent) => { + // 检查是否已取消 + if (abortController.signal.aborted) return; + + // 转发子 Agent 事件到前端 + broadcastToSession(sessionId, { + type: event.type, + sessionId, + payload: event, + }); + }); + } + try { // 调用 Agent 的 chat 方法,使用流式回调和 AbortSignal const result = await agent.chat(content, { @@ -554,6 +590,10 @@ export async function processMessage( emitLogEvent(sessionId, 'error', errorMessage); } finally { + // 取消子 Agent 事件订阅 + if (unsubscribeSubagentEvents) { + unsubscribeSubagentEvents(); + } // 清理 AbortController abortControllerCache.delete(sessionId); sessionManager.updateStatus(sessionId, 'idle' as SessionStatus); diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index f8c5f0b..67388f7 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -123,7 +123,13 @@ export interface ServerMessage { | 'error' | 'session_updated' | 'permission_request' - | 'mode_switched'; // 模式切换完成 + | 'mode_switched' // 模式切换完成 + // 子 Agent 事件 + | 'subagent:start' // 子 Agent 开始执行 + | 'subagent:end' // 子 Agent 执行结束 + | 'subagent:stream' // 子 Agent 流式输出 + | 'subagent:tool_start' // 子 Agent 工具调用开始 + | 'subagent:tool_end'; // 子 Agent 工具调用结束 sessionId: string; payload?: unknown; } @@ -144,6 +150,67 @@ export interface ToolEndPayload { duration?: number; } +// ============ 子 Agent 事件 Payload ============ + +/** 子 Agent 开始事件 Payload */ +export interface SubagentStartPayload { + type: 'subagent:start'; + sessionId: string; + agentId: string; + agentName: string; + description: string; + parentToolCallId?: string; +} + +/** 子 Agent 结束事件 Payload */ +export interface SubagentEndPayload { + type: 'subagent:end'; + sessionId: string; + agentId: string; + agentName: string; + success: boolean; + duration: number; + error?: string; +} + +/** 子 Agent 流式输出事件 Payload */ +export interface SubagentStreamPayload { + type: 'subagent:stream'; + sessionId: string; + agentId: string; + content: string; +} + +/** 子 Agent 工具调用开始事件 Payload */ +export interface SubagentToolStartPayload { + type: 'subagent:tool_start'; + sessionId: string; + agentId: string; + toolCallId: string; + toolName: string; + args: Record; +} + +/** 子 Agent 工具调用结束事件 Payload */ +export interface SubagentToolEndPayload { + type: 'subagent:tool_end'; + sessionId: string; + agentId: string; + toolCallId: string; + status: 'completed' | 'error'; + result?: unknown; + error?: string; + duration?: number; +} + +/** 子 Agent 事件 Payload 联合类型 */ +export type SubagentEventPayload = + | SubagentStartPayload + | SubagentEndPayload + | SubagentStreamPayload + | SubagentToolStartPayload + | SubagentToolEndPayload; + // ============ Permission 相关 ============ export type PermissionType = 'bash' | 'file' | 'git' | 'web'; diff --git a/packages/ui/src/api/types.ts b/packages/ui/src/api/types.ts index b2280c0..466e7ed 100644 --- a/packages/ui/src/api/types.ts +++ b/packages/ui/src/api/types.ts @@ -912,3 +912,95 @@ export interface ToolEndPayload { duration?: number; } +// ============ 子 Agent 事件 Payload ============ + +/** 子 Agent 开始事件 Payload */ +export interface SubagentStartPayload { + type: 'subagent:start'; + sessionId: string; + agentId: string; + agentName: string; + description: string; + parentToolCallId?: string; +} + +/** 子 Agent 结束事件 Payload */ +export interface SubagentEndPayload { + type: 'subagent:end'; + sessionId: string; + agentId: string; + agentName: string; + success: boolean; + duration: number; + error?: string; +} + +/** 子 Agent 流式输出事件 Payload */ +export interface SubagentStreamPayload { + type: 'subagent:stream'; + sessionId: string; + agentId: string; + content: string; +} + +/** 子 Agent 工具调用开始事件 Payload */ +export interface SubagentToolStartPayload { + type: 'subagent:tool_start'; + sessionId: string; + agentId: string; + toolCallId: string; + toolName: string; + args: Record; +} + +/** 子 Agent 工具调用结束事件 Payload */ +export interface SubagentToolEndPayload { + type: 'subagent:tool_end'; + sessionId: string; + agentId: string; + toolCallId: string; + status: 'completed' | 'error'; + result?: unknown; + error?: string; + duration?: number; +} + +/** 子 Agent 事件 Payload 联合类型 */ +export type SubagentEventPayload = + | SubagentStartPayload + | SubagentEndPayload + | SubagentStreamPayload + | SubagentToolStartPayload + | SubagentToolEndPayload; + +/** 子 Agent 工具调用信息(用于 UI 展示) */ +export interface SubagentToolInfo { + id: string; + toolName: string; + status: ToolCallStatus; + args: Record; + result?: unknown; + error?: string; + duration?: number; +} + +/** 子 Agent 状态(用于 UI 展示) */ +export interface SubagentState { + /** 子 Agent 实例 ID */ + id: string; + /** Agent 类型名称(如 guide, explore) */ + name: string; + /** 任务描述 */ + description: string; + /** 执行状态 */ + status: 'running' | 'completed' | 'error'; + /** 嵌套的工具调用列表 */ + tools: SubagentToolInfo[]; + /** 流式输出内容 */ + streamContent: string; + /** 执行时长 (ms) */ + duration?: number; + /** 错误信息 */ + error?: string; +} + diff --git a/packages/ui/src/components/SubagentProgress.tsx b/packages/ui/src/components/SubagentProgress.tsx new file mode 100644 index 0000000..ef3bf9c --- /dev/null +++ b/packages/ui/src/components/SubagentProgress.tsx @@ -0,0 +1,243 @@ +/** + * Subagent Progress Component + * + * 展示子 Agent 执行进度的可折叠组件 + */ + +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Bot, + ChevronDown, + ChevronRight, + Wrench, + Clock, + Loader2, + CheckCircle2, + AlertCircle, +} from 'lucide-react'; +import { cn } from '../utils/cn'; +import { getAgentDisplayName } from '../utils/agent'; +import type { SubagentState, SubagentToolInfo } from '../api/types'; + +interface SubagentProgressProps { + subagent: SubagentState; +} + +/** + * 子 Agent 进度显示组件 + */ +export function SubagentProgress({ subagent }: SubagentProgressProps) { + const [expanded, setExpanded] = useState(true); + const isComplete = subagent.status === 'completed' || subagent.status === 'error'; + + return ( + + {/* 头部:Agent 名称、描述、状态 */} + + + {/* 展开的内容 */} + + {expanded && ( + +
+ {/* 工具调用列表 */} + {subagent.tools.length > 0 && ( +
+ {subagent.tools.map((tool) => ( + + ))} +
+ )} + + {/* 流式文本输出(如果有) */} + {subagent.streamContent && ( +
+
{subagent.streamContent}
+ {!isComplete && ( + + )} +
+ )} + + {/* 错误信息 */} + {subagent.error && ( +
+ Error: {subagent.error} +
+ )} + + {/* 如果没有工具调用和流式内容,显示等待状态 */} + {subagent.tools.length === 0 && !subagent.streamContent && !isComplete && ( +
+ + Processing... +
+ )} +
+
+ )} +
+
+ ); +} + +/** + * 折叠版子 Agent 进度(用于完成后的简洁展示) + */ +export function SubagentProgressCompact({ subagent }: SubagentProgressProps) { + const toolCount = subagent.tools.length; + const completedCount = subagent.tools.filter( + (t) => t.status === 'completed' || t.status === 'error' + ).length; + + return ( +
+ + {getAgentDisplayName(subagent.name)} + {toolCount > 0 && ( + + ({completedCount}/{toolCount} tools) + + )} + {subagent.duration && {formatDuration(subagent.duration)}} + {getSubagentStatusIcon(subagent.status, 12)} +
+ ); +} + +/** + * 子 Agent 工具调用项 + */ +interface SubagentToolItemProps { + tool: SubagentToolInfo; +} + +function SubagentToolItem({ tool }: SubagentToolItemProps) { + const [expanded, setExpanded] = useState(false); + const hasDetails = tool.result !== undefined || tool.error !== undefined; + + return ( +
+ + + {/* 展开的详情 */} + + {expanded && hasDetails && ( + +
+ {tool.result !== undefined && ( +
+                  {typeof tool.result === 'string'
+                    ? tool.result.slice(0, 500) + (tool.result.length > 500 ? '...' : '')
+                    : JSON.stringify(tool.result, null, 2).slice(0, 500)}
+                
+ )} + {tool.error && ( +
+                  {tool.error}
+                
+ )} +
+
+ )} +
+
+ ); +} + +/** + * 获取子 Agent 状态图标 + */ +function getSubagentStatusIcon(status: SubagentState['status'], size: number = 14) { + switch (status) { + case 'running': + return ; + case 'completed': + return ; + case 'error': + return ; + } +} + +/** + * 获取工具状态图标 + */ +function getToolStatusIcon(status: SubagentToolInfo['status'], size: number = 14) { + switch (status) { + case 'pending': + return ; + case 'running': + return ; + case 'completed': + return ; + case 'error': + return ; + } +} + +/** + * 格式化执行时长 + */ +function formatDuration(ms?: number): string { + if (!ms) return ''; + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} diff --git a/packages/ui/src/hooks/useChat.ts b/packages/ui/src/hooks/useChat.ts index e773e63..3565789 100644 --- a/packages/ui/src/hooks/useChat.ts +++ b/packages/ui/src/hooks/useChat.ts @@ -14,6 +14,13 @@ import type { MessagePart, ToolMessagePart, AgentModeType, + SubagentStartPayload, + SubagentEndPayload, + SubagentStreamPayload, + SubagentToolStartPayload, + SubagentToolEndPayload, + SubagentState, + SubagentToolInfo, } from '../api/types.js'; interface UseChatOptions { @@ -38,6 +45,8 @@ interface ChatState { autoApprove: boolean; /** 当前正在执行的 Agent 名称 */ currentAgent: string; + /** 当前正在执行的子 Agent 状态 */ + currentSubagent: SubagentState | null; } export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated, onConfigError }: UseChatOptions) { @@ -50,6 +59,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate agentMode: 'build', autoApprove: false, currentAgent: 'build', + currentSubagent: null, }); const wsRef = useRef(null); @@ -334,6 +344,122 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate })); } break; + + // ============ 子 Agent 事件处理 ============ + + case 'subagent:start': { + const payload = message.payload as SubagentStartPayload; + setState((prev) => ({ + ...prev, + currentAgent: payload.agentName, + currentSubagent: { + id: payload.agentId, + name: payload.agentName, + description: payload.description, + status: 'running', + tools: [], + streamContent: '', + }, + })); + break; + } + + case 'subagent:tool_start': { + const payload = message.payload as SubagentToolStartPayload; + setState((prev) => { + if (!prev.currentSubagent || prev.currentSubagent.id !== payload.agentId) { + return prev; + } + const newTool: SubagentToolInfo = { + id: payload.toolCallId, + toolName: payload.toolName, + status: 'running', + args: payload.args, + }; + return { + ...prev, + currentSubagent: { + ...prev.currentSubagent, + tools: [...prev.currentSubagent.tools, newTool], + }, + }; + }); + break; + } + + case 'subagent:stream': { + const payload = message.payload as SubagentStreamPayload; + setState((prev) => { + if (!prev.currentSubagent || prev.currentSubagent.id !== payload.agentId) { + return prev; + } + return { + ...prev, + currentSubagent: { + ...prev.currentSubagent, + streamContent: prev.currentSubagent.streamContent + payload.content, + }, + }; + }); + break; + } + + case 'subagent:tool_end': { + const payload = message.payload as SubagentToolEndPayload; + setState((prev) => { + if (!prev.currentSubagent || prev.currentSubagent.id !== payload.agentId) { + return prev; + } + const updatedTools = prev.currentSubagent.tools.map((tool) => { + if (tool.id === payload.toolCallId) { + return { + ...tool, + status: payload.status === 'completed' ? 'completed' : 'error', + result: payload.result, + error: payload.error, + duration: payload.duration, + } as SubagentToolInfo; + } + return tool; + }); + return { + ...prev, + currentSubagent: { + ...prev.currentSubagent, + tools: updatedTools, + }, + }; + }); + break; + } + + case 'subagent:end': { + const payload = message.payload as SubagentEndPayload; + setState((prev) => { + // 只有当 agentId 匹配时才处理 + if (!prev.currentSubagent || prev.currentSubagent.id !== payload.agentId) { + return prev; + } + return { + ...prev, + currentAgent: prev.agentMode, // 恢复为主 Agent + currentSubagent: { + ...prev.currentSubagent, + status: payload.success ? 'completed' : 'error', + duration: payload.duration, + error: payload.error, + }, + }; + }); + // 完成后短暂延迟再清除,让 UI 能显示最终状态 + setTimeout(() => { + setState((prev) => ({ + ...prev, + currentSubagent: null, + })); + }, 1000); + break; + } } } catch { // 忽略解析错误 @@ -476,6 +602,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate agentMode: 'build', autoApprove: false, currentAgent: 'build', + currentSubagent: null, }); reconnectAttemptsRef.current = 0; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 8ecbb20..b3828fc 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -221,6 +221,7 @@ export { Toaster } from './components/Toaster.js'; export { Skeleton, MessageSkeleton, SessionSkeleton, FileSkeleton } from './components/Skeleton.js'; export { Markdown } from './components/Markdown.js'; export { CodeBlock, InlineCode } from './components/CodeBlock.js'; +export { SubagentProgress, SubagentProgressCompact } from './components/SubagentProgress.js'; // Toast function (re-export from sonner) export { toast } from 'sonner'; diff --git a/packages/web/src/pages/Chat.tsx b/packages/web/src/pages/Chat.tsx index 3e1621d..e061fd0 100644 --- a/packages/web/src/pages/Chat.tsx +++ b/packages/web/src/pages/Chat.tsx @@ -13,6 +13,7 @@ import { ChatInput, PermissionDialog, ContextUsage, + SubagentProgress, } from '@ai-assistant/ui'; interface ChatPageProps { @@ -63,6 +64,7 @@ export function ChatPage({ setAgentMode, setAutoApprove, currentAgent, + currentSubagent, } = useChat({ sessionId, onError: (error) => { @@ -298,7 +300,12 @@ export function ChatPage({ )} - {isLoading && !streamingMessage && } + {/* 子 Agent 进度显示 */} + {currentSubagent && ( + + )} + + {isLoading && !streamingMessage && !currentSubagent && }