@steerable/agent-shell-web 0.6.49 → 0.6.51

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.49",
3
+ "version": "0.6.51",
4
4
  "type": "module",
5
5
  "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
6
6
  "license": "Apache-2.0",
@@ -45,8 +45,8 @@
45
45
  "react-router-dom": "^6.30.0",
46
46
  "rehype-raw": "^7.0.0",
47
47
  "remark-gfm": "^4.0.1",
48
- "@steerable/agent-protocol": "0.6.49",
49
- "@steerable/agent-ui": "0.6.49"
48
+ "@steerable/agent-protocol": "0.6.51",
49
+ "@steerable/agent-ui": "0.6.51"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tailwindcss/vite": "^4.3.0",
@@ -59,7 +59,7 @@
59
59
  "typescript": "^5.8.3",
60
60
  "vite": "^5.4.21",
61
61
  "vitest": "^3.2.4",
62
- "@steerable/pack-sdk": "0.6.49"
62
+ "@steerable/pack-sdk": "0.6.51"
63
63
  },
64
64
  "scripts": {
65
65
  "gen": "echo agent-shell-web",
@@ -61,7 +61,7 @@ export interface ExecutedAction {
61
61
  sandbox?: { backend?: string; enforcement: string };
62
62
  }
63
63
 
64
- interface ExecutedActionsCardProps {
64
+ export interface ExecutedActionsCardProps {
65
65
  actions: ExecutedAction[];
66
66
  /** Drop the outer card chrome when nested in a turn-process group. */
67
67
  compact?: boolean;
@@ -20,6 +20,8 @@ import type { TurnFile } from './turn-files';
20
20
  import type { LlmSpeedSnapshot } from './process-status';
21
21
  import { SessionTodoList } from './SessionTodoList';
22
22
  import { resolveLatestSessionTodos } from './todo-list-model';
23
+ import { SessionOrchestrationFlow } from './SessionOrchestrationFlow';
24
+ import { resolveLatestSessionOrchestrationFlow } from './orchestration-flow-model';
23
25
  import {
24
26
  appendAttachmentRefs,
25
27
  collectImageAttachments,
@@ -327,6 +329,28 @@ export function LocalChatPanel({
327
329
  ],
328
330
  );
329
331
 
332
+ const sessionOrchestrationFlow = useMemo(
333
+ () =>
334
+ resolveLatestSessionOrchestrationFlow({
335
+ messages,
336
+ executedActionsByMessageId,
337
+ currentTurnActions,
338
+ currentTurnChildren,
339
+ orchestrationChildrenByMessageId,
340
+ timelineByMessageId,
341
+ currentTurnTimeline,
342
+ }),
343
+ [
344
+ messages,
345
+ executedActionsByMessageId,
346
+ currentTurnActions,
347
+ currentTurnChildren,
348
+ orchestrationChildrenByMessageId,
349
+ timelineByMessageId,
350
+ currentTurnTimeline,
351
+ ],
352
+ );
353
+
330
354
  // Sync `chat-input-box-width` CSS var so bubbles match the input width
331
355
  // (mirrors the rendered input box width so message bubbles + input visually align in a column).
332
356
  const containerRef = useRef<HTMLDivElement>(null);
@@ -390,7 +414,20 @@ export function LocalChatPanel({
390
414
  onOpenSettings={onOpenSettings}
391
415
  toolbarExtras={inputToolbarExtras}
392
416
  leadingChrome={inputLeadingChrome}
393
- trailingChrome={sessionTodos ? <SessionTodoList todos={sessionTodos} /> : undefined}
417
+ trailingChrome={
418
+ sessionOrchestrationFlow || sessionTodos ? (
419
+ <div className="flex items-center gap-1.5 min-w-0">
420
+ {sessionOrchestrationFlow ? (
421
+ <SessionOrchestrationFlow
422
+ flow={sessionOrchestrationFlow}
423
+ chatId={chatId}
424
+ onInspectTask={onInspectTask}
425
+ />
426
+ ) : null}
427
+ {sessionTodos ? <SessionTodoList todos={sessionTodos} /> : null}
428
+ </div>
429
+ ) : undefined
430
+ }
394
431
  mode={mode}
395
432
  onModeChange={onModeChange}
396
433
  execPolicy={execPolicy}
@@ -0,0 +1,474 @@
1
+ import { useEffect, useState } from 'react';
2
+ import {
3
+ LuCircle,
4
+ LuCircleCheck,
5
+ LuCircleDot,
6
+ LuCirclePause,
7
+ LuCircleX,
8
+ LuChevronDown,
9
+ LuChevronRight,
10
+ LuGitFork,
11
+ LuLoaderCircle,
12
+ LuBot,
13
+ LuCheck,
14
+ LuMessageSquare,
15
+ LuExternalLink,
16
+ } from 'react-icons/lu';
17
+ import {
18
+ type OrchestrationFlowData,
19
+ type OrchestrationNodeStatus,
20
+ } from './orchestration-flow-model';
21
+ import type { InspectTaskInput } from './executed-actions-model';
22
+
23
+ function NodeStatusIcon({
24
+ status,
25
+ className = '',
26
+ }: {
27
+ status: OrchestrationNodeStatus;
28
+ className?: string;
29
+ }) {
30
+ const cls = `h-3.5 w-3.5 shrink-0 ${className}`.trim();
31
+ switch (status) {
32
+ case 'completed':
33
+ return <LuCircleCheck className={`${cls} text-emerald-600 dark:text-emerald-400`} />;
34
+ case 'running':
35
+ return <LuCircleDot className={`${cls} animate-pulse text-blue-600 dark:text-blue-400`} />;
36
+ case 'interrupted':
37
+ return <LuCirclePause className={`${cls} text-amber-600 dark:text-amber-400`} />;
38
+ case 'closed':
39
+ return <LuCircleX className={`${cls} text-agent-muted-foreground`} />;
40
+ case 'failed':
41
+ return <LuCircleX className={`${cls} text-agent-destructive`} />;
42
+ case 'pending':
43
+ default:
44
+ return <LuCircle className={`${cls} text-agent-muted-foreground/60`} />;
45
+ }
46
+ }
47
+
48
+ function statusBadgeText(status: OrchestrationNodeStatus): string {
49
+ switch (status) {
50
+ case 'completed':
51
+ return '已完成';
52
+ case 'running':
53
+ return '执行中';
54
+ case 'interrupted':
55
+ return '已暂停';
56
+ case 'closed':
57
+ return '已关闭';
58
+ case 'failed':
59
+ return '失败';
60
+ case 'pending':
61
+ default:
62
+ return '等待中';
63
+ }
64
+ }
65
+
66
+ function statusBadgeStyle(status: OrchestrationNodeStatus): string {
67
+ switch (status) {
68
+ case 'completed':
69
+ return 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 border-emerald-500/20';
70
+ case 'running':
71
+ return 'bg-blue-500/10 text-blue-700 dark:text-blue-300 border-blue-500/20';
72
+ case 'interrupted':
73
+ return 'bg-amber-500/10 text-amber-700 dark:text-amber-300 border-amber-500/20';
74
+ case 'closed':
75
+ return 'bg-agent-muted text-agent-muted-foreground border-agent-border';
76
+ case 'failed':
77
+ return 'bg-agent-destructive/10 text-agent-destructive border-agent-destructive/20';
78
+ case 'pending':
79
+ default:
80
+ return 'bg-agent-muted text-agent-muted-foreground border-agent-border';
81
+ }
82
+ }
83
+
84
+ function ForkLines({ count }: { count: number }) {
85
+ if (count <= 1) {
86
+ return (
87
+ <div className="flex h-5 w-full items-center justify-center">
88
+ <svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
89
+ <line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
90
+ <polygon points="5,13 8,19 11,13" fill="currentColor" />
91
+ </svg>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ if (count === 2) {
97
+ return (
98
+ <div className="relative h-6 w-full">
99
+ <svg
100
+ className="absolute inset-0 h-full w-full text-violet-500/70"
101
+ viewBox="0 0 100 24"
102
+ preserveAspectRatio="none"
103
+ fill="none"
104
+ >
105
+ <path
106
+ d="M 50 0 L 50 12 M 25 12 L 75 12 M 25 12 L 25 18 M 75 12 L 75 18"
107
+ stroke="currentColor"
108
+ strokeWidth="1.5"
109
+ strokeLinecap="round"
110
+ strokeLinejoin="round"
111
+ vectorEffect="non-scaling-stroke"
112
+ />
113
+ </svg>
114
+ <div className="absolute top-[17px] left-[25%] -translate-x-1/2">
115
+ <svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
116
+ <polygon points="1,1 5,7 9,1" fill="currentColor" />
117
+ </svg>
118
+ </div>
119
+ <div className="absolute top-[17px] left-[75%] -translate-x-1/2">
120
+ <svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
121
+ <polygon points="1,1 5,7 9,1" fill="currentColor" />
122
+ </svg>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ if (count === 3) {
129
+ return (
130
+ <div className="relative h-6 w-full">
131
+ <svg
132
+ className="absolute inset-0 h-full w-full text-violet-500/70"
133
+ viewBox="0 0 100 24"
134
+ preserveAspectRatio="none"
135
+ fill="none"
136
+ >
137
+ <path
138
+ d="M 50 0 L 50 18 M 17 12 L 83 12 M 17 12 L 17 18 M 83 12 L 83 18"
139
+ stroke="currentColor"
140
+ strokeWidth="1.5"
141
+ strokeLinecap="round"
142
+ strokeLinejoin="round"
143
+ vectorEffect="non-scaling-stroke"
144
+ />
145
+ </svg>
146
+ <div className="absolute top-[17px] left-[17%] -translate-x-1/2">
147
+ <svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
148
+ <polygon points="1,1 5,7 9,1" fill="currentColor" />
149
+ </svg>
150
+ </div>
151
+ <div className="absolute top-[17px] left-[50%] -translate-x-1/2">
152
+ <svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
153
+ <polygon points="1,1 5,7 9,1" fill="currentColor" />
154
+ </svg>
155
+ </div>
156
+ <div className="absolute top-[17px] left-[83%] -translate-x-1/2">
157
+ <svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
158
+ <polygon points="1,1 5,7 9,1" fill="currentColor" />
159
+ </svg>
160
+ </div>
161
+ </div>
162
+ );
163
+ }
164
+
165
+ return (
166
+ <div className="flex h-5 w-full items-center justify-center">
167
+ <svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
168
+ <line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
169
+ <polygon points="5,13 8,19 11,13" fill="currentColor" />
170
+ </svg>
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function JoinLines({ count }: { count: number }) {
176
+ if (count <= 1) {
177
+ return (
178
+ <div className="flex h-5 w-full items-center justify-center">
179
+ <svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
180
+ <line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
181
+ <polygon points="5,13 8,19 11,13" fill="currentColor" />
182
+ </svg>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ if (count === 2) {
188
+ return (
189
+ <div className="relative h-6 w-full">
190
+ <svg
191
+ className="absolute inset-0 h-full w-full text-violet-500/70"
192
+ viewBox="0 0 100 24"
193
+ preserveAspectRatio="none"
194
+ fill="none"
195
+ >
196
+ <path
197
+ d="M 25 0 L 25 12 M 75 0 L 75 12 M 25 12 L 75 12 M 50 12 L 50 18"
198
+ stroke="currentColor"
199
+ strokeWidth="1.5"
200
+ strokeLinecap="round"
201
+ strokeLinejoin="round"
202
+ vectorEffect="non-scaling-stroke"
203
+ />
204
+ </svg>
205
+ <div className="absolute top-[17px] left-[50%] -translate-x-1/2">
206
+ <svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
207
+ <polygon points="1,1 5,7 9,1" fill="currentColor" />
208
+ </svg>
209
+ </div>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ if (count === 3) {
215
+ return (
216
+ <div className="relative h-6 w-full">
217
+ <svg
218
+ className="absolute inset-0 h-full w-full text-violet-500/70"
219
+ viewBox="0 0 100 24"
220
+ preserveAspectRatio="none"
221
+ fill="none"
222
+ >
223
+ <path
224
+ d="M 17 0 L 17 12 M 50 0 L 50 18 M 83 0 L 83 12 M 17 12 L 83 12 M 50 12 L 50 18"
225
+ stroke="currentColor"
226
+ strokeWidth="1.5"
227
+ strokeLinecap="round"
228
+ strokeLinejoin="round"
229
+ vectorEffect="non-scaling-stroke"
230
+ />
231
+ </svg>
232
+ <div className="absolute top-[17px] left-[50%] -translate-x-1/2">
233
+ <svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
234
+ <polygon points="1,1 5,7 9,1" fill="currentColor" />
235
+ </svg>
236
+ </div>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ return (
242
+ <div className="flex h-5 w-full items-center justify-center">
243
+ <svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
244
+ <line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
245
+ <polygon points="5,13 8,19 11,13" fill="currentColor" />
246
+ </svg>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ export function OrchestrationFlowCard({
252
+ flow,
253
+ chatId,
254
+ onInspectTask,
255
+ className = '',
256
+ defaultExpanded = false,
257
+ }: {
258
+ flow: OrchestrationFlowData;
259
+ chatId?: string | null;
260
+ onInspectTask?: (task: InspectTaskInput) => void;
261
+ className?: string;
262
+ defaultExpanded?: boolean;
263
+ }) {
264
+ const { nodes, isAllCompleted, hasActive, summaryCopy } = flow;
265
+
266
+ const [userToggled, setUserToggled] = useState<boolean | null>(defaultExpanded ? true : null);
267
+ const expanded = userToggled ?? defaultExpanded;
268
+
269
+ useEffect(() => {
270
+ if (isAllCompleted && defaultExpanded) {
271
+ setUserToggled(false);
272
+ }
273
+ }, [isAllCompleted, defaultExpanded]);
274
+
275
+ const gridColsClass =
276
+ nodes.length === 2
277
+ ? 'grid-cols-2'
278
+ : nodes.length === 3
279
+ ? 'grid-cols-3'
280
+ : 'grid-cols-1 sm:grid-cols-2';
281
+
282
+ return (
283
+ <div
284
+ className={`my-2 overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-xs ${className}`}
285
+ data-testid="orchestration-flow-card"
286
+ >
287
+ {/* 顶部 Todo 风格状态胶囊 Header */}
288
+ <button
289
+ type="button"
290
+ onClick={() => setUserToggled((prev) => (prev === null ? !expanded : !prev))}
291
+ className="flex w-full items-center justify-between border-b border-agent-border bg-agent-muted/30 px-3 py-2 text-left transition-colors hover:bg-agent-muted/50"
292
+ aria-expanded={expanded}
293
+ >
294
+ <div className="flex min-w-0 items-center gap-2">
295
+ <div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-violet-500/10 text-violet-600 dark:text-violet-400">
296
+ <LuGitFork className="h-3 w-3" />
297
+ </div>
298
+ <div className="flex min-w-0 items-center gap-1.5 text-xs font-semibold text-agent-foreground">
299
+ <span>多智能体协同编排</span>
300
+ <span className="rounded-full bg-violet-500/10 px-1.5 py-0.2 text-[10px] font-medium text-violet-700 dark:text-violet-300">
301
+ Fork-Join 流程
302
+ </span>
303
+ </div>
304
+ </div>
305
+
306
+ <div className="flex items-center gap-2 text-xs">
307
+ <span
308
+ className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors ${
309
+ isAllCompleted
310
+ ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
311
+ : hasActive
312
+ ? 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-300'
313
+ : 'border-agent-border bg-agent-muted text-agent-muted-foreground'
314
+ }`}
315
+ >
316
+ {isAllCompleted ? (
317
+ <LuCircleCheck className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
318
+ ) : hasActive ? (
319
+ <LuLoaderCircle className="h-3 w-3 animate-spin text-blue-600 dark:text-blue-400" />
320
+ ) : (
321
+ <LuCheck className="h-3 w-3" />
322
+ )}
323
+ <span>{summaryCopy}</span>
324
+ </span>
325
+ {expanded ? (
326
+ <LuChevronDown className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
327
+ ) : (
328
+ <LuChevronRight className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
329
+ )}
330
+ </div>
331
+ </button>
332
+
333
+ {/* 展开后的流程图 (真正的 Fork-Join DAG 图形) */}
334
+ {expanded ? (
335
+ <div className="p-3">
336
+ <div className="flex flex-col items-center">
337
+ {/* 1. 起点:目标分发 (Fork) 居中胶囊节点 */}
338
+ <div className="inline-flex items-center gap-1.5 rounded-full border border-violet-500/30 bg-violet-500/10 px-3 py-1 text-xs font-semibold text-violet-800 dark:text-violet-200 shadow-2xs">
339
+ <LuBot className="h-3.5 w-3.5 text-violet-600 dark:text-violet-400" />
340
+ <span>主智能体目标分发 (Fork)</span>
341
+ <span className="rounded-full bg-violet-500/20 px-1.5 py-0.2 text-[10px] font-medium text-violet-700 dark:text-violet-300">
342
+ 并发 {nodes.length} 分支
343
+ </span>
344
+ </div>
345
+
346
+ {/* 2. 向下的分流连线 */}
347
+ <ForkLines count={nodes.length} />
348
+
349
+ {/* 3. 并行分支节点网格 */}
350
+ <div className={`grid w-full gap-2.5 ${gridColsClass}`}>
351
+ {nodes.map((node, index) => {
352
+ const done = node.status === 'completed';
353
+ const active = node.status === 'running' || node.status === 'pending';
354
+ const canInspect = Boolean(chatId && onInspectTask && node.recordId);
355
+
356
+ return (
357
+ <div
358
+ key={node.childId || `node-${index}`}
359
+ className="flex flex-col justify-between rounded-agent-md border border-agent-border/80 bg-agent-muted/20 p-2.5 text-xs shadow-2xs transition-all hover:border-agent-border hover:bg-agent-muted/30"
360
+ >
361
+ <div>
362
+ {/* 节点顶栏:状态与编号 */}
363
+ <div className="flex items-center justify-between gap-1 border-b border-agent-border/40 pb-1.5">
364
+ <div className="flex items-center gap-1.5 min-w-0">
365
+ <NodeStatusIcon status={node.status} />
366
+ <span className="font-semibold text-agent-foreground text-[11px] truncate">
367
+ 分支 #{index + 1}
368
+ </span>
369
+ </div>
370
+ <div className="flex items-center gap-1">
371
+ <span
372
+ className={`rounded-full border px-1.5 py-0.2 text-[10px] font-medium ${statusBadgeStyle(
373
+ node.status,
374
+ )}`}
375
+ >
376
+ {statusBadgeText(node.status)}
377
+ </span>
378
+ {canInspect && chatId ? (
379
+ <button
380
+ type="button"
381
+ onClick={() =>
382
+ onInspectTask?.({
383
+ id: node.childId,
384
+ chatId,
385
+ recordId: node.recordId,
386
+ live: node.status === 'running',
387
+ title: node.task || '子代理执行过程',
388
+ })
389
+ }
390
+ className="inline-flex shrink-0 items-center gap-0.5 rounded px-1 text-[10px] text-agent-muted-foreground hover:bg-agent-muted hover:text-agent-foreground"
391
+ title="查看推理过程"
392
+ >
393
+ <LuExternalLink className="h-2.5 w-2.5" />
394
+ <span>过程</span>
395
+ </button>
396
+ ) : null}
397
+ </div>
398
+ </div>
399
+
400
+ {/* 任务描述 */}
401
+ <div className="mt-1.5 leading-relaxed">
402
+ <p
403
+ className={`line-clamp-3 text-[11px] ${
404
+ done
405
+ ? 'text-agent-foreground/90 font-medium'
406
+ : active
407
+ ? 'text-agent-foreground font-semibold'
408
+ : 'text-agent-muted-foreground'
409
+ }`}
410
+ title={node.task}
411
+ >
412
+ {node.task || `子任务 (${node.childId})`}
413
+ </p>
414
+ </div>
415
+
416
+ {/* 追加指令 */}
417
+ {node.steers.length > 0 ? (
418
+ <div className="mt-1.5 space-y-0.5 rounded bg-amber-500/5 p-1 text-[10px] text-amber-800 dark:text-amber-200">
419
+ {node.steers.map((s, sIdx) => (
420
+ <div key={sIdx} className="flex items-start gap-1">
421
+ <LuMessageSquare className="mt-0.5 h-2.5 w-2.5 shrink-0 text-amber-600" />
422
+ <span className="truncate">{s.message}</span>
423
+ </div>
424
+ ))}
425
+ </div>
426
+ ) : null}
427
+ </div>
428
+
429
+ {/* 结论预览 */}
430
+ {node.answer ? (
431
+ <div className="mt-2 rounded bg-emerald-500/10 border border-emerald-500/20 px-2 py-1 text-[11px]">
432
+ <div className="flex items-center gap-1 font-semibold text-emerald-800 dark:text-emerald-200 text-[10px]">
433
+ <LuCheck className="h-3 w-3" />
434
+ <span>结论:</span>
435
+ <span className="font-mono text-emerald-950 dark:text-emerald-50 truncate">
436
+ {node.answer}
437
+ </span>
438
+ </div>
439
+ </div>
440
+ ) : node.error ? (
441
+ <div className="mt-2 rounded bg-agent-destructive/10 px-1.5 py-1 text-[10px] text-agent-destructive truncate">
442
+ 失败: {node.error}
443
+ </div>
444
+ ) : null}
445
+ </div>
446
+ );
447
+ })}
448
+ </div>
449
+
450
+ {/* 4. 向下的汇聚连线 */}
451
+ <JoinLines count={nodes.length} />
452
+
453
+ {/* 5. 终点:结果汇聚 (Join) 居中胶囊节点 */}
454
+ <div
455
+ className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold shadow-2xs ${
456
+ isAllCompleted
457
+ ? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200'
458
+ : 'border-agent-border bg-agent-muted text-agent-muted-foreground'
459
+ }`}
460
+ >
461
+ <LuCircleCheck className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
462
+ <span>结果汇聚 (Join)</span>
463
+ <span className="text-[10px] opacity-80">
464
+ {isAllCompleted ? '所有分支已汇聚并完成回答' : '等待分支就绪...'}
465
+ </span>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ ) : null}
470
+ </div>
471
+ );
472
+ }
473
+
474
+ export default OrchestrationFlowCard;