@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,16 @@
1
1
  import { useMemo } from 'react';
2
- import { OrchestrationPlanCard } from '@steerable/agent-ui/cards';
2
+ import {
3
+ OrchestrationPlanCard,
4
+ ORCHESTRATION_DOT_CLASS,
5
+ ORCHESTRATION_STATUS_LABEL,
6
+ } from '@steerable/agent-ui/cards';
3
7
  import {
4
8
  childrenToCardModel,
9
+ findAgentForProfile,
10
+ shouldShowOrchestrationBoard,
5
11
  type ChildInfo,
6
12
  } from './orchestration-children-model';
13
+ import type { LocalChatAgent } from '@/lib/local-api';
7
14
 
8
15
  /**
9
16
  * OrchestrationChildrenCard — the desktop surface of the framework's P3.1
@@ -13,25 +20,82 @@ import {
13
20
  * payload is synthesized from spawn events, so there is no separate "plan"
14
21
  * step: the plan IS the set of spawned children.
15
22
  *
23
+ * Rows are overridden (`renderTaskRow`) to add the agent's colored initial
24
+ * next to its display name, matching the `@提及` chip in the composer and in
25
+ * user bubbles. Status labels and dot colors come from the card's own
26
+ * exported tables so the two surfaces cannot drift.
27
+ *
16
28
  * The mapping logic lives in `./orchestration-children-model` (pure,
17
29
  * runtime-dependency-free) so node-side tests do not pull the agent-ui
18
- * dist build.
30
+ * dist build. A single delegate child is not rendered here: the tool row
31
+ * already shows 「委派 ·」.
32
+ *
33
+ * **Currently unmounted.** Nothing renders this card: every child is a
34
+ * `delegate_subagent` call, and that tool row carries the same progress
35
+ * (执行中 → 已完成) plus the expandable task brief and the child's report.
36
+ * `ChildInfo` and the event fold stay in use — they feed the turn's agent
37
+ * badges — so putting the board back is one JSX line in `AssistantMessage`.
19
38
  */
20
39
 
21
40
  export type { ChildInfo } from './orchestration-children-model';
22
41
 
