Skip to content

Commit a3254c3

Browse files
author
echoVic
committed
feat(subagent): 添加子任务执行进度显示功能
添加子任务执行进度状态管理及UI显示组件,包括: 1. 在SubagentContext中添加onToolStart回调 2. 新增SubagentProgress类型和状态管理 3. 实现进度显示组件SubagentProgress 4. 在task工具中集成进度显示逻辑 5. 优化错误信息展示
1 parent f99a563 commit a3254c3

7 files changed

Lines changed: 266 additions & 23 deletions

File tree

src/agent/subagents/SubagentExecutor.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,26 @@ export class SubagentExecutor {
3434
let toolCallCount = 0;
3535
let tokensUsed = 0;
3636

37-
const loopResult = await agent.runAgenticLoop(context.prompt, {
38-
messages: [],
39-
userId: 'subagent',
40-
sessionId: context.parentSessionId || `subagent_${Date.now()}`,
41-
workspaceRoot: process.cwd(),
42-
permissionMode: context.permissionMode, // 继承父 Agent 的权限模式
43-
systemPrompt, // 🆕 无状态设计:通过 context 传入 systemPrompt
44-
});
37+
const loopResult = await agent.runAgenticLoop(
38+
context.prompt,
39+
{
40+
messages: [],
41+
userId: 'subagent',
42+
sessionId: context.parentSessionId || `subagent_${Date.now()}`,
43+
workspaceRoot: process.cwd(),
44+
permissionMode: context.permissionMode, // 继承父 Agent 的权限模式
45+
systemPrompt, // 🆕 无状态设计:通过 context 传入 systemPrompt
46+
},
47+
{
48+
onToolStart: context.onToolStart
49+
? (toolCall) => {
50+
const name =
51+
'function' in toolCall ? toolCall.function.name : 'unknown';
52+
context.onToolStart!(name);
53+
}
54+
: undefined,
55+
}
56+
);
4557

4658
if (loopResult.success) {
4759
finalMessage = loopResult.finalMessage || '';

src/agent/subagents/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export interface SubagentContext {
120120

121121
/** 父 Agent 的权限模式(继承给子 Agent) */
122122
permissionMode?: PermissionMode;
123+
124+
/** 工具执行开始回调(用于 UI 进度显示) */
125+
onToolStart?: (toolName: string) => void;
123126
}
124127

125128
/**

src/store/slices/appSlice.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const initialAppState: AppState = {
3333
todos: [],
3434
awaitingSecondCtrlC: false,
3535
thinkingModeEnabled: false, // Thinking 模式默认关闭
36+
subagentProgress: null, // 当前无 subagent 执行
3637
};
3738

3839
/**
@@ -158,5 +159,67 @@ export const createAppSlice: StateCreator<BladeStore, [], [], AppSlice> = (set)
158159
app: { ...state.app, thinkingModeEnabled: !state.app.thinkingModeEnabled },
159160
}));
160161
},
162+
163+
// ==================== Subagent 进度相关 actions ====================
164+
165+
/**
166+
* 开始 subagent 执行进度
167+
*/
168+
startSubagentProgress: (id: string, type: string, description: string) => {
169+
set((state) => ({
170+
app: {
171+
...state.app,
172+
subagentProgress: {
173+
id,
174+
type,
175+
description,
176+
status: 'running',
177+
startTime: Date.now(),
178+
},
179+
},
180+
}));
181+
},
182+
183+
/**
184+
* 更新当前执行的工具名称
185+
*/
186+
updateSubagentTool: (toolName: string) => {
187+
set((state) => {
188+
if (!state.app.subagentProgress) return state;
189+
return {
190+
app: {
191+
...state.app,
192+
subagentProgress: {
193+
...state.app.subagentProgress,
194+
currentTool: toolName,
195+
},
196+
},
197+
};
198+
});
199+
},
200+
201+
/**
202+
* 完成 subagent 执行
203+
*/
204+
completeSubagentProgress: (success: boolean) => {
205+
set((state) => {
206+
if (!state.app.subagentProgress) return state;
207+
return {
208+
app: {
209+
...state.app,
210+
subagentProgress: {
211+
...state.app.subagentProgress,
212+
status: success ? 'completed' : 'failed',
213+
currentTool: undefined,
214+
},
215+
},
216+
};
217+
});
218+
setTimeout(() => {
219+
set((state) => ({
220+
app: { ...state.app, subagentProgress: null },
221+
}));
222+
}, 1500);
223+
},
161224
},
162225
});

