@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.
- package/cloud-os/chat/cloud-os-chat-messages.tsx +44 -178
- package/cloud-os/chat/rich-blocks.tsx +276 -247
- package/cloud-os/chat/tool-rows.tsx +69 -306
- package/cloud-os/chat/transcript-model.ts +1 -108
- package/cloud-os/index.ts +0 -5
- package/cloud-os/styles/cloud-os.css +0 -12
- package/demo/chat-scenarios.ts +3 -9
- package/package.json +1 -1
- package/cloud-os/assets/thinking-spark.svg +0 -9
- package/cloud-os/chat/use-lifecycle-disclosure.ts +0 -65
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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-
|
|
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(
|
|
97
|
-
className="flex h-4 w-4 items-center justify-center text-
|
|
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
|
-
<
|
|
104
|
-
{
|
|
105
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
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=
|
|
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=
|
|
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={
|
|
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,
|
|
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={
|
|
235
|
-
className=
|
|
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=
|
|
156
|
+
<span className="relative grid size-4 flex-shrink-0 place-items-center" aria-hidden="true">
|
|
240
157
|
<Atom
|
|
241
|
-
size={
|
|
158
|
+
size={14}
|
|
242
159
|
className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
|
|
243
160
|
/>
|
|
244
161
|
<CaretDown
|
|
245
|
-
size={
|
|
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
|
-
<
|
|
176
|
+
<div
|
|
260
177
|
id={contentId}
|
|
261
|
-
text
|
|
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
|
-
{
|
|
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
|
|
199
|
+
<div className="group -ml-0.5">
|
|
453
200
|
<button
|
|
454
201
|
type="button"
|
|
455
202
|
onClick={() => onToggle(group.key)}
|
|
456
|
-
className=
|
|
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=
|
|
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={
|
|
476
|
-
|
|
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-
|
|
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
|
-
|
|
488
|
-
<
|
|
489
|
-
|
|
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
|
|
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";
|