23
- export function OrchestrationChildrenCard({ children }: { children: ChildInfo[] }) {
42
+ export function OrchestrationChildrenCard({
43
+ children,
44
+ agents = [],
45
+ }: {
46
+ children: ChildInfo[];
47
+ agents?: ReadonlyArray<Pick<LocalChatAgent, 'id' | 'slug' | 'name' | 'color'>>;
48
+ }) {
24
49
  const { payload, taskStatuses } = useMemo(
25
50
  () => childrenToCardModel(children),
26
51
  [children],
27
52
  );
53
+ const delegateOnly = children.every((child) => Boolean(child.profile));
28
54
 
29
- if (children.length === 0) return null;
55
+ if (!shouldShowOrchestrationBoard(children)) return null;
30
56
  return (
31
57
  <OrchestrationPlanCard
32
58
  payload={payload}
33
59
  taskStatuses={taskStatuses}
34
- agentNameFor={(id) => id}
60
+ headerLabel={delegateOnly ? '子代理' : undefined}
61
+ hideMode={delegateOnly}
62
+ renderTaskRow={(task, status) => {
63
+ // 没有匹配的智能体:编排六件套的子代理(只有 lineage id)或被删掉的
64
+ // 智能体,显示画像原文。
65
+ const agent = findAgentForProfile(task.agentId, agents);
66
+ const displayName = agent?.name ?? task.agentId;
67
+ return (
68
+ <li className="flex items-start gap-2 text-sm">
69
+ <span
70
+ className={`mt-1.5 h-2 w-2 flex-shrink-0 rounded-full ${ORCHESTRATION_DOT_CLASS[status]}`}
71
+ aria-hidden
72
+ />
73
+ <div className="min-w-0 flex-1">
74
+ <div className="flex items-center gap-2">
75
+ <span
76
+ data-agent-color-dot=""
77
+ className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full text-[8px] font-semibold text-white"
78
+ style={{ backgroundColor: agent?.color || '#7c3aed' }}
79
+ aria-hidden
80
+ >
81
+ {displayName.trim()[0]?.toUpperCase() ?? 'A'}
82
+ </span>
83
+ <span className="font-medium text-[var(--agent-foreground,#111827)]">
84
+ {displayName}
85
+ </span>
86
+ <span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
87
+ {ORCHESTRATION_STATUS_LABEL[status]}
88
+ </span>
89
+ </div>
90
+ {task.prompt ? (
91
+ <p className="mt-0.5 line-clamp-2 text-xs text-[var(--agent-muted-foreground,#6b7280)]">
92
+ {task.prompt}
93
+ </p>
94
+ ) : null}
95
+ </div>
96
+ </li>
97
+ );
98
+ }}
35
99
  defaultExpanded
36
100
  />
37
101
  );
@@ -1,11 +1,12 @@
1
- import { useMemo, useState } from 'react';
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  LuCircle,
4
4
  LuCircleCheck,
5
5
  LuCircleDot,
6
6
  LuChevronDown,
7
- LuChevronRight,
8
7
  LuListChecks,
8
+ LuLoaderCircle,
9
+ LuX,
9
10
  } from 'react-icons/lu';
10
11
  import {
11
12
  summarizeTodos,
@@ -60,7 +61,7 @@ export function TodoItems({
60
61
  }) {
61
62
  return (
62
63
  <ol
63
- className={`m-0 list-none ${compact ? 'max-h-40 space-y-1 overflow-y-auto px-2.5 py-1.5' : 'space-y-1 px-2.5 py-1.5'}`}
64
+ className={`m-0 list-none ${compact ? 'max-h-48 space-y-1 overflow-y-auto px-2.5 py-1.5' : 'space-y-1 px-2.5 py-1.5'}`}
64
65
  >
65
66
  {todos.map((todo) => {
66
67
  const done = todo.status === 'completed';
@@ -70,7 +71,7 @@ export function TodoItems({
70
71
  key={todo.id}
71
72
  data-testid={`todo-item-${todo.id}`}
72
73
  data-status={todo.status}
73
- className="flex items-start gap-2 text-[12px] leading-relaxed"
74
+ className="flex items-start gap-2 text-[11px] leading-relaxed"
74
75
  >
75
76
  <StatusIcon status={todo.status} className="mt-[2px]" />
76
77
  <span
@@ -79,7 +80,7 @@ export function TodoItems({
79
80
  ? 'text-agent-muted-foreground line-through'
80
81
  : active
81
82
  ? 'font-medium text-agent-foreground'
82
- : 'text-agent-foreground'
83
+ : 'text-agent-muted-foreground/85'
83
84
  }
84
85
  >
85
86
  {todo.content}
@@ -92,46 +93,142 @@ export function TodoItems({
92
93
  }
93
94
 
94
95
  export function SessionTodoList({ todos }: { todos: SessionTodo[] }) {
95
- const [expanded, setExpanded] = useState(false);
96
+ const containerRef = useRef<HTMLDivElement>(null);
96
97
  const current = useMemo(() => currentTodoStep(todos), [todos]);
97
98
  const progressLabel = useMemo(() => todoProgressCopy(todos), [todos]);
99
+
100
+ const hasActive = useMemo(
101
+ () => todos.some((t) => t.status === 'in_progress'),
102
+ [todos],
103
+ );
104
+ const isAllCompleted = useMemo(() => {
105
+ const summary = summarizeTodos(todos);
106
+ return summary.total > 0 && summary.completed === summary.total;
107
+ }, [todos]);
108
+
109
+ // 执行中默认展开,全部完成默认折叠;允许用户手动点击收起或展开
110
+ const [userToggled, setUserToggled] = useState<boolean | null>(null);
111
+ const expanded = userToggled ?? hasActive;
112
+
113
+ // 当任务全部执行完毕时,自动收起为紧凑胶囊
114
+ useEffect(() => {
115
+ if (isAllCompleted) {
116
+ setUserToggled(false);
117
+ }
118
+ }, [isAllCompleted]);
119
+
120
+ // 当新出现执行中的任务时,恢复默认展开
121
+ useEffect(() => {
122
+ if (hasActive) {
123
+ setUserToggled(null);
124
+ }
125
+ }, [hasActive]);
126
+
127
+ // 任务全部完成后,如果展开了浮层,点击外部区域时自动收起
128
+ useEffect(() => {
129
+ if (!expanded || !isAllCompleted) return;
130
+ const handlePointerDown = (event: PointerEvent) => {
131
+ if (
132
+ containerRef.current &&
133
+ !containerRef.current.contains(event.target as Node)
134
+ ) {
135
+ setUserToggled(false);
136
+ }
137
+ };
138
+ document.addEventListener('pointerdown', handlePointerDown);
139
+ return () => document.removeEventListener('pointerdown', handlePointerDown);
140
+ }, [expanded, isAllCompleted]);
98
141
 
99
142
  return (
100
143
  <div
101
- className="mb-1 overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-sm"
144
+ ref={containerRef}
145
+ className="relative inline-flex items-center"
102
146
  data-testid="session-todo-list"
103
147
  >
104
148
  <button
105
149
  type="button"
106
- onClick={() => setExpanded((value) => !value)}
107
- className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left"
150
+ onClick={() => setUserToggled(!expanded)}
151
+ className={[
152
+ 'flex h-6 max-w-[240px] sm:max-w-[320px] items-center gap-1.5 rounded-full px-2 text-left text-[11px] transition-all duration-200 select-none shadow-2xs',
153
+ isAllCompleted
154
+ ? 'border border-emerald-500/25 bg-emerald-500/[0.04] text-emerald-700 dark:text-emerald-300 hover:bg-emerald-500/[0.08]'
155
+ : hasActive
156
+ ? 'border border-blue-500/30 bg-blue-500/[0.06] text-blue-700 dark:text-blue-300 hover:bg-blue-500/10'
157
+ : 'border border-agent-border/80 bg-agent-canvas text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
158
+ ].join(' ')}
108
159
  aria-expanded={expanded}
109
160
  >
110
- {expanded ? (
111
- <LuChevronDown className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
161
+ {isAllCompleted ? (
162
+ <LuCircleCheck className="h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
163
+ ) : hasActive ? (
164
+ <LuLoaderCircle className="h-3.5 w-3.5 shrink-0 animate-spin text-blue-600 dark:text-blue-400" />
165
+ ) : current ? (
166
+ <StatusIcon status={current.status} />
112
167
  ) : (
113
- <LuChevronRight className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
114
- )}
115
- {expanded || !current ? (
116
168
  <LuListChecks className="h-3.5 w-3.5 shrink-0 text-agent-foreground" />
117
- ) : (
118
- <StatusIcon status={current.status} />
119
169
  )}
120
- <span className="min-w-0 flex-1 truncate text-[12px] font-medium text-agent-foreground">
121
- {expanded ? '任务清单' : (current?.content ?? '任务清单')}
170
+ <span className="min-w-0 flex-1 truncate font-medium">
171
+ {isAllCompleted ? '任务清单' : (current?.content ?? '任务清单')}
122
172
  </span>
123
173
  {progressLabel ? (
124
174
  <span
125
- className="shrink-0 rounded-full bg-agent-muted px-1.5 py-0.5 text-[10px] font-medium text-agent-muted-foreground"
175
+ className={`shrink-0 rounded-full px-1.5 py-0.2 text-[10px] font-medium ${
176
+ isAllCompleted
177
+ ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
178
+ : hasActive
179
+ ? 'bg-blue-500/15 text-blue-700 dark:text-blue-300'
180
+ : 'bg-agent-muted text-agent-muted-foreground'
181
+ }`}
126
182
  data-testid="todo-progress"
127
183
  >
128
184
  {progressLabel}
129
185
  </span>
130
186
  ) : null}
187
+ <LuChevronDown
188
+ className={`h-3 w-3 shrink-0 text-current transition-transform duration-200 ${
189
+ expanded ? 'rotate-180' : ''
190
+ }`}
191
+ />
131
192
  </button>
132
- {expanded ? <TodoItems todos={todos} compact /> : null}
193
+
194
+ {expanded && (
195
+ <div
196
+ className="absolute bottom-full right-0 mb-1.5 w-72 sm:w-80 overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas/98 backdrop-blur-md shadow-xl z-40 animate-in fade-in slide-in-from-bottom-1 duration-150"
197
+ >
198
+ <div className="flex items-center justify-between border-b border-agent-border/60 bg-agent-muted/30 px-2.5 py-1.5">
199
+ <div className="flex items-center gap-1.5 text-[11px] font-medium text-agent-foreground">
200
+ <LuListChecks className="h-3.5 w-3.5 text-agent-foreground" />
201
+ <span>任务清单</span>
202
+ </div>
203
+ <div className="flex items-center gap-1.5">
204
+ {progressLabel && (
205
+ <span
206
+ className={`rounded-full px-1.5 py-0.2 text-[10px] font-medium ${
207
+ isAllCompleted
208
+ ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
209
+ : 'bg-blue-500/10 text-blue-600 dark:text-blue-400'
210
+ }`}
211
+ >
212
+ {progressLabel}
213
+ </span>
214
+ )}
215
+ <button
216
+ type="button"
217
+ onClick={() => setUserToggled(false)}
218
+ className="flex h-5 w-5 items-center justify-center rounded text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
219
+ title="收起清单"
220
+ aria-label="收起清单"
221
+ >
222
+ <LuX className="h-3 w-3" />
223
+ </button>
224
+ </div>
225
+ </div>
226
+ <TodoItems todos={todos} compact />
227
+ </div>
228
+ )}
133
229
  </div>
134
230
  );
135
231
  }
136
232
 
137
233
  export default SessionTodoList;
234
+
@@ -1,3 +1,5 @@
1
+ import { LuSparkles } from 'react-icons/lu';
2
+
1
3
  /**
2
4
  * WorkBuddy-style follow-up chips: next-turn user inputs rendered under the
3
5
  * latest assistant reply. Source is the model's judgment of `[next_steps]`
@@ -15,18 +17,23 @@ export function SuggestedReplies({
15
17
 
16
18
  return (
17
19
  <div
18
- className="mx-auto flex w-full max-w-[var(--chat-input-box-width)] flex-col items-start gap-1 px-1 pt-0.5 text-[12px]"
20
+ className="mx-auto mt-2.5 flex w-full max-w-[var(--chat-input-box-width)] flex-wrap items-center gap-1.5 px-1 pb-0.5"
19
21
  data-testid="suggested-replies"
20
22
  >
23
+ <div className="mr-0.5 inline-flex items-center gap-1 text-[11px] font-medium text-agent-muted-foreground/70 select-none">
24
+ <LuSparkles className="h-3 w-3 text-agent-muted-foreground/80" />
25
+ <span>建议回复</span>
26
+ </div>
21
27
  {suggestions.map((text) => (
22
28
  <button
23
29
  key={text}
24
30
  type="button"
25
31
  data-testid="suggested-reply"
26
32
  onClick={() => onSelect(text)}
27
- className="max-w-full rounded-full border border-agent-border bg-agent-muted/40 px-2 py-0.5 text-left text-[12px] leading-[1.45] text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
33
+ title={text}
34
+ className="inline-flex max-w-[280px] sm:max-w-[340px] items-center rounded-full border border-agent-border/80 bg-agent-canvas px-2.5 py-1 text-left text-[12px] leading-[1.4] text-agent-muted-foreground transition-all duration-150 hover:border-agent-foreground/30 hover:bg-agent-foreground/5 hover:text-agent-foreground shadow-2xs active:scale-[0.98]"
28
35
  >
29
- {text}
36
+ <span className="truncate">{text}</span>
30
37
  </button>
31
38
  ))}
32
39
  </div>
@@ -34,3 +41,4 @@ export function SuggestedReplies({
34
41
  }
35
42
 
36
43
  export default SuggestedReplies;
44
+
@@ -1,18 +1,34 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { LuArrowDown, LuLoaderCircle, LuTerminal } from 'react-icons/lu';
3
3
  import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
4
- import { getTaskProcess } from '@/lib/local-api';
4
+ import { getChildProcess, getTaskProcess } from '@/lib/local-api';
5
5
  import { getElectronBridge } from '@/lib/electron-bridge';
6
6
  import { Markdown } from '@/components/chat/Markdown';
7
7
  import { TurnProcessGroup } from '@/components/chat/TurnProcessGroup';
8
- import { parseTurnBlocks, type TurnBlock } from '@/components/chat/turn-timeline';
8
+ import {
9
+ parseTurnBlocks,
10
+ timelineContentSignature,
11
+ type TurnBlock,
12
+ } from '@/components/chat/turn-timeline';
9
13
 
10
14
  export interface InspectedTask {
11
15
  id: string;
12
16
  chatId: string;
13
17
  title: string;
18
+ /**
19
+ * 子代理过程:按它自己的 durable record 读,而不是任务表。设了它就走
20
+ * `GET /child-process`,`live` 期间轮询(子回合边跑边写 record)。
21
+ */
22
+ recordId?: string;
23
+ /** 目标仍在运行——决定轮询与标题状态。 */
24
+ live?: boolean;
14
25
  }
15
26
 
27
+ const CHILD_POLL_INTERVAL_MS = 1500;
28
+
29
+ /** 连续这么多次轮询读到同一份内容就认为子代理跑完了(≈60s)。 */
30
+ const CHILD_IDLE_POLLS_UNTIL_DONE = 40;
31
+
16
32
  const NEAR_BOTTOM_THRESHOLD_PX = 100;
17
33
 
18
34
  /**
@@ -74,27 +90,46 @@ export function TaskProcessPanel({
74
90
 
75
91
  // live 增量在 paint 前钉住底部。smooth scroll 会先露出一帧半截内容再往下
76
92
  // 滑,Windows 经典滚动条就会上下跳。
77
- const timelineSig = blocks
78
- .map((block) =>
79
- block.type === 'tools' ? `t${block.actions.length}` : `c${block.content.length}`,
80
- )
81
- .join('|');
93
+ const timelineSig = timelineContentSignature(blocks);
82
94
  useLayoutEffect(() => {
83
95
  if (!isAtBottomRef.current) return;
84
96
  scrollToBottom('auto');
85
97
  }, [timelineSig, live, scrollToBottom]);
86
98
 
99
+ const childRecordId = inspected.recordId;
100
+
87
101
  useEffect(() => {
88
102
  let cancelled = false;
103
+ let timer: number | undefined;
104
+ let lastSig: string | null = null;
105
+ let idlePolls = 0;
89
106
  setLoading(true);
90
107
  setError(null);
91
- void (async () => {
108
+ const load = async () => {
92
109
  try {
93
- const snapshot = await getTaskProcess(inspected.id);
94
- if (cancelled) return;
95
- setBlocks(parseTurnBlocks(snapshot.timeline) ?? []);
96
- setLive(snapshot.live);
97
- setStale(snapshot.stale);
110
+ if (childRecordId) {
111
+ const snapshot = await getChildProcess(childRecordId);
112
+ if (cancelled) return;
113
+ const next = parseTurnBlocks(snapshot.timeline) ?? [];
114
+ const sig = timelineContentSignature(next);
115
+ idlePolls = sig === lastSig ? idlePolls + 1 : 0;
116
+ lastSig = sig;
117
+ setBlocks(next);
118
+ // 子代理跑完不会再有事件通知这个面板(它的 live 是点开那刻的快照),
119
+ // 所以用「record 不再变化」当结束信号:停轮询、标题转「已结束」。
120
+ if (timer !== undefined && idlePolls >= CHILD_IDLE_POLLS_UNTIL_DONE) {
121
+ window.clearInterval(timer);
122
+ timer = undefined;
123
+ setLive(false);
124
+ }
125
+ setStale(false);
126
+ } else {
127
+ const snapshot = await getTaskProcess(inspected.id);
128
+ if (cancelled) return;
129
+ setBlocks(parseTurnBlocks(snapshot.timeline) ?? []);
130
+ setLive(snapshot.live);
131
+ setStale(snapshot.stale);
132
+ }
98
133
  } catch (err) {
99
134
  if (cancelled) return;
100
135
  setError(err instanceof Error ? err.message : String(err));
@@ -102,13 +137,22 @@ export function TaskProcessPanel({
102
137
  } finally {
103
138
  if (!cancelled) setLoading(false);
104
139
  }
105
- })();
140
+ };
141
+ if (childRecordId) setLive(Boolean(inspected.live));
142
+ void load();
143
+ // 子代理没有 task-process 广播通道(它不是任务),运行中靠轮询它的
144
+ // record 追进度;终态打开只读一次。
145
+ if (childRecordId && inspected.live) {
146
+ timer = window.setInterval(() => void load(), CHILD_POLL_INTERVAL_MS);
147
+ }
106
148
  return () => {
107
149
  cancelled = true;
150
+ if (timer !== undefined) window.clearInterval(timer);
108
151
  };
109
- }, [inspected.id]);
152
+ }, [inspected.id, childRecordId, inspected.live]);
110
153
 
111
154
  useEffect(() => {
155
+ if (childRecordId) return;
112
156
  const bridge = getElectronBridge();
113
157
  if (!bridge?.onTaskProcess) return;
114
158
  return bridge.onTaskProcess((payload) => {
@@ -121,13 +165,14 @@ export function TaskProcessPanel({
121
165
  setLive(payload.live);
122
166
  if (payload.live) setStale(false);
123
167
  });
124
- }, [inspected.id]);
168
+ }, [inspected.id, childRecordId]);
125
169
 
126
170
  const statusLabel = live
127
171
  ? '正在推理'
128
172
  : stale
129
173
  ? '进程已中断(表上仍是运行中)'
130
174
  : '已结束';
175
+ const panelTitle = `${childRecordId ? '子代理推理' : '后台推理'} · ${statusLabel}`;
131
176
 
132
177
  return (
133
178
  <div
@@ -135,7 +180,7 @@ export function TaskProcessPanel({
135
180
  data-testid="task-process-panel"
136
181
  >
137
182
  <DockPanelHeader
138
- title={`后台推理 · ${statusLabel}`}
183
+ title={panelTitle}
139
184
  onClose={onClose}
140
185
  closeLabel="关闭过程面板"
141
186
  actions={
@@ -7,6 +7,8 @@ import {
7
7
  } from '@/lib/show-thinking-content';
8
8
  import { Markdown } from './Markdown';
9
9
  import { ToolsFlow } from './ExecutedActionsCard';
10
+ import type { InspectTaskInput } from './executed-actions-model';
11
+ import type { ChildInfo } from './OrchestrationChildrenCard';
10
12
  import {
11
13
  estimateReasoningDurationMs,
12
14
  processStatusLabel,
@@ -176,6 +178,8 @@ function ProcessBlockItems({
176
178
  agents,
177
179
  chats,
178
180
  chatId,
181
+ onInspectTask,
182
+ orchestrationChildren,
179
183
  thinkingDisplay,
180
184
  keepFinishedThinkingOpen = false,
181
185
  thinkingElapsedByIndex,
@@ -185,6 +189,8 @@ function ProcessBlockItems({
185
189
  agents: LocalChatAgent[];
186
190
  chats: LocalChat[];
187
191
  chatId?: string | null;
192
+ onInspectTask?: (task: InspectTaskInput) => void;
193
+ orchestrationChildren?: ChildInfo[];
188
194
  thinkingDisplay: ThinkingDisplayMode;
189
195
  keepFinishedThinkingOpen?: boolean;
190
196
  thinkingElapsedByIndex?: Array<number | undefined>;
@@ -218,7 +224,14 @@ function ProcessBlockItems({
218
224
  if (block.type === 'tools') {
219
225
  return (
220
226
  <div key={`tools-${index}`} data-testid="turn-tools">
221
- <ToolsFlow actions={block.actions} compact />
227
+ <ToolsFlow
228
+ actions={block.actions}
229
+ compact
230
+ agents={agents}
231
+ chatId={chatId}
232
+ onInspectTask={onInspectTask}
233
+ orchestrationChildren={orchestrationChildren}
234
+ />
222
235
  </div>
223
236
  );
224
237
  }
@@ -245,6 +258,8 @@ function ProcessBlocks({
245
258
  agents,
246
259
  chats,
247
260
  chatId,
261
+ onInspectTask,
262
+ orchestrationChildren,
248
263
  thinkingDisplay,
249
264
  keepFinishedThinkingOpen,
250
265
  thinkingElapsedByIndex,
@@ -254,6 +269,8 @@ function ProcessBlocks({
254
269
  agents: LocalChatAgent[];
255
270
  chats: LocalChat[];
256
271
  chatId?: string | null;
272
+ onInspectTask?: (task: InspectTaskInput) => void;
273
+ orchestrationChildren?: ChildInfo[];
257
274
  thinkingDisplay: ThinkingDisplayMode;
258
275
  keepFinishedThinkingOpen: boolean;
259
276
  thinkingElapsedByIndex?: Array<number | undefined>;
@@ -266,6 +283,8 @@ function ProcessBlocks({
266
283
  agents={agents}
267
284
  chats={chats}
268
285
  chatId={chatId}
286
+ onInspectTask={onInspectTask}
287
+ orchestrationChildren={orchestrationChildren}
269
288
  thinkingDisplay={thinkingDisplay}
270
289
  keepFinishedThinkingOpen={keepFinishedThinkingOpen}
271
290
  thinkingElapsedByIndex={thinkingElapsedByIndex}
@@ -280,6 +299,8 @@ export function TurnProcessGroup({
280
299
  agents,
281
300
  chats,
282
301
  chatId,
302
+ onInspectTask,
303
+ orchestrationChildren,
283
304
  emptyFallback,
284
305
  streamingHint,
285
306
  startedAtMs,
@@ -294,6 +315,9 @@ export function TurnProcessGroup({
294
315
  agents: LocalChatAgent[];
295
316
  chats: LocalChat[];
296
317
  chatId?: string | null;
318
+ onInspectTask?: (task: InspectTaskInput) => void;
319
+ /** 本回合子代理:委派行据此跳转到子代理过程。 */
320
+ orchestrationChildren?: ChildInfo[];
297
321
  emptyFallback: ReactNode;
298
322
  /** Shown while streaming after a tools row and before the summary lands. */
299
323
  streamingHint?: ReactNode;
@@ -422,6 +446,8 @@ export function TurnProcessGroup({
422
446
  agents={agents}
423
447
  chats={chats}
424
448
  chatId={chatId}
449
+ onInspectTask={onInspectTask}
450
+ orchestrationChildren={orchestrationChildren}
425
451
  thinkingDisplay={thinkingDisplay}
426
452
  keepFinishedThinkingOpen={!collapseWhenFinished}
427
453
  thinkingElapsedByIndex={thinkingElapsedByIndex}
@@ -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-1"
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 px-2.5 py-2 text-xs 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}