@springbrand/message-panel 0.1.3-alpha.33 → 0.1.3-alpha.36
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/README.md +2 -1
- package/cloud-os/assets/thinking-star.svg +9 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +290 -239
- package/cloud-os/chat/code-runner.tsx +106 -0
- package/cloud-os/chat/range.ts +8 -0
- package/cloud-os/chat/rich-blocks.tsx +186 -256
- package/cloud-os/chat/surfaces.tsx +90 -0
- package/cloud-os/chat/tool-call.tsx +94 -0
- package/cloud-os/chat/tool-presentation.ts +3 -11
- package/cloud-os/chat/tool-rows.tsx +145 -378
- package/cloud-os/chat/tool-timeline.tsx +123 -0
- package/cloud-os/chat/transcript-model.ts +86 -116
- package/cloud-os/chat/web-search.tsx +135 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +1 -1
- package/cloud-os/index.ts +10 -5
- package/cloud-os/primitives/collapsible.tsx +21 -0
- package/cloud-os/styles/cloud-os.css +18 -12
- package/demo/chat-scenarios.ts +3 -9
- package/package.json +3 -1
- package/cloud-os/assets/thinking-spark.svg +0 -9
- package/cloud-os/chat/use-lifecycle-disclosure.ts +0 -65
|
@@ -1,37 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./markdown-message";
|
|
1
|
+
import { memo, useEffect, useId, useState } from "react";
|
|
2
|
+
import { CodeRunner } from "./code-runner";
|
|
3
|
+
import { MarkdownMessage } from "./markdown-message";
|
|
4
|
+
import { ToolCall } from "./tool-call";
|
|
5
|
+
import { ToolTimeline } from "./tool-timeline";
|
|
6
|
+
import { WebSearchToolCall } from "./web-search";
|
|
8
7
|
import {
|
|
9
8
|
getToolCallSummary,
|
|
10
|
-
getToolIcon,
|
|
11
9
|
normalizeOutputText,
|
|
10
|
+
safeStringify,
|
|
12
11
|
type CloudOsToolCall,
|
|
13
12
|
type PhosphorIcon,
|
|
14
13
|
type ToolCallGroup,
|
|
15
14
|
} 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
15
|
|
|
23
|
-
const
|
|
24
|
-
"flex min-w-0 cursor-pointer gap-2 rounded-xl text-left text-[14px] leading-[1.4] text-
|
|
16
|
+
const DISCLOSURE_TITLE_BASE =
|
|
17
|
+
"flex min-w-0 cursor-pointer gap-2 rounded-xl text-left text-[14px] leading-[1.4] text-kumo-inactive transition-colors duration-150 ease-out hover:text-kumo-subtle focus-visible:text-kumo-subtle focus-visible:outline-none active:scale-[0.995]";
|
|
25
18
|
|
|
26
|
-
export const
|
|
27
|
-
`${
|
|
19
|
+
export const DISCLOSURE_TITLE_WITH_ICON =
|
|
20
|
+
`${DISCLOSURE_TITLE_BASE} items-start py-1`;
|
|
28
21
|
|
|
29
|
-
export const
|
|
30
|
-
`${SECONDARY_DISCLOSURE_TITLE_BASE} items-center py-1 pr-1.5`;
|
|
31
|
-
|
|
32
|
-
export const SECONDARY_DISCLOSURE_ICON =
|
|
22
|
+
export const DISCLOSURE_ICON =
|
|
33
23
|
"mt-0.5 flex size-4 shrink-0 items-center justify-center";
|
|
34
24
|
|
|
25
|
+
const thinkingStar = new URL("../assets/thinking-star.svg", import.meta.url).href;
|
|
26
|
+
|
|
35
27
|
/**
|
|
36
28
|
* 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
|
|
37
29
|
* ToolGroupRow / ThinkingTraceRow。渲染 UIMessage 归一化后的 CloudOsToolCall。
|
|
@@ -56,76 +48,18 @@ function displayOutput(value: unknown, fallback: string): string {
|
|
|
56
48
|
return displayValue(record);
|
|
57
49
|
}
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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");
|
|
87
|
-
return (
|
|
88
|
-
<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"
|
|
90
|
-
data-output-card=""
|
|
91
|
-
>
|
|
92
|
-
<div className="flex items-center justify-between gap-3 text-[14px] font-medium leading-5">
|
|
93
|
-
<span>Output</span>
|
|
94
|
-
<button
|
|
95
|
-
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"
|
|
98
|
-
aria-label="Copy output"
|
|
99
|
-
>
|
|
100
|
-
<Copy size={16} />
|
|
101
|
-
</button>
|
|
102
|
-
</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>
|
|
127
|
-
</div>
|
|
128
|
-
);
|
|
51
|
+
function toolCallView(tc: CloudOsToolCall) {
|
|
52
|
+
const settled = getToolCallSummary({ ...tc, running: false });
|
|
53
|
+
const active = getToolCallSummary({ ...tc, running: true });
|
|
54
|
+
return {
|
|
55
|
+
label: settled.verb,
|
|
56
|
+
activeLabel: active.verb,
|
|
57
|
+
query: settled.target ?? "",
|
|
58
|
+
result:
|
|
59
|
+
tc.errorText ||
|
|
60
|
+
displayOutput(tc.output, tc.outputText) ||
|
|
61
|
+
(tc.running ? "Running…" : "No additional data"),
|
|
62
|
+
};
|
|
129
63
|
}
|
|
130
64
|
|
|
131
65
|
export const ToolCallDetails = memo(function ToolCallDetails({
|
|
@@ -133,15 +67,14 @@ export const ToolCallDetails = memo(function ToolCallDetails({
|
|
|
133
67
|
}: {
|
|
134
68
|
toolCall: CloudOsToolCall;
|
|
135
69
|
}) {
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return <OutputCard items={detailItems(calls)} />;
|
|
70
|
+
return (
|
|
71
|
+
<ToolCall
|
|
72
|
+
{...toolCallView(tc)}
|
|
73
|
+
running={tc.running}
|
|
74
|
+
open
|
|
75
|
+
onOpenChange={() => undefined}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
145
78
|
});
|
|
146
79
|
|
|
147
80
|
export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
@@ -153,64 +86,51 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
153
86
|
open: boolean;
|
|
154
87
|
onToggle: (key: string) => void;
|
|
155
88
|
}) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const Icon = getToolIcon(tc.kind);
|
|
89
|
+
if (tc.kind === "web-search" && !tc.failed) {
|
|
90
|
+
return <WebSearchToolCall toolCall={tc} />;
|
|
91
|
+
}
|
|
160
92
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
93
|
+
if (tc.kind === "javascript") {
|
|
94
|
+
const code = typeof tc.input.code === "string"
|
|
95
|
+
? tc.input.code
|
|
96
|
+
: safeStringify(tc.input);
|
|
97
|
+
const output =
|
|
98
|
+
tc.errorText || displayOutput(tc.output, tc.outputText);
|
|
99
|
+
const key = `call-${tc.toolCallId}`;
|
|
100
|
+
return (
|
|
101
|
+
<ToolCall
|
|
102
|
+
{...toolCallView(tc)}
|
|
103
|
+
running={tc.running}
|
|
104
|
+
open={open}
|
|
105
|
+
onOpenChange={(next) => {
|
|
106
|
+
if (next !== open) onToggle(key);
|
|
107
|
+
}}
|
|
108
|
+
className="max-w-none"
|
|
168
109
|
>
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<CaretRight
|
|
180
|
-
size={16}
|
|
181
|
-
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
182
|
-
/>
|
|
183
|
-
</span>
|
|
184
|
-
</button>
|
|
185
|
-
{open && (
|
|
186
|
-
<div className="mt-1 space-y-3">
|
|
187
|
-
<ToolCallDetails toolCall={tc} />
|
|
188
|
-
</div>
|
|
189
|
-
)}
|
|
190
|
-
</div>
|
|
191
|
-
);
|
|
192
|
-
});
|
|
110
|
+
<CodeRunner
|
|
111
|
+
language="javascript"
|
|
112
|
+
code={code}
|
|
113
|
+
state={tc.running ? "running" : tc.failed ? "error" : "ok"}
|
|
114
|
+
output={output ? output.split(/\r?\n/) : []}
|
|
115
|
+
className="max-w-none"
|
|
116
|
+
/>
|
|
117
|
+
</ToolCall>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
193
120
|
|
|
194
|
-
|
|
195
|
-
id,
|
|
196
|
-
text,
|
|
197
|
-
resolveUrl,
|
|
198
|
-
className = "",
|
|
199
|
-
}: {
|
|
200
|
-
id?: string;
|
|
201
|
-
text: string;
|
|
202
|
-
resolveUrl?: CloudOsUrlResolver;
|
|
203
|
-
className?: string;
|
|
204
|
-
}) {
|
|
121
|
+
const key = `call-${tc.toolCallId}`;
|
|
205
122
|
return (
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
123
|
+
<ToolCall
|
|
124
|
+
{...toolCallView(tc)}
|
|
125
|
+
running={tc.running}
|
|
126
|
+
open={open}
|
|
127
|
+
onOpenChange={(next) => {
|
|
128
|
+
if (next !== open) onToggle(key);
|
|
129
|
+
}}
|
|
130
|
+
className="max-w-none"
|
|
131
|
+
/>
|
|
212
132
|
);
|
|
213
|
-
}
|
|
133
|
+
});
|
|
214
134
|
|
|
215
135
|
export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
216
136
|
reasoning,
|
|
@@ -219,7 +139,7 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
|
219
139
|
reasoning: string;
|
|
220
140
|
running: boolean;
|
|
221
141
|
}) {
|
|
222
|
-
const [open,
|
|
142
|
+
const [open, setOpen] = useState(false);
|
|
223
143
|
const contentId = useId();
|
|
224
144
|
const summary = reasoning.replace(/\s+/g, " ").trim();
|
|
225
145
|
|
|
@@ -231,20 +151,13 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
|
231
151
|
>
|
|
232
152
|
<button
|
|
233
153
|
type="button"
|
|
234
|
-
onClick={
|
|
235
|
-
className={`${
|
|
154
|
+
onClick={() => setOpen((current) => !current)}
|
|
155
|
+
className={`${DISCLOSURE_TITLE_WITH_ICON} group w-fit max-w-[80%]`}
|
|
236
156
|
aria-expanded={open}
|
|
237
157
|
aria-controls={contentId}
|
|
238
158
|
>
|
|
239
|
-
<span className={
|
|
240
|
-
<
|
|
241
|
-
size={15}
|
|
242
|
-
className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
|
|
243
|
-
/>
|
|
244
|
-
<CaretDown
|
|
245
|
-
size={15}
|
|
246
|
-
className={`absolute opacity-0 transition-[opacity,transform] group-hover:opacity-100 group-focus-visible:opacity-100 ${open ? "" : "-rotate-90"}`}
|
|
247
|
-
/>
|
|
159
|
+
<span className={DISCLOSURE_ICON} aria-hidden="true">
|
|
160
|
+
<img src={thinkingStar} alt="" className="size-[12.2369px]" />
|
|
248
161
|
</span>
|
|
249
162
|
<span className="flex-shrink-0">Think</span>
|
|
250
163
|
<span aria-hidden="true">·</span>
|
|
@@ -256,185 +169,30 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
|
256
169
|
</span>
|
|
257
170
|
</button>
|
|
258
171
|
{open && (
|
|
259
|
-
<
|
|
172
|
+
<div
|
|
260
173
|
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"
|
|
174
|
+
className="cos-markdown w-full px-4 text-[14px] leading-[1.4] text-kumo-default/70"
|
|
370
175
|
>
|
|
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}
|
|
176
|
+
<MarkdownMessage message={reasoning} />
|
|
417
177
|
</div>
|
|
418
178
|
)}
|
|
419
179
|
</div>
|
|
420
180
|
);
|
|
421
181
|
});
|
|
422
182
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
);
|
|
437
|
-
});
|
|
183
|
+
function useElapsedSeconds(running: boolean) {
|
|
184
|
+
const [elapsed, setElapsed] = useState(0);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!running) return undefined;
|
|
187
|
+
const startedAt = Date.now() - elapsed * 1000;
|
|
188
|
+
const timer = window.setInterval(
|
|
189
|
+
() => setElapsed(Math.floor((Date.now() - startedAt) / 1000)),
|
|
190
|
+
1000,
|
|
191
|
+
);
|
|
192
|
+
return () => window.clearInterval(timer);
|
|
193
|
+
}, [running]);
|
|
194
|
+
return elapsed;
|
|
195
|
+
}
|
|
438
196
|
|
|
439
197
|
export const ToolGroupRow = memo(function ToolGroupRow({
|
|
440
198
|
group,
|
|
@@ -447,48 +205,57 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
447
205
|
expandedKeys: ReadonlySet<string>;
|
|
448
206
|
onToggle: (key: string) => void;
|
|
449
207
|
}) {
|
|
450
|
-
|
|
208
|
+
const elapsed = useElapsedSeconds(group.hasRunning && group.calls.length > 1);
|
|
209
|
+
const onlyCall = group.calls.length === 1 ? group.calls[0] : undefined;
|
|
210
|
+
if (onlyCall) {
|
|
211
|
+
return (
|
|
212
|
+
<NestedToolCallRow
|
|
213
|
+
toolCall={onlyCall}
|
|
214
|
+
open={expandedKeys.has(`call-${onlyCall.toolCallId}`)}
|
|
215
|
+
onToggle={onToggle}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (group.calls.every((call) => call.kind === "web-search")) {
|
|
220
|
+
return (
|
|
221
|
+
<div className="flex w-full max-w-sm flex-col gap-4">
|
|
222
|
+
{group.calls.map((toolCall) => (
|
|
223
|
+
<NestedToolCallRow
|
|
224
|
+
key={toolCall.toolCallId}
|
|
225
|
+
toolCall={toolCall}
|
|
226
|
+
open={expandedKeys.has(`call-${toolCall.toolCallId}`)}
|
|
227
|
+
onToggle={onToggle}
|
|
228
|
+
/>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
451
233
|
return (
|
|
452
|
-
<
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
{
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
/>
|
|
478
|
-
</span>
|
|
479
|
-
{group.detailLines.length > 1 && (
|
|
480
|
-
<span className="mt-2 block truncate text-[12px] leading-4 text-[rgba(0,0,0,0.3)]">
|
|
481
|
-
{group.detailLines.join(" · ")}
|
|
482
|
-
</span>
|
|
483
|
-
)}
|
|
484
|
-
</span>
|
|
485
|
-
</button>
|
|
486
|
-
{open && (
|
|
487
|
-
<div className="mt-3">
|
|
488
|
-
<ToolGroupDetails calls={group.calls} />
|
|
489
|
-
</div>
|
|
490
|
-
)}
|
|
491
|
-
</div>
|
|
234
|
+
<ToolTimeline
|
|
235
|
+
steps={[]}
|
|
236
|
+
visibleSteps={0}
|
|
237
|
+
streaming={group.hasRunning}
|
|
238
|
+
open={open}
|
|
239
|
+
onOpenChange={(next) => {
|
|
240
|
+
if (next !== open) onToggle(group.key);
|
|
241
|
+
}}
|
|
242
|
+
restingLabel={group.label}
|
|
243
|
+
activeLabel={`Working for ${elapsed}s`}
|
|
244
|
+
stats={[]}
|
|
245
|
+
className="max-w-none"
|
|
246
|
+
>
|
|
247
|
+
{group.calls.map((toolCall) => {
|
|
248
|
+
const key = `call-${toolCall.toolCallId}`;
|
|
249
|
+
return (
|
|
250
|
+
<NestedToolCallRow
|
|
251
|
+
key={toolCall.toolCallId}
|
|
252
|
+
toolCall={toolCall}
|
|
253
|
+
open={expandedKeys.has(key)}
|
|
254
|
+
onToggle={onToggle}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
})}
|
|
258
|
+
</ToolTimeline>
|
|
492
259
|
);
|
|
493
260
|
});
|
|
494
261
|
|