@springbrand/message-panel 0.1.3-alpha.9 → 0.2.0-alpha.37
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 +4 -3
- package/cloud-os/assets/followup-arrow.svg +3 -0
- package/cloud-os/assets/loading-corner.svg +3 -0
- package/cloud-os/assets/loading-mark.svg +16 -0
- package/cloud-os/assets/loading-spark.svg +3 -0
- package/cloud-os/assets/model-selected.svg +5 -0
- package/cloud-os/assets/thinking-star.svg +9 -0
- package/cloud-os/capability-chip.tsx +1 -1
- package/cloud-os/chat/cloud-os-chat-messages.tsx +441 -98
- package/cloud-os/chat/code-runner.tsx +106 -0
- package/cloud-os/chat/image-generation.tsx +76 -0
- package/cloud-os/chat/loading-state.tsx +25 -0
- package/cloud-os/chat/markdown-message.tsx +144 -41
- package/cloud-os/chat/range.ts +8 -0
- package/cloud-os/chat/rich-blocks.tsx +285 -227
- package/cloud-os/chat/surfaces.tsx +90 -0
- package/cloud-os/chat/tool-call.tsx +94 -0
- package/cloud-os/chat/tool-presentation.ts +31 -22
- package/cloud-os/chat/tool-rows.tsx +195 -143
- package/cloud-os/chat/tool-timeline.tsx +123 -0
- package/cloud-os/chat/transcript-model.ts +258 -20
- package/cloud-os/chat/web-search.tsx +135 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +29 -88
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +195 -12
- package/cloud-os/index.ts +24 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +60 -3
- package/cloud-os/primitives/collapsible.tsx +21 -0
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +132 -1
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +214 -40
- package/demo/cloud-os-chat-showcase.tsx +37 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +8 -4
- package/springbrand-pet/LICENSE.bloub +21 -0
- package/springbrand-pet/bot/cycles.test.ts +221 -0
- package/springbrand-pet/bot/cycles.ts +225 -0
- package/springbrand-pet/bot/decor.ts +285 -0
- package/springbrand-pet/bot/engine.test.ts +543 -0
- package/springbrand-pet/bot/engine.ts +558 -0
- package/springbrand-pet/bot/expressions.test.ts +135 -0
- package/springbrand-pet/bot/expressions.ts +192 -0
- package/springbrand-pet/bot/eyefit.ts +455 -0
- package/springbrand-pet/bot/face.test.ts +101 -0
- package/springbrand-pet/bot/face.ts +179 -0
- package/springbrand-pet/bot/math.ts +42 -0
- package/springbrand-pet/bot/profiles.ts +22 -0
- package/springbrand-pet/bot/repere.ts +31 -0
- package/springbrand-pet/bot/shape.test.ts +97 -0
- package/springbrand-pet/bot/shape.ts +310 -0
- package/springbrand-pet/bot/skins.test.ts +325 -0
- package/springbrand-pet/bot/skins.ts +161 -0
- package/springbrand-pet/bot/states.ts +616 -0
- package/springbrand-pet/drag.test.ts +19 -0
- package/springbrand-pet/index.tsx +569 -0
- package/springbrand-pet/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/springbrand-pet/vitest.config.ts +9 -0
- package/src/camel/camel-chat-messages.tsx +78 -78
- package/src/camel/camel-tool-presentation.tsx +19 -15
- package/src/camel/camel-turn.ts +1 -17
- package/src/composer/composer-attachments.tsx +1 -1
- package/src/composer/composer.tsx +2 -2
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +1 -24
- package/src/parts/index.tsx +103 -64
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { cn } from "../internal/cn";
|
|
6
|
+
|
|
7
|
+
export const paper = "bg-background border border-border/60 dark:bg-popover";
|
|
8
|
+
|
|
9
|
+
export const field = "bg-foreground/[0.04] dark:bg-foreground/[0.06]";
|
|
10
|
+
|
|
11
|
+
export const ghostButton =
|
|
12
|
+
"flex items-center justify-center rounded-full text-foreground/45 outline-none transition-[background-color,color,scale] duration-150 hover:bg-foreground/[0.06] hover:text-foreground/90 active:scale-[0.96] focus-visible:ring-1 focus-visible:ring-foreground/20 motion-reduce:transition-none dark:hover:bg-foreground/[0.09]";
|
|
13
|
+
|
|
14
|
+
export const labelSwap =
|
|
15
|
+
"col-start-1 row-start-1 flex w-max items-center gap-1.5 leading-none transition-[opacity,filter] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none";
|
|
16
|
+
|
|
17
|
+
export const labelSwapIn = "opacity-100 blur-none";
|
|
18
|
+
|
|
19
|
+
export const labelSwapOut =
|
|
20
|
+
"pointer-events-none select-none opacity-0 blur-[2px]";
|
|
21
|
+
|
|
22
|
+
export const collapsePanel =
|
|
23
|
+
"h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] data-[ending-style]:h-0 data-[starting-style]:h-0 motion-reduce:transition-none";
|
|
24
|
+
|
|
25
|
+
export const mono = "font-mono text-[11px] tracking-tight";
|
|
26
|
+
|
|
27
|
+
export const codeScroll = "max-h-48 overflow-auto";
|
|
28
|
+
|
|
29
|
+
export const codeSurface = "w-max min-w-full";
|
|
30
|
+
|
|
31
|
+
export function ShimmerLabel({
|
|
32
|
+
active = true,
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: ComponentProps<"span"> & { active?: boolean }) {
|
|
36
|
+
return (
|
|
37
|
+
<span
|
|
38
|
+
className={cn(active && "shimmer motion-reduce:animate-none", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function SwapLabel({
|
|
45
|
+
active,
|
|
46
|
+
children,
|
|
47
|
+
className,
|
|
48
|
+
}: {
|
|
49
|
+
active: 0 | 1;
|
|
50
|
+
children: [React.ReactNode, React.ReactNode];
|
|
51
|
+
className?: string;
|
|
52
|
+
}) {
|
|
53
|
+
const layers = [useRef<HTMLSpanElement>(null), useRef<HTMLSpanElement>(null)];
|
|
54
|
+
const [width, setWidth] = useState<number | null>(null);
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
const target = layers[active]?.current;
|
|
58
|
+
if (!target) return undefined;
|
|
59
|
+
const measure = () =>
|
|
60
|
+
setWidth(Math.ceil(target.getBoundingClientRect().width));
|
|
61
|
+
measure();
|
|
62
|
+
const observer = new ResizeObserver(measure);
|
|
63
|
+
observer.observe(target);
|
|
64
|
+
return () => observer.disconnect();
|
|
65
|
+
}, [active]);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<span
|
|
69
|
+
style={width === null ? undefined : { width }}
|
|
70
|
+
className={cn(
|
|
71
|
+
"grid overflow-x-clip transition-[width] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{children.map((layer, index) => (
|
|
76
|
+
<span
|
|
77
|
+
key={index}
|
|
78
|
+
ref={layers[index]}
|
|
79
|
+
aria-hidden={active !== index}
|
|
80
|
+
className={cn(
|
|
81
|
+
labelSwap,
|
|
82
|
+
active === index ? labelSwapIn : labelSwapOut,
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{layer}
|
|
86
|
+
</span>
|
|
87
|
+
))}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
Collapsible,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
CollapsibleTrigger,
|
|
8
|
+
} from "../primitives/collapsible";
|
|
9
|
+
import { cn } from "../internal/cn";
|
|
10
|
+
import {
|
|
11
|
+
collapsePanel,
|
|
12
|
+
field,
|
|
13
|
+
mono,
|
|
14
|
+
ShimmerLabel,
|
|
15
|
+
SwapLabel,
|
|
16
|
+
} from "./surfaces";
|
|
17
|
+
|
|
18
|
+
export interface ToolCallProps {
|
|
19
|
+
label: string;
|
|
20
|
+
activeLabel: string;
|
|
21
|
+
query: string;
|
|
22
|
+
result: string;
|
|
23
|
+
running: boolean;
|
|
24
|
+
open: boolean;
|
|
25
|
+
onOpenChange: (open: boolean) => void;
|
|
26
|
+
className?: string;
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ToolCall({
|
|
31
|
+
label,
|
|
32
|
+
activeLabel,
|
|
33
|
+
query,
|
|
34
|
+
result,
|
|
35
|
+
running,
|
|
36
|
+
open,
|
|
37
|
+
onOpenChange,
|
|
38
|
+
className,
|
|
39
|
+
children,
|
|
40
|
+
}: ToolCallProps) {
|
|
41
|
+
return (
|
|
42
|
+
<Collapsible
|
|
43
|
+
data-slot="tool-call"
|
|
44
|
+
open={open}
|
|
45
|
+
onOpenChange={onOpenChange}
|
|
46
|
+
className={cn("w-full max-w-sm", className)}
|
|
47
|
+
>
|
|
48
|
+
<CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-2 rounded-md py-1 text-[13.5px] transition-colors outline-none">
|
|
49
|
+
<ChevronRightIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-90 group-data-panel-open/trigger:rotate-90 motion-reduce:transition-none" />
|
|
50
|
+
<SwapLabel active={running ? 0 : 1} className="text-start">
|
|
51
|
+
<ShimmerLabel
|
|
52
|
+
active={running}
|
|
53
|
+
className="relative inline-block leading-none"
|
|
54
|
+
>
|
|
55
|
+
{activeLabel}
|
|
56
|
+
</ShimmerLabel>
|
|
57
|
+
<>{label}</>
|
|
58
|
+
</SwapLabel>
|
|
59
|
+
{query && (
|
|
60
|
+
<span
|
|
61
|
+
className={cn(
|
|
62
|
+
mono,
|
|
63
|
+
"bg-foreground/[0.06] text-foreground/70 rounded-md px-1.5 py-0.5",
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{query}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
<span className="ms-auto flex w-4 items-center justify-end">
|
|
70
|
+
{!running && (
|
|
71
|
+
<CheckIcon className="fade-in zoom-in-90 animate-in size-3.5 text-emerald-500 duration-200" />
|
|
72
|
+
)}
|
|
73
|
+
</span>
|
|
74
|
+
</CollapsibleTrigger>
|
|
75
|
+
<CollapsibleContent className={cn(collapsePanel, "outline-none")}>
|
|
76
|
+
{children ? (
|
|
77
|
+
<div className="mt-2">{children}</div>
|
|
78
|
+
) : (
|
|
79
|
+
<div className={cn(field, "mt-2 overflow-hidden rounded-2xl text-xs")}>
|
|
80
|
+
<div className="px-3.5 py-2.5">
|
|
81
|
+
<p className={cn(mono, "text-foreground/35 mb-1")}>Result</p>
|
|
82
|
+
<p
|
|
83
|
+
className="text-foreground/90 overflow-auto leading-4"
|
|
84
|
+
style={{ maxHeight: "5rem" }}
|
|
85
|
+
>
|
|
86
|
+
{result}
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</CollapsibleContent>
|
|
92
|
+
</Collapsible>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -213,7 +213,7 @@ export function canonicalToolKind(name: string): CloudOsToolKind {
|
|
|
213
213
|
return "web-fetch";
|
|
214
214
|
}
|
|
215
215
|
if (
|
|
216
|
-
["javascript", "js_exec", "node_repl", "execute_javascript", "execute_code"].includes(
|
|
216
|
+
["javascript", "js_exec", "node_repl", "execute", "execute_javascript", "execute_code"].includes(
|
|
217
217
|
normalized,
|
|
218
218
|
)
|
|
219
219
|
) {
|
|
@@ -298,7 +298,8 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
298
298
|
return { verb: running ? "Editing" : "Edited", target: filename || undefined };
|
|
299
299
|
case "bash": {
|
|
300
300
|
const command =
|
|
301
|
-
stringField(input, "description") ||
|
|
301
|
+
stringField(input, "description") ||
|
|
302
|
+
stringField(input, "script", "command", "cmd");
|
|
302
303
|
return {
|
|
303
304
|
verb: running ? "Running" : "Ran",
|
|
304
305
|
target: command ? truncate(command, 60) : undefined,
|
|
@@ -328,16 +329,8 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
328
329
|
target: url ? hostnameOf(url) : undefined,
|
|
329
330
|
};
|
|
330
331
|
}
|
|
331
|
-
case "javascript":
|
|
332
|
-
|
|
333
|
-
.split("\n")
|
|
334
|
-
.map((line) => line.trim())
|
|
335
|
-
.find((line) => line.length > 0);
|
|
336
|
-
return {
|
|
337
|
-
verb: running ? "Running code" : "Ran code",
|
|
338
|
-
target: firstLine ? truncate(firstLine, 60) : undefined,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
332
|
+
case "javascript":
|
|
333
|
+
return { verb: running ? "Running code" : "Ran code" };
|
|
341
334
|
case "tasks":
|
|
342
335
|
return { verb: running ? "Updating plan" : "Updated plan" };
|
|
343
336
|
case "agent": {
|
|
@@ -498,7 +491,7 @@ export function toCloudOsToolCall(
|
|
|
498
491
|
*
|
|
499
492
|
* - `pending`:还等着用户操作,选项可点。**只有 input 态才算 pending** ——
|
|
500
493
|
* 服务端一旦结算,这次 interaction 就不再接受响应。
|
|
501
|
-
* - `answered`:`ask_user` 的 settle
|
|
494
|
+
* - `answered`:`ask_user` 的 settle 写了按问题对应的 `{ answers }`。
|
|
502
495
|
* - `closed`:被结算掉了但不是一个答案 —— 用户改成直接发言(`{ cancelled: true }`)、
|
|
503
496
|
* Turn 提前结束、或工具出错。必须和 pending 分开,否则卡片会留一个点了拿
|
|
504
497
|
* `{ ok: false }` 的假 Submit,读起来就是「点了没反应」。
|
|
@@ -508,7 +501,14 @@ export function toCloudOsToolCall(
|
|
|
508
501
|
*/
|
|
509
502
|
export type AskUserOutcome =
|
|
510
503
|
| { kind: "pending" }
|
|
511
|
-
| {
|
|
504
|
+
| {
|
|
505
|
+
kind: "answered";
|
|
506
|
+
answers: Array<{
|
|
507
|
+
question: string;
|
|
508
|
+
selections: string[];
|
|
509
|
+
text: string;
|
|
510
|
+
}>;
|
|
511
|
+
}
|
|
512
512
|
| { kind: "closed"; reason: "user_replied_freeform" | "other" };
|
|
513
513
|
|
|
514
514
|
export function askUserOutcome(call: CloudOsToolCall): AskUserOutcome {
|
|
@@ -516,14 +516,23 @@ export function askUserOutcome(call: CloudOsToolCall): AskUserOutcome {
|
|
|
516
516
|
return { kind: "pending" };
|
|
517
517
|
}
|
|
518
518
|
const record = recordOf(call.output);
|
|
519
|
-
if (Array.isArray(record.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
|
|
519
|
+
if (Array.isArray(record.answers) && record.answers.length > 0) {
|
|
520
|
+
const answers = record.answers.map(recordOf);
|
|
521
|
+
if (answers.every((answer) =>
|
|
522
|
+
typeof answer.question === "string" &&
|
|
523
|
+
Array.isArray(answer.selections) &&
|
|
524
|
+
answer.selections.every((value) => typeof value === "string") &&
|
|
525
|
+
(answer.text === undefined || typeof answer.text === "string")
|
|
526
|
+
)) {
|
|
527
|
+
return {
|
|
528
|
+
kind: "answered",
|
|
529
|
+
answers: answers.map((answer) => ({
|
|
530
|
+
question: answer.question as string,
|
|
531
|
+
selections: answer.selections as string[],
|
|
532
|
+
text: typeof answer.text === "string" ? answer.text : "",
|
|
533
|
+
})),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
527
536
|
}
|
|
528
537
|
return {
|
|
529
538
|
kind: "closed",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { memo, useEffect, useId, useState } from "react";
|
|
2
|
+
import { CodeRunner } from "./code-runner";
|
|
3
3
|
import { MarkdownMessage } from "./markdown-message";
|
|
4
|
+
import { ToolCall } from "./tool-call";
|
|
5
|
+
import { ToolTimeline } from "./tool-timeline";
|
|
6
|
+
import { WebSearchToolCall } from "./web-search";
|
|
4
7
|
import {
|
|
5
8
|
getToolCallSummary,
|
|
6
|
-
getToolIcon,
|
|
7
9
|
normalizeOutputText,
|
|
8
10
|
safeStringify,
|
|
9
11
|
type CloudOsToolCall,
|
|
@@ -11,31 +13,53 @@ import {
|
|
|
11
13
|
type ToolCallGroup,
|
|
12
14
|
} from "./tool-presentation";
|
|
13
15
|
|
|
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]";
|
|
18
|
+
|
|
19
|
+
export const DISCLOSURE_TITLE_WITH_ICON =
|
|
20
|
+
`${DISCLOSURE_TITLE_BASE} items-start py-1`;
|
|
21
|
+
|
|
22
|
+
export const DISCLOSURE_ICON =
|
|
23
|
+
"mt-0.5 flex size-4 shrink-0 items-center justify-center";
|
|
24
|
+
|
|
25
|
+
const thinkingStar = new URL("../assets/thinking-star.svg", import.meta.url).href;
|
|
26
|
+
|
|
14
27
|
/**
|
|
15
|
-
* 工作行(work rows)
|
|
16
|
-
*
|
|
17
|
-
* className 一个字没改;改的只有取数——从 gadgets 的 AiToolCall 换成
|
|
18
|
-
* UIMessage 归一化后的 CloudOsToolCall。
|
|
28
|
+
* 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
|
|
29
|
+
* ToolGroupRow / ThinkingTraceRow。渲染 UIMessage 归一化后的 CloudOsToolCall。
|
|
19
30
|
*/
|
|
20
31
|
|
|
21
32
|
export function WorkIcon({ Icon }: { Icon: PhosphorIcon }) {
|
|
22
33
|
return <Icon size={15} className="text-kumo-inactive" />;
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
function
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
function displayOutput(value: unknown, fallback: string): string {
|
|
37
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
const record = value as Record<string, unknown>;
|
|
41
|
+
if (typeof record.summary === "string" && record.summary.trim()) {
|
|
42
|
+
return record.summary.trim();
|
|
43
|
+
}
|
|
44
|
+
const displayValue = (item: unknown): string =>
|
|
45
|
+
item != null && typeof item === "object"
|
|
46
|
+
? Object.entries(item).map(([key, child]) => `${key}: ${displayValue(child)}`).join("\n")
|
|
47
|
+
: String(item ?? "");
|
|
48
|
+
return displayValue(record);
|
|
49
|
+
}
|
|
50
|
+
|
|
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
|
+
};
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
export const ToolCallDetails = memo(function ToolCallDetails({
|
|
@@ -43,38 +67,13 @@ export const ToolCallDetails = memo(function ToolCallDetails({
|
|
|
43
67
|
}: {
|
|
44
68
|
toolCall: CloudOsToolCall;
|
|
45
69
|
}) {
|
|
46
|
-
const code =
|
|
47
|
-
typeof tc.input.code === "string"
|
|
48
|
-
? tc.input.code
|
|
49
|
-
: typeof tc.input.command === "string"
|
|
50
|
-
? tc.input.command
|
|
51
|
-
: "";
|
|
52
|
-
const inputText =
|
|
53
|
-
Object.keys(tc.input).length > 0 ? safeStringify(tc.input) : "";
|
|
54
70
|
return (
|
|
55
|
-
<
|
|
56
|
-
{tc
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{code ? (
|
|
62
|
-
<>
|
|
63
|
-
<OutputPre label="Code" value={code} />
|
|
64
|
-
<OutputPre label="Output" value={tc.outputText} />
|
|
65
|
-
</>
|
|
66
|
-
) : (
|
|
67
|
-
<>
|
|
68
|
-
<OutputPre value={inputText} />
|
|
69
|
-
{tc.outputText && <OutputPre label="Output" value={tc.outputText} />}
|
|
70
|
-
</>
|
|
71
|
-
)}
|
|
72
|
-
{!code && !inputText && !tc.outputText && !tc.errorText && (
|
|
73
|
-
<p className="m-0 text-[12px] leading-4 text-kumo-inactive">
|
|
74
|
-
No additional data
|
|
75
|
-
</p>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
71
|
+
<ToolCall
|
|
72
|
+
{...toolCallView(tc)}
|
|
73
|
+
running={tc.running}
|
|
74
|
+
open
|
|
75
|
+
onOpenChange={() => undefined}
|
|
76
|
+
/>
|
|
78
77
|
);
|
|
79
78
|
});
|
|
80
79
|
|
|
@@ -87,58 +86,117 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
87
86
|
open: boolean;
|
|
88
87
|
onToggle: (key: string) => void;
|
|
89
88
|
}) {
|
|
89
|
+
if (tc.kind === "web-search" && !tc.failed) {
|
|
90
|
+
return <WebSearchToolCall toolCall={tc} />;
|
|
91
|
+
}
|
|
92
|
+
|
|
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"
|
|
109
|
+
>
|
|
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
|
+
}
|
|
120
|
+
|
|
90
121
|
const key = `call-${tc.toolCallId}`;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
return (
|
|
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
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
136
|
+
reasoning,
|
|
137
|
+
running,
|
|
138
|
+
}: {
|
|
139
|
+
reasoning: string;
|
|
140
|
+
running: boolean;
|
|
141
|
+
}) {
|
|
142
|
+
const [open, setOpen] = useState(false);
|
|
143
|
+
const contentId = useId();
|
|
144
|
+
const summary = reasoning.replace(/\s+/g, " ").trim();
|
|
94
145
|
|
|
95
146
|
return (
|
|
96
|
-
<div
|
|
147
|
+
<div
|
|
148
|
+
className="flex min-w-0 flex-col items-start gap-2"
|
|
149
|
+
data-thinking-trace=""
|
|
150
|
+
data-state={open ? "open" : "closed"}
|
|
151
|
+
>
|
|
97
152
|
<button
|
|
98
153
|
type="button"
|
|
99
|
-
onClick={() =>
|
|
100
|
-
className=
|
|
154
|
+
onClick={() => setOpen((current) => !current)}
|
|
155
|
+
className={`${DISCLOSURE_TITLE_WITH_ICON} group w-fit max-w-[80%]`}
|
|
101
156
|
aria-expanded={open}
|
|
157
|
+
aria-controls={contentId}
|
|
102
158
|
>
|
|
103
|
-
<span className=
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{tc.failed && (
|
|
109
|
-
<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">
|
|
110
|
-
Error
|
|
111
|
-
</span>
|
|
112
|
-
)}
|
|
113
|
-
<CaretRight
|
|
114
|
-
size={13}
|
|
115
|
-
weight="bold"
|
|
116
|
-
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
159
|
+
<span className={DISCLOSURE_ICON} aria-hidden="true">
|
|
160
|
+
<img
|
|
161
|
+
src={thinkingStar}
|
|
162
|
+
alt=""
|
|
163
|
+
className={`size-[12.2369px] ${running ? "cos-thinking-star" : ""}`}
|
|
117
164
|
/>
|
|
118
165
|
</span>
|
|
166
|
+
<span className="flex-shrink-0">Think</span>
|
|
167
|
+
<span aria-hidden="true">·</span>
|
|
168
|
+
<span
|
|
169
|
+
className="min-w-0 flex-1 truncate"
|
|
170
|
+
style={running ? { direction: "rtl" } : undefined}
|
|
171
|
+
>
|
|
172
|
+
<bdi dir="auto">{summary}</bdi>
|
|
173
|
+
</span>
|
|
119
174
|
</button>
|
|
120
175
|
{open && (
|
|
121
|
-
<div
|
|
122
|
-
|
|
176
|
+
<div
|
|
177
|
+
id={contentId}
|
|
178
|
+
className="cos-markdown w-full px-4 text-[14px] leading-[1.4] text-kumo-default/70"
|
|
179
|
+
>
|
|
180
|
+
<MarkdownMessage message={reasoning} />
|
|
123
181
|
</div>
|
|
124
182
|
)}
|
|
125
183
|
</div>
|
|
126
184
|
);
|
|
127
185
|
});
|
|
128
186
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
187
|
+
function useElapsedSeconds(running: boolean) {
|
|
188
|
+
const [elapsed, setElapsed] = useState(0);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!running) return undefined;
|
|
191
|
+
const startedAt = Date.now() - elapsed * 1000;
|
|
192
|
+
const timer = window.setInterval(
|
|
193
|
+
() => setElapsed(Math.floor((Date.now() - startedAt) / 1000)),
|
|
194
|
+
1000,
|
|
195
|
+
);
|
|
196
|
+
return () => window.clearInterval(timer);
|
|
197
|
+
}, [running]);
|
|
198
|
+
return elapsed;
|
|
199
|
+
}
|
|
142
200
|
|
|
143
201
|
export const ToolGroupRow = memo(function ToolGroupRow({
|
|
144
202
|
group,
|
|
@@ -151,63 +209,57 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
151
209
|
expandedKeys: ReadonlySet<string>;
|
|
152
210
|
onToggle: (key: string) => void;
|
|
153
211
|
}) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
</span>
|
|
176
|
-
)}
|
|
177
|
-
<CaretRight
|
|
178
|
-
size={13}
|
|
179
|
-
weight="bold"
|
|
180
|
-
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
181
|
-
/>
|
|
182
|
-
</span>
|
|
183
|
-
{group.detailLines.length > 1 && (
|
|
184
|
-
<span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
|
|
185
|
-
{group.detailLines.join(" · ")}
|
|
186
|
-
</span>
|
|
187
|
-
)}
|
|
188
|
-
</span>
|
|
189
|
-
</button>
|
|
190
|
-
{open &&
|
|
191
|
-
(group.calls.length === 1 ? (
|
|
192
|
-
<div className="themed-surface-inset ml-8 mt-1 space-y-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3">
|
|
193
|
-
<ToolCallDetails toolCall={group.calls[0]} />
|
|
194
|
-
</div>
|
|
195
|
-
) : (
|
|
196
|
-
<div className="ml-8 mt-1 space-y-1">
|
|
197
|
-
{group.calls.map((toolCall) => {
|
|
198
|
-
const key = `call-${toolCall.toolCallId}`;
|
|
199
|
-
return (
|
|
200
|
-
<NestedToolCallRow
|
|
201
|
-
key={toolCall.toolCallId}
|
|
202
|
-
toolCall={toolCall}
|
|
203
|
-
open={expandedKeys.has(key)}
|
|
204
|
-
onToggle={onToggle}
|
|
205
|
-
/>
|
|
206
|
-
);
|
|
207
|
-
})}
|
|
208
|
-
</div>
|
|
212
|
+
const elapsed = useElapsedSeconds(group.hasRunning && group.calls.length > 1);
|
|
213
|
+
const onlyCall = group.calls.length === 1 ? group.calls[0] : undefined;
|
|
214
|
+
if (onlyCall) {
|
|
215
|
+
return (
|
|
216
|
+
<NestedToolCallRow
|
|
217
|
+
toolCall={onlyCall}
|
|
218
|
+
open={expandedKeys.has(`call-${onlyCall.toolCallId}`)}
|
|
219
|
+
onToggle={onToggle}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (group.calls.every((call) => call.kind === "web-search")) {
|
|
224
|
+
return (
|
|
225
|
+
<div className="flex w-full max-w-sm flex-col gap-4">
|
|
226
|
+
{group.calls.map((toolCall) => (
|
|
227
|
+
<NestedToolCallRow
|
|
228
|
+
key={toolCall.toolCallId}
|
|
229
|
+
toolCall={toolCall}
|
|
230
|
+
open={expandedKeys.has(`call-${toolCall.toolCallId}`)}
|
|
231
|
+
onToggle={onToggle}
|
|
232
|
+
/>
|
|
209
233
|
))}
|
|
210
|
-
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return (
|
|
238
|
+
<ToolTimeline
|
|
239
|
+
steps={[]}
|
|
240
|
+
visibleSteps={0}
|
|
241
|
+
streaming={group.hasRunning}
|
|
242
|
+
open={open}
|
|
243
|
+
onOpenChange={(next) => {
|
|
244
|
+
if (next !== open) onToggle(group.key);
|
|
245
|
+
}}
|
|
246
|
+
restingLabel={group.label}
|
|
247
|
+
activeLabel={`Working for ${elapsed}s`}
|
|
248
|
+
stats={[]}
|
|
249
|
+
className="max-w-none"
|
|
250
|
+
>
|
|
251
|
+
{group.calls.map((toolCall) => {
|
|
252
|
+
const key = `call-${toolCall.toolCallId}`;
|
|
253
|
+
return (
|
|
254
|
+
<NestedToolCallRow
|
|
255
|
+
key={toolCall.toolCallId}
|
|
256
|
+
toolCall={toolCall}
|
|
257
|
+
open={expandedKeys.has(key)}
|
|
258
|
+
onToggle={onToggle}
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</ToolTimeline>
|
|
211
263
|
);
|
|
212
264
|
});
|
|
213
265
|
|