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
+127
View File
@@ -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<WebSocket | null>(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;