@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.30
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 +6 -6
- 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-spark.svg +9 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +2 -2
- package/cloud-os/chat/cloud-os-chat-messages.tsx +374 -63
- 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/rich-blocks.tsx +548 -246
- package/cloud-os/chat/tool-presentation.ts +35 -16
- package/cloud-os/chat/tool-rows.tsx +330 -85
- package/cloud-os/chat/transcript-model.ts +428 -56
- package/cloud-os/composer/cloud-os-chat-input.tsx +228 -142
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +441 -0
- package/cloud-os/index.ts +35 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +58 -15
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +100 -20
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +223 -43
- package/demo/cloud-os-chat-showcase.tsx +47 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +3 -4
- 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-trigger-popover.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
|
@@ -145,7 +145,7 @@ function truncate(value: string, maxLength: number): string {
|
|
|
145
145
|
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function
|
|
148
|
+
export function humanizeToolName(name: string): string {
|
|
149
149
|
const spaced = name
|
|
150
150
|
.replace(/^mcp__/, "")
|
|
151
151
|
.replace(/__/g, " ")
|
|
@@ -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,
|
|
@@ -343,7 +344,7 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
343
344
|
case "agent": {
|
|
344
345
|
const name =
|
|
345
346
|
stringField(input, "subagentName", "agent", "name", "agentType") ||
|
|
346
|
-
|
|
347
|
+
humanizeToolName(toolName);
|
|
347
348
|
return { verb: running ? "Running" : "Ran", target: name };
|
|
348
349
|
}
|
|
349
350
|
case "skill": {
|
|
@@ -378,13 +379,15 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
378
379
|
const [, server = "", ...rest] = toolName.split("__");
|
|
379
380
|
return {
|
|
380
381
|
verb: running ? "Calling" : "Called",
|
|
381
|
-
target: rest.length
|
|
382
|
+
target: rest.length
|
|
383
|
+
? `${humanizeToolName(rest.join(" "))} · ${humanizeToolName(server)}`
|
|
384
|
+
: undefined,
|
|
382
385
|
};
|
|
383
386
|
}
|
|
384
387
|
case "generic":
|
|
385
388
|
break;
|
|
386
389
|
}
|
|
387
|
-
const displayName =
|
|
390
|
+
const displayName = humanizeToolName(toolName);
|
|
388
391
|
return { verb: running ? `Using ${displayName}` : `Used ${displayName}` };
|
|
389
392
|
}
|
|
390
393
|
|
|
@@ -435,7 +438,7 @@ export function describeToolCallCount(
|
|
|
435
438
|
case "generic":
|
|
436
439
|
break;
|
|
437
440
|
}
|
|
438
|
-
const displayName =
|
|
441
|
+
const displayName = humanizeToolName(toolName);
|
|
439
442
|
return count === 1 ? `Used ${displayName}` : `Used ${displayName} ${formatTimes(count)}`;
|
|
440
443
|
}
|
|
441
444
|
|
|
@@ -496,7 +499,7 @@ export function toCloudOsToolCall(
|
|
|
496
499
|
*
|
|
497
500
|
* - `pending`:还等着用户操作,选项可点。**只有 input 态才算 pending** ——
|
|
498
501
|
* 服务端一旦结算,这次 interaction 就不再接受响应。
|
|
499
|
-
* - `answered`:`ask_user` 的 settle
|
|
502
|
+
* - `answered`:`ask_user` 的 settle 写了按问题对应的 `{ answers }`。
|
|
500
503
|
* - `closed`:被结算掉了但不是一个答案 —— 用户改成直接发言(`{ cancelled: true }`)、
|
|
501
504
|
* Turn 提前结束、或工具出错。必须和 pending 分开,否则卡片会留一个点了拿
|
|
502
505
|
* `{ ok: false }` 的假 Submit,读起来就是「点了没反应」。
|
|
@@ -506,7 +509,14 @@ export function toCloudOsToolCall(
|
|
|
506
509
|
*/
|
|
507
510
|
export type AskUserOutcome =
|
|
508
511
|
| { kind: "pending" }
|
|
509
|
-
| {
|
|
512
|
+
| {
|
|
513
|
+
kind: "answered";
|
|
514
|
+
answers: Array<{
|
|
515
|
+
question: string;
|
|
516
|
+
selections: string[];
|
|
517
|
+
text: string;
|
|
518
|
+
}>;
|
|
519
|
+
}
|
|
510
520
|
| { kind: "closed"; reason: "user_replied_freeform" | "other" };
|
|
511
521
|
|
|
512
522
|
export function askUserOutcome(call: CloudOsToolCall): AskUserOutcome {
|
|
@@ -514,14 +524,23 @@ export function askUserOutcome(call: CloudOsToolCall): AskUserOutcome {
|
|
|
514
524
|
return { kind: "pending" };
|
|
515
525
|
}
|
|
516
526
|
const record = recordOf(call.output);
|
|
517
|
-
if (Array.isArray(record.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
|
|
527
|
+
if (Array.isArray(record.answers) && record.answers.length > 0) {
|
|
528
|
+
const answers = record.answers.map(recordOf);
|
|
529
|
+
if (answers.every((answer) =>
|
|
530
|
+
typeof answer.question === "string" &&
|
|
531
|
+
Array.isArray(answer.selections) &&
|
|
532
|
+
answer.selections.every((value) => typeof value === "string") &&
|
|
533
|
+
(answer.text === undefined || typeof answer.text === "string")
|
|
534
|
+
)) {
|
|
535
|
+
return {
|
|
536
|
+
kind: "answered",
|
|
537
|
+
answers: answers.map((answer) => ({
|
|
538
|
+
question: answer.question as string,
|
|
539
|
+
selections: answer.selections as string[],
|
|
540
|
+
text: typeof answer.text === "string" ? answer.text : "",
|
|
541
|
+
})),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
525
544
|
}
|
|
526
545
|
return {
|
|
527
546
|
kind: "closed",
|
|
@@ -1,40 +1,117 @@
|
|
|
1
|
-
import { CaretRight } from "@phosphor-icons/react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
|
|
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,
|
|
7
11
|
normalizeOutputText,
|
|
8
|
-
safeStringify,
|
|
9
12
|
type CloudOsToolCall,
|
|
10
13
|
type PhosphorIcon,
|
|
11
14
|
type ToolCallGroup,
|
|
12
15
|
} from "./tool-presentation";
|
|
13
16
|
|
|
17
|
+
const thinkingSpark = new URL(
|
|
18
|
+
"../assets/thinking-spark.svg",
|
|
19
|
+
import.meta.url,
|
|
20
|
+
).href;
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
|
-
* 工作行(work rows)
|
|
16
|
-
*
|
|
17
|
-
* className 一个字没改;改的只有取数——从 gadgets 的 AiToolCall 换成
|
|
18
|
-
* UIMessage 归一化后的 CloudOsToolCall。
|
|
23
|
+
* 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
|
|
24
|
+
* ToolGroupRow / ThinkingTraceRow。渲染 UIMessage 归一化后的 CloudOsToolCall。
|
|
19
25
|
*/
|
|
20
26
|
|
|
21
27
|
export function WorkIcon({ Icon }: { Icon: PhosphorIcon }) {
|
|
22
28
|
return <Icon size={15} className="text-kumo-inactive" />;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
function
|
|
26
|
-
if (
|
|
31
|
+
function displayOutput(value: unknown, fallback: string): string {
|
|
32
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
const record = value as Record<string, unknown>;
|
|
36
|
+
if (typeof record.summary === "string" && record.summary.trim()) {
|
|
37
|
+
return record.summary.trim();
|
|
38
|
+
}
|
|
39
|
+
const displayValue = (item: unknown): string =>
|
|
40
|
+
item != null && typeof item === "object"
|
|
41
|
+
? Object.entries(item).map(([key, child]) => `${key}: ${displayValue(child)}`).join("\n")
|
|
42
|
+
: String(item ?? "");
|
|
43
|
+
return displayValue(record);
|
|
44
|
+
}
|
|
45
|
+
|
|
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");
|
|
27
74
|
return (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
75
|
+
<div
|
|
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"
|
|
77
|
+
data-output-card=""
|
|
78
|
+
>
|
|
79
|
+
<div className="flex items-center justify-between gap-3 text-[14px] font-medium leading-5">
|
|
80
|
+
<span>Output</span>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
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"
|
|
85
|
+
aria-label="Copy output"
|
|
86
|
+
>
|
|
87
|
+
<Copy size={16} />
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
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>
|
|
114
|
+
</div>
|
|
38
115
|
);
|
|
39
116
|
}
|
|
40
117
|
|
|
@@ -43,39 +120,15 @@ export const ToolCallDetails = memo(function ToolCallDetails({
|
|
|
43
120
|
}: {
|
|
44
121
|
toolCall: CloudOsToolCall;
|
|
45
122
|
}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<div className="space-y-2">
|
|
56
|
-
{tc.errorText && (
|
|
57
|
-
<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">
|
|
58
|
-
{tc.errorText}
|
|
59
|
-
</pre>
|
|
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>
|
|
78
|
-
);
|
|
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)} />;
|
|
79
132
|
});
|
|
80
133
|
|
|
81
134
|
export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
@@ -118,7 +171,7 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
118
171
|
</span>
|
|
119
172
|
</button>
|
|
120
173
|
{open && (
|
|
121
|
-
<div className="
|
|
174
|
+
<div className="ml-8 mt-1 space-y-3">
|
|
122
175
|
<ToolCallDetails toolCall={tc} />
|
|
123
176
|
</div>
|
|
124
177
|
)}
|
|
@@ -128,14 +181,221 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
128
181
|
|
|
129
182
|
export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
130
183
|
reasoning,
|
|
184
|
+
running,
|
|
131
185
|
}: {
|
|
132
186
|
reasoning: string;
|
|
187
|
+
running: boolean;
|
|
133
188
|
}) {
|
|
189
|
+
const [open, setOpen] = useState(false);
|
|
190
|
+
const contentId = useId();
|
|
191
|
+
const summary = reasoning.replace(/\s+/g, " ").trim();
|
|
192
|
+
|
|
134
193
|
return (
|
|
135
|
-
<div
|
|
136
|
-
|
|
137
|
-
|
|
194
|
+
<div
|
|
195
|
+
className="flex min-w-0 flex-col items-start gap-2"
|
|
196
|
+
data-thinking-trace=""
|
|
197
|
+
data-state={open ? "open" : "closed"}
|
|
198
|
+
>
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={() => setOpen((current) => !current)}
|
|
202
|
+
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"
|
|
203
|
+
aria-expanded={open}
|
|
204
|
+
aria-controls={contentId}
|
|
205
|
+
>
|
|
206
|
+
<span className="relative grid size-4 flex-shrink-0 place-items-center" aria-hidden="true">
|
|
207
|
+
<Atom
|
|
208
|
+
size={14}
|
|
209
|
+
className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
|
|
210
|
+
/>
|
|
211
|
+
<CaretDown
|
|
212
|
+
size={14}
|
|
213
|
+
className={`absolute opacity-0 transition-[opacity,transform] group-hover:opacity-100 group-focus-visible:opacity-100 ${open ? "" : "-rotate-90"}`}
|
|
214
|
+
/>
|
|
215
|
+
</span>
|
|
216
|
+
<span className="flex-shrink-0 text-kumo-subtle">Think</span>
|
|
217
|
+
<span aria-hidden="true">·</span>
|
|
218
|
+
<span
|
|
219
|
+
className="min-w-0 flex-1 truncate"
|
|
220
|
+
style={running ? { direction: "rtl" } : undefined}
|
|
221
|
+
>
|
|
222
|
+
<bdi dir="auto">{summary}</bdi>
|
|
223
|
+
</span>
|
|
224
|
+
</button>
|
|
225
|
+
{open && (
|
|
226
|
+
<div
|
|
227
|
+
id={contentId}
|
|
228
|
+
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"
|
|
229
|
+
>
|
|
230
|
+
<MarkdownMessage message={reasoning} />
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
});
|
|
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>
|
|
138
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} />
|
|
139
399
|
</div>
|
|
140
400
|
);
|
|
141
401
|
});
|
|
@@ -151,19 +411,20 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
151
411
|
expandedKeys: ReadonlySet<string>;
|
|
152
412
|
onToggle: (key: string) => void;
|
|
153
413
|
}) {
|
|
414
|
+
void expandedKeys;
|
|
154
415
|
return (
|
|
155
|
-
<div className="group
|
|
416
|
+
<div className="group/tool-call">
|
|
156
417
|
<button
|
|
157
418
|
type="button"
|
|
158
419
|
onClick={() => onToggle(group.key)}
|
|
159
|
-
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"
|
|
160
421
|
aria-expanded={open}
|
|
161
422
|
>
|
|
162
|
-
<span className="
|
|
423
|
+
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center">
|
|
163
424
|
<WorkIcon Icon={group.Icon} />
|
|
164
425
|
</span>
|
|
165
426
|
<span className="min-w-0 flex-1">
|
|
166
|
-
<span className="flex min-w-0 items-center gap-2
|
|
427
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
167
428
|
<span
|
|
168
429
|
className={`min-w-0 truncate ${group.hasRunning ? "cos-thinking-shimmer" : ""}`}
|
|
169
430
|
>
|
|
@@ -175,38 +436,22 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
175
436
|
</span>
|
|
176
437
|
)}
|
|
177
438
|
<CaretRight
|
|
178
|
-
size={
|
|
179
|
-
|
|
180
|
-
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" : ""}`}
|
|
181
441
|
/>
|
|
182
442
|
</span>
|
|
183
443
|
{group.detailLines.length > 1 && (
|
|
184
|
-
<span className="mt-
|
|
444
|
+
<span className="mt-2 block truncate text-[12px] leading-4 text-[rgba(0,0,0,0.3)]">
|
|
185
445
|
{group.detailLines.join(" · ")}
|
|
186
446
|
</span>
|
|
187
447
|
)}
|
|
188
448
|
</span>
|
|
189
449
|
</button>
|
|
190
|
-
{open &&
|
|
191
|
-
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
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>
|
|
209
|
-
))}
|
|
450
|
+
{open && (
|
|
451
|
+
<div className="mt-3 pl-6">
|
|
452
|
+
<ToolGroupDetails calls={group.calls} />
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
210
455
|
</div>
|
|
211
456
|
);
|
|
212
457
|
});
|