@steerable/agent-shell-web 0.6.27 → 0.6.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/AgentSidebar.tsx +27 -27
- package/src/components/BrandLockup.tsx +20 -4
- package/src/components/ChatHeader.tsx +5 -15
- package/src/components/LocalLlmSettingsModal.tsx +5 -5
- package/src/components/TerminalPanel.tsx +1 -1
- package/src/components/chat/ApprovalModal.tsx +5 -5
- package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
- package/src/components/chat/AssistantMessage.tsx +122 -36
- package/src/components/chat/ChatInput.tsx +34 -26
- package/src/components/chat/ExecPolicyPicker.tsx +4 -4
- package/src/components/chat/ExecutedActionsCard.tsx +398 -29
- package/src/components/chat/FilePathCode.tsx +2 -1
- package/src/components/chat/LocalChatPanel.tsx +42 -11
- package/src/components/chat/Markdown.tsx +15 -14
- package/src/components/chat/MessageList.tsx +23 -5
- package/src/components/chat/ModelPicker.tsx +6 -6
- package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
- package/src/components/chat/SessionTodoList.tsx +234 -0
- package/src/components/chat/SessionTreeModal.tsx +3 -3
- package/src/components/chat/SuggestedReplies.tsx +14 -5
- package/src/components/chat/TaskPanelModal.tsx +3 -3
- package/src/components/chat/TaskProcessPanel.tsx +67 -21
- package/src/components/chat/TurnFilesCard.tsx +2 -2
- package/src/components/chat/TurnProcessGroup.tsx +252 -99
- package/src/components/chat/UserMessage.tsx +7 -7
- package/src/components/chat/executed-actions-model.ts +469 -0
- package/src/components/chat/next-steps-tags.ts +28 -0
- package/src/components/chat/orchestration-children-model.ts +120 -0
- package/src/components/chat/process-status.ts +125 -53
- package/src/components/chat/todo-list-model.ts +167 -0
- package/src/components/chat/turn-timeline.ts +123 -11
- package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
- package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
- package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
- package/src/components/settings/LlmSettingsPanel.tsx +10 -10
- package/src/components/settings/McpSettingsPanel.tsx +3 -3
- package/src/components/settings/ModelIdCombobox.tsx +2 -2
- package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
- package/src/components/settings/SettingsSaveButton.tsx +1 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
- package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
- package/src/components/settings/UsagePanel.tsx +14 -10
- package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
- package/src/layouts/AgentLayout.tsx +3 -3
- package/src/lib/chat-transport.ts +68 -12
- package/src/lib/local-api.ts +11 -0
- package/src/lib/show-thinking-content.ts +4 -4
- package/src/pages/AgentPage.tsx +82 -11
- package/src/pages/ChatPanelPreviewPage.tsx +26 -4
- package/src/pages/SettingsPage.tsx +39 -39
- package/src/styles/index.css +12 -2
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { LuCircleCheck, LuCircleX } from 'react-icons/lu';
|
|
2
|
+
import { LuCircleCheck, LuCircleX, LuListTodo } from 'react-icons/lu';
|
|
3
3
|
import { ToolExecutionCard } from '@steerable/agent-ui/cards';
|
|
4
4
|
import type { ToolExecutionPayload } from '@steerable/agent-protocol';
|
|
5
|
+
import type { LocalChatAgent } from '@/lib/local-api';
|
|
5
6
|
import { UnifiedDiff } from './UnifiedDiff';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
summarizeRunCodeAction,
|
|
9
|
+
summarizeWebAction,
|
|
10
|
+
expandRunCodeActions,
|
|
11
|
+
parseDelegateSubagent,
|
|
12
|
+
summarizeDelegateSubagent,
|
|
13
|
+
delegateSubagentDisplayName,
|
|
14
|
+
parseTaskRun,
|
|
15
|
+
parseTaskToolOutput,
|
|
16
|
+
parseToolEnvelope,
|
|
17
|
+
humanizeDelegateError,
|
|
18
|
+
summarizeTaskAction,
|
|
19
|
+
taskToolLabel,
|
|
20
|
+
isTaskFamilyTool,
|
|
21
|
+
inspectableTaskFromAction,
|
|
22
|
+
shortTaskId,
|
|
23
|
+
type InspectTaskInput,
|
|
24
|
+
} from './executed-actions-model';
|
|
25
|
+
import {
|
|
26
|
+
findAgentForProfile,
|
|
27
|
+
matchChildForDelegation,
|
|
28
|
+
type ChildInfo,
|
|
29
|
+
} from './orchestration-children-model';
|
|
7
30
|
|
|
8
31
|
/**
|
|
9
32
|
* ExecutedActionsCard — visual surface for the `executed_actions` SSE event
|
|
@@ -42,15 +65,26 @@ interface ExecutedActionsCardProps {
|
|
|
42
65
|
actions: ExecutedAction[];
|
|
43
66
|
/** Drop the outer card chrome when nested in a turn-process group. */
|
|
44
67
|
compact?: boolean;
|
|
68
|
+
/** Show args/output instead of a one-line summary. Defaults to collapsed. */
|
|
69
|
+
defaultExpanded?: boolean;
|
|
70
|
+
/** Used to map `delegate_subagent` 画像名 to the agent's display name + color. */
|
|
71
|
+
agents?: ReadonlyArray<Pick<LocalChatAgent, 'id' | 'slug' | 'name' | 'color'>>;
|
|
72
|
+
chatId?: string | null;
|
|
73
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
74
|
+
/**
|
|
75
|
+
* 本回合的子代理生命周期。委派行据此找到子代理自己的 record,点一下就能
|
|
76
|
+
* 在右侧看它的推理过程。
|
|
77
|
+
*/
|
|
78
|
+
orchestrationChildren?: ChildInfo[];
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
function deriveStatus(action: ExecutedAction): ToolExecutionPayload['status'] {
|
|
48
82
|
const result = action.result;
|
|
49
83
|
if (result === undefined || result === null) return 'running';
|
|
84
|
+
const env = parseToolEnvelope(result);
|
|
85
|
+
if (env.success === false || env.error) return 'failed';
|
|
86
|
+
if (env.success === true) return 'succeeded';
|
|
50
87
|
if (typeof result !== 'object') return 'succeeded';
|
|
51
|
-
if ('success' in result) {
|
|
52
|
-
return (result as { success?: unknown }).success ? 'succeeded' : 'failed';
|
|
53
|
-
}
|
|
54
88
|
if ('error' in result) return 'failed';
|
|
55
89
|
return 'succeeded';
|
|
56
90
|
}
|
|
@@ -83,15 +117,22 @@ function sandboxBadge(sandbox: ExecutedAction['sandbox']): string | null {
|
|
|
83
117
|
|
|
84
118
|
function actionToTool(action: ExecutedAction, idx: number): ToolExecutionPayload {
|
|
85
119
|
const status = deriveStatus(action);
|
|
120
|
+
const env = parseToolEnvelope(action.result);
|
|
86
121
|
const errorText =
|
|
87
|
-
status === 'failed'
|
|
88
|
-
?
|
|
89
|
-
? (
|
|
90
|
-
:
|
|
122
|
+
status === 'failed'
|
|
123
|
+
? action.tool === 'delegate_subagent'
|
|
124
|
+
? humanizeDelegateError(env.error)
|
|
125
|
+
: env.error
|
|
91
126
|
: null;
|
|
92
127
|
// W5-2: web_search/web_fetch 用结构化中文摘要(查询词/URL + 结果计数/
|
|
93
128
|
// 状态码),其余工具走通用参数摘要。
|
|
94
129
|
const summary =
|
|
130
|
+
(action.tool === 'delegate_subagent'
|
|
131
|
+
? summarizeDelegateSubagent(action.arguments)
|
|
132
|
+
: null) ??
|
|
133
|
+
(isTaskFamilyTool(action.tool)
|
|
134
|
+
? summarizeTaskAction(action.tool, action.arguments, action.result)
|
|
135
|
+
: null) ??
|
|
95
136
|
summarizeRunCodeAction(action.tool, action.arguments, action.result) ??
|
|
96
137
|
summarizeWebAction(action.tool, action.arguments, action.result) ??
|
|
97
138
|
summarizeArguments(action.arguments);
|
|
@@ -144,10 +185,312 @@ function renderActionOutput(output: unknown): ReactNode {
|
|
|
144
185
|
);
|
|
145
186
|
}
|
|
146
187
|
|
|
188
|
+
const BUILTIN_DELEGATE_COLORS: Record<string, string> = {
|
|
189
|
+
explore: '#0ea5e9',
|
|
190
|
+
research: '#2563eb',
|
|
191
|
+
coder: '#16a34a',
|
|
192
|
+
'general-purpose': '#7c3aed',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
function delegateColor(
|
|
196
|
+
profile: string | null,
|
|
197
|
+
agents: ReadonlyArray<Pick<LocalChatAgent, 'id' | 'slug' | 'name' | 'color'>>,
|
|
198
|
+
): string {
|
|
199
|
+
if (profile) {
|
|
200
|
+
const agent = findAgentForProfile(profile, agents);
|
|
201
|
+
if (agent?.color) return agent.color;
|
|
202
|
+
if (BUILTIN_DELEGATE_COLORS[profile]) return BUILTIN_DELEGATE_COLORS[profile];
|
|
203
|
+
}
|
|
204
|
+
return '#7c3aed';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function DelegateLead({
|
|
208
|
+
name,
|
|
209
|
+
color,
|
|
210
|
+
}: {
|
|
211
|
+
name: string;
|
|
212
|
+
color: string;
|
|
213
|
+
}) {
|
|
214
|
+
return (
|
|
215
|
+
<span
|
|
216
|
+
data-agent-color-dot=""
|
|
217
|
+
className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full text-[8px] font-semibold text-white"
|
|
218
|
+
style={{ backgroundColor: color }}
|
|
219
|
+
aria-hidden
|
|
220
|
+
>
|
|
221
|
+
{name.trim()[0]?.toUpperCase() ?? 'A'}
|
|
222
|
+
</span>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderDelegateArgs(
|
|
227
|
+
args: unknown,
|
|
228
|
+
agents: ReadonlyArray<Pick<LocalChatAgent, 'id' | 'slug' | 'name' | 'color'>>,
|
|
229
|
+
): ReactNode {
|
|
230
|
+
const { profile, task } = parseDelegateSubagent(args);
|
|
231
|
+
if (!profile && !task) {
|
|
232
|
+
return (
|
|
233
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5 text-[11px] text-agent-foreground">
|
|
234
|
+
{typeof args === 'string' ? args : JSON.stringify(args, null, 2)}
|
|
235
|
+
</pre>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
// 自建智能体没有 slug 时画像名是 `agent-<id 片段>`,对用户无意义——出中文名。
|
|
239
|
+
const displayName = delegateSubagentDisplayName(profile, agents);
|
|
240
|
+
return (
|
|
241
|
+
<div className="space-y-1.5 text-[11px] text-agent-foreground">
|
|
242
|
+
{profile ? (
|
|
243
|
+
<section>
|
|
244
|
+
<div className="mb-0.5 text-[10px] uppercase tracking-wider text-agent-muted-foreground">
|
|
245
|
+
子代理
|
|
246
|
+
</div>
|
|
247
|
+
<div className="text-[11px]">{displayName}</div>
|
|
248
|
+
</section>
|
|
249
|
+
) : null}
|
|
250
|
+
{task ? (
|
|
251
|
+
<section>
|
|
252
|
+
<div className="mb-0.5 text-[10px] uppercase tracking-wider text-agent-muted-foreground">
|
|
253
|
+
任务
|
|
254
|
+
</div>
|
|
255
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
|
|
256
|
+
{task}
|
|
257
|
+
</pre>
|
|
258
|
+
</section>
|
|
259
|
+
) : null}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderDelegateOutput(output: unknown): ReactNode {
|
|
265
|
+
const env = parseToolEnvelope(output);
|
|
266
|
+
if (env.message) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="space-y-1.5">
|
|
269
|
+
{kvBlock(
|
|
270
|
+
'回报',
|
|
271
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
|
|
272
|
+
{env.message}
|
|
273
|
+
</pre>,
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (env.error || env.success === false) {
|
|
279
|
+
return <div className="text-[11px] text-agent-muted-foreground">没有文字回报</div>;
|
|
280
|
+
}
|
|
281
|
+
return renderActionOutput(output);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function TaskLead() {
|
|
285
|
+
return (
|
|
286
|
+
<span
|
|
287
|
+
data-task-lead=""
|
|
288
|
+
className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded bg-amber-100/80 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300"
|
|
289
|
+
aria-hidden
|
|
290
|
+
>
|
|
291
|
+
<LuListTodo className="h-2.5 w-2.5" />
|
|
292
|
+
</span>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const TASK_STATUS_ZH: Record<string, string> = {
|
|
297
|
+
running: '已启动',
|
|
298
|
+
blocked: '等待依赖',
|
|
299
|
+
completed: '已完成',
|
|
300
|
+
failed: '失败',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
function kvBlock(label: string, value: ReactNode): ReactNode {
|
|
304
|
+
return (
|
|
305
|
+
<section>
|
|
306
|
+
<div className="mb-0.5 text-[10px] uppercase tracking-wider text-agent-muted-foreground">
|
|
307
|
+
{label}
|
|
308
|
+
</div>
|
|
309
|
+
<div className="text-[11px] text-agent-foreground">{value}</div>
|
|
310
|
+
</section>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderTaskArgs(tool: string, args: unknown): ReactNode {
|
|
315
|
+
if (tool === 'task_run') {
|
|
316
|
+
const parsed = parseTaskRun(args);
|
|
317
|
+
if (!parsed.task && parsed.dependsOn.length === 0 && !parsed.worktree) {
|
|
318
|
+
return renderActionOutput(args);
|
|
319
|
+
}
|
|
320
|
+
return (
|
|
321
|
+
<div className="space-y-1.5">
|
|
322
|
+
{parsed.task
|
|
323
|
+
? kvBlock(
|
|
324
|
+
'任务',
|
|
325
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
|
|
326
|
+
{parsed.task}
|
|
327
|
+
</pre>,
|
|
328
|
+
)
|
|
329
|
+
: null}
|
|
330
|
+
{parsed.dependsOn.length > 0
|
|
331
|
+
? kvBlock('依赖', parsed.dependsOn.map(shortTaskId).join('、'))
|
|
332
|
+
: null}
|
|
333
|
+
{parsed.worktree
|
|
334
|
+
? kvBlock('工作区', parsed.worktreeName ? `隔离 · ${parsed.worktreeName}` : '隔离')
|
|
335
|
+
: null}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const obj = args && typeof args === 'object' ? (args as Record<string, unknown>) : null;
|
|
340
|
+
const taskId = typeof obj?.taskId === 'string' ? obj.taskId : null;
|
|
341
|
+
const message = typeof obj?.message === 'string' ? obj.message : null;
|
|
342
|
+
if (!taskId && !message) return renderActionOutput(args);
|
|
343
|
+
return (
|
|
344
|
+
<div className="space-y-1.5">
|
|
345
|
+
{taskId ? kvBlock('编号', <span className="font-mono">{shortTaskId(taskId)}</span>) : null}
|
|
346
|
+
{message
|
|
347
|
+
? kvBlock(
|
|
348
|
+
'消息',
|
|
349
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
|
|
350
|
+
{message}
|
|
351
|
+
</pre>,
|
|
352
|
+
)
|
|
353
|
+
: null}
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function renderTaskOutput(output: unknown): ReactNode {
|
|
359
|
+
const parsed = parseTaskToolOutput(output);
|
|
360
|
+
if (parsed.items.length === 0 && !parsed.hint) {
|
|
361
|
+
return renderActionOutput(output);
|
|
362
|
+
}
|
|
363
|
+
return (
|
|
364
|
+
<div className="space-y-2">
|
|
365
|
+
{parsed.items.map((item, index) => (
|
|
366
|
+
<div key={item.taskId ?? `task-${index}`} className="space-y-1.5">
|
|
367
|
+
{item.taskId
|
|
368
|
+
? kvBlock('编号', <span className="font-mono">{shortTaskId(item.taskId)}</span>)
|
|
369
|
+
: null}
|
|
370
|
+
{item.status ? kvBlock('状态', TASK_STATUS_ZH[item.status] ?? item.status) : null}
|
|
371
|
+
{item.task
|
|
372
|
+
? kvBlock(
|
|
373
|
+
'任务',
|
|
374
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
|
|
375
|
+
{item.task}
|
|
376
|
+
</pre>,
|
|
377
|
+
)
|
|
378
|
+
: null}
|
|
379
|
+
{item.worktreeLabel ? kvBlock('工作区', item.worktreeLabel) : null}
|
|
380
|
+
{item.hint ? kvBlock('说明', item.hint) : null}
|
|
381
|
+
{item.answer
|
|
382
|
+
? kvBlock(
|
|
383
|
+
'结果',
|
|
384
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
|
|
385
|
+
{item.answer}
|
|
386
|
+
</pre>,
|
|
387
|
+
)
|
|
388
|
+
: null}
|
|
389
|
+
{item.error ? kvBlock('原因', item.error) : null}
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
{parsed.hint && parsed.items.every((item) => item.hint !== parsed.hint)
|
|
393
|
+
? kvBlock('说明', parsed.hint)
|
|
394
|
+
: null}
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function toolCardExtras(
|
|
400
|
+
action: ExecutedAction,
|
|
401
|
+
agents: ReadonlyArray<Pick<LocalChatAgent, 'id' | 'slug' | 'name' | 'color'>>,
|
|
402
|
+
): {
|
|
403
|
+
label?: string;
|
|
404
|
+
lead?: ReactNode;
|
|
405
|
+
renderArgs?: (args: unknown) => ReactNode;
|
|
406
|
+
renderOutput?: (output: unknown) => ReactNode;
|
|
407
|
+
} {
|
|
408
|
+
if (action.tool === 'delegate_subagent') {
|
|
409
|
+
const { profile } = parseDelegateSubagent(action.arguments);
|
|
410
|
+
const displayName = delegateSubagentDisplayName(profile, agents);
|
|
411
|
+
return {
|
|
412
|
+
label: `委派 · ${displayName}`,
|
|
413
|
+
lead: <DelegateLead name={displayName} color={delegateColor(profile, agents)} />,
|
|
414
|
+
renderArgs: (args) => renderDelegateArgs(args, agents),
|
|
415
|
+
renderOutput: renderDelegateOutput,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (isTaskFamilyTool(action.tool)) {
|
|
419
|
+
return {
|
|
420
|
+
label: taskToolLabel(action.tool, action.arguments) ?? undefined,
|
|
421
|
+
lead: <TaskLead />,
|
|
422
|
+
renderArgs: (args) => renderTaskArgs(action.tool, args),
|
|
423
|
+
renderOutput: renderTaskOutput,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return { renderOutput: renderActionOutput };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function inspectActivate(
|
|
430
|
+
action: ExecutedAction,
|
|
431
|
+
chatId: string | null | undefined,
|
|
432
|
+
onInspectTask?: (task: InspectTaskInput) => void,
|
|
433
|
+
child?: ChildInfo,
|
|
434
|
+
): { onActivate?: () => void; activateTitle?: string } {
|
|
435
|
+
if (!chatId || !onInspectTask) return {};
|
|
436
|
+
if (action.tool === 'delegate_subagent') {
|
|
437
|
+
if (!child?.recordId) return {};
|
|
438
|
+
const { task } = parseDelegateSubagent(action.arguments);
|
|
439
|
+
return {
|
|
440
|
+
onActivate: () =>
|
|
441
|
+
onInspectTask({
|
|
442
|
+
id: child.childId,
|
|
443
|
+
chatId,
|
|
444
|
+
recordId: child.recordId,
|
|
445
|
+
live: child.status === 'running',
|
|
446
|
+
title: task ?? '子代理任务',
|
|
447
|
+
}),
|
|
448
|
+
activateTitle: '查看子代理执行过程',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const target = inspectableTaskFromAction(action.tool, action.arguments, action.result);
|
|
452
|
+
if (!target) return {};
|
|
453
|
+
return {
|
|
454
|
+
onActivate: () => onInspectTask({ id: target.id, chatId, title: target.title }),
|
|
455
|
+
activateTitle: '查看后台执行过程',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** 逐行配对委派与子代理:同画像多份活时按出现顺序对上。 */
|
|
460
|
+
function delegationChildren(
|
|
461
|
+
actions: readonly ExecutedAction[],
|
|
462
|
+
children: readonly ChildInfo[],
|
|
463
|
+
): Map<number, ChildInfo> {
|
|
464
|
+
const paired = new Map<number, ChildInfo>();
|
|
465
|
+
if (children.length === 0) return paired;
|
|
466
|
+
const taken = new Set<string>();
|
|
467
|
+
actions.forEach((action, index) => {
|
|
468
|
+
if (action.tool !== 'delegate_subagent') return;
|
|
469
|
+
const match = matchChildForDelegation(
|
|
470
|
+
parseDelegateSubagent(action.arguments),
|
|
471
|
+
children,
|
|
472
|
+
taken,
|
|
473
|
+
);
|
|
474
|
+
if (!match) return;
|
|
475
|
+
taken.add(match.childId);
|
|
476
|
+
paired.set(index, match);
|
|
477
|
+
});
|
|
478
|
+
return paired;
|
|
479
|
+
}
|
|
480
|
+
|
|
147
481
|
/** Inline tool rows for a mixed think→act timeline (no summary banner). */
|
|
148
|
-
export function ToolsFlow({
|
|
482
|
+
export function ToolsFlow({
|
|
483
|
+
actions,
|
|
484
|
+
compact = false,
|
|
485
|
+
defaultExpanded = false,
|
|
486
|
+
agents = [],
|
|
487
|
+
chatId = null,
|
|
488
|
+
onInspectTask,
|
|
489
|
+
orchestrationChildren = [],
|
|
490
|
+
}: ExecutedActionsCardProps) {
|
|
149
491
|
if (!actions || actions.length === 0) return null;
|
|
150
492
|
const expanded = expandRunCodeActions(actions);
|
|
493
|
+
const childByIndex = delegationChildren(expanded, orchestrationChildren);
|
|
151
494
|
return (
|
|
152
495
|
<div
|
|
153
496
|
className={
|
|
@@ -157,30 +500,47 @@ export function ToolsFlow({ actions, compact = false }: ExecutedActionsCardProps
|
|
|
157
500
|
}
|
|
158
501
|
>
|
|
159
502
|
<div className="space-y-px">
|
|
160
|
-
{expanded.map((action, i) =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
503
|
+
{expanded.map((action, i) => {
|
|
504
|
+
const extras = toolCardExtras(action, agents);
|
|
505
|
+
const inspect = inspectActivate(action, chatId, onInspectTask, childByIndex.get(i));
|
|
506
|
+
return (
|
|
507
|
+
<ToolExecutionCard
|
|
508
|
+
key={action.id ?? `${action.tool}-${i}`}
|
|
509
|
+
payload={actionToTool(action, i)}
|
|
510
|
+
defaultExpanded={defaultExpanded}
|
|
511
|
+
label={extras.label}
|
|
512
|
+
lead={extras.lead}
|
|
513
|
+
renderArgs={extras.renderArgs}
|
|
514
|
+
renderOutput={extras.renderOutput ?? renderActionOutput}
|
|
515
|
+
onActivate={inspect.onActivate}
|
|
516
|
+
activateTitle={inspect.activateTitle}
|
|
517
|
+
className="rounded-none border-0 border-t border-agent-border first:border-t-0"
|
|
518
|
+
/>
|
|
519
|
+
);
|
|
520
|
+
})}
|
|
168
521
|
</div>
|
|
169
522
|
</div>
|
|
170
523
|
);
|
|
171
524
|
}
|
|
172
525
|
|
|
173
|
-
export function ExecutedActionsCard({
|
|
526
|
+
export function ExecutedActionsCard({
|
|
527
|
+
actions,
|
|
528
|
+
agents = [],
|
|
529
|
+
chatId = null,
|
|
530
|
+
onInspectTask,
|
|
531
|
+
orchestrationChildren = [],
|
|
532
|
+
}: ExecutedActionsCardProps) {
|
|
174
533
|
if (!actions || actions.length === 0) return null;
|
|
175
534
|
|
|
176
535
|
const expanded = expandRunCodeActions(actions);
|
|
536
|
+
const childByIndex = delegationChildren(expanded, orchestrationChildren);
|
|
177
537
|
const successCount = expanded.filter((a) => deriveStatus(a) === 'succeeded').length;
|
|
178
538
|
const failureCount = expanded.filter((a) => deriveStatus(a) === 'failed').length;
|
|
179
539
|
const runningCount = expanded.filter((a) => deriveStatus(a) === 'running').length;
|
|
180
540
|
|
|
181
541
|
return (
|
|
182
|
-
<div className="my-
|
|
183
|
-
<div className="flex items-center justify-between border-b border-agent-border bg-agent-muted/30 px-
|
|
542
|
+
<div className="my-1.5 overflow-hidden rounded-agent-md border border-agent-border bg-agent-canvas">
|
|
543
|
+
<div className="flex items-center justify-between border-b border-agent-border bg-agent-muted/30 px-2.5 py-1">
|
|
184
544
|
<div className="flex items-center gap-1.5 text-xs">
|
|
185
545
|
{failureCount === 0 ? (
|
|
186
546
|
<LuCircleCheck className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
|
|
@@ -200,14 +560,23 @@ export function ExecutedActionsCard({ actions }: ExecutedActionsCardProps) {
|
|
|
200
560
|
</span>
|
|
201
561
|
</div>
|
|
202
562
|
<div className="space-y-px">
|
|
203
|
-
{expanded.map((action, i) =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
563
|
+
{expanded.map((action, i) => {
|
|
564
|
+
const extras = toolCardExtras(action, agents);
|
|
565
|
+
const inspect = inspectActivate(action, chatId, onInspectTask, childByIndex.get(i));
|
|
566
|
+
return (
|
|
567
|
+
<ToolExecutionCard
|
|
568
|
+
key={action.id ?? `${action.tool}-${i}`}
|
|
569
|
+
payload={actionToTool(action, i)}
|
|
570
|
+
label={extras.label}
|
|
571
|
+
lead={extras.lead}
|
|
572
|
+
renderArgs={extras.renderArgs}
|
|
573
|
+
renderOutput={extras.renderOutput ?? renderActionOutput}
|
|
574
|
+
onActivate={inspect.onActivate}
|
|
575
|
+
activateTitle={inspect.activateTitle}
|
|
576
|
+
className="rounded-none border-0 border-t border-agent-border first:border-t-0"
|
|
577
|
+
/>
|
|
578
|
+
);
|
|
579
|
+
})}
|
|
211
580
|
</div>
|
|
212
581
|
</div>
|
|
213
582
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
import { openLocalPath, type ResolvedLocalPath } from '@/lib/local-api';
|
|
3
3
|
import { peekResolvedPath, subscribeResolvedPath } from './path-mentions';
|
|
4
|
+
import { splitTurnFilePath } from './turn-files';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 行内代码里的文件路径:后端确认存在后变成可点击,点击用系统默认应用打开。
|
|
@@ -59,7 +60,7 @@ export function FilePathCode({ candidate, chatId, children, ...rest }: FilePathC
|
|
|
59
60
|
openError ? 'text-red-600 dark:text-red-400' : ''
|
|
60
61
|
}`}
|
|
61
62
|
>
|
|
62
|
-
{children}
|
|
63
|
+
{splitTurnFilePath(resolved.path).name || children}
|
|
63
64
|
</button>
|
|
64
65
|
);
|
|
65
66
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import type { ChatMessage } from '@steerable/agent-protocol';
|
|
3
3
|
import { ChatPanel } from '@steerable/agent-ui';
|
|
4
4
|
import type { SteerOutcome } from '@steerable/agent-ui';
|
|
@@ -13,9 +13,13 @@ import {
|
|
|
13
13
|
import { EmptyChat } from './EmptyChat';
|
|
14
14
|
import { MessageList } from './MessageList';
|
|
15
15
|
import type { ExecutedAction } from './ExecutedActionsCard';
|
|
16
|
+
import type { InspectTaskInput } from './executed-actions-model';
|
|
16
17
|
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
17
18
|
import type { TurnBlock } from './turn-timeline';
|
|
18
19
|
import type { TurnFile } from './turn-files';
|
|
20
|
+
import type { LlmSpeedSnapshot } from './process-status';
|
|
21
|
+
import { SessionTodoList } from './SessionTodoList';
|
|
22
|
+
import { resolveLatestSessionTodos } from './todo-list-model';
|
|
19
23
|
import {
|
|
20
24
|
isImageFile,
|
|
21
25
|
saveChatAttachments,
|
|
@@ -98,6 +102,8 @@ export interface LocalChatPanelProps {
|
|
|
98
102
|
currentTurnTimeline?: TurnBlock[];
|
|
99
103
|
currentTurnStartedAtMs?: number;
|
|
100
104
|
durationByMessageId?: Record<string, number>;
|
|
105
|
+
currentLlmSpeed?: LlmSpeedSnapshot;
|
|
106
|
+
llmSpeedByMessageId?: Record<string, LlmSpeedSnapshot>;
|
|
101
107
|
/** 回合产物文件列表(按落库消息 id 键控),转发给 MessageList。 */
|
|
102
108
|
turnFilesByMessageId?: Record<string, TurnFile[]>;
|
|
103
109
|
/** 当轮产物文件(流尾声到达、尚未归档到落库 id 的尾部消息用)。 */
|
|
@@ -141,7 +147,7 @@ export interface LocalChatPanelProps {
|
|
|
141
147
|
/** 本次挂载期间跑完的后台任务——消息列尾部的终态通知卡。 */
|
|
142
148
|
finishedTasks?: LocalTask[];
|
|
143
149
|
/** 终态卡「查看过程」:在右侧栏打开该任务的推理过程。 */
|
|
144
|
-
onInspectTask?: (task:
|
|
150
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
145
151
|
/** 终态卡「忽略」(本次挂载内隐藏,不落库)。 */
|
|
146
152
|
onDismissFinishedTask?: (taskId: string) => void;
|
|
147
153
|
/** 分享当前对话(截图)。落到最近一条助手消息的时间戳行。 */
|
|
@@ -176,6 +182,8 @@ export function LocalChatPanel({
|
|
|
176
182
|
currentTurnTimeline,
|
|
177
183
|
currentTurnStartedAtMs,
|
|
178
184
|
durationByMessageId,
|
|
185
|
+
currentLlmSpeed,
|
|
186
|
+
llmSpeedByMessageId,
|
|
179
187
|
turnFilesByMessageId,
|
|
180
188
|
currentTurnFiles,
|
|
181
189
|
currentTurnChildren,
|
|
@@ -225,7 +233,8 @@ export function LocalChatPanel({
|
|
|
225
233
|
// 把上传的文件持久化到会话空间:成功项用落盘路径(稳定、可被 agent
|
|
226
234
|
// 读回),失败项退回原源路径。落地页(无 chatId)由调用方创建会话后
|
|
227
235
|
// 自行持久化,这里 chatId 为空时原样用源路径。
|
|
228
|
-
const resolvedFiles =
|
|
236
|
+
const resolvedFiles =
|
|
237
|
+
chatId && files.length > 0 ? await saveChatAttachments(chatId, files) : files;
|
|
229
238
|
|
|
230
239
|
// Append file references to the user message content
|
|
231
240
|
if (resolvedFiles.length > 0) {
|
|
@@ -285,9 +294,26 @@ export function LocalChatPanel({
|
|
|
285
294
|
// 空会话 hero:messages 为空且未在流式时整面板切到居中首屏布局。
|
|
286
295
|
const showEmptyHero = Boolean(emptyHero) && messages.length === 0 && !isStreaming;
|
|
287
296
|
|
|
297
|
+
const sessionTodos = useMemo(
|
|
298
|
+
() =>
|
|
299
|
+
resolveLatestSessionTodos({
|
|
300
|
+
messages,
|
|
301
|
+
executedActionsByMessageId,
|
|
302
|
+
currentTurnActions,
|
|
303
|
+
timelineByMessageId,
|
|
304
|
+
currentTurnTimeline,
|
|
305
|
+
}),
|
|
306
|
+
[
|
|
307
|
+
messages,
|
|
308
|
+
executedActionsByMessageId,
|
|
309
|
+
currentTurnActions,
|
|
310
|
+
timelineByMessageId,
|
|
311
|
+
currentTurnTimeline,
|
|
312
|
+
],
|
|
313
|
+
);
|
|
314
|
+
|
|
288
315
|
// Sync `chat-input-box-width` CSS var so bubbles match the input width
|
|
289
|
-
// (
|
|
290
|
-
// bubbles + input visually align in a column).
|
|
316
|
+
// (mirrors the rendered input box width so message bubbles + input visually align in a column).
|
|
291
317
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
292
318
|
useEffect(() => {
|
|
293
319
|
const containerEl = containerRef.current;
|
|
@@ -300,7 +326,7 @@ export function LocalChatPanel({
|
|
|
300
326
|
if (width > 0) {
|
|
301
327
|
containerEl.style.setProperty(
|
|
302
328
|
'--chat-input-box-width',
|
|
303
|
-
`${Math.round(width
|
|
329
|
+
`${Math.round(width)}px`,
|
|
304
330
|
);
|
|
305
331
|
}
|
|
306
332
|
};
|
|
@@ -339,6 +365,7 @@ export function LocalChatPanel({
|
|
|
339
365
|
onOpenSettings={onOpenSettings}
|
|
340
366
|
toolbarExtras={inputToolbarExtras}
|
|
341
367
|
leadingChrome={inputLeadingChrome}
|
|
368
|
+
trailingChrome={sessionTodos ? <SessionTodoList todos={sessionTodos} /> : undefined}
|
|
342
369
|
mode={mode}
|
|
343
370
|
onModeChange={onModeChange}
|
|
344
371
|
execPolicy={execPolicy}
|
|
@@ -356,14 +383,14 @@ export function LocalChatPanel({
|
|
|
356
383
|
>
|
|
357
384
|
<ChatPanel.Root className="flex h-full flex-col overflow-hidden" unstyled>
|
|
358
385
|
{showEmptyHero ? (
|
|
359
|
-
<div className="flex min-h-0 flex-1 items-center justify-center p-
|
|
360
|
-
<div className="flex w-full max-w-
|
|
386
|
+
<div className="flex min-h-0 flex-1 items-center justify-center p-3 sm:p-5">
|
|
387
|
+
<div className="flex w-full max-w-3xl flex-col items-center gap-4">
|
|
361
388
|
<div className="text-center">
|
|
362
389
|
<h1 className="text-xl font-semibold tracking-tight text-agent-foreground">
|
|
363
390
|
{emptyHero?.title}
|
|
364
391
|
</h1>
|
|
365
392
|
{emptyHero?.subtitle && (
|
|
366
|
-
<p className="mt-1.5 text-
|
|
393
|
+
<p className="mt-1.5 text-xs text-agent-muted-foreground">
|
|
367
394
|
{emptyHero.subtitle}
|
|
368
395
|
</p>
|
|
369
396
|
)}
|
|
@@ -396,6 +423,8 @@ export function LocalChatPanel({
|
|
|
396
423
|
currentTurnTimeline={currentTurnTimeline}
|
|
397
424
|
currentTurnStartedAtMs={currentTurnStartedAtMs}
|
|
398
425
|
durationByMessageId={durationByMessageId}
|
|
426
|
+
currentLlmSpeed={currentLlmSpeed}
|
|
427
|
+
llmSpeedByMessageId={llmSpeedByMessageId}
|
|
399
428
|
turnFilesByMessageId={turnFilesByMessageId}
|
|
400
429
|
currentTurnFiles={currentTurnFiles}
|
|
401
430
|
currentTurnChildren={currentTurnChildren}
|
|
@@ -414,8 +443,10 @@ export function LocalChatPanel({
|
|
|
414
443
|
suggestedReplies={suggestedReplies}
|
|
415
444
|
onSelectSuggestion={onSelectSuggestion}
|
|
416
445
|
/>
|
|
417
|
-
|
|
418
|
-
|
|
446
|
+
<div className="mx-auto w-full max-w-3xl">
|
|
447
|
+
{inputBanner}
|
|
448
|
+
{chatInputNode}
|
|
449
|
+
</div>
|
|
419
450
|
</div>
|
|
420
451
|
</>
|
|
421
452
|
)}
|