@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.
@@ -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 { summarizeRunCodeAction, summarizeWebAction, expandRunCodeActions } from './executed-actions-model';
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' && action.result && typeof action.result === 'object'
90
- ? typeof (action.result as { error?: unknown }).error === 'string'
91
- ? ((action.result as { error: string }).error)
92
- : null
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
- <ToolExecutionCard
168
- key={action.id ?? `${action.tool}-${i}`}
169
- payload={actionToTool(action, i)}
170
- defaultExpanded={defaultExpanded}
171
- renderOutput={renderActionOutput}
172
- className="rounded-none border-0 border-t border-agent-border first:border-t-0"
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({ actions }: ExecutedActionsCardProps) {
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
- <ToolExecutionCard
212
- key={action.id ?? `${action.tool}-${i}`}
213
- payload={actionToTool(action, i)}
214
- renderOutput={renderActionOutput}
215
- className="rounded-none border-0 border-t border-agent-border first:border-t-0"
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: LocalTask) => void;
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 = chatId ? await saveChatAttachments(chatId, files) : files;
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
- // (cloud product uses 95% of the input box; we mirror that here so message
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 * 0.95)}px`,
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-2xl flex-col items-center gap-4">
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
- {inputBanner}
452
- {sessionTodoBanner}
453
- {chatInputNode}
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: LocalTask) => void;
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-2.5 py-2"
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-1">
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
- previousUserCreatedAt = visibleMessages[i].createdAt;
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 &&