@springbrand/message-panel 0.1.3-alpha.24 → 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 +129 -9
- 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 +108 -1
- package/cloud-os/index.ts +5 -0
- package/cloud-os/styles/cloud-os.css +12 -0
- package/demo/chat-scenarios.ts +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg preserveAspectRatio="none" overflow="visible" width="12.2369" height="12.2369" viewBox="0 0 12.2369 12.2369" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M5.18061 0.652963C5.50276 -0.217655 6.73415 -0.217654 7.05631 0.652964L7.64089 2.23277C8.04603 3.32764 8.90927 4.19089 10.0042 4.59603L11.584 5.18061C12.4546 5.50276 12.4546 6.73415 11.584 7.05631L10.0041 7.64089C8.90927 8.04603 8.04603 8.90927 7.64089 10.0042L7.05631 11.584C6.73415 12.4546 5.50276 12.4546 5.18061 11.584L4.59603 10.0041C4.19089 8.90927 3.32764 8.04603 2.23277 7.64089L0.652963 7.05631C-0.217655 6.73415 -0.217654 5.50276 0.652964 5.18061L2.23277 4.59603C3.32764 4.19089 4.19089 3.32764 4.59603 2.23277L5.18061 0.652963Z" fill="url(#thinking-spark-gradient)"/>
|
|
3
|
+
<defs>
|
|
4
|
+
<radialGradient id="thinking-spark-gradient" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.11846 2.11846) rotate(90) scale(12)">
|
|
5
|
+
<stop stop-color="#FF9938"/>
|
|
6
|
+
<stop offset="1" stop-color="#FFE46C"/>
|
|
7
|
+
</radialGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
</svg>
|
|
@@ -33,7 +33,11 @@ import {
|
|
|
33
33
|
SuggestionsBlock,
|
|
34
34
|
type ApprovalDecision,
|
|
35
35
|
} from "./rich-blocks";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
ToolGroupRow,
|
|
38
|
+
WorkDescriptionRow,
|
|
39
|
+
WorkTraceDisclosure,
|
|
40
|
+
} from "./tool-rows";
|
|
37
41
|
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
38
42
|
import { ProductGeneration } from "./image-generation";
|
|
39
43
|
import { GenerationLoader } from "./loading-state";
|
|
@@ -47,6 +51,7 @@ import {
|
|
|
47
51
|
deriveTurnActivity,
|
|
48
52
|
formatClockTime,
|
|
49
53
|
formatFullTimestamp,
|
|
54
|
+
partitionAssistantBlocks,
|
|
50
55
|
rhythmTopClass,
|
|
51
56
|
type CloudOsEntry,
|
|
52
57
|
type CloudOsActionPresentation,
|
|
@@ -103,6 +108,18 @@ export interface CloudOsChatMessagesProps {
|
|
|
103
108
|
onCopy?: (text: string) => void;
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
export function collapseEntriesWithNewNarrative(
|
|
112
|
+
expanded: ReadonlySet<string>,
|
|
113
|
+
previousNarrativeKeys: ReadonlySet<string>,
|
|
114
|
+
narrativeKeys: ReadonlySet<string>,
|
|
115
|
+
): ReadonlySet<string> {
|
|
116
|
+
const next = new Set(expanded);
|
|
117
|
+
for (const key of narrativeKeys) {
|
|
118
|
+
if (!previousNarrativeKeys.has(key)) next.delete(key);
|
|
119
|
+
}
|
|
120
|
+
return next.size === expanded.size ? expanded : next;
|
|
121
|
+
}
|
|
122
|
+
|
|
106
123
|
function useCopyAction(): {
|
|
107
124
|
copied: string | null;
|
|
108
125
|
copy: (text: string) => void;
|
|
@@ -311,6 +328,9 @@ export function CloudOsChatMessages({
|
|
|
311
328
|
const [expandedToolCalls, setExpandedToolCalls] = useState<ReadonlySet<string>>(
|
|
312
329
|
() => new Set(),
|
|
313
330
|
);
|
|
331
|
+
const [expandedWorkEntries, setExpandedWorkEntries] = useState<
|
|
332
|
+
ReadonlySet<string>
|
|
333
|
+
>(() => new Set());
|
|
314
334
|
const [expandedCompactions, setExpandedCompactions] = useState<
|
|
315
335
|
ReadonlySet<string>
|
|
316
336
|
>(() => new Set());
|
|
@@ -328,6 +348,31 @@ export function CloudOsChatMessages({
|
|
|
328
348
|
}),
|
|
329
349
|
[approvalIdOf, isActive, messages, showThinkingTraces],
|
|
330
350
|
);
|
|
351
|
+
const narrativeEntryKeys = useMemo(
|
|
352
|
+
() =>
|
|
353
|
+
new Set(
|
|
354
|
+
entries.flatMap((entry) =>
|
|
355
|
+
entry.type === "assistant" &&
|
|
356
|
+
partitionAssistantBlocks(entry.blocks).content.some(
|
|
357
|
+
(block) => block.kind === "text",
|
|
358
|
+
)
|
|
359
|
+
? [entry.key]
|
|
360
|
+
: [],
|
|
361
|
+
),
|
|
362
|
+
),
|
|
363
|
+
[entries],
|
|
364
|
+
);
|
|
365
|
+
const previousNarrativeEntryKeys = useRef<ReadonlySet<string>>(
|
|
366
|
+
narrativeEntryKeys,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const previous = previousNarrativeEntryKeys.current;
|
|
371
|
+
previousNarrativeEntryKeys.current = narrativeEntryKeys;
|
|
372
|
+
setExpandedWorkEntries((expanded) =>
|
|
373
|
+
collapseEntriesWithNewNarrative(expanded, previous, narrativeEntryKeys),
|
|
374
|
+
);
|
|
375
|
+
}, [narrativeEntryKeys]);
|
|
331
376
|
|
|
332
377
|
const entryTopClasses = useMemo(
|
|
333
378
|
() =>
|
|
@@ -346,6 +391,15 @@ export function CloudOsChatMessages({
|
|
|
346
391
|
});
|
|
347
392
|
}, []);
|
|
348
393
|
|
|
394
|
+
const toggleWorkEntry = useCallback((key: string) => {
|
|
395
|
+
setExpandedWorkEntries((current) => {
|
|
396
|
+
const next = new Set(current);
|
|
397
|
+
if (next.has(key)) next.delete(key);
|
|
398
|
+
else next.add(key);
|
|
399
|
+
return next;
|
|
400
|
+
});
|
|
401
|
+
}, []);
|
|
402
|
+
|
|
349
403
|
const toggleCompaction = useCallback((key: string) => {
|
|
350
404
|
setExpandedCompactions((current) => {
|
|
351
405
|
const next = new Set(current);
|
|
@@ -493,7 +547,7 @@ export function CloudOsChatMessages({
|
|
|
493
547
|
<span className="h-px flex-1 bg-kumo-line" aria-hidden="true" />
|
|
494
548
|
</div>
|
|
495
549
|
{expanded && (
|
|
496
|
-
<div className="
|
|
550
|
+
<div className="cos-markdown mt-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3.5 text-[13px] leading-[19px]">
|
|
497
551
|
<MarkdownMessage
|
|
498
552
|
message={entry.summary}
|
|
499
553
|
resolveUrl={resolveUrl}
|
|
@@ -504,24 +558,90 @@ export function CloudOsChatMessages({
|
|
|
504
558
|
);
|
|
505
559
|
}
|
|
506
560
|
|
|
507
|
-
const
|
|
561
|
+
const { work: workBlocks, content: contentBlocks } =
|
|
562
|
+
partitionAssistantBlocks(entry.blocks);
|
|
563
|
+
const workGroups = workBlocks.flatMap((block) =>
|
|
564
|
+
block.kind === "toolGroup" ? [block.group] : [],
|
|
565
|
+
);
|
|
566
|
+
const hasNarrative = contentBlocks.some(
|
|
508
567
|
(block) => block.kind === "text",
|
|
509
568
|
);
|
|
510
569
|
|
|
511
570
|
return (
|
|
512
571
|
<div
|
|
513
572
|
key={entry.key}
|
|
514
|
-
className={`${topClass} min-w-0 w-full max-w-[860px]
|
|
573
|
+
className={`${topClass} min-w-0 w-full max-w-[860px]`}
|
|
515
574
|
>
|
|
516
|
-
<div className="group/agentMessage relative space-y-
|
|
517
|
-
{
|
|
575
|
+
<div className="group/agentMessage relative space-y-4">
|
|
576
|
+
{workBlocks.length > 0 && (
|
|
577
|
+
<WorkTraceDisclosure
|
|
578
|
+
durationMs={entry.durationMs}
|
|
579
|
+
groups={workGroups}
|
|
580
|
+
completed={!isActive}
|
|
581
|
+
open={expandedWorkEntries.has(entry.key)}
|
|
582
|
+
onToggle={() => toggleWorkEntry(entry.key)}
|
|
583
|
+
>
|
|
584
|
+
{workBlocks.map((block) => {
|
|
585
|
+
switch (block.kind) {
|
|
586
|
+
case "reasoning":
|
|
587
|
+
case "text":
|
|
588
|
+
return (
|
|
589
|
+
<WorkDescriptionRow
|
|
590
|
+
key={block.key}
|
|
591
|
+
text={block.text}
|
|
592
|
+
resolveUrl={resolveUrl}
|
|
593
|
+
/>
|
|
594
|
+
);
|
|
595
|
+
case "toolGroup":
|
|
596
|
+
return (
|
|
597
|
+
<ToolGroupRow
|
|
598
|
+
key={block.key}
|
|
599
|
+
group={block.group}
|
|
600
|
+
open={expandedToolCalls.has(block.group.key)}
|
|
601
|
+
expandedKeys={expandedToolCalls}
|
|
602
|
+
onToggle={toggleToolCall}
|
|
603
|
+
/>
|
|
604
|
+
);
|
|
605
|
+
case "plan":
|
|
606
|
+
return (
|
|
607
|
+
<PlanBlock
|
|
608
|
+
key={block.key}
|
|
609
|
+
steps={block.steps}
|
|
610
|
+
running={block.running}
|
|
611
|
+
defaultOpen={false}
|
|
612
|
+
showCollapsedSummary={false}
|
|
613
|
+
/>
|
|
614
|
+
);
|
|
615
|
+
case "parallel":
|
|
616
|
+
return (
|
|
617
|
+
<ParallelBlock
|
|
618
|
+
key={block.key}
|
|
619
|
+
label={block.label}
|
|
620
|
+
tools={block.tools}
|
|
621
|
+
/>
|
|
622
|
+
);
|
|
623
|
+
case "subagents":
|
|
624
|
+
return (
|
|
625
|
+
<SubAgentsBlock
|
|
626
|
+
key={block.key}
|
|
627
|
+
agents={block.agents}
|
|
628
|
+
/>
|
|
629
|
+
);
|
|
630
|
+
default:
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
})}
|
|
634
|
+
</WorkTraceDisclosure>
|
|
635
|
+
)}
|
|
636
|
+
|
|
637
|
+
{contentBlocks.map((block) => {
|
|
518
638
|
switch (block.kind) {
|
|
519
639
|
case "reasoning":
|
|
520
640
|
return (
|
|
521
|
-
<
|
|
641
|
+
<WorkDescriptionRow
|
|
522
642
|
key={block.key}
|
|
523
|
-
|
|
524
|
-
|
|
643
|
+
text={block.text}
|
|
644
|
+
resolveUrl={resolveUrl}
|
|
525
645
|
/>
|
|
526
646
|
);
|
|
527
647
|
case "text": {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TextMessagePartProvider,
|
|
3
|
+
useMessagePartText,
|
|
4
|
+
useSmooth,
|
|
5
|
+
type SmoothOptions,
|
|
6
|
+
} from "@assistant-ui/react";
|
|
1
7
|
import { memo, useMemo, type ReactNode } from "react";
|
|
2
8
|
import {
|
|
3
9
|
defaultRehypePlugins,
|
|
@@ -14,7 +20,10 @@ import {
|
|
|
14
20
|
export type CloudOsUrlResolver = (url: string) => string | null | undefined;
|
|
15
21
|
|
|
16
22
|
const ALLOWED_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
17
|
-
const
|
|
23
|
+
const SMOOTH_STREAMING = {
|
|
24
|
+
maxCharsPerFrame: 12,
|
|
25
|
+
minCommitMs: 32,
|
|
26
|
+
} satisfies SmoothOptions;
|
|
18
27
|
|
|
19
28
|
export function safeExternalUrl(href: string | undefined): string | undefined {
|
|
20
29
|
if (!href) return undefined;
|
|
@@ -101,6 +110,20 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
101
110
|
resolveUrl?: CloudOsUrlResolver;
|
|
102
111
|
streaming?: boolean;
|
|
103
112
|
}): ReactNode {
|
|
113
|
+
return (
|
|
114
|
+
<TextMessagePartProvider text={message} isRunning={streaming}>
|
|
115
|
+
<MarkdownMessageContent resolveUrl={resolveUrl} />
|
|
116
|
+
</TextMessagePartProvider>
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function MarkdownMessageContent({
|
|
121
|
+
resolveUrl,
|
|
122
|
+
}: {
|
|
123
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
124
|
+
}) {
|
|
125
|
+
const part = useSmooth(useMessagePartText(), SMOOTH_STREAMING);
|
|
126
|
+
const streaming = part.status.type === "running";
|
|
104
127
|
const rehypePlugins = useMemo(
|
|
105
128
|
() =>
|
|
106
129
|
resolveUrl
|
|
@@ -110,13 +133,7 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
110
133
|
);
|
|
111
134
|
return (
|
|
112
135
|
<Streamdown
|
|
113
|
-
animated={
|
|
114
|
-
streaming
|
|
115
|
-
? CJK_TEXT.test(message)
|
|
116
|
-
? { sep: "char", duration: 100, stagger: 5 }
|
|
117
|
-
: true
|
|
118
|
-
: false
|
|
119
|
-
}
|
|
136
|
+
animated={false}
|
|
120
137
|
className="space-y-0"
|
|
121
138
|
controls={false}
|
|
122
139
|
mode={streaming ? "streaming" : "static"}
|
|
@@ -125,7 +142,7 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
125
142
|
skipHtml
|
|
126
143
|
components={DEFAULT_COMPONENTS}
|
|
127
144
|
>
|
|
128
|
-
{
|
|
145
|
+
{part.text}
|
|
129
146
|
</Streamdown>
|
|
130
147
|
);
|
|
131
|
-
}
|
|
148
|
+
}
|
|
@@ -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
|
});
|
|
@@ -27,6 +27,7 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
|
27
27
|
createdAt?: number;
|
|
28
28
|
error?: string;
|
|
29
29
|
interruptedByUser?: boolean;
|
|
30
|
+
turnId?: string;
|
|
30
31
|
turnDurationMs?: number;
|
|
31
32
|
turnStartedAt?: number;
|
|
32
33
|
turnStatus?: string;
|
|
@@ -142,8 +143,10 @@ export type CloudOsEntry =
|
|
|
142
143
|
type: "assistant";
|
|
143
144
|
key: string;
|
|
144
145
|
messageId: string;
|
|
146
|
+
turnId?: string;
|
|
145
147
|
blocks: AssistantBlock[];
|
|
146
148
|
copyText: string;
|
|
149
|
+
durationMs?: number;
|
|
147
150
|
timestamp?: number;
|
|
148
151
|
terminal?: { kind: "interrupted" | "error"; title: string; body?: string };
|
|
149
152
|
};
|
|
@@ -167,6 +170,10 @@ function nonNegativeNumber(value: unknown): number | undefined {
|
|
|
167
170
|
: undefined;
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
174
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
170
177
|
export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
|
|
171
178
|
return recordOf(message.metadata) as CloudOsMessageMetadata;
|
|
172
179
|
}
|
|
@@ -221,6 +228,54 @@ export function formatFullTimestamp(timestamp: number): string {
|
|
|
221
228
|
});
|
|
222
229
|
}
|
|
223
230
|
|
|
231
|
+
function turnDuration(metadata: CloudOsMessageMetadata): number | undefined {
|
|
232
|
+
const explicit = nonNegativeNumber(metadata.turnDurationMs);
|
|
233
|
+
if (explicit !== undefined) return explicit;
|
|
234
|
+
const startedAt = nonNegativeNumber(metadata.turnStartedAt);
|
|
235
|
+
const completedAt = nonNegativeNumber(metadata.completedAt);
|
|
236
|
+
return startedAt !== undefined && completedAt !== undefined
|
|
237
|
+
? Math.max(0, completedAt - startedAt)
|
|
238
|
+
: undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isWorkBlock(block: AssistantBlock): boolean {
|
|
242
|
+
return (
|
|
243
|
+
block.kind === "reasoning" ||
|
|
244
|
+
block.kind === "toolGroup" ||
|
|
245
|
+
block.kind === "plan" ||
|
|
246
|
+
block.kind === "parallel" ||
|
|
247
|
+
block.kind === "subagents"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 把一条 assistant 消息拆成可折叠的工作过程与始终可见的最终内容。
|
|
253
|
+
* 最后一段工作之前的 text 是过程旁白;最后一次工作之后的 text 才是正文。
|
|
254
|
+
*/
|
|
255
|
+
export function partitionAssistantBlocks(blocks: readonly AssistantBlock[]): {
|
|
256
|
+
work: AssistantBlock[];
|
|
257
|
+
content: AssistantBlock[];
|
|
258
|
+
} {
|
|
259
|
+
let lastWorkIndex = -1;
|
|
260
|
+
blocks.forEach((block, index) => {
|
|
261
|
+
if (isWorkBlock(block)) lastWorkIndex = index;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const work: AssistantBlock[] = [];
|
|
265
|
+
const content: AssistantBlock[] = [];
|
|
266
|
+
blocks.forEach((block, index) => {
|
|
267
|
+
if (
|
|
268
|
+
isWorkBlock(block) ||
|
|
269
|
+
(block.kind === "text" && index < lastWorkIndex)
|
|
270
|
+
) {
|
|
271
|
+
work.push(block);
|
|
272
|
+
} else {
|
|
273
|
+
content.push(block);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return { work, content };
|
|
277
|
+
}
|
|
278
|
+
|
|
224
279
|
function toolNameOf(part: unknown): string | null {
|
|
225
280
|
const type = String(recordOf(part).type ?? "");
|
|
226
281
|
if (type === "dynamic-tool") {
|
|
@@ -620,6 +675,9 @@ export function buildCloudOsEntries({
|
|
|
620
675
|
}
|
|
621
676
|
return -1;
|
|
622
677
|
})();
|
|
678
|
+
const activeTurnId = isActive && lastAssistantIndex >= 0
|
|
679
|
+
? nonEmptyString(cloudOsMetadata(messages[lastAssistantIndex]!).turnId)
|
|
680
|
+
: undefined;
|
|
623
681
|
|
|
624
682
|
messages.forEach((message, index) => {
|
|
625
683
|
if (message.role === "system") return;
|
|
@@ -674,8 +732,10 @@ export function buildCloudOsEntries({
|
|
|
674
732
|
type: "assistant",
|
|
675
733
|
key: `assistant-${message.id}-${index}`,
|
|
676
734
|
messageId: message.id,
|
|
735
|
+
turnId: nonEmptyString(metadata.turnId),
|
|
677
736
|
blocks,
|
|
678
737
|
copyText: messageText(message),
|
|
738
|
+
durationMs: turnDuration(metadata),
|
|
679
739
|
timestamp:
|
|
680
740
|
nonNegativeNumber(metadata.completedAt) ??
|
|
681
741
|
nonNegativeNumber(metadata.createdAt),
|
|
@@ -683,7 +743,54 @@ export function buildCloudOsEntries({
|
|
|
683
743
|
});
|
|
684
744
|
});
|
|
685
745
|
|
|
686
|
-
return
|
|
746
|
+
return aggregateCompletedTurnWork(
|
|
747
|
+
dropSupersededActionPresentations(dropSupersededPlans(entries)),
|
|
748
|
+
activeTurnId,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Collapse every completed Turn's assistant segments into one display entry. */
|
|
753
|
+
function aggregateCompletedTurnWork(
|
|
754
|
+
entries: CloudOsEntry[],
|
|
755
|
+
activeTurnId: string | undefined,
|
|
756
|
+
): CloudOsEntry[] {
|
|
757
|
+
const indexesByTurn = new Map<string, number[]>();
|
|
758
|
+
entries.forEach((entry, index) => {
|
|
759
|
+
if (entry.type !== "assistant" || !entry.turnId) return;
|
|
760
|
+
const indexes = indexesByTurn.get(entry.turnId) ?? [];
|
|
761
|
+
indexes.push(index);
|
|
762
|
+
indexesByTurn.set(entry.turnId, indexes);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const hidden = new Set<number>();
|
|
766
|
+
const replacements = new Map<number, CloudOsEntry>();
|
|
767
|
+
for (const [turnId, indexes] of indexesByTurn) {
|
|
768
|
+
if (turnId === activeTurnId || indexes.length < 2) continue;
|
|
769
|
+
const assistantEntries = indexes.map(
|
|
770
|
+
(index) => entries[index] as Extract<CloudOsEntry, { type: "assistant" }>,
|
|
771
|
+
);
|
|
772
|
+
const anchorIndex = indexes[indexes.length - 1]!;
|
|
773
|
+
const anchor = assistantEntries[assistantEntries.length - 1]!;
|
|
774
|
+
const durations = assistantEntries.flatMap((entry) =>
|
|
775
|
+
entry.durationMs === undefined ? [] : [entry.durationMs]
|
|
776
|
+
);
|
|
777
|
+
const timestamps = assistantEntries.flatMap((entry) =>
|
|
778
|
+
entry.timestamp === undefined ? [] : [entry.timestamp]
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
indexes.slice(0, -1).forEach((index) => hidden.add(index));
|
|
782
|
+
replacements.set(anchorIndex, {
|
|
783
|
+
...anchor,
|
|
784
|
+
key: `assistant-turn-${turnId}`,
|
|
785
|
+
blocks: assistantEntries.flatMap((entry) => entry.blocks),
|
|
786
|
+
...(durations.length > 0 ? { durationMs: Math.max(...durations) } : {}),
|
|
787
|
+
...(timestamps.length > 0 ? { timestamp: Math.max(...timestamps) } : {}),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return entries.flatMap((entry, index) =>
|
|
792
|
+
hidden.has(index) ? [] : [replacements.get(index) ?? entry]
|
|
793
|
+
);
|
|
687
794
|
}
|
|
688
795
|
|
|
689
796
|
function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntry[] {
|
package/cloud-os/index.ts
CHANGED
|
@@ -65,8 +65,12 @@ export type { CloudOsUrlResolver } from "./chat/markdown-message";
|
|
|
65
65
|
export {
|
|
66
66
|
ThinkingTraceRow,
|
|
67
67
|
ToolCallDetails,
|
|
68
|
+
ToolGroupDetails,
|
|
68
69
|
ToolGroupRow,
|
|
70
|
+
WorkDescriptionRow,
|
|
69
71
|
WorkIcon,
|
|
72
|
+
WorkTraceDisclosure,
|
|
73
|
+
formatWorkDuration,
|
|
70
74
|
} from "./chat/tool-rows";
|
|
71
75
|
export {
|
|
72
76
|
ApprovalBlock,
|
|
@@ -91,6 +95,7 @@ export {
|
|
|
91
95
|
formatClockTime,
|
|
92
96
|
formatFullTimestamp,
|
|
93
97
|
messageText,
|
|
98
|
+
partitionAssistantBlocks,
|
|
94
99
|
requestedCapabilitiesOf,
|
|
95
100
|
rhythmTopClass,
|
|
96
101
|
} from "./chat/transcript-model";
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
--color-kumo-success: oklch(72.3% 0.219 149.579);
|
|
52
52
|
--color-kumo-success-tint: oklch(92% 0.08 150);
|
|
53
53
|
|
|
54
|
+
--color-operation-base: #ffffff;
|
|
55
|
+
|
|
54
56
|
--color-kumo-tip-shadow: #14111014;
|
|
55
57
|
--color-kumo-tip-stroke: transparent;
|
|
56
58
|
|
|
@@ -73,6 +75,10 @@
|
|
|
73
75
|
--color-cos-status-live: oklch(72.3% 0.219 149.579);
|
|
74
76
|
--color-cos-status-draft: oklch(68.1% 0.162 75.834);
|
|
75
77
|
--color-cos-status-building: oklch(62.3% 0.214 259.815);
|
|
78
|
+
--text-color-cos-work-description: rgba(0, 0, 0, 0.5);
|
|
79
|
+
--text-color-cos-work-description-subtle: rgba(0, 0, 0, 0.7);
|
|
80
|
+
--text-color-cos-tool-group: rgba(0, 0, 0, 0.5);
|
|
81
|
+
--text-color-cos-output: #000000b3;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
/* ── 作用域根:通用 token 只在 cloud-os 子树内生效 ─────────────────────────── */
|
|
@@ -152,6 +158,8 @@
|
|
|
152
158
|
--color-kumo-success: oklch(52.7% 0.154 150.069);
|
|
153
159
|
--color-kumo-success-tint: oklch(0.25 0.06 150.069);
|
|
154
160
|
|
|
161
|
+
--color-operation-base: var(--color-kumo-base);
|
|
162
|
+
|
|
155
163
|
--color-kumo-tip-shadow: #00000066;
|
|
156
164
|
--color-kumo-tip-stroke: oklch(0.34 0.022 285);
|
|
157
165
|
|
|
@@ -172,6 +180,10 @@
|
|
|
172
180
|
--color-cos-status-live: oklch(79.2% 0.209 151.711);
|
|
173
181
|
--color-cos-status-draft: oklch(82.8% 0.189 84.429);
|
|
174
182
|
--color-cos-status-building: oklch(74% 0.16 250);
|
|
183
|
+
--text-color-cos-work-description: var(--text-color-kumo-subtle);
|
|
184
|
+
--text-color-cos-work-description-subtle: var(--text-color-kumo-subtle);
|
|
185
|
+
--text-color-cos-tool-group: var(--text-color-kumo-inactive);
|
|
186
|
+
--text-color-cos-output: var(--text-color-kumo-subtle);
|
|
175
187
|
|
|
176
188
|
--color-selection-bg: color-mix(in srgb, var(--color-kumo-brand) 28%, transparent);
|
|
177
189
|
--color-selection-text: oklch(0.97 0.006 285);
|
package/demo/chat-scenarios.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface ComposerCommandLike {
|
|
|
18
18
|
}
|
|
19
19
|
const demoCompletedAt = Date.now() - 1_000;
|
|
20
20
|
const demoTurnStartedAt = demoCompletedAt - 18_000;
|
|
21
|
+
const demoTurnId = "camel-demo-research-turn";
|
|
21
22
|
export const defaultPrompt =
|
|
22
23
|
"请调研 2026 年 AI Agent 产品的交互趋势,并给出可落地的消息 UI 建议。";
|
|
23
24
|
export const demoModels = [
|
|
@@ -744,7 +745,7 @@ const commandMessages = [
|
|
|
744
745
|
const runningWork = {
|
|
745
746
|
id: "camel-demo-running-work",
|
|
746
747
|
role: "assistant",
|
|
747
|
-
metadata: { turnStartedAt: demoTurnStartedAt },
|
|
748
|
+
metadata: { turnId: demoTurnId, turnStartedAt: demoTurnStartedAt },
|
|
748
749
|
parts: [
|
|
749
750
|
{
|
|
750
751
|
type: "reasoning",
|
|
@@ -771,7 +772,7 @@ const runningWork = {
|
|
|
771
772
|
const completedResearch = {
|
|
772
773
|
id: "camel-demo-completed-research",
|
|
773
774
|
role: "assistant",
|
|
774
|
-
metadata: { turnStartedAt: demoTurnStartedAt },
|
|
775
|
+
metadata: { turnId: demoTurnId, turnStartedAt: demoTurnStartedAt },
|
|
775
776
|
parts: [
|
|
776
777
|
{
|
|
777
778
|
type: "reasoning",
|
|
@@ -806,6 +807,7 @@ const completedResearch = {
|
|
|
806
807
|
const completedSynthesis = {
|
|
807
808
|
id: "camel-demo-completed-synthesis",
|
|
808
809
|
role: "assistant",
|
|
810
|
+
metadata: { turnId: demoTurnId },
|
|
809
811
|
parts: [
|
|
810
812
|
{
|
|
811
813
|
type: "tool-update_plan",
|
|
@@ -837,6 +839,7 @@ const completedSynthesis = {
|
|
|
837
839
|
const approvalRequest = {
|
|
838
840
|
id: "camel-demo-approval-request",
|
|
839
841
|
role: "assistant",
|
|
842
|
+
metadata: { turnId: demoTurnId },
|
|
840
843
|
parts: [
|
|
841
844
|
{
|
|
842
845
|
type: "tool-write_file",
|
|
@@ -852,6 +855,7 @@ const approvalRequest = {
|
|
|
852
855
|
const collectedAnswer = {
|
|
853
856
|
id: "camel-demo-collected-answer",
|
|
854
857
|
role: "assistant",
|
|
858
|
+
metadata: { turnId: demoTurnId },
|
|
855
859
|
parts: [
|
|
856
860
|
{
|
|
857
861
|
type: "text",
|
|
@@ -868,6 +872,7 @@ const finalAnswer = {
|
|
|
868
872
|
id: "camel-demo-final-answer",
|
|
869
873
|
role: "assistant",
|
|
870
874
|
metadata: {
|
|
875
|
+
turnId: demoTurnId,
|
|
871
876
|
createdAt: demoCompletedAt,
|
|
872
877
|
completedAt: demoCompletedAt,
|
|
873
878
|
turnDurationMs: 18_000,
|
|
@@ -936,6 +941,7 @@ const interruptedByUser = {
|
|
|
936
941
|
id: "camel-demo-interrupted",
|
|
937
942
|
role: "assistant",
|
|
938
943
|
metadata: {
|
|
944
|
+
turnId: demoTurnId,
|
|
939
945
|
createdAt: demoCompletedAt,
|
|
940
946
|
completedAt: demoCompletedAt + 2_000,
|
|
941
947
|
turnDurationMs: 2_000,
|
|
@@ -965,7 +971,7 @@ const compactSummary = {
|
|
|
965
971
|
const failedWork = {
|
|
966
972
|
id: "camel-demo-failed-work",
|
|
967
973
|
role: "assistant",
|
|
968
|
-
metadata: { turnStartedAt: demoTurnStartedAt },
|
|
974
|
+
metadata: { turnId: demoTurnId, turnStartedAt: demoTurnStartedAt },
|
|
969
975
|
parts: [
|
|
970
976
|
...completedResearch.parts,
|
|
971
977
|
{
|