@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.
Files changed (53) hide show
  1. package/package.json +4 -4
  2. package/src/components/AgentSidebar.tsx +27 -27
  3. package/src/components/BrandLockup.tsx +20 -4
  4. package/src/components/ChatHeader.tsx +5 -15
  5. package/src/components/LocalLlmSettingsModal.tsx +5 -5
  6. package/src/components/TerminalPanel.tsx +1 -1
  7. package/src/components/chat/ApprovalModal.tsx +5 -5
  8. package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
  9. package/src/components/chat/AssistantMessage.tsx +122 -36
  10. package/src/components/chat/ChatInput.tsx +34 -26
  11. package/src/components/chat/ExecPolicyPicker.tsx +4 -4
  12. package/src/components/chat/ExecutedActionsCard.tsx +398 -29
  13. package/src/components/chat/FilePathCode.tsx +2 -1
  14. package/src/components/chat/LocalChatPanel.tsx +42 -11
  15. package/src/components/chat/Markdown.tsx +15 -14
  16. package/src/components/chat/MessageList.tsx +23 -5
  17. package/src/components/chat/ModelPicker.tsx +6 -6
  18. package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
  19. package/src/components/chat/SessionTodoList.tsx +234 -0
  20. package/src/components/chat/SessionTreeModal.tsx +3 -3
  21. package/src/components/chat/SuggestedReplies.tsx +14 -5
  22. package/src/components/chat/TaskPanelModal.tsx +3 -3
  23. package/src/components/chat/TaskProcessPanel.tsx +67 -21
  24. package/src/components/chat/TurnFilesCard.tsx +2 -2
  25. package/src/components/chat/TurnProcessGroup.tsx +252 -99
  26. package/src/components/chat/UserMessage.tsx +7 -7
  27. package/src/components/chat/executed-actions-model.ts +469 -0
  28. package/src/components/chat/next-steps-tags.ts +28 -0
  29. package/src/components/chat/orchestration-children-model.ts +120 -0
  30. package/src/components/chat/process-status.ts +125 -53
  31. package/src/components/chat/todo-list-model.ts +167 -0
  32. package/src/components/chat/turn-timeline.ts +123 -11
  33. package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
  34. package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
  35. package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
  36. package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
  37. package/src/components/settings/LlmSettingsPanel.tsx +10 -10
  38. package/src/components/settings/McpSettingsPanel.tsx +3 -3
  39. package/src/components/settings/ModelIdCombobox.tsx +2 -2
  40. package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
  41. package/src/components/settings/SettingsSaveButton.tsx +1 -1
  42. package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
  43. package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
  44. package/src/components/settings/UsagePanel.tsx +14 -10
  45. package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
  46. package/src/layouts/AgentLayout.tsx +3 -3
  47. package/src/lib/chat-transport.ts +68 -12
  48. package/src/lib/local-api.ts +11 -0
  49. package/src/lib/show-thinking-content.ts +4 -4
  50. package/src/pages/AgentPage.tsx +82 -11
  51. package/src/pages/ChatPanelPreviewPage.tsx +26 -4
  52. package/src/pages/SettingsPage.tsx +39 -39
  53. package/src/styles/index.css +12 -2
@@ -7,23 +7,41 @@ import {
7
7
  } from '@/lib/show-thinking-content';
8
8
  import { Markdown } from './Markdown';
9
9
  import { ToolsFlow } from './ExecutedActionsCard';
10
- import { processStatusLabel } from './process-status';
11
- import { processHasReasoning, splitTurnProcess, type TurnBlock } from './turn-timeline';
10
+ import type { InspectTaskInput } from './executed-actions-model';
11
+ import type { ChildInfo } from './OrchestrationChildrenCard';
12
+ import {
13
+ estimateReasoningDurationMs,
14
+ processStatusLabel,
15
+ thinkingFoldLabel,
16
+ } from './process-status';
17
+ import { splitTurnProcess, type TurnBlock } from './turn-timeline';
12
18
 
