feat(ui): 添加子 Agent 进度显示功能

当 build agent 调用 guide/explore 等子 agent 时,
用户可以在 web 页面实时看到子 agent 的执行进度

实现方案:
- Core: 使用 EventEmitter 模式发射子 agent 事件
- Server: 订阅事件并转发到 WebSocket
- UI: 处理事件并渲染 SubagentProgress 组件

新增文件:
- packages/core/src/agent/events.ts
- packages/ui/src/components/SubagentProgress.tsx

修改文件:
- core: executor.ts, manager.ts, types.ts, task.ts
- server: adapter.ts, types.ts
- ui: useChat.ts, types.ts
- web: Chat.tsx
This commit is contained in:
2025-12-16 19:38:36 +08:00
parent f0ff887129
commit 08d481483c
13 changed files with 921 additions and 19 deletions
+40
View File
@@ -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);
+68 -1
View File
@@ -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<string, unknown>;
}
/** 子 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';