src/store/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,18 @@ export type ActiveModal =
187187
| 'hooksManager'
188188
| 'pluginsManager';
189189

190+
/**
191+
* Subagent 进度状态
192+
*/
193+
export interface SubagentProgress {
194+
id: string;
195+
type: string;
196+
description: string;
197+
status: 'running' | 'completed' | 'failed';
198+
currentTool?: string;
199+
startTime: number;
200+
}
201+
190202
/**
191203
* 应用状态(纯 UI 状态)
192204
*/
@@ -199,6 +211,7 @@ export interface AppState {
199211
todos: TodoItem[];
200212
awaitingSecondCtrlC: boolean; // 是否等待第二次 Ctrl+C 退出
201213
thinkingModeEnabled: boolean; // Thinking 模式是否启用(Tab 切换)
214+
subagentProgress: SubagentProgress | null; // 当前 subagent 执行进度
202215
}
203216

204217
/**
@@ -217,6 +230,10 @@ export interface AppActions {
217230
// Thinking 模式相关
218231
setThinkingModeEnabled: (enabled: boolean) => void;
219232
toggleThinkingMode: () => void;
233+
// Subagent 进度相关
234+
startSubagentProgress: (id: string, type: string, description: string) => void;
235+
updateSubagentTool: (toolName: string) => void;
236+
completeSubagentProgress: (success: boolean) => void;
220237
}
221238

222239
/**

src/tools/builtin/task/task.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* 6. 会话恢复 - 支持 resume 参数
1010
*/
1111

12+
import { nanoid } from 'nanoid';
1213
import { z } from 'zod';
1314
import { BackgroundAgentManager } from '../../../agent/subagents/BackgroundAgentManager.js';
1415
import { SubagentExecutor } from '../../../agent/subagents/SubagentExecutor.js';
@@ -19,10 +20,48 @@ import type {
1920
} from '../../../agent/subagents/types.js';
2021
import { PermissionMode } from '../../../config/types.js';
2122
import { HookManager } from '../../../hooks/HookManager.js';
23+
import { vanillaStore } from '../../../store/vanilla.js';
2224
import { createTool } from '../../core/createTool.js';
2325
import type { ExecutionContext, ToolResult } from '../../types/index.js';
2426
import { ToolErrorType, ToolKind } from '../../types/index.js';
2527

