@springbrand/message-panel 0.1.3-alpha.32 → 0.1.3-alpha.34

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,10 +1,6 @@
1
1
  import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
2
- import { motion, useReducedMotion } from "motion/react";
3
- import { memo, useId, type ReactNode } from "react";
4
- import {
5
- MarkdownMessage,
6
- type CloudOsUrlResolver,
7
- } from "./markdown-message";
2
+ import { memo, useId, useState } from "react";
3
+ import { MarkdownMessage } from "./markdown-message";
8
4
  import {
9
5
  getToolCallSummary,
10
6
  getToolIcon,
@@ -13,24 +9,6 @@ import {
13
9
  type PhosphorIcon,
14
10
  type ToolCallGroup,
15
11
  } from "./tool-presentation";
16
- import { useLifecycleDisclosure } from "./use-lifecycle-disclosure";
17
-
18
- const thinkingSpark = new URL(
19
- "../assets/thinking-spark.svg",
20
- import.meta.url,
21
- ).href;
22
-
23
- const SECONDARY_DISCLOSURE_TITLE_BASE =
24
- "flex min-w-0 cursor-pointer gap-2 rounded-xl text-left text-[14px] leading-[1.4] text-cos-work-description transition-colors duration-150 ease-out hover:text-cos-work-description-subtle focus-visible:text-cos-work-description-subtle focus-visible:outline-none active:scale-[0.995]";
25
-
26
- export const SECONDARY_DISCLOSURE_TITLE_WITH_ICON =
27
- `${SECONDARY_DISCLOSURE_TITLE_BASE} items-start px-1.5 py-1`;
28
-
29
- export const SECONDARY_DISCLOSURE_TITLE_WITHOUT_ICON =
30
- `${SECONDARY_DISCLOSURE_TITLE_BASE} items-center py-1 pr-1.5`;
31
-
32
- export const SECONDARY_DISCLOSURE_ICON =
33
- "mt-0.5 flex size-4 shrink-0 items-center justify-center";
34
12
 
35
13
  /**
36
14
  * 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
@@ -56,74 +34,26 @@ function displayOutput(value: unknown, fallback: string): string {
56
34
  return displayValue(record);
57
35
  }
58
36
 
59
- interface ToolDetailItem {
60
- key: string;
61
- label: string;
62
- output: string;
63
- error: string;
64
- }
65
-
66
- function detailItems(calls: readonly CloudOsToolCall[]): ToolDetailItem[] {
67
- return calls.map((call) => {
68
- const summary = getToolCallSummary(call);
69
- return {
70
- key: call.toolCallId,
71
- label: `${summary.verb}${summary.target ? ` ${summary.target}` : ""}`,
72
- output: displayOutput(call.output, call.outputText),
73
- error: call.errorText,
74
- };
75
- });
76
- }
77
-
78
- function OutputCard({ items }: { items: readonly ToolDetailItem[] }) {
79
- const copyValue = items
80
- .map((item) => {
81
- const detail = [item.error, item.output]
82
- .filter(Boolean)
83
- .join("\n\n") || "No additional data";
84
- return items.length === 1 ? detail : `${item.label}\n${detail}`;
85
- })
86
- .join("\n\n");
37
+ function OutputPre({ value }: { value: string }) {
87
38
  return (
88
39
  <div
89
- className="rounded-xl border border-[rgba(0,0,0,0.07)] bg-[rgba(0,0,0,0.03)] px-4 py-3 text-cos-output"
40
+ className="rounded-xl border border-kumo-line/70 bg-kumo-elevated/45 px-4 py-3 text-kumo-subtle"
90
41
  data-output-card=""
91
42
  >
92
43
  <div className="flex items-center justify-between gap-3 text-[14px] font-medium leading-5">
93
44
  <span>Output</span>
94
45
  <button
95
46
  type="button"
96
- onClick={() => navigator.clipboard.writeText(copyValue)}
97
- className="flex h-4 w-4 items-center justify-center text-cos-output transition-colors hover:text-kumo-default focus-visible:outline-none focus-visible:text-kumo-default"
47
+ onClick={() => navigator.clipboard.writeText(value)}
48
+ className="flex h-4 w-4 items-center justify-center text-kumo-subtle transition-colors hover:text-kumo-default focus-visible:outline-none focus-visible:text-kumo-default"
98
49
  aria-label="Copy output"
99
50
  >
100
51
  <Copy size={16} />
101
52
  </button>
102
53
  </div>
103
- <div className="mt-3 max-h-56 space-y-3 overflow-auto border-t border-kumo-line/70 pt-3 text-[14px] font-normal leading-[1.4]">
104
- {items.map((item) => (
105
- <div key={item.key} className="space-y-1.5">
106
- {items.length > 1 && (
107
- <p className="m-0 text-[12px] leading-4 text-cos-output">
108
- {item.label}
109
- </p>
110
- )}
111
- {item.error && (
112
- <p className="m-0 whitespace-pre-wrap text-kumo-danger">
113
- {item.error}
114
- </p>
115
- )}
116
- {item.output && (
117
- <p className="m-0 whitespace-pre-wrap text-cos-output">
118
- {item.output}
119
- </p>
120
- )}
121
- {!item.error && !item.output && (
122
- <p className="m-0 text-cos-output">No additional data</p>
123
- )}
124
- </div>
125
- ))}
126
- </div>
54
+ <pre className="mt-2 max-h-56 overflow-auto border-t border-kumo-line/70 pt-3 font-sans text-[14px] font-normal leading-5 whitespace-pre-wrap">
55
+ {value}
56
+ </pre>
127
57
  </div>
128
58
  );
129
59
  }
@@ -133,15 +63,22 @@ export const ToolCallDetails = memo(function ToolCallDetails({
133
63
  }: {
134
64
  toolCall: CloudOsToolCall;
135
65
  }) {
136
- return <OutputCard items={detailItems([tc])} />;
137
- });
138
-
139
- export const ToolGroupDetails = memo(function ToolGroupDetails({
140
- calls,
141
- }: {
142
- calls: readonly CloudOsToolCall[];
143
- }) {
144
- return <OutputCard items={detailItems(calls)} />;
66
+ const output = displayOutput(tc.output, tc.outputText);
67
+ return (
68
+ <div className="space-y-2">
69
+ {tc.errorText && (
70
+ <pre className="rounded-xl border border-kumo-danger/20 bg-kumo-danger-tint/40 p-3 font-mono text-[12px] leading-[18px] text-kumo-danger whitespace-pre-wrap">
71
+ {tc.errorText}
72
+ </pre>
73
+ )}
74
+ {output && <OutputPre value={output} />}
75
+ {!output && !tc.errorText && (
76
+ <p className="m-0 text-[12px] leading-4 text-kumo-inactive">
77
+ No additional data
78
+ </p>
79
+ )}
80
+ </div>
81
+ );
145
82
  });
146
83
 
147
84
  export const NestedToolCallRow = memo(function NestedToolCallRow({
@@ -163,13 +100,13 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
163
100
  <button
164
101
  type="button"
165
102
  onClick={() => onToggle(key)}
166
- className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} w-full`}
103
+ className="flex w-full cursor-pointer items-center gap-3 rounded-xl px-1.5 py-1 text-left text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.995]"
167
104
  aria-expanded={open}
168
105
  >
169
- <span className={SECONDARY_DISCLOSURE_ICON}>
106
+ <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
170
107
  <WorkIcon Icon={Icon} />
171
108
  </span>
172
- <span className="flex min-w-0 flex-1 items-center gap-2">
109
+ <span className="flex min-w-0 flex-1 items-center gap-2 text-[14px] leading-5 tracking-[-0.25px]">
173
110
  <span className="min-w-0 truncate">{label}</span>
174
111
  {tc.failed && (
175
112
  <span className="flex-shrink-0 rounded-full bg-kumo-danger-tint px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.04em] text-kumo-danger">
@@ -177,13 +114,14 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
177
114
  </span>
178
115
  )}
179
116
  <CaretRight
180
- size={16}
117
+ size={13}
118
+ weight="bold"
181
119
  className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
182
120
  />
183
121
  </span>
184
122
  </button>
185
123
  {open && (
186
- <div className="mt-1 space-y-3">
124
+ <div className="ml-8 mt-1 space-y-3">
187
125
  <ToolCallDetails toolCall={tc} />
188
126
  </div>
189
127
  )}
@@ -191,27 +129,6 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
191
129
  );
192
130
  });
193
131
 
194
- function ThinkingContent({
195
- id,
196
- text,
197
- resolveUrl,
198
- className = "",
199
- }: {
200
- id?: string;
201
- text: string;
202
- resolveUrl?: CloudOsUrlResolver;
203
- className?: string;
204
- }) {
205
- return (
206
- <div
207
- id={id}
208
- className={`cos-markdown max-h-[240px] overflow-y-auto rounded-xl border border-[rgba(0,0,0,0.07)] bg-[#F2F2F2] px-4 py-3 text-[14px] leading-[1.4] text-[rgba(0,0,0,0.7)] ${className}`}
209
- >
210
- <MarkdownMessage message={text} resolveUrl={resolveUrl} />
211
- </div>
212
- );
213
- }
214
-
215
132
  export const ThinkingTraceRow = memo(function ThinkingTraceRow({
216
133
  reasoning,
217
134
  running,
@@ -219,7 +136,7 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
219
136
  reasoning: string;
220
137
  running: boolean;
221
138
  }) {
222
- const [open, toggle] = useLifecycleDisclosure(running);
139
+ const [open, setOpen] = useState(false);
223
140
  const contentId = useId();
224
141
  const summary = reasoning.replace(/\s+/g, " ").trim();
225
142
 
@@ -231,22 +148,22 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
231
148
  >
232
149
  <button
233
150
  type="button"
234
- onClick={toggle}
235
- className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} group w-fit max-w-[80%]`}
151
+ onClick={() => setOpen((current) => !current)}
152
+ className="group -ml-0.5 flex w-fit max-w-[80%] min-w-0 items-center gap-1.5 rounded-xl px-1.5 py-1 text-left text-[14px] leading-5 text-kumo-inactive transition-colors hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
236
153
  aria-expanded={open}
237
154
  aria-controls={contentId}
238
155
  >
239
- <span className={`${SECONDARY_DISCLOSURE_ICON} relative`} aria-hidden="true">
156
+ <span className="relative grid size-4 flex-shrink-0 place-items-center" aria-hidden="true">
240
157
  <Atom
241
- size={15}
158
+ size={14}
242
159
  className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
243
160
  />
244
161
  <CaretDown
245
- size={15}
162
+ size={14}
246
163
  className={`absolute opacity-0 transition-[opacity,transform] group-hover:opacity-100 group-focus-visible:opacity-100 ${open ? "" : "-rotate-90"}`}
247
164
  />
248
165
  </span>
249
- <span className="flex-shrink-0">Think</span>
166
+ <span className="flex-shrink-0 text-kumo-subtle">Think</span>
250
167
  <span aria-hidden="true">·</span>
251
168
  <span
252
169
  className="min-w-0 flex-1 truncate"
@@ -256,186 +173,17 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
256
173
  </span>
257
174
  </button>
258
175
  {open && (
259
- <ThinkingContent
176
+ <div
260
177
  id={contentId}
261
- text={reasoning}
262
- className="w-full"
263
- />
264
- )}
265
- </div>
266
- );
267
- });
268
-
269
- export function formatWorkDuration(milliseconds: number): string {
270
- const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
271
- const hours = Math.floor(totalSeconds / 3600);
272
- const minutes = Math.floor((totalSeconds % 3600) / 60);
273
- const seconds = totalSeconds % 60;
274
- if (hours > 0) {
275
- return `${hours}h ${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
276
- }
277
- if (minutes > 0) return `${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
278
- return `${seconds}s`;
279
- }
280
-
281
- interface ActivitySummary {
282
- leading: string;
283
- emphasis?: string;
284
- trailing?: string;
285
- }
286
-
287
- function plural(count: number, singular: string, plural = `${singular}s`): string {
288
- return `${count} ${count === 1 ? singular : plural}`;
289
- }
290
-
291
- function activitySummary(groups: readonly ToolCallGroup[]): ActivitySummary | null {
292
- const calls = groups.flatMap((group) => group.calls);
293
- const count = (kind: CloudOsToolCall["kind"]) =>
294
- calls.filter((call) => call.kind === kind).length;
295
- const webSearches = count("web-search");
296
- const webPages = count("web-fetch");
297
- if (webSearches > 0 && webPages > 0) {
298
- return {
299
- leading: "searched the web and retrieved ",
300
- emphasis: `${webPages} web`,
301
- trailing: webPages === 1 ? " page" : " pages",
302
- };
303
- }
304
- if (webSearches > 0) return { leading: "searched the web" };
305
- if (webPages > 0) {
306
- return {
307
- leading: "retrieved ",
308
- emphasis: `${webPages} web`,
309
- trailing: webPages === 1 ? " page" : " pages",
310
- };
311
- }
312
-
313
- const firstKnown = calls.find((call) =>
314
- ["read", "write", "edit", "bash", "skill", "javascript"].includes(
315
- call.kind,
316
- ),
317
- );
318
- if (!firstKnown) return null;
319
- const total = count(firstKnown.kind);
320
- switch (firstKnown.kind) {
321
- case "read":
322
- return { leading: `read ${plural(total, "file")}` };
323
- case "write":
324
- return { leading: `wrote ${plural(total, "file")}` };
325
- case "edit":
326
- return { leading: `made ${plural(total, "edit")}` };
327
- case "bash":
328
- return { leading: `ran ${plural(total, "command")}` };
329
- case "skill":
330
- return { leading: `read ${plural(total, "instruction set")}` };
331
- case "javascript":
332
- return { leading: total === 1 ? "ran code" : `ran code ${total} times` };
333
- default:
334
- return null;
335
- }
336
- }
337
-
338
- export const WorkTraceDisclosure = memo(function WorkTraceDisclosure({
339
- durationMs,
340
- groups,
341
- completed = false,
342
- open,
343
- onToggle,
344
- children,
345
- }: {
346
- durationMs?: number;
347
- groups: readonly ToolCallGroup[];
348
- completed?: boolean;
349
- open: boolean;
350
- onToggle: () => void;
351
- children: ReactNode;
352
- }) {
353
- const contentId = useId();
354
- const reduceMotion = useReducedMotion();
355
- const activity = activitySummary(groups);
356
- const duration = durationMs === undefined ? "" : ` for ${formatWorkDuration(durationMs)}`;
357
- const verb = completed ? "Worked" : "Thought";
358
- const label = `${verb}${duration}${activity ? `, ${activity.leading}${activity.emphasis ?? ""}${activity.trailing ?? ""}` : ""}`;
359
-
360
- return (
361
- <div data-agent-work="" data-state={open ? "open" : "closed"}>
362
- <div className="w-full pb-2">
363
- <button
364
- type="button"
365
- onClick={onToggle}
366
- aria-expanded={open}
367
- aria-controls={contentId}
368
- aria-label={label}
369
- className="flex min-w-0 items-center gap-2 text-left text-[14px] leading-[1.4] text-cos-work-description transition-colors duration-150 ease-out hover:text-cos-work-description-subtle focus-visible:text-cos-work-description-subtle focus-visible:outline-none"
178
+ className="cos-markdown w-full rounded-xl border border-kumo-line bg-kumo-elevated/45 px-4 py-3 text-[14px] leading-[1.4] text-kumo-default/70"
370
179
  >
371
- {!completed && (
372
- <span className="grid size-4 shrink-0 place-items-center" aria-hidden="true">
373
- <motion.img
374
- src={thinkingSpark}
375
- alt=""
376
- className="size-[12.24px]"
377
- initial={{ rotate: 0 }}
378
- animate={
379
- reduceMotion ? { rotate: 0 } : { rotate: [0, 360, -360, -360] }
380
- }
381
- transition={
382
- reduceMotion
383
- ? { duration: 0 }
384
- : {
385
- rotate: {
386
- duration: 1.68,
387
- times: [0, 0.8368, 0.8373, 1],
388
- ease: [
389
- [0.5, 0, 0.5, 1],
390
- [0.5, 0, 0.5, 1],
391
- "linear",
392
- ],
393
- repeat: Infinity,
394
- },
395
- }
396
- }
397
- />
398
- </span>
399
- )}
400
- <span className="min-w-0 truncate">
401
- <span>{`${verb}${duration}${activity ? `, ${activity.leading}` : ""}`}</span>
402
- {activity?.emphasis && (
403
- <span className="text-kumo-default">{activity.emphasis}</span>
404
- )}
405
- {activity?.trailing}
406
- </span>
407
- <CaretRight
408
- size={16}
409
- className={`shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
410
- aria-hidden="true"
411
- />
412
- </button>
413
- </div>
414
- {open && (
415
- <div id={contentId} className="mt-4 space-y-3" data-agent-work-content="">
416
- {children}
180
+ <MarkdownMessage message={reasoning} />
417
181
  </div>
418
182
  )}
419
183
  </div>
420
184
  );
421
185
  });
422
186
 
423
- export const WorkDescriptionRow = memo(function WorkDescriptionRow({
424
- text,
425
- resolveUrl,
426
- }: {
427
- text: string;
428
- running?: boolean;
429
- resolveUrl?: CloudOsUrlResolver;
430
- }) {
431
- return (
432
- <ThinkingContent
433
- text={text}
434
- resolveUrl={resolveUrl}
435
- />
436
- );
437
- });
438
-
439
187
  export const ToolGroupRow = memo(function ToolGroupRow({
440
188
  group,
441
189
  open,
@@ -447,20 +195,19 @@ export const ToolGroupRow = memo(function ToolGroupRow({
447
195
  expandedKeys: ReadonlySet<string>;
448
196
  onToggle: (key: string) => void;
449
197
  }) {
450
- void expandedKeys;
451
198
  return (
452
- <div className="group/tool-call">
199
+ <div className="group -ml-0.5">
453
200
  <button
454
201
  type="button"
455
202
  onClick={() => onToggle(group.key)}
456
- className={SECONDARY_DISCLOSURE_TITLE_WITH_ICON}
203
+ className="flex w-full cursor-pointer items-center gap-3 rounded-xl px-1.5 py-1 text-left text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.995]"
457
204
  aria-expanded={open}
458
205
  >
459
- <span className={SECONDARY_DISCLOSURE_ICON}>
206
+ <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
460
207
  <WorkIcon Icon={group.Icon} />
461
208
  </span>
462
209
  <span className="min-w-0 flex-1">
463
- <span className="flex min-w-0 items-center gap-2">
210
+ <span className="flex min-w-0 items-center gap-2 text-[14px] leading-5 tracking-[-0.25px]">
464
211
  <span
465
212
  className={`min-w-0 truncate ${group.hasRunning ? "cos-thinking-shimmer" : ""}`}
466
213
  >
@@ -472,22 +219,38 @@ export const ToolGroupRow = memo(function ToolGroupRow({
472
219
  </span>
473
220
  )}
474
221
  <CaretRight
475
- size={16}
476
- className={`shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
222
+ size={13}
223
+ weight="bold"
224
+ className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
477
225
  />
478
226
  </span>
479
227
  {group.detailLines.length > 1 && (
480
- <span className="mt-2 block truncate text-[12px] leading-4 text-[rgba(0,0,0,0.3)]">
228
+ <span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
481
229
  {group.detailLines.join(" · ")}
482
230
  </span>
483
231
  )}
484
232
  </span>
485
233
  </button>
486
- {open && (
487
- <div className="mt-3">
488
- <ToolGroupDetails calls={group.calls} />
489
- </div>
490
- )}
234
+ {open &&
235
+ (group.calls.length === 1 ? (
236
+ <div className="ml-8 mt-1 space-y-3">
237
+ <ToolCallDetails toolCall={group.calls[0]} />
238
+ </div>
239
+ ) : (
240
+ <div className="ml-8 mt-1 space-y-1">
241
+ {group.calls.map((toolCall) => {
242
+ const key = `call-${toolCall.toolCallId}`;
243
+ return (
244
+ <NestedToolCallRow
245
+ key={toolCall.toolCallId}
246
+ toolCall={toolCall}
247
+ open={expandedKeys.has(key)}
248
+ onToggle={onToggle}
249
+ />
250
+ );
251
+ })}
252
+ </div>
253
+ ))}
491
254
  </div>
492
255
  );
493
256
  });
@@ -27,7 +27,6 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
27
27
  createdAt?: number;
28
28
  error?: string;
29
29
  interruptedByUser?: boolean;
30
- turnId?: string;
31
30
  turnDurationMs?: number;
32
31
  turnStartedAt?: number;
33
32
  turnStatus?: string;
@@ -144,10 +143,8 @@ export type CloudOsEntry =
144
143
  type: "assistant";
145
144
  key: string;
146
145
  messageId: string;
147
- turnId?: string;
148
146
  blocks: AssistantBlock[];
149
147
  copyText: string;
150
- durationMs?: number;
151
148
  timestamp?: number;
152
149
  terminal?: { kind: "interrupted" | "error"; title: string; body?: string };
153
150
  };
@@ -171,10 +168,6 @@ function nonNegativeNumber(value: unknown): number | undefined {
171
168
  : undefined;
172
169
  }
173
170
 
174
- function nonEmptyString(value: unknown): string | undefined {
175
- return typeof value === "string" && value.trim() ? value : undefined;
176
- }
177
-
178
171
  export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
179
172
  return recordOf(message.metadata) as CloudOsMessageMetadata;
180
173
  }
@@ -229,54 +222,6 @@ export function formatFullTimestamp(timestamp: number): string {
229
222
  });
230
223
  }
231
224
 
232
- function turnDuration(metadata: CloudOsMessageMetadata): number | undefined {
233
- const explicit = nonNegativeNumber(metadata.turnDurationMs);
234
- if (explicit !== undefined) return explicit;
235
- const startedAt = nonNegativeNumber(metadata.turnStartedAt);
236
- const completedAt = nonNegativeNumber(metadata.completedAt);
237
- return startedAt !== undefined && completedAt !== undefined
238
- ? Math.max(0, completedAt - startedAt)
239
- : undefined;
240
- }
241
-
242
- function isWorkBlock(block: AssistantBlock): boolean {
243
- return (
244
- block.kind === "reasoning" ||
245
- block.kind === "toolGroup" ||
246
- block.kind === "plan" ||
247
- block.kind === "parallel" ||
248
- block.kind === "subagents"
249
- );
250
- }
251
-
252
- /**
253
- * 把一条 assistant 消息拆成可折叠的工作过程与始终可见的最终内容。
254
- * 最后一段工作之前的 text 是过程旁白;最后一次工作之后的 text 才是正文。
255
- */
256
- export function partitionAssistantBlocks(blocks: readonly AssistantBlock[]): {
257
- work: AssistantBlock[];
258
- content: AssistantBlock[];
259
- } {
260
- let lastWorkIndex = -1;
261
- blocks.forEach((block, index) => {
262
- if (isWorkBlock(block)) lastWorkIndex = index;
263
- });
264
-
265
- const work: AssistantBlock[] = [];
266
- const content: AssistantBlock[] = [];
267
- blocks.forEach((block, index) => {
268
- if (
269
- isWorkBlock(block) ||
270
- (block.kind === "text" && index < lastWorkIndex)
271
- ) {
272
- work.push(block);
273
- } else {
274
- content.push(block);
275
- }
276
- });
277
- return { work, content };
278
- }
279
-
280
225
  function toolNameOf(part: unknown): string | null {
281
226
  const type = String(recordOf(part).type ?? "");
282
227
  if (type === "dynamic-tool") {
@@ -683,9 +628,6 @@ export function buildCloudOsEntries({
683
628
  }
684
629
  return -1;
685
630
  })();
686
- const activeTurnId = isActive && lastAssistantIndex >= 0
687
- ? nonEmptyString(cloudOsMetadata(messages[lastAssistantIndex]!).turnId)
688
- : undefined;
689
631
 
690
632
  messages.forEach((message, index) => {
691
633
  if (message.role === "system") return;
@@ -740,10 +682,8 @@ export function buildCloudOsEntries({
740
682
  type: "assistant",
741
683
  key: `assistant-${message.id}-${index}`,
742
684
  messageId: message.id,
743
- turnId: nonEmptyString(metadata.turnId),
744
685
  blocks,
745
686
  copyText: messageText(message),
746
- durationMs: turnDuration(metadata),
747
687
  timestamp:
748
688
  nonNegativeNumber(metadata.completedAt) ??
749
689
  nonNegativeNumber(metadata.createdAt),
@@ -751,54 +691,7 @@ export function buildCloudOsEntries({
751
691
  });
752
692
  });
753
693
 
754
- return aggregateCompletedTurnWork(
755
- dropSupersededActionPresentations(dropSupersededPlans(entries)),
756
- activeTurnId,
757
- );
758
- }
759
-
760
- /** Collapse every completed Turn's assistant segments into one display entry. */
761
- function aggregateCompletedTurnWork(
762
- entries: CloudOsEntry[],
763
- activeTurnId: string | undefined,
764
- ): CloudOsEntry[] {
765
- const indexesByTurn = new Map<string, number[]>();
766
- entries.forEach((entry, index) => {
767
- if (entry.type !== "assistant" || !entry.turnId) return;
768
- const indexes = indexesByTurn.get(entry.turnId) ?? [];
769
- indexes.push(index);
770
- indexesByTurn.set(entry.turnId, indexes);
771
- });
772
-
773
- const hidden = new Set<number>();
774
- const replacements = new Map<number, CloudOsEntry>();
775
- for (const [turnId, indexes] of indexesByTurn) {
776
- if (turnId === activeTurnId || indexes.length < 2) continue;
777
- const assistantEntries = indexes.map(
778
- (index) => entries[index] as Extract<CloudOsEntry, { type: "assistant" }>,
779
- );
780
- const anchorIndex = indexes[indexes.length - 1]!;
781
- const anchor = assistantEntries[assistantEntries.length - 1]!;
782
- const durations = assistantEntries.flatMap((entry) =>
783
- entry.durationMs === undefined ? [] : [entry.durationMs]
784
- );
785
- const timestamps = assistantEntries.flatMap((entry) =>
786
- entry.timestamp === undefined ? [] : [entry.timestamp]
787
- );
788
-
789
- indexes.slice(0, -1).forEach((index) => hidden.add(index));
790
- replacements.set(anchorIndex, {
791
- ...anchor,
792
- key: `assistant-turn-${turnId}`,
793
- blocks: assistantEntries.flatMap((entry) => entry.blocks),
794
- ...(durations.length > 0 ? { durationMs: Math.max(...durations) } : {}),
795
- ...(timestamps.length > 0 ? { timestamp: Math.max(...timestamps) } : {}),
796
- });
797
- }
798
-
799
- return entries.flatMap((entry, index) =>
800
- hidden.has(index) ? [] : [replacements.get(index) ?? entry]
801
- );
694
+ return dropSupersededActionPresentations(dropSupersededPlans(entries));
802
695
  }
803
696
 
804
697
  function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntry[] {
package/cloud-os/index.ts CHANGED
@@ -68,12 +68,8 @@ export type {
68
68
  export {
69
69
  ThinkingTraceRow,
70
70
  ToolCallDetails,
71
- ToolGroupDetails,
72
71
  ToolGroupRow,
73
- WorkDescriptionRow,
74
72
  WorkIcon,
75
- WorkTraceDisclosure,
76
- formatWorkDuration,
77
73
  } from "./chat/tool-rows";
78
74
  export {
79
75
  ApprovalBlock,
@@ -98,7 +94,6 @@ export {
98
94
  formatClockTime,
99
95
  formatFullTimestamp,
100
96
  messageText,
101
- partitionAssistantBlocks,
102
97
  requestedCapabilitiesOf,
103
98
  rhythmTopClass,
104
99
  } from "./chat/transcript-model";