13
19
  /**
14
- * Codex / DeepSeek-style turn process: think + tool rows stay in one
15
- * disclosure. Settings 「显示思考内容」 has three modes: hidden / 5-line
16
- * peek / full. The status line still shows 思考中 / 工具名 / tok/s /
17
- * elapsed. The summary itself stays outside the fold.
20
+ * Codex / DeepSeek-style turn process. Each LLM round keeps three surfaces
21
+ * distinct: 思考 (muted CoT), 思考后的 response (reply bubble), 工具
22
+ * (cards). Trailing text becomes 最后一次结论 only after the stream ends —
23
+ * promoting it earlier would restyle it as thinking when the next round
24
+ * starts. Settings 「显示思考内容」 only clip the 思考正文: hidden /
25
+ * 5-line peek / full. A finished 思考 fold auto-collapses in chat;
26
+ * only the live round stays open. Tools and the work row stay independent.
18
27
  */
19
28
 
20
29
  export const THINKING_PEEK_LINES = 5;
30
+ /** 思考正文行高(`leading-snug`)。 */
31
+ const THINKING_LINE_HEIGHT = 1.375;
21
32
  /**
22
- * 5 行 `text-xs` + `leading-relaxed` 的稳定高度。
33
+ * 最多 5 行:用 max-height,短思考按内容收,不撑空。
23
34
  * 不用 CSS `lh`:Windows Electron 在中文字体尚未就绪时 `lh` 会算成 0,
24
35
  * 思考 peek 整块消失,字体加载后又把主列表高度撑跳。
25
36
  */
26
- export const THINKING_PEEK_HEIGHT = `${THINKING_PEEK_LINES * 1.625}em`;
37
+ export const THINKING_PEEK_HEIGHT = `${THINKING_PEEK_LINES * THINKING_LINE_HEIGHT}em`;
38
+
39
+ const THINKING_TEXT =
40
+ 'border-l border-agent-border/70 pl-2 text-xs leading-snug text-agent-muted-foreground';
41
+
42
+ function splitThinkingParagraphs(content: string): string[] {
43
+ return content.split(/\n{2,}/).filter((part) => part.length > 0);
44
+ }
27
45
 
