@springbrand/message-panel 0.1.3-alpha.23 → 0.1.3-alpha.25
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/assets/thinking-spark.svg +9 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +218 -9
- 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 +27 -10
- package/cloud-os/chat/rich-blocks.tsx +88 -58
- package/cloud-os/chat/tool-rows.tsx +254 -53
- package/cloud-os/chat/transcript-model.ts +250 -1
- package/cloud-os/composer/cloud-os-chat-input.tsx +1 -21
- package/cloud-os/file-view.tsx +155 -10
- package/cloud-os/index.ts +6 -0
- package/cloud-os/styles/cloud-os.css +12 -0
- package/demo/chat-scenarios.ts +123 -3
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Bell,
|
|
5
5
|
CaretRight,
|
|
6
6
|
Check,
|
|
7
|
+
CheckCircle,
|
|
7
8
|
CircleNotch,
|
|
8
9
|
Clock,
|
|
9
10
|
Key,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
* UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
|
|
33
34
|
* 后续建议、定时任务、并行/子 Agent、原位授权。视觉语言全部沿用 cloud-os:
|
|
34
35
|
* 折叠工作行用 `px-1.5 py-1` 的行式布局,卡片用
|
|
35
|
-
* `
|
|
36
|
+
* `rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
|
|
36
37
|
* 授权卡沿用 renderActionCard 的 blocking callout(brand/40 边 + brand/10 底)。
|
|
37
38
|
*/
|
|
38
39
|
|
|
@@ -41,13 +42,17 @@ import {
|
|
|
41
42
|
export function PlanBlock({
|
|
42
43
|
steps,
|
|
43
44
|
running,
|
|
45
|
+
defaultOpen = true,
|
|
46
|
+
showCollapsedSummary = true,
|
|
44
47
|
}: {
|
|
45
48
|
steps: PlanStep[];
|
|
46
49
|
running: boolean;
|
|
50
|
+
defaultOpen?: boolean;
|
|
51
|
+
showCollapsedSummary?: boolean;
|
|
47
52
|
}) {
|
|
48
53
|
const done = steps.filter((step) => step.status === "done").length;
|
|
49
54
|
const current = steps.find((step) => step.status === "in_progress");
|
|
50
|
-
const [open, setOpen] = useState(
|
|
55
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
51
56
|
|
|
52
57
|
return (
|
|
53
58
|
<div className="group -ml-0.5">
|
|
@@ -71,7 +76,7 @@ export function PlanBlock({
|
|
|
71
76
|
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
72
77
|
/>
|
|
73
78
|
</span>
|
|
74
|
-
{!open && current && (
|
|
79
|
+
{!open && showCollapsedSummary && current && (
|
|
75
80
|
<span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
|
|
76
81
|
{current.text}
|
|
77
82
|
</span>
|
|
@@ -79,35 +84,34 @@ export function PlanBlock({
|
|
|
79
84
|
</span>
|
|
80
85
|
</button>
|
|
81
86
|
{open && (
|
|
82
|
-
<div className="
|
|
87
|
+
<div className="ml-8 mt-1 flex flex-col gap-2 rounded-xl border border-kumo-line bg-[rgba(0,0,0,0.03)] px-4 py-3">
|
|
83
88
|
{steps.map((step, index) => (
|
|
84
89
|
<div
|
|
85
90
|
key={`${step.text}-${index}`}
|
|
86
|
-
className="flex items-
|
|
91
|
+
className="flex items-center gap-2 text-[14px] leading-[1.4] font-normal tracking-normal"
|
|
87
92
|
>
|
|
88
93
|
<span
|
|
89
|
-
className="
|
|
94
|
+
className="flex size-4 flex-shrink-0 items-center justify-center"
|
|
90
95
|
aria-hidden="true"
|
|
91
96
|
>
|
|
92
97
|
{step.status === "done" ? (
|
|
93
|
-
<
|
|
98
|
+
<CheckCircle size={16} weight="fill" className="text-[#00bf75]" />
|
|
94
99
|
) : step.status === "in_progress" ? (
|
|
95
100
|
<CircleNotch
|
|
96
|
-
size={
|
|
97
|
-
|
|
98
|
-
className="animate-spin text-kumo-brand motion-reduce:animate-none"
|
|
101
|
+
size={16}
|
|
102
|
+
className="animate-spin text-[#ff9938] motion-reduce:animate-none"
|
|
99
103
|
/>
|
|
100
104
|
) : (
|
|
101
|
-
<span className="h-1.5 w-1.5 rounded-full bg-kumo-
|
|
105
|
+
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--text-color-kumo-default)] opacity-20" />
|
|
102
106
|
)}
|
|
103
107
|
</span>
|
|
104
108
|
<span
|
|
105
109
|
className={
|
|
106
110
|
step.status === "done"
|
|
107
|
-
? "min-w-0 flex-1 text-kumo-
|
|
111
|
+
? "min-w-0 flex-1 text-kumo-default/30 line-through"
|
|
108
112
|
: step.status === "in_progress"
|
|
109
|
-
? "min-w-0 flex-1
|
|
110
|
-
: "min-w-0 flex-1 text-kumo-
|
|
113
|
+
? "min-w-0 flex-1 text-kumo-default"
|
|
114
|
+
: "min-w-0 flex-1 text-kumo-default/70"
|
|
111
115
|
}
|
|
112
116
|
>
|
|
113
117
|
{step.text}
|
|
@@ -255,7 +259,7 @@ export function AskUserBlock({
|
|
|
255
259
|
data-part-type="ask_user"
|
|
256
260
|
data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
|
|
257
261
|
>
|
|
258
|
-
<div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-
|
|
262
|
+
<div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-operation-base px-3 py-2.5">
|
|
259
263
|
{outcome.kind === "pending" ? (
|
|
260
264
|
<>
|
|
261
265
|
{questions.length > 1 && (
|
|
@@ -308,7 +312,7 @@ export function AskUserBlock({
|
|
|
308
312
|
} ${
|
|
309
313
|
selected
|
|
310
314
|
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
311
|
-
: "border-kumo-line bg-
|
|
315
|
+
: "border-kumo-line bg-operation-base"
|
|
312
316
|
}`}
|
|
313
317
|
>
|
|
314
318
|
<input
|
|
@@ -543,7 +547,7 @@ export function ScheduleConfirmation({
|
|
|
543
547
|
<section
|
|
544
548
|
data-slot="schedule-card"
|
|
545
549
|
aria-label={`Confirm scheduled task: ${name}`}
|
|
546
|
-
className="
|
|
550
|
+
className="flex w-full max-w-sm flex-col gap-3 rounded-2xl border border-kumo-line/70 bg-operation-base p-4 text-kumo-default shadow-[0_1px_2px_rgba(0,0,0,0.04),0_12px_32px_-16px_rgba(0,0,0,0.12)]"
|
|
547
551
|
>
|
|
548
552
|
<div className="flex items-center gap-2.5">
|
|
549
553
|
<span
|
|
@@ -607,7 +611,7 @@ export function ScheduleBlock({
|
|
|
607
611
|
"aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
|
|
608
612
|
}
|
|
609
613
|
: {})}
|
|
610
|
-
className={`w-full rounded-2xl border border-kumo-line bg-
|
|
614
|
+
className={`w-full rounded-2xl border border-kumo-line bg-operation-base px-4 py-3 text-left${
|
|
611
615
|
clickable
|
|
612
616
|
? " cursor-pointer transition-colors duration-150 ease-out hover:bg-kumo-tint focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/50"
|
|
613
617
|
: ""
|
|
@@ -685,7 +689,7 @@ export function PermissionGrant({
|
|
|
685
689
|
return (
|
|
686
690
|
<section
|
|
687
691
|
aria-label={ariaLabel}
|
|
688
|
-
className="
|
|
692
|
+
className="w-full rounded-xl border border-kumo-line bg-operation-base p-4 text-kumo-default"
|
|
689
693
|
>
|
|
690
694
|
<div className="flex items-center gap-2">
|
|
691
695
|
<span
|
|
@@ -814,67 +818,93 @@ export function ParallelBlock({
|
|
|
814
818
|
tools: ParallelTool[];
|
|
815
819
|
}) {
|
|
816
820
|
const done = tools.filter((tool) => tool.status === "done").length;
|
|
821
|
+
const [open, setOpen] = useState(false);
|
|
817
822
|
return (
|
|
818
823
|
<div className="max-w-[860px]">
|
|
819
|
-
<
|
|
824
|
+
<button
|
|
825
|
+
type="button"
|
|
826
|
+
onClick={() => setOpen((value) => !value)}
|
|
827
|
+
className="flex cursor-pointer items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
|
|
828
|
+
aria-expanded={open}
|
|
829
|
+
>
|
|
820
830
|
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
821
831
|
<WorkIcon Icon={ListChecks} />
|
|
822
832
|
</span>
|
|
823
833
|
<span className="min-w-0 truncate">
|
|
824
834
|
{label} · {done}/{tools.length}
|
|
825
835
|
</span>
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
836
|
+
<CaretRight
|
|
837
|
+
size={13}
|
|
838
|
+
weight="bold"
|
|
839
|
+
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
840
|
+
/>
|
|
841
|
+
</button>
|
|
842
|
+
{open && (
|
|
843
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
844
|
+
{tools.map((tool, index) => (
|
|
845
|
+
<div
|
|
846
|
+
key={`${tool.label}-${index}`}
|
|
847
|
+
className="flex items-center gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle"
|
|
848
|
+
>
|
|
849
|
+
<StatusDot status={tool.status} />
|
|
850
|
+
<span className="min-w-0 flex-1 truncate">{tool.label}</span>
|
|
851
|
+
{tool.meta && (
|
|
852
|
+
<span className="flex-shrink-0 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
853
|
+
{tool.meta}
|
|
854
|
+
</span>
|
|
855
|
+
)}
|
|
856
|
+
</div>
|
|
857
|
+
))}
|
|
858
|
+
</div>
|
|
859
|
+
)}
|
|
843
860
|
</div>
|
|
844
861
|
);
|
|
845
862
|
}
|
|
846
863
|
|
|
847
864
|
export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
|
|
865
|
+
const [open, setOpen] = useState(false);
|
|
848
866
|
return (
|
|
849
867
|
<div className="max-w-[860px]">
|
|
850
|
-
<
|
|
868
|
+
<button
|
|
869
|
+
type="button"
|
|
870
|
+
onClick={() => setOpen((value) => !value)}
|
|
871
|
+
className="flex cursor-pointer items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
|
|
872
|
+
aria-expanded={open}
|
|
873
|
+
>
|
|
851
874
|
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
852
875
|
<WorkIcon Icon={UsersThree} />
|
|
853
876
|
</span>
|
|
854
877
|
<span className="min-w-0 truncate">
|
|
855
878
|
{agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
|
|
856
879
|
</span>
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
880
|
+
<CaretRight
|
|
881
|
+
size={13}
|
|
882
|
+
weight="bold"
|
|
883
|
+
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
884
|
+
/>
|
|
885
|
+
</button>
|
|
886
|
+
{open && (
|
|
887
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
888
|
+
{agents.map((agent, index) => (
|
|
889
|
+
<div
|
|
890
|
+
key={`${agent.name}-${index}`}
|
|
891
|
+
className="flex items-start gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px]"
|
|
892
|
+
>
|
|
893
|
+
<span className="mt-1.5 flex">
|
|
894
|
+
<StatusDot status={agent.status} />
|
|
870
895
|
</span>
|
|
871
|
-
|
|
872
|
-
<span className="block text-kumo-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
896
|
+
<span className="min-w-0 flex-1">
|
|
897
|
+
<span className="block truncate font-medium text-kumo-default">
|
|
898
|
+
{agent.name}
|
|
899
|
+
</span>
|
|
900
|
+
{agent.summary && (
|
|
901
|
+
<span className="block text-kumo-subtle">{agent.summary}</span>
|
|
902
|
+
)}
|
|
903
|
+
</span>
|
|
904
|
+
</div>
|
|
905
|
+
))}
|
|
906
|
+
</div>
|
|
907
|
+
)}
|
|
878
908
|
</div>
|
|
879
909
|
);
|
|
880
910
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
3
|
+
import { memo, useId, useState, type ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
MarkdownMessage,
|
|
6
|
+
type CloudOsUrlResolver,
|
|
7
|
+
} from "./markdown-message";
|
|
4
8
|
import {
|
|
5
9
|
getToolCallSummary,
|
|
6
10
|
getToolIcon,
|
|
@@ -10,6 +14,11 @@ import {
|
|
|
10
14
|
type ToolCallGroup,
|
|
11
15
|
} from "./tool-presentation";
|
|
12
16
|
|
|
17
|
+
const thinkingSpark = new URL(
|
|
18
|
+
"../assets/thinking-spark.svg",
|
|
19
|
+
import.meta.url,
|
|
20
|
+
).href;
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
|
|
15
24
|
* ToolGroupRow / ThinkingTraceRow。渲染 UIMessage 归一化后的 CloudOsToolCall。
|
|
@@ -34,26 +43,74 @@ function displayOutput(value: unknown, fallback: string): string {
|
|
|
34
43
|
return displayValue(record);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
interface ToolDetailItem {
|
|
47
|
+
key: string;
|
|
48
|
+
label: string;
|
|
49
|
+
output: string;
|
|
50
|
+
error: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function detailItems(calls: readonly CloudOsToolCall[]): ToolDetailItem[] {
|
|
54
|
+
return calls.map((call) => {
|
|
55
|
+
const summary = getToolCallSummary(call);
|
|
56
|
+
return {
|
|
57
|
+
key: call.toolCallId,
|
|
58
|
+
label: `${summary.verb}${summary.target ? ` ${summary.target}` : ""}`,
|
|
59
|
+
output: displayOutput(call.output, call.outputText),
|
|
60
|
+
error: call.errorText,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function OutputCard({ items }: { items: readonly ToolDetailItem[] }) {
|
|
66
|
+
const copyValue = items
|
|
67
|
+
.map((item) => {
|
|
68
|
+
const detail = [item.error, item.output]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n\n") || "No additional data";
|
|
71
|
+
return items.length === 1 ? detail : `${item.label}\n${detail}`;
|
|
72
|
+
})
|
|
73
|
+
.join("\n\n");
|
|
38
74
|
return (
|
|
39
75
|
<div
|
|
40
|
-
className="rounded-xl border border-
|
|
76
|
+
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"
|
|
41
77
|
data-output-card=""
|
|
42
78
|
>
|
|
43
79
|
<div className="flex items-center justify-between gap-3 text-[14px] font-medium leading-5">
|
|
44
80
|
<span>Output</span>
|
|
45
81
|
<button
|
|
46
82
|
type="button"
|
|
47
|
-
onClick={() => navigator.clipboard.writeText(
|
|
48
|
-
className="flex h-4 w-4 items-center justify-center text-
|
|
83
|
+
onClick={() => navigator.clipboard.writeText(copyValue)}
|
|
84
|
+
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"
|
|
49
85
|
aria-label="Copy output"
|
|
50
86
|
>
|
|
51
87
|
<Copy size={16} />
|
|
52
88
|
</button>
|
|
53
89
|
</div>
|
|
54
|
-
<
|
|
55
|
-
{
|
|
56
|
-
|
|
90
|
+
<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]">
|
|
91
|
+
{items.map((item) => (
|
|
92
|
+
<div key={item.key} className="space-y-1.5">
|
|
93
|
+
{items.length > 1 && (
|
|
94
|
+
<p className="m-0 text-[12px] leading-4 text-cos-output">
|
|
95
|
+
{item.label}
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
{item.error && (
|
|
99
|
+
<p className="m-0 whitespace-pre-wrap text-kumo-danger">
|
|
100
|
+
{item.error}
|
|
101
|
+
</p>
|
|
102
|
+
)}
|
|
103
|
+
{item.output && (
|
|
104
|
+
<p className="m-0 whitespace-pre-wrap text-cos-output">
|
|
105
|
+
{item.output}
|
|
106
|
+
</p>
|
|
107
|
+
)}
|
|
108
|
+
{!item.error && !item.output && (
|
|
109
|
+
<p className="m-0 text-cos-output">No additional data</p>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
57
114
|
</div>
|
|
58
115
|
);
|
|
59
116
|
}
|
|
@@ -63,22 +120,15 @@ export const ToolCallDetails = memo(function ToolCallDetails({
|
|
|
63
120
|
}: {
|
|
64
121
|
toolCall: CloudOsToolCall;
|
|
65
122
|
}) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
);
|
|
123
|
+
return <OutputCard items={detailItems([tc])} />;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export const ToolGroupDetails = memo(function ToolGroupDetails({
|
|
127
|
+
calls,
|
|
128
|
+
}: {
|
|
129
|
+
calls: readonly CloudOsToolCall[];
|
|
130
|
+
}) {
|
|
131
|
+
return <OutputCard items={detailItems(calls)} />;
|
|
82
132
|
});
|
|
83
133
|
|
|
84
134
|
export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
@@ -184,6 +234,172 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
|
184
234
|
);
|
|
185
235
|
});
|
|
186
236
|
|
|
237
|
+
export function formatWorkDuration(milliseconds: number): string {
|
|
238
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
239
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
240
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
241
|
+
const seconds = totalSeconds % 60;
|
|
242
|
+
if (hours > 0) {
|
|
243
|
+
return `${hours}h ${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
|
|
244
|
+
}
|
|
245
|
+
if (minutes > 0) return `${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
|
|
246
|
+
return `${seconds}s`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface ActivitySummary {
|
|
250
|
+
leading: string;
|
|
251
|
+
emphasis?: string;
|
|
252
|
+
trailing?: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function plural(count: number, singular: string, plural = `${singular}s`): string {
|
|
256
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function activitySummary(groups: readonly ToolCallGroup[]): ActivitySummary | null {
|
|
260
|
+
const calls = groups.flatMap((group) => group.calls);
|
|
261
|
+
const count = (kind: CloudOsToolCall["kind"]) =>
|
|
262
|
+
calls.filter((call) => call.kind === kind).length;
|
|
263
|
+
const webSearches = count("web-search");
|
|
264
|
+
const webPages = count("web-fetch");
|
|
265
|
+
if (webSearches > 0 && webPages > 0) {
|
|
266
|
+
return {
|
|
267
|
+
leading: "searched the web and retrieved ",
|
|
268
|
+
emphasis: `${webPages} web`,
|
|
269
|
+
trailing: webPages === 1 ? " page" : " pages",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (webSearches > 0) return { leading: "searched the web" };
|
|
273
|
+
if (webPages > 0) {
|
|
274
|
+
return {
|
|
275
|
+
leading: "retrieved ",
|
|
276
|
+
emphasis: `${webPages} web`,
|
|
277
|
+
trailing: webPages === 1 ? " page" : " pages",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const firstKnown = calls.find((call) =>
|
|
282
|
+
["read", "write", "edit", "bash", "skill", "javascript"].includes(
|
|
283
|
+
call.kind,
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
if (!firstKnown) return null;
|
|
287
|
+
const total = count(firstKnown.kind);
|
|
288
|
+
switch (firstKnown.kind) {
|
|
289
|
+
case "read":
|
|
290
|
+
return { leading: `read ${plural(total, "file")}` };
|
|
291
|
+
case "write":
|
|
292
|
+
return { leading: `wrote ${plural(total, "file")}` };
|
|
293
|
+
case "edit":
|
|
294
|
+
return { leading: `made ${plural(total, "edit")}` };
|
|
295
|
+
case "bash":
|
|
296
|
+
return { leading: `ran ${plural(total, "command")}` };
|
|
297
|
+
case "skill":
|
|
298
|
+
return { leading: `read ${plural(total, "instruction set")}` };
|
|
299
|
+
case "javascript":
|
|
300
|
+
return { leading: total === 1 ? "ran code" : `ran code ${total} times` };
|
|
301
|
+
default:
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const WorkTraceDisclosure = memo(function WorkTraceDisclosure({
|
|
307
|
+
durationMs,
|
|
308
|
+
groups,
|
|
309
|
+
completed = false,
|
|
310
|
+
open,
|
|
311
|
+
onToggle,
|
|
312
|
+
children,
|
|
313
|
+
}: {
|
|
314
|
+
durationMs?: number;
|
|
315
|
+
groups: readonly ToolCallGroup[];
|
|
316
|
+
completed?: boolean;
|
|
317
|
+
open: boolean;
|
|
318
|
+
onToggle: () => void;
|
|
319
|
+
children: ReactNode;
|
|
320
|
+
}) {
|
|
321
|
+
const contentId = useId();
|
|
322
|
+
const reduceMotion = useReducedMotion();
|
|
323
|
+
const activity = activitySummary(groups);
|
|
324
|
+
const duration = durationMs === undefined ? "" : ` for ${formatWorkDuration(durationMs)}`;
|
|
325
|
+
const verb = completed ? "Worked" : "Thought";
|
|
326
|
+
const label = `${verb}${duration}${activity ? `, ${activity.leading}${activity.emphasis ?? ""}${activity.trailing ?? ""}` : ""}`;
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div data-agent-work="" data-state={open ? "open" : "closed"}>
|
|
330
|
+
<div className="w-full border-b border-kumo-line pb-2">
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
onClick={onToggle}
|
|
334
|
+
aria-expanded={open}
|
|
335
|
+
aria-controls={contentId}
|
|
336
|
+
aria-label={label}
|
|
337
|
+
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"
|
|
338
|
+
>
|
|
339
|
+
<span className="grid size-4 shrink-0 place-items-center" aria-hidden="true">
|
|
340
|
+
<motion.img
|
|
341
|
+
src={thinkingSpark}
|
|
342
|
+
alt=""
|
|
343
|
+
className="size-[12.24px]"
|
|
344
|
+
initial={{ rotate: 0 }}
|
|
345
|
+
animate={
|
|
346
|
+
reduceMotion ? { rotate: 0 } : { rotate: [0, 360, -360, -360] }
|
|
347
|
+
}
|
|
348
|
+
transition={
|
|
349
|
+
reduceMotion
|
|
350
|
+
? { duration: 0 }
|
|
351
|
+
: {
|
|
352
|
+
rotate: {
|
|
353
|
+
duration: 1.68,
|
|
354
|
+
times: [0, 0.8368, 0.8373, 1],
|
|
355
|
+
ease: [
|
|
356
|
+
[0.5, 0, 0.5, 1],
|
|
357
|
+
[0.5, 0, 0.5, 1],
|
|
358
|
+
"linear",
|
|
359
|
+
],
|
|
360
|
+
repeat: Infinity,
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/>
|
|
365
|
+
</span>
|
|
366
|
+
<span className="min-w-0 truncate">
|
|
367
|
+
<span>{`${verb}${duration}${activity ? `, ${activity.leading}` : ""}`}</span>
|
|
368
|
+
{activity?.emphasis && (
|
|
369
|
+
<span className="text-kumo-default">{activity.emphasis}</span>
|
|
370
|
+
)}
|
|
371
|
+
{activity?.trailing}
|
|
372
|
+
</span>
|
|
373
|
+
<CaretRight
|
|
374
|
+
size={16}
|
|
375
|
+
className={`shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
376
|
+
aria-hidden="true"
|
|
377
|
+
/>
|
|
378
|
+
</button>
|
|
379
|
+
</div>
|
|
380
|
+
{open && (
|
|
381
|
+
<div id={contentId} className="mt-4 space-y-3" data-agent-work-content="">
|
|
382
|
+
{children}
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
export const WorkDescriptionRow = memo(function WorkDescriptionRow({
|
|
390
|
+
text,
|
|
391
|
+
resolveUrl,
|
|
392
|
+
}: {
|
|
393
|
+
text: string;
|
|
394
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
395
|
+
}) {
|
|
396
|
+
return (
|
|
397
|
+
<div className="cos-markdown text-[14px] leading-[1.4] text-cos-work-description">
|
|
398
|
+
<MarkdownMessage message={text} resolveUrl={resolveUrl} />
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
187
403
|
export const ToolGroupRow = memo(function ToolGroupRow({
|
|
188
404
|
group,
|
|
189
405
|
open,
|
|
@@ -195,19 +411,20 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
195
411
|
expandedKeys: ReadonlySet<string>;
|
|
196
412
|
onToggle: (key: string) => void;
|
|
197
413
|
}) {
|
|
414
|
+
void expandedKeys;
|
|
198
415
|
return (
|
|
199
|
-
<div className="group
|
|
416
|
+
<div className="group/tool-call">
|
|
200
417
|
<button
|
|
201
418
|
type="button"
|
|
202
419
|
onClick={() => onToggle(group.key)}
|
|
203
|
-
className="flex w-
|
|
420
|
+
className="flex min-w-0 items-start gap-2 text-left text-[14px] leading-[1.4] text-cos-tool-group transition-colors duration-150 ease-out hover:text-kumo-subtle focus-visible:text-kumo-subtle focus-visible:outline-none"
|
|
204
421
|
aria-expanded={open}
|
|
205
422
|
>
|
|
206
|
-
<span className="
|
|
423
|
+
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center">
|
|
207
424
|
<WorkIcon Icon={group.Icon} />
|
|
208
425
|
</span>
|
|
209
426
|
<span className="min-w-0 flex-1">
|
|
210
|
-
<span className="flex min-w-0 items-center gap-2
|
|
427
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
211
428
|
<span
|
|
212
429
|
className={`min-w-0 truncate ${group.hasRunning ? "cos-thinking-shimmer" : ""}`}
|
|
213
430
|
>
|
|
@@ -219,38 +436,22 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
219
436
|
</span>
|
|
220
437
|
)}
|
|
221
438
|
<CaretRight
|
|
222
|
-
size={
|
|
223
|
-
|
|
224
|
-
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
439
|
+
size={16}
|
|
440
|
+
className={`shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
225
441
|
/>
|
|
226
442
|
</span>
|
|
227
443
|
{group.detailLines.length > 1 && (
|
|
228
|
-
<span className="mt-
|
|
444
|
+
<span className="mt-2 block truncate text-[12px] leading-4 text-[rgba(0,0,0,0.3)]">
|
|
229
445
|
{group.detailLines.join(" · ")}
|
|
230
446
|
</span>
|
|
231
447
|
)}
|
|
232
448
|
</span>
|
|
233
449
|
</button>
|
|
234
|
-
{open &&
|
|
235
|
-
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
))}
|
|
450
|
+
{open && (
|
|
451
|
+
<div className="mt-3 pl-6">
|
|
452
|
+
<ToolGroupDetails calls={group.calls} />
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
254
455
|
</div>
|
|
255
456
|
);
|
|
256
457
|
});
|