28+
/**
29+
* 从错误中提取用户友好的错误信息
30+
*/
31+
function extractUserFriendlyError(error: Error): string {
32+
const message = error.message || 'Unknown error';
33+
34+
// 检查是否是 API 限流错误
35+
if (message.includes('Too Many Requests') || message.includes('429')) {
36+
// 尝试从错误链中提取更详细的信息
37+
const cause = (error as { cause?: { responseBody?: string } }).cause;
38+
if (cause?.responseBody) {
39+
try {
40+
const body = JSON.parse(cause.responseBody);
41+
if (body.message) {
42+
return body.message;
43+
}
44+
} catch {
45+
// 忽略解析错误
46+
}
47+
}
48+
return 'API 请求过于频繁,请稍后重试';
49+
}
50+
51+
// 检查是否是网络错误
52+
if (message.includes('ECONNREFUSED') || message.includes('ETIMEDOUT')) {
53+
return '网络连接失败,请检查网络设置';
54+
}
55+
56+
// 检查是否是认证错误
57+
if (message.includes('401') || message.includes('Unauthorized')) {
58+
return 'API 认证失败,请检查 API Key 配置';
59+
}
60+
61+
// 返回简化的错误信息(不包含堆栈)
62+
return message.split('\n')[0];
63+
}
64+
2665
/**
2766
* 获取可用的 subagent 类型(用于 Zod 枚举)
2867
*/
@@ -191,11 +230,20 @@ export const taskTool = createTool({
191230
// 创建执行器
192231
const executor = new SubagentExecutor(subagentConfig);
193232

233+
// 生成唯一 ID 并启动进度显示
234+
const subagentId = nanoid(8);
235+
vanillaStore
236+
.getState()
237+
.app.actions.startSubagentProgress(subagentId, subagent_type, description);
238+
194239
// 构建执行上下文
195240
const subagentContext: SubagentContext = {
196241
prompt,
197242
parentSessionId: context.sessionId,
198243
permissionMode: context.permissionMode, // 继承父 Agent 的权限模式
244+
onToolStart: (toolName) => {
245+
vanillaStore.getState().app.actions.updateSubagentTool(toolName);
246+
},
199247
};
200248

201249
updateOutput?.(`⚙️ 执行任务中...`);
@@ -247,7 +295,10 @@ export const taskTool = createTool({
247295
console.warn('[Task] SubagentStop hook execution failed:', hookError);
248296
}
249297

250-
// 6. 返回结果
298+
// 6. 完成进度显示
299+
vanillaStore.getState().app.actions.completeSubagentProgress(result.success);
300+
301+
// 7. 返回结果
251302
if (result.success) {
252303
const outputPreview =
253304
result.message.length > 1000
@@ -289,11 +340,16 @@ export const taskTool = createTool({
289340
};
290341
}
291342
} catch (error) {
343+
// 异常时也要完成进度显示
344+
vanillaStore.getState().app.actions.completeSubagentProgress(false);
345+
292346
const err = error as Error;
347+
const errorMessage = extractUserFriendlyError(err);
348+
293349
return {
294350
success: false,
295351
llmContent: `Subagent execution error: ${err.message}`,
296-
displayContent: `❌ Subagent 执行异常\n\n${err.message}\n\n${err.stack || ''}`,
352+
displayContent: `❌ Subagent 执行异常\n\n${errorMessage}`,
297353
error: {
298354
type: ToolErrorType.EXECUTION_ERROR,
299355
message: err.message,

src/ui/components/BladeInterface.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@ import { useMemoizedFn } from 'ahooks';
22
import { Box, useApp } from 'ink';
33
import React, { useEffect, useRef } from 'react';
44
import {
5-
type ModelConfig,
6-
PermissionMode,
7-
type SetupConfig,
5+
type ModelConfig,
6+
PermissionMode,
7+
type SetupConfig,
88
} from '../../config/types.js';
99
import { createLogger, LogCategory } from '../../logging/Logger.js';
1010
import { safeExit } from '../../services/GracefulShutdown.js';
1111
import { SessionService } from '../../services/SessionService.js';
1212
import { SpecManager } from '../../spec/SpecManager.js';
1313
import {
14-
useActiveModal,
15-
useAppActions,
16-
useFocusActions,
17-
useInitializationError,
18-
useInitializationStatus,
19-
useIsProcessing,
20-
useModelEditorTarget,
21-
usePermissionMode,
22-
useSessionActions,
23-
useSessionSelectorData,
14+
useActiveModal,
15+
useAppActions,
16+
useFocusActions,
17+
useInitializationError,
18+
useInitializationStatus,
19+
useIsProcessing,
20+
useModelEditorTarget,
21+
usePermissionMode,
22+
useSessionActions,
23+
useSessionSelectorData,
2424
} from '../../store/selectors/index.js';
2525
import { FocusId } from '../../store/types.js';
2626
import { configActions, getMessages } from '../../store/vanilla.js';
@@ -49,6 +49,7 @@ import { QuestionPrompt } from './QuestionPrompt.js';
4949
import { SessionSelector } from './SessionSelector.js';
5050
import { SkillsManager } from './SkillsManager.js';
5151
import { SpecStatusPanel } from './SpecStatusPanel.js';
52+
import { SubagentProgress } from './SubagentProgress.js';
5253
import { ThemeSelector } from './ThemeSelector.js';
5354

5455
// 创建 BladeInterface 专用 Logger
@@ -640,6 +641,9 @@ export const BladeInterface: React.FC<BladeInterfaceProps> = ({
640641
{/* MessageArea 内部直接获取状态,不需要 props */}
641642
<MessageArea />
642643

644+
{/* Subagent 进度指示器 - 显示在加载指示器上方 */}
645+
<SubagentProgress />
646+
643647
{/* 加载指示器 - 当有阻塞弹窗时暂停动画,避免无意义的重渲染 */}
644648
<LoadingIndicator paused={hasBlockingModal} />
645649

0 commit comments

Comments
 (0)