@steerable/agent-shell-web 0.6.28 → 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 +3 -3
- package/src/components/BrandLockup.tsx +20 -4
- package/src/components/chat/AssistantMessage.tsx +83 -25
- package/src/components/chat/ChatInput.tsx +11 -3
- package/src/components/chat/ExecutedActionsCard.tsx +389 -27
- package/src/components/chat/LocalChatPanel.tsx +12 -16
- package/src/components/chat/MessageList.tsx +9 -4
- package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
- package/src/components/chat/SessionTodoList.tsx +117 -20
- package/src/components/chat/SuggestedReplies.tsx +11 -3
- package/src/components/chat/TaskProcessPanel.tsx +62 -17
- package/src/components/chat/TurnProcessGroup.tsx +27 -1
- package/src/components/chat/UserMessage.tsx +5 -5
- package/src/components/chat/executed-actions-model.ts +469 -0
- package/src/components/chat/orchestration-children-model.ts +120 -0
- package/src/components/chat/turn-timeline.ts +23 -0
- package/src/lib/local-api.ts +11 -0
- package/src/pages/AgentPage.tsx +27 -6
|
@@ -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
|
|
@@ -44,15 +67,24 @@ interface ExecutedActionsCardProps {
|
|
|
44
67
|
compact?: boolean;
|
|
45
68
|
/** Show args/output instead of a one-line summary. Defaults to collapsed. */
|
|
46
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[];
|
|
47
79
|
}
|
|
48
80
|
|
|
49
81
|
function deriveStatus(action: ExecutedAction): ToolExecutionPayload['status'] {
|
|
50
82
|
const result = action.result;
|
|
51
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';
|
|
52
87
|
if (typeof result !== 'object') return 'succeeded';
|
|
53
|
-
if ('success' in result) {
|
|
54
|
-
return (result as { success?: unknown }).success ? 'succeeded' : 'failed';
|
|
55
|
-
}
|
|
56
88
|
if ('error' in result) return 'failed';
|
|
57
89
|
return 'succeeded';
|
|
58
90
|
}
|
|
@@ -85,15 +117,22 @@ function sandboxBadge(sandbox: ExecutedAction['sandbox']): string | null {
|
|
|
85
117
|
|
|
86
118
|
function actionToTool(action: ExecutedAction, idx: number): ToolExecutionPayload {
|
|
87
119
|
const status = deriveStatus(action);
|
|
120
|
+
const env = parseToolEnvelope(action.result);
|
|
88
121
|
const errorText =
|
|
89
|
-
status === 'failed'
|
|
90
|
-
?
|
|
91
|
-
? (
|
|
92
|
-
:
|
|
122
|
+
status === 'failed'
|
|
123
|
+
? action.tool === 'delegate_subagent'
|
|
124
|
+
? humanizeDelegateError(env.error)
|
|
125
|
+
: env.error
|
|
93
126
|
: null;
|
|
94
127
|
// W5-2: web_search/web_fetch 用结构化中文摘要(查询词/URL + 结果计数/
|
|
95
128
|
// 状态码),其余工具走通用参数摘要。
|
|
96
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) ??
|
|
97
136
|
summarizeRunCodeAction(action.tool, action.arguments, action.result) ??
|
|
98
137
|
summarizeWebAction(action.tool, action.arguments, action.result) ??
|
|
99
138
|
summarizeArguments(action.arguments);
|
|
@@ -146,14 +185,312 @@ function renderActionOutput(output: unknown): ReactNode {
|
|
|
146
185
|
);
|
|
147
186
|
}
|
|
148
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
|
+
|
|
149
481
|
/** Inline tool rows for a mixed think→act timeline (no summary banner). */
|
|
150
482
|
export function ToolsFlow({
|
|
151
483
|
actions,
|
|
152
484
|
compact = false,
|
|
153
485
|
defaultExpanded = false,
|
|
486
|
+
agents = [],
|
|
487
|
+
chatId = null,
|
|
488
|
+
onInspectTask,
|
|
489
|
+
orchestrationChildren = [],
|
|
154
490
|
}: ExecutedActionsCardProps) {
|
|
155
491
|
if (!actions || actions.length === 0) return null;
|
|
156
492
|
const expanded = expandRunCodeActions(actions);
|
|
493
|
+
const childByIndex = delegationChildren(expanded, orchestrationChildren);
|
|
157
494
|
return (
|
|
158
495
|
<div
|
|
159
496
|
className={
|
|
@@ -163,24 +500,40 @@ export function ToolsFlow({
|
|
|
163
500
|
}
|
|
164
501
|
>
|
|
165
502
|
<div className="space-y-px">
|
|
166
|
-
{expanded.map((action, i) =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
})}
|
|
175
521
|
</div>
|
|
176
522
|
</div>
|
|
177
523
|
);
|
|
178
524
|
}
|
|
179
525
|
|
|
180
|
-
export function ExecutedActionsCard({
|
|
526
|
+
export function ExecutedActionsCard({
|
|
527
|
+
actions,
|
|
528
|
+
agents = [],
|
|
529
|
+
chatId = null,
|
|
530
|
+
onInspectTask,
|
|
531
|
+
orchestrationChildren = [],
|
|
532
|
+
}: ExecutedActionsCardProps) {
|
|
181
533
|
if (!actions || actions.length === 0) return null;
|
|
182
534
|
|
|
183
535
|
const expanded = expandRunCodeActions(actions);
|
|
536
|
+
const childByIndex = delegationChildren(expanded, orchestrationChildren);
|
|
184
537
|
const successCount = expanded.filter((a) => deriveStatus(a) === 'succeeded').length;
|
|
185
538
|
const failureCount = expanded.filter((a) => deriveStatus(a) === 'failed').length;
|
|
186
539
|
const runningCount = expanded.filter((a) => deriveStatus(a) === 'running').length;
|
|
@@ -207,14 +560,23 @@ export function ExecutedActionsCard({ actions }: ExecutedActionsCardProps) {
|
|
|
207
560
|
</span>
|
|
208
561
|
</div>
|
|
209
562
|
<div className="space-y-px">
|
|
210
|
-
{expanded.map((action, i) =>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
})}
|
|
218
580
|
</div>
|
|
219
581
|
</div>
|
|
220
582
|
);
|
|
@@ -13,6 +13,7 @@ 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';
|
|
@@ -146,7 +147,7 @@ export interface LocalChatPanelProps {
|
|
|
146
147
|
/** 本次挂载期间跑完的后台任务——消息列尾部的终态通知卡。 */
|
|
147
148
|
finishedTasks?: LocalTask[];
|
|
148
149
|
/** 终态卡「查看过程」:在右侧栏打开该任务的推理过程。 */
|
|
149
|
-
onInspectTask?: (task:
|
|
150
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
150
151
|
/** 终态卡「忽略」(本次挂载内隐藏,不落库)。 */
|
|
151
152
|
onDismissFinishedTask?: (taskId: string) => void;
|
|
152
153
|
/** 分享当前对话(截图)。落到最近一条助手消息的时间戳行。 */
|
|
@@ -232,7 +233,8 @@ export function LocalChatPanel({
|
|
|
232
233
|
// 把上传的文件持久化到会话空间:成功项用落盘路径(稳定、可被 agent
|
|
233
234
|
// 读回),失败项退回原源路径。落地页(无 chatId)由调用方创建会话后
|
|
234
235
|
// 自行持久化,这里 chatId 为空时原样用源路径。
|
|
235
|
-
const resolvedFiles =
|
|
236
|
+
const resolvedFiles =
|
|
237
|
+
chatId && files.length > 0 ? await saveChatAttachments(chatId, files) : files;
|
|
236
238
|
|
|
237
239
|
// Append file references to the user message content
|
|
238
240
|
if (resolvedFiles.length > 0) {
|
|
@@ -310,15 +312,8 @@ export function LocalChatPanel({
|
|
|
310
312
|
],
|
|
311
313
|
);
|
|
312
314
|
|
|
313
|
-
const sessionTodoBanner = sessionTodos ? (
|
|
314
|
-
<div className="px-2.5">
|
|
315
|
-
<SessionTodoList todos={sessionTodos} />
|
|
316
|
-
</div>
|
|
317
|
-
) : null;
|
|
318
|
-
|
|
319
315
|
// Sync `chat-input-box-width` CSS var so bubbles match the input width
|
|
320
|
-
// (
|
|
321
|
-
// bubbles + input visually align in a column).
|
|
316
|
+
// (mirrors the rendered input box width so message bubbles + input visually align in a column).
|
|
322
317
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
323
318
|
useEffect(() => {
|
|
324
319
|
const containerEl = containerRef.current;
|
|
@@ -331,7 +326,7 @@ export function LocalChatPanel({
|
|
|
331
326
|
if (width > 0) {
|
|
332
327
|
containerEl.style.setProperty(
|
|
333
328
|
'--chat-input-box-width',
|
|
334
|
-
`${Math.round(width
|
|
329
|
+
`${Math.round(width)}px`,
|
|
335
330
|
);
|
|
336
331
|
}
|
|
337
332
|
};
|
|
@@ -370,6 +365,7 @@ export function LocalChatPanel({
|
|
|
370
365
|
onOpenSettings={onOpenSettings}
|
|
371
366
|
toolbarExtras={inputToolbarExtras}
|
|
372
367
|
leadingChrome={inputLeadingChrome}
|
|
368
|
+
trailingChrome={sessionTodos ? <SessionTodoList todos={sessionTodos} /> : undefined}
|
|
373
369
|
mode={mode}
|
|
374
370
|
onModeChange={onModeChange}
|
|
375
371
|
execPolicy={execPolicy}
|
|
@@ -388,7 +384,7 @@ export function LocalChatPanel({
|
|
|
388
384
|
<ChatPanel.Root className="flex h-full flex-col overflow-hidden" unstyled>
|
|
389
385
|
{showEmptyHero ? (
|
|
390
386
|
<div className="flex min-h-0 flex-1 items-center justify-center p-3 sm:p-5">
|
|
391
|
-
<div className="flex w-full max-w-
|
|
387
|
+
<div className="flex w-full max-w-3xl flex-col items-center gap-4">
|
|
392
388
|
<div className="text-center">
|
|
393
389
|
<h1 className="text-xl font-semibold tracking-tight text-agent-foreground">
|
|
394
390
|
{emptyHero?.title}
|
|
@@ -401,7 +397,6 @@ export function LocalChatPanel({
|
|
|
401
397
|
</div>
|
|
402
398
|
<div className="w-full">
|
|
403
399
|
{inputBanner}
|
|
404
|
-
{sessionTodoBanner}
|
|
405
400
|
{chatInputNode}
|
|
406
401
|
</div>
|
|
407
402
|
</div>
|
|
@@ -448,9 +443,10 @@ export function LocalChatPanel({
|
|
|
448
443
|
suggestedReplies={suggestedReplies}
|
|
449
444
|
onSelectSuggestion={onSelectSuggestion}
|
|
450
445
|
/>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
446
|
+
<div className="mx-auto w-full max-w-3xl">
|
|
447
|
+
{inputBanner}
|
|
448
|
+
{chatInputNode}
|
|
449
|
+
</div>
|
|
454
450
|
</div>
|
|
455
451
|
</>
|
|
456
452
|
)}
|
|
@@ -15,6 +15,7 @@ import { InterruptedTurnCard } from './InterruptedTurnCard';
|
|
|
15
15
|
import { TaskOutcomeCards } from './TaskOutcomeCards';
|
|
16
16
|
import { SuggestedReplies } from './SuggestedReplies';
|
|
17
17
|
import type { ExecutedAction } from './ExecutedActionsCard';
|
|
18
|
+
import type { InspectTaskInput } from './executed-actions-model';
|
|
18
19
|
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
19
20
|
import type { ChatMode } from './ChatInput';
|
|
20
21
|
import type { TurnBlock } from './turn-timeline';
|
|
@@ -134,7 +135,7 @@ interface MessageListProps {
|
|
|
134
135
|
*/
|
|
135
136
|
finishedTasks?: LocalTask[];
|
|
136
137
|
/** 点「查看过程」:在右侧栏打开该任务的推理过程。 */
|
|
137
|
-
onInspectTask?: (task:
|
|
138
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
138
139
|
/** 点「忽略」:隐藏这条终态通知(本次挂载内,不落库)。 */
|
|
139
140
|
onDismissFinishedTask?: (taskId: string) => void;
|
|
140
141
|
/** 分享当前对话(截图)。只画在最近一条助手消息的时间戳行上。 */
|
|
@@ -248,12 +249,12 @@ export function MessageList({
|
|
|
248
249
|
<div className="relative flex-1 overflow-hidden">
|
|
249
250
|
<div
|
|
250
251
|
ref={containerRef}
|
|
251
|
-
className="h-full overflow-y-auto overflow-anchor-none px-
|
|
252
|
+
className="h-full overflow-y-auto overflow-anchor-none px-3 pt-3 pb-6"
|
|
252
253
|
>
|
|
253
254
|
{visibleMessages.length === 0 ? (
|
|
254
255
|
emptyState ?? null
|
|
255
256
|
) : (
|
|
256
|
-
<div className="mx-auto w-full space-y-
|
|
257
|
+
<div className="mx-auto w-full max-w-3xl space-y-4">
|
|
257
258
|
{visibleMessages.map((message, index) => {
|
|
258
259
|
const isLast = index === visibleMessages.length - 1;
|
|
259
260
|
if (message.role === 'user') {
|
|
@@ -306,10 +307,12 @@ export function MessageList({
|
|
|
306
307
|
typeof message.messageMetadata === 'string'
|
|
307
308
|
? message.messageMetadata
|
|
308
309
|
: undefined;
|
|
310
|
+
let previousUser: ChatMessage | undefined;
|
|
309
311
|
let previousUserCreatedAt: string | undefined;
|
|
310
312
|
for (let i = index - 1; i >= 0; i -= 1) {
|
|
311
313
|
if (visibleMessages[i].role === 'user') {
|
|
312
|
-
|
|
314
|
+
previousUser = visibleMessages[i];
|
|
315
|
+
previousUserCreatedAt = previousUser.createdAt;
|
|
313
316
|
break;
|
|
314
317
|
}
|
|
315
318
|
}
|
|
@@ -336,6 +339,7 @@ export function MessageList({
|
|
|
336
339
|
executedActions={actions}
|
|
337
340
|
timeline={turnTimeline}
|
|
338
341
|
orchestrationChildren={childList}
|
|
342
|
+
previousUser={previousUser}
|
|
339
343
|
currentRound={isStreamingTail ? currentRound : undefined}
|
|
340
344
|
isPlanMode={isPlanMode}
|
|
341
345
|
onRegenerate={onRegenerate}
|
|
@@ -344,6 +348,7 @@ export function MessageList({
|
|
|
344
348
|
llmSpeed={llmSpeed}
|
|
345
349
|
turnFiles={turnFiles}
|
|
346
350
|
onShare={message.id === lastAssistantId ? onShare : undefined}
|
|
351
|
+
onInspectTask={onInspectTask}
|
|
347
352
|
/>
|
|
348
353
|
{!isStreaming &&
|
|
349
354
|
message.id === lastAssistantId &&
|