28
46
  function ReasoningBody({
29
47
  content,
@@ -32,9 +50,16 @@ function ReasoningBody({
32
50
  content: string;
33
51
  showCursor?: boolean;
34
52
  }) {
53
+ const parts = splitThinkingParagraphs(content);
35
54
  return (
36
55
  <>
37
- <div className="whitespace-pre-wrap break-words">{content}</div>
56
+ <div className="space-y-1">
57
+ {parts.map((part, index) => (
58
+ <div key={index} className="whitespace-pre-wrap break-words">
59
+ {part}
60
+ </div>
61
+ ))}
62
+ </div>
38
63
  {showCursor ? (
39
64
  <span className="ml-0.5 inline-block h-3 w-[2px] animate-agent-cursor-blink bg-agent-muted-foreground/60 align-text-bottom" />
40
65
  ) : null}
@@ -64,83 +89,206 @@ function useLiveNow(enabled: boolean): number {
64
89
  return now;
65
90
  }
66
91
 
67
- function ProcessBlocks({
92
+ function ReasoningPane({
93
+ content,
94
+ showCursor,
95
+ clipped,
96
+ }: {
97
+ content: string;
98
+ showCursor?: boolean;
99
+ clipped?: boolean;
100
+ }) {
101
+ const scrollerRef = useRef<HTMLDivElement>(null);
102
+ useLayoutEffect(() => {
103
+ if (!clipped) return;
104
+ const el = scrollerRef.current;
105
+ if (!el) return;
106
+ el.scrollTop = el.scrollHeight;
107
+ }, [clipped, content]);
108
+ const body = (
109
+ <div className={THINKING_TEXT}>
110
+ <ReasoningBody content={content} showCursor={showCursor} />
111
+ </div>
112
+ );
113
+ if (!clipped) return body;
114
+ return (
115
+ <div
116
+ ref={scrollerRef}
117
+ className="overflow-y-auto overflow-anchor-none [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
118
+ style={{ maxHeight: THINKING_PEEK_HEIGHT }}
119
+ data-thinking-peek=""
120
+ data-testid="thinking-peek"
121
+ data-peek-lines={THINKING_PEEK_LINES}
122
+ >
123
+ {body}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function ThinkingFold({
129
+ content,
130
+ label,
131
+ showCursor,
132
+ mode,
133
+ defaultOpen,
134
+ }: {
135
+ content: string;
136
+ label: string;
137
+ showCursor?: boolean;
138
+ mode: ThinkingDisplayMode;
139
+ defaultOpen: boolean;
140
+ }) {
141
+ const [open, setOpen] = useState(defaultOpen);
142
+ useEffect(() => {
143
+ setOpen(defaultOpen);
144
+ }, [defaultOpen]);
145
+
146
+ if (mode === 'hidden') return null;
147
+
148
+ return (
149
+ <div className="space-y-1" data-testid="turn-thinking">
150
+ <button
151
+ type="button"
152
+ onClick={() => setOpen((value) => !value)}
153
+ className="flex min-w-0 items-center gap-1 py-0.5 text-left text-[11px] text-agent-muted-foreground/80 transition-colors hover:text-agent-foreground"
154
+ aria-expanded={open}
155
+ data-thinking-fold=""
156
+ >
157
+ <span className="truncate">{label}</span>
158
+ {open ? (
159
+ <LuChevronDown className="h-3 w-3 shrink-0" />
160
+ ) : (
161
+ <LuChevronRight className="h-3 w-3 shrink-0" />
162
+ )}
163
+ </button>
164
+ {open ? (
165
+ <ReasoningPane
166
+ content={content}
167
+ showCursor={showCursor}
168
+ clipped={mode === 'peek'}
169
+ />
170
+ ) : null}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function ProcessBlockItems({
68
176
  blocks,
69
177
  isStreaming,
70
178
  agents,
71
179
  chats,
72
180
  chatId,
181
+ onInspectTask,
182
+ orchestrationChildren,
183
+ thinkingDisplay,
184
+ keepFinishedThinkingOpen = false,
185
+ thinkingElapsedByIndex,
73
186
  }: {
74
187
  blocks: TurnBlock[];
75
188
  isStreaming: boolean;
76
189
  agents: LocalChatAgent[];
77
190
  chats: LocalChat[];
78
191
  chatId?: string | null;
192
+ onInspectTask?: (task: InspectTaskInput) => void;
193
+ orchestrationChildren?: ChildInfo[];
194
+ thinkingDisplay: ThinkingDisplayMode;
195
+ keepFinishedThinkingOpen?: boolean;
196
+ thinkingElapsedByIndex?: Array<number | undefined>;
79
197
  }) {
80
198
  const lastIndex = blocks.length - 1;
81
199
  return (
82
- <div className="space-y-1.5 border-l border-agent-border/70 pl-3">
200
+ <>
83
201
  {blocks.map((block, index) => {
84
202
  const isLast = index === lastIndex;
85
203
  if (block.type === 'reasoning') {
204
+ const isLive = isStreaming && isLast;
205
+ const elapsedMs =
206
+ thinkingElapsedByIndex?.[index]
207
+ ?? block.durationMs
208
+ ?? (isLive ? undefined : estimateReasoningDurationMs(block.content));
86
209
  return (
87
- <div
210
+ <ThinkingFold
88
211
  key={`reasoning-${index}`}
89
- className="text-xs leading-relaxed text-agent-muted-foreground"
90
- >
91
- <ReasoningBody content={block.content} showCursor={isStreaming && isLast} />
92
- </div>
212
+ content={block.content}
213
+ label={thinkingFoldLabel({
214
+ content: block.content,
215
+ isLive,
216
+ elapsedMs,
217
+ })}
218
+ showCursor={isLive}
219
+ mode={thinkingDisplay}
220
+ defaultOpen={isLive || keepFinishedThinkingOpen}
221
+ />
93
222
  );
94
223
  }
95
224
  if (block.type === 'tools') {
96
- return <ToolsFlow key={`tools-${index}`} actions={block.actions} compact />;
225
+ return (
226
+ <div key={`tools-${index}`} data-testid="turn-tools">
227
+ <ToolsFlow
228
+ actions={block.actions}
229
+ compact
230
+ agents={agents}
231
+ chatId={chatId}
232
+ onInspectTask={onInspectTask}
233
+ orchestrationChildren={orchestrationChildren}
234
+ />
235
+ </div>
236
+ );
97
237
  }
98
238
  return (
99
- <div
100
- key={`text-${index}`}
101
- className="markdown-content text-xs leading-relaxed text-agent-muted-foreground"
102
- >
103
- <Markdown agents={agents} chats={chats} chatId={chatId}>{block.content}</Markdown>
239
+ <div key={`text-${index}`} data-testid="turn-response">
240
+ <div>
241
+ <div className="markdown-content text-xs leading-relaxed text-agent-foreground">
242
+ <Markdown agents={agents} chats={chats} chatId={chatId}>{block.content}</Markdown>
243
+ </div>
244
+ {isStreaming && isLast ? (
245
+ <span className="ml-0.5 inline-block h-3.5 w-[3px] animate-agent-cursor-blink bg-agent-foreground/60 align-text-bottom" />
246
+ ) : null}
247
+ </div>
104
248
  </div>
105
249
  );
106
250
  })}
107
- </div>
251
+ </>
108
252
  );
109
253
  }
110
254
 
111
- function ThinkingPeek({
255
+ function ProcessBlocks({
112
256
  blocks,
113
257
  isStreaming,
258
+ agents,
259
+ chats,
260
+ chatId,
261
+ onInspectTask,
262
+ orchestrationChildren,
263
+ thinkingDisplay,
264
+ keepFinishedThinkingOpen,
265
+ thinkingElapsedByIndex,
114
266
  }: {
115
267
  blocks: TurnBlock[];
116
268
  isStreaming: boolean;
269
+ agents: LocalChatAgent[];
270
+ chats: LocalChat[];
271
+ chatId?: string | null;
272
+ onInspectTask?: (task: InspectTaskInput) => void;
273
+ orchestrationChildren?: ChildInfo[];
274
+ thinkingDisplay: ThinkingDisplayMode;
275
+ keepFinishedThinkingOpen: boolean;
276
+ thinkingElapsedByIndex?: Array<number | undefined>;
117
277
  }) {
118
- const scrollerRef = useRef<HTMLDivElement>(null);
119
- const reasoning = blocks.filter(
120
- (block): block is Extract<TurnBlock, { type: 'reasoning' }> =>
121
- block.type === 'reasoning' && block.content.trim().length > 0,
122
- );
123
- const sig = reasoning.map((block) => block.content.length).join(',');
124
- useLayoutEffect(() => {
125
- const el = scrollerRef.current;
126
- if (!el) return;
127
- el.scrollTop = el.scrollHeight;
128
- }, [sig]);
129
- const lastIndex = reasoning.length - 1;
130
278
  return (
131
- <div
132
- ref={scrollerRef}
133
- className="overflow-y-auto overflow-anchor-none [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden border-l border-agent-border/70 pl-3 text-xs leading-relaxed text-agent-muted-foreground"
134
- style={{ height: THINKING_PEEK_HEIGHT }}
135
- data-thinking-peek=""
136
- data-testid="thinking-peek"
137
- data-peek-lines={THINKING_PEEK_LINES}
138
- >
139
- {reasoning.map((block, index) => (
140
- <div key={`peek-reasoning-${index}`}>
141
- <ReasoningBody content={block.content} showCursor={isStreaming && index === lastIndex} />
142
- </div>
143
- ))}
279
+ <div className="space-y-1.5">
280
+ <ProcessBlockItems
281
+ blocks={blocks}
282
+ isStreaming={isStreaming}
283
+ agents={agents}
284
+ chats={chats}
285
+ chatId={chatId}
286
+ onInspectTask={onInspectTask}
287
+ orchestrationChildren={orchestrationChildren}
288
+ thinkingDisplay={thinkingDisplay}
289
+ keepFinishedThinkingOpen={keepFinishedThinkingOpen}
290
+ thinkingElapsedByIndex={thinkingElapsedByIndex}
291
+ />
144
292
  </div>
145
293
  );
146
294
  }
@@ -151,12 +299,15 @@ export function TurnProcessGroup({
151
299
  agents,
152
300
  chats,
153
301
  chatId,
302
+ onInspectTask,
303
+ orchestrationChildren,
154
304
  emptyFallback,
155
305
  streamingHint,
156
306
  startedAtMs,
157
307
  durationMs,
158
308
  thinkingDisplay: thinkingDisplayProp,
159
309
  showThinkingContent: showThinkingContentProp,
310
+ collapseWhenFinished = true,
160
311
  renderAnswer,
161
312
  }: {
162
313
  blocks: TurnBlock[];
@@ -164,6 +315,9 @@ export function TurnProcessGroup({
164
315
  agents: LocalChatAgent[];
165
316
  chats: LocalChat[];
166
317
  chatId?: string | null;
318
+ onInspectTask?: (task: InspectTaskInput) => void;
319
+ /** 本回合子代理:委派行据此跳转到子代理过程。 */
320
+ orchestrationChildren?: ChildInfo[];
167
321
  emptyFallback: ReactNode;
168
322
  /** Shown while streaming after a tools row and before the summary lands. */
169
323
  streamingHint?: ReactNode;
@@ -178,6 +332,11 @@ export function TurnProcessGroup({
178
332
  * Task process panel passes true so the inspector stays expanded.
179
333
  */
180
334
  showThinkingContent?: boolean;
335
+ /**
336
+ * After the stream ends, fold the work row to the summary line.
337
+ * Task inspector sets false so the replay stays open.
338
+ */
339
+ collapseWhenFinished?: boolean;
181
340
  renderAnswer: (block: Extract<TurnBlock, { type: 'text' }>, isLast: boolean) => ReactNode;
182
341
  }) {
183
342
  const preference = useThinkingDisplay();
@@ -187,15 +346,16 @@ export function TurnProcessGroup({
187
346
  showThinkingContentProp,
188
347
  );
189
348
  const showFullThinking = thinkingDisplay === 'full';
190
- const showThinkingPeekAllowed = thinkingDisplay === 'peek';
191
- const { process, answer } = splitTurnProcess(blocks);
192
- const [open, setOpen] = useState(
193
- () => showFullThinking && (isStreaming || answer.length === 0),
194
- );
195
- const wasStreamingRef = useRef(isStreaming);
196
- const isStreamingRef = useRef(isStreaming);
349
+ const { process, answer } = splitTurnProcess(blocks, { finalize: !isStreaming });
350
+ const defaultOpen = isStreaming
351
+ ? thinkingDisplay !== 'hidden'
352
+ : showFullThinking && !collapseWhenFinished;
353
+ const [userOpen, setUserOpen] = useState<boolean | null>(null);
354
+ const open = userOpen ?? defaultOpen;
355
+ useEffect(() => {
356
+ setUserOpen(null);
357
+ }, [isStreaming]);
197
358
  const answerLenRef = useRef(answer.length);
198
- isStreamingRef.current = isStreaming;
199
359
  answerLenRef.current = answer.length;
200
360
  const now = useLiveNow(isStreaming);
201
361
  const streamClockRef = useRef<number | undefined>(undefined);
@@ -216,76 +376,67 @@ export function TurnProcessGroup({
216
376
  : durationMs ?? lastLiveElapsedRef.current;
217
377
 
218
378
  const lastProcess = process[process.length - 1];
379
+ const lastIsReasoning = lastProcess?.type === 'reasoning';
380
+ const reasoningDurationsRef = useRef<number[]>([]);
219
381
  const reasoningClockRef = useRef<{ index: number; startedAt: number } | null>(null);
220
- if (isStreaming && lastProcess?.type === 'reasoning') {
382
+ if (isStreaming && lastIsReasoning) {
221
383
  const index = process.length - 1;
222
384
  if (reasoningClockRef.current?.index !== index) {
385
+ const prev = reasoningClockRef.current;
386
+ if (prev) {
387
+ reasoningDurationsRef.current[prev.index] = Math.max(0, now - prev.startedAt);
388
+ }
223
389
  reasoningClockRef.current = { index, startedAt: now };
224
390
  }
225
- } else if (!isStreaming) {
391
+ } else if (reasoningClockRef.current) {
392
+ const prev = reasoningClockRef.current;
393
+ reasoningDurationsRef.current[prev.index] = Math.max(0, now - prev.startedAt);
226
394
  reasoningClockRef.current = null;
227
395
  }
228
- const reasoningElapsedMs =
229
- isStreaming && lastProcess?.type === 'reasoning' && reasoningClockRef.current
230
- ? Math.max(0, now - reasoningClockRef.current.startedAt)
231
- : undefined;
232
-
233
- useEffect(() => {
234
- if (!showFullThinking) {
235
- setOpen(false);
236
- return;
237
- }
238
- if (isStreamingRef.current || answerLenRef.current === 0) setOpen(true);
239
- }, [showFullThinking]);
240
-
241
- useEffect(() => {
242
- const wasStreaming = wasStreamingRef.current;
243
- if (wasStreaming && !isStreaming && answer.length > 0) {
244
- setOpen(false);
245
- }
246
- if (!wasStreaming && isStreaming) {
247
- setOpen(showFullThinking);
396
+ const thinkingElapsedByIndex = process.map((block, index) => {
397
+ if (block.type !== 'reasoning') return undefined;
398
+ if (
399
+ isStreaming &&
400
+ lastIsReasoning &&
401
+ index === process.length - 1 &&
402
+ reasoningClockRef.current
403
+ ) {
404
+ return Math.max(0, now - reasoningClockRef.current.startedAt);
248
405
  }
249
- wasStreamingRef.current = isStreaming;
250
- }, [isStreaming, answer.length, showFullThinking]);
406
+ return reasoningDurationsRef.current[index];
407
+ });
251
408
 
252
409
  if (blocks.length === 0) return <>{emptyFallback}</>;
253
410
 
254
411
  const showToggle = process.length > 0;
255
412
  const showProcess = !showToggle || open;
256
- const showThinkingPeek =
257
- showThinkingPeekAllowed &&
258
- isStreaming &&
259
- !open &&
260
- showToggle &&
261
- processHasReasoning(process);
262
413
  const showStreamingHint =
263
414
  Boolean(streamingHint) && isStreaming && answer.length === 0 && !showToggle;
264
415
 
265
416
  return (
266
- <div className="space-y-1.5" data-turn-timeline>
417
+ <div className="space-y-1" data-turn-timeline>
267
418
  {showToggle && (
268
419
  <button
269
420
  type="button"
270
- onClick={() => setOpen((value) => !value)}
421
+ onClick={() => setUserOpen(!open)}
271
422
  className="flex w-full items-center gap-1 py-0.5 text-left text-xs text-agent-muted-foreground transition-colors hover:text-agent-foreground"
272
423
  aria-expanded={open}
273
424
  data-turn-process=""
425
+ data-testid="turn-process-toggle"
274
426
  data-open={open || undefined}
275
427
  >
276
- {open ? (
277
- <LuChevronDown className="h-3.5 w-3.5 shrink-0" />
278
- ) : (
279
- <LuChevronRight className="h-3.5 w-3.5 shrink-0" />
280
- )}
281
- <span className="truncate">
428
+ <span className="min-w-0 truncate">
282
429
  {processStatusLabel({
283
430
  process,
284
431
  isStreaming,
285
432
  elapsedMs,
286
- reasoningElapsedMs,
287
433
  })}
288
434
  </span>
435
+ {open ? (
436
+ <LuChevronDown className="h-3.5 w-3.5 shrink-0" />
437
+ ) : (
438
+ <LuChevronRight className="h-3.5 w-3.5 shrink-0" />
439
+ )}
289
440
  </button>
290
441
  )}
291
442
  {showProcess && (
@@ -295,11 +446,13 @@ export function TurnProcessGroup({
295
446
  agents={agents}
296
447
  chats={chats}
297
448
  chatId={chatId}
449
+ onInspectTask={onInspectTask}
450
+ orchestrationChildren={orchestrationChildren}
451
+ thinkingDisplay={thinkingDisplay}
452
+ keepFinishedThinkingOpen={!collapseWhenFinished}
453
+ thinkingElapsedByIndex={thinkingElapsedByIndex}
298
454
  />
299
455
  )}
300
- {showThinkingPeek && (
301
- <ThinkingPeek blocks={process} isStreaming={answer.length === 0} />
302
- )}
303
456
  {showStreamingHint ? streamingHint : null}
304
457
  {answer.map((block, index) => (
305
458
  <Fragment key={`answer-${index}`}>
@@ -20,9 +20,9 @@ import { useCopy } from './useCopy';
20
20
  * - **No edit button.** The cloud product's `editChatMessage` rewrites the
21
21
  * SSE turn in place — we don't have that machinery yet. Tracking under
22
22
  * phase 2c.
23
- * - **No mentioned-agent chip / inline `ref://` / `tool://` / `agent://`
24
- * parsing.** Agent-only mode doesn't surface multi-agent mentions or
25
- * pinned-ref shortcuts, so the markdown content is rendered directly.
23
+ * - **No inline `ref://` / `tool://` / `agent://` parsing.** Pinned-ref
24
+ * shortcuts stay cloud-only. `@智能体名` is a Markdown chip painted from
25
+ * the agent list + message text (refresh restores from content).
26
26
  *
27
27
  * What we KEEP from the old version (and why it matters for parity):
28
28
  * - Left-aligned bubble (not right-aligned). The product chat layout stacks
@@ -62,7 +62,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
62
62
 
63
63
  return (
64
64
  <motion.div
65
- className="group/message mb-2"
65
+ className="group/message"
66
66
  data-message-role="user"
67
67
  data-message-id={message.id}
68
68
  initial={{ opacity: 0, y: 20 }}
@@ -70,7 +70,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
70
70
  transition={{ duration: 0.3 }}
71
71
  >
72
72
  <div className="flex justify-start">
73
- <div className="mx-auto w-full max-w-[var(--chat-input-box-width)] rounded-lg border border-agent-border bg-agent-muted p-3 text-sm text-agent-foreground">
73
+ <div className="mx-auto w-full max-w-[var(--chat-input-box-width)] rounded-agent-lg border border-agent-border/80 bg-agent-muted/70 px-3 py-2 text-xs text-agent-foreground">
74
74
  <div className="relative">
75
75
  <div
76
76
  ref={contentRef}
@@ -121,7 +121,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
121
121
  </button>
122
122
  )}
123
123
 
124
- <div className="mt-2 flex items-center justify-between">
124
+ <div className="mt-1 flex items-center justify-between">
125
125
  <button
126
126
  type="button"
127
127
  onClick={() => void copy()}
@@ -140,7 +140,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
140
140
  <LuCopy className="h-3 w-3" />
141
141
  )}
142
142
  </button>
143
- <div className="text-[13px] text-agent-muted-foreground">
143
+ <div className="text-[11px] text-agent-muted-foreground">
144
144
  {message.createdAt
145
145
  ? getFriendlyDate(new Date(message.createdAt))
146
146
  : getFriendlyDate(new Date())}