@springbrand/message-panel 0.1.3-alpha.31 → 0.1.3-alpha.32
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.
|
@@ -57,6 +57,9 @@ import {
|
|
|
57
57
|
type CloudOsEntry,
|
|
58
58
|
type CloudOsActionPresentation,
|
|
59
59
|
} from "./transcript-model";
|
|
60
|
+
import { useDisclosureExpandedKeys } from "./use-lifecycle-disclosure";
|
|
61
|
+
|
|
62
|
+
const NO_RUNNING_DISCLOSURES: ReadonlySet<string> = new Set();
|
|
60
63
|
|
|
61
64
|
export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
62
65
|
|
|
@@ -113,18 +116,6 @@ export interface CloudOsChatMessagesProps {
|
|
|
113
116
|
onCopy?: (text: string) => void;
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
export function collapseEntriesWithNewNarrative(
|
|
117
|
-
expanded: ReadonlySet<string>,
|
|
118
|
-
previousNarrativeKeys: ReadonlySet<string>,
|
|
119
|
-
narrativeKeys: ReadonlySet<string>,
|
|
120
|
-
): ReadonlySet<string> {
|
|
121
|
-
const next = new Set(expanded);
|
|
122
|
-
for (const key of narrativeKeys) {
|
|
123
|
-
if (!previousNarrativeKeys.has(key)) next.delete(key);
|
|
124
|
-
}
|
|
125
|
-
return next.size === expanded.size ? expanded : next;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
119
|
function useCopyAction(): {
|
|
129
120
|
copied: string | null;
|
|
130
121
|
copy: (text: string) => void;
|
|
@@ -362,15 +353,6 @@ export function CloudOsChatMessages({
|
|
|
362
353
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
363
354
|
const endRef = useRef<HTMLDivElement>(null);
|
|
364
355
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
365
|
-
const [expandedToolCalls, setExpandedToolCalls] = useState<ReadonlySet<string>>(
|
|
366
|
-
() => new Set(),
|
|
367
|
-
);
|
|
368
|
-
const [expandedWorkEntries, setExpandedWorkEntries] = useState<
|
|
369
|
-
ReadonlySet<string>
|
|
370
|
-
>(() => new Set());
|
|
371
|
-
const [expandedCompactions, setExpandedCompactions] = useState<
|
|
372
|
-
ReadonlySet<string>
|
|
373
|
-
>(() => new Set());
|
|
374
356
|
const { copied, copy } = useCopyAction();
|
|
375
357
|
const isActive =
|
|
376
358
|
turnActive ?? (status === "submitted" || status === "streaming");
|
|
@@ -385,31 +367,74 @@ export function CloudOsChatMessages({
|
|
|
385
367
|
}),
|
|
386
368
|
[approvalIdOf, isActive, messages, showThinkingTraces],
|
|
387
369
|
);
|
|
388
|
-
const
|
|
370
|
+
const workEntryKeys = useMemo(
|
|
389
371
|
() =>
|
|
390
372
|
new Set(
|
|
391
373
|
entries.flatMap((entry) =>
|
|
392
374
|
entry.type === "assistant" &&
|
|
393
|
-
partitionAssistantBlocks(entry.blocks).
|
|
394
|
-
(block) => block.kind === "text",
|
|
395
|
-
)
|
|
375
|
+
partitionAssistantBlocks(entry.blocks).work.length > 0
|
|
396
376
|
? [entry.key]
|
|
397
377
|
: [],
|
|
398
378
|
),
|
|
399
379
|
),
|
|
400
380
|
[entries],
|
|
401
381
|
);
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const previous = previousNarrativeEntryKeys.current;
|
|
408
|
-
previousNarrativeEntryKeys.current = narrativeEntryKeys;
|
|
409
|
-
setExpandedWorkEntries((expanded) =>
|
|
410
|
-
collapseEntriesWithNewNarrative(expanded, previous, narrativeEntryKeys),
|
|
382
|
+
const runningWorkEntryKeys = useMemo(() => {
|
|
383
|
+
const keys = new Set<string>();
|
|
384
|
+
if (!isActive) return keys;
|
|
385
|
+
const entry = entries.findLast(
|
|
386
|
+
(candidate) => candidate.type === "assistant",
|
|
411
387
|
);
|
|
412
|
-
|
|
388
|
+
if (entry?.type !== "assistant") return keys;
|
|
389
|
+
const { work, content } = partitionAssistantBlocks(entry.blocks);
|
|
390
|
+
if (work.length > 0 && !content.some((block) => block.kind === "text")) {
|
|
391
|
+
keys.add(entry.key);
|
|
392
|
+
}
|
|
393
|
+
return keys;
|
|
394
|
+
}, [entries, isActive]);
|
|
395
|
+
const toolCallKeys = useMemo(
|
|
396
|
+
() =>
|
|
397
|
+
new Set(
|
|
398
|
+
entries.flatMap((entry) =>
|
|
399
|
+
entry.type === "assistant"
|
|
400
|
+
? entry.blocks.flatMap((block) =>
|
|
401
|
+
block.kind === "toolGroup" ? [block.group.key] : [],
|
|
402
|
+
)
|
|
403
|
+
: [],
|
|
404
|
+
),
|
|
405
|
+
),
|
|
406
|
+
[entries],
|
|
407
|
+
);
|
|
408
|
+
const runningToolCallKeys = useMemo(
|
|
409
|
+
() =>
|
|
410
|
+
new Set(
|
|
411
|
+
entries.flatMap((entry) =>
|
|
412
|
+
entry.type === "assistant" && runningWorkEntryKeys.has(entry.key)
|
|
413
|
+
? entry.blocks.flatMap((block) =>
|
|
414
|
+
block.kind === "toolGroup" && block.group.hasRunning
|
|
415
|
+
? [block.group.key]
|
|
416
|
+
: [],
|
|
417
|
+
)
|
|
418
|
+
: [],
|
|
419
|
+
),
|
|
420
|
+
),
|
|
421
|
+
[entries, runningWorkEntryKeys],
|
|
422
|
+
);
|
|
423
|
+
const compactionKeys = useMemo(
|
|
424
|
+
() =>
|
|
425
|
+
new Set(
|
|
426
|
+
entries.flatMap((entry) =>
|
|
427
|
+
entry.type === "compactionBoundary" ? [entry.key] : [],
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
[entries],
|
|
431
|
+
);
|
|
432
|
+
const [expandedToolCalls, toggleToolCall] =
|
|
433
|
+
useDisclosureExpandedKeys(toolCallKeys, runningToolCallKeys);
|
|
434
|
+
const [expandedWorkEntries, toggleWorkEntry] =
|
|
435
|
+
useDisclosureExpandedKeys(workEntryKeys, runningWorkEntryKeys);
|
|
436
|
+
const [expandedCompactions, toggleCompaction] =
|
|
437
|
+
useDisclosureExpandedKeys(compactionKeys, NO_RUNNING_DISCLOSURES);
|
|
413
438
|
|
|
414
439
|
const entryTopClasses = useMemo(
|
|
415
440
|
() =>
|
|
@@ -419,33 +444,6 @@ export function CloudOsChatMessages({
|
|
|
419
444
|
[entries],
|
|
420
445
|
);
|
|
421
446
|
|
|
422
|
-
const toggleToolCall = useCallback((key: string) => {
|
|
423
|
-
setExpandedToolCalls((current) => {
|
|
424
|
-
const next = new Set(current);
|
|
425
|
-
if (next.has(key)) next.delete(key);
|
|
426
|
-
else next.add(key);
|
|
427
|
-
return next;
|
|
428
|
-
});
|
|
429
|
-
}, []);
|
|
430
|
-
|
|
431
|
-
const toggleWorkEntry = useCallback((key: string) => {
|
|
432
|
-
setExpandedWorkEntries((current) => {
|
|
433
|
-
const next = new Set(current);
|
|
434
|
-
if (next.has(key)) next.delete(key);
|
|
435
|
-
else next.add(key);
|
|
436
|
-
return next;
|
|
437
|
-
});
|
|
438
|
-
}, []);
|
|
439
|
-
|
|
440
|
-
const toggleCompaction = useCallback((key: string) => {
|
|
441
|
-
setExpandedCompactions((current) => {
|
|
442
|
-
const next = new Set(current);
|
|
443
|
-
if (next.has(key)) next.delete(key);
|
|
444
|
-
else next.add(key);
|
|
445
|
-
return next;
|
|
446
|
-
});
|
|
447
|
-
}, []);
|
|
448
|
-
|
|
449
447
|
const syncScrollButton = useCallback(() => {
|
|
450
448
|
const element = scrollRef.current;
|
|
451
449
|
if (!element) return;
|
|
@@ -628,7 +626,7 @@ export function CloudOsChatMessages({
|
|
|
628
626
|
<WorkTraceDisclosure
|
|
629
627
|
durationMs={entry.durationMs}
|
|
630
628
|
groups={workGroups}
|
|
631
|
-
completed={!
|
|
629
|
+
completed={!runningWorkEntryKeys.has(entry.key)}
|
|
632
630
|
open={expandedWorkEntries.has(entry.key)}
|
|
633
631
|
onToggle={() => toggleWorkEntry(entry.key)}
|
|
634
632
|
>
|
|
@@ -640,6 +638,7 @@ export function CloudOsChatMessages({
|
|
|
640
638
|
<WorkDescriptionRow
|
|
641
639
|
key={block.key}
|
|
642
640
|
text={block.text}
|
|
641
|
+
running={runningWorkEntryKeys.has(entry.key)}
|
|
643
642
|
resolveUrl={resolveUrl}
|
|
644
643
|
/>
|
|
645
644
|
);
|
|
@@ -658,8 +657,9 @@ export function CloudOsChatMessages({
|
|
|
658
657
|
<PlanBlock
|
|
659
658
|
key={block.key}
|
|
660
659
|
steps={block.steps}
|
|
661
|
-
running={
|
|
662
|
-
|
|
660
|
+
running={
|
|
661
|
+
runningWorkEntryKeys.has(entry.key) && block.running
|
|
662
|
+
}
|
|
663
663
|
showCollapsedSummary={false}
|
|
664
664
|
/>
|
|
665
665
|
);
|
|
@@ -669,6 +669,7 @@ export function CloudOsChatMessages({
|
|
|
669
669
|
key={block.key}
|
|
670
670
|
label={block.label}
|
|
671
671
|
tools={block.tools}
|
|
672
|
+
running={runningWorkEntryKeys.has(entry.key)}
|
|
672
673
|
/>
|
|
673
674
|
);
|
|
674
675
|
case "subagents":
|
|
@@ -676,6 +677,7 @@ export function CloudOsChatMessages({
|
|
|
676
677
|
<SubAgentsBlock
|
|
677
678
|
key={block.key}
|
|
678
679
|
agents={block.agents}
|
|
680
|
+
running={runningWorkEntryKeys.has(entry.key)}
|
|
679
681
|
/>
|
|
680
682
|
);
|
|
681
683
|
default:
|
|
@@ -692,6 +694,7 @@ export function CloudOsChatMessages({
|
|
|
692
694
|
<WorkDescriptionRow
|
|
693
695
|
key={block.key}
|
|
694
696
|
text={block.text}
|
|
697
|
+
running={block.running}
|
|
695
698
|
resolveUrl={resolveUrl}
|
|
696
699
|
/>
|
|
697
700
|
);
|
|
@@ -730,7 +733,9 @@ export function CloudOsChatMessages({
|
|
|
730
733
|
<PlanBlock
|
|
731
734
|
key={block.key}
|
|
732
735
|
steps={block.steps}
|
|
733
|
-
running={
|
|
736
|
+
running={
|
|
737
|
+
runningWorkEntryKeys.has(entry.key) && block.running
|
|
738
|
+
}
|
|
734
739
|
/>
|
|
735
740
|
);
|
|
736
741
|
case "askUser":
|
|
@@ -742,13 +747,7 @@ export function CloudOsChatMessages({
|
|
|
742
747
|
/>
|
|
743
748
|
);
|
|
744
749
|
case "suggestions":
|
|
745
|
-
return
|
|
746
|
-
<SuggestionsBlock
|
|
747
|
-
key={block.key}
|
|
748
|
-
items={block.items}
|
|
749
|
-
onSuggestion={(text) => onSuggestion?.(text)}
|
|
750
|
-
/>
|
|
751
|
-
);
|
|
750
|
+
return null;
|
|
752
751
|
case "schedule":
|
|
753
752
|
return (
|
|
754
753
|
<ScheduleBlock
|
|
@@ -785,11 +784,16 @@ export function CloudOsChatMessages({
|
|
|
785
784
|
key={block.key}
|
|
786
785
|
label={block.label}
|
|
787
786
|
tools={block.tools}
|
|
787
|
+
running={runningWorkEntryKeys.has(entry.key)}
|
|
788
788
|
/>
|
|
789
789
|
);
|
|
790
790
|
case "subagents":
|
|
791
791
|
return (
|
|
792
|
-
<SubAgentsBlock
|
|
792
|
+
<SubAgentsBlock
|
|
793
|
+
key={block.key}
|
|
794
|
+
agents={block.agents}
|
|
795
|
+
running={runningWorkEntryKeys.has(entry.key)}
|
|
796
|
+
/>
|
|
793
797
|
);
|
|
794
798
|
case "image":
|
|
795
799
|
{
|
|
@@ -870,7 +874,7 @@ export function CloudOsChatMessages({
|
|
|
870
874
|
)}
|
|
871
875
|
|
|
872
876
|
{hasNarrative && entry.timestamp !== undefined && (
|
|
873
|
-
<div className="mt-0.5 -ml-1 flex items-center gap-1
|
|
877
|
+
<div className="mt-0.5 -ml-1 flex items-center gap-1">
|
|
874
878
|
<Tooltip
|
|
875
879
|
content={copied === entry.copyText ? "Copied!" : "Copy message"}
|
|
876
880
|
asChild
|
|
@@ -895,6 +899,16 @@ export function CloudOsChatMessages({
|
|
|
895
899
|
</Tooltip>
|
|
896
900
|
</div>
|
|
897
901
|
)}
|
|
902
|
+
|
|
903
|
+
{contentBlocks.map((block) =>
|
|
904
|
+
block.kind === "suggestions" ? (
|
|
905
|
+
<SuggestionsBlock
|
|
906
|
+
key={block.key}
|
|
907
|
+
items={block.items}
|
|
908
|
+
onSuggestion={(text) => onSuggestion?.(text)}
|
|
909
|
+
/>
|
|
910
|
+
) : null
|
|
911
|
+
)}
|
|
898
912
|
</div>
|
|
899
913
|
</div>
|
|
900
914
|
);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ArrowLeft,
|
|
3
|
-
ArrowRight,
|
|
4
2
|
Bell,
|
|
5
3
|
CaretRight,
|
|
6
4
|
Check,
|
|
@@ -12,11 +10,16 @@ import {
|
|
|
12
10
|
UsersThree,
|
|
13
11
|
WarningCircle,
|
|
14
12
|
} from "@phosphor-icons/react";
|
|
15
|
-
import {
|
|
13
|
+
import { useId, useState } from "react";
|
|
16
14
|
import { Tooltip } from "../primitives/tooltip";
|
|
17
15
|
import { WorkshopButton } from "../primitives/workshop-controls";
|
|
18
16
|
import { MarkdownMessage } from "./markdown-message";
|
|
19
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
SECONDARY_DISCLOSURE_ICON,
|
|
19
|
+
SECONDARY_DISCLOSURE_TITLE_WITH_ICON,
|
|
20
|
+
SECONDARY_DISCLOSURE_TITLE_WITHOUT_ICON,
|
|
21
|
+
WorkIcon,
|
|
22
|
+
} from "./tool-rows";
|
|
20
23
|
import type {
|
|
21
24
|
ParallelTool,
|
|
22
25
|
PlanStep,
|
|
@@ -28,6 +31,7 @@ import {
|
|
|
28
31
|
safeStringify,
|
|
29
32
|
type CloudOsToolCall,
|
|
30
33
|
} from "./tool-presentation";
|
|
34
|
+
import { useLifecycleDisclosure } from "./use-lifecycle-disclosure";
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
37
|
* UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
|
|
@@ -42,37 +46,34 @@ import {
|
|
|
42
46
|
export function PlanBlock({
|
|
43
47
|
steps,
|
|
44
48
|
running,
|
|
45
|
-
defaultOpen = true,
|
|
46
49
|
showCollapsedSummary = true,
|
|
47
50
|
}: {
|
|
48
51
|
steps: PlanStep[];
|
|
49
52
|
running: boolean;
|
|
50
|
-
defaultOpen?: boolean;
|
|
51
53
|
showCollapsedSummary?: boolean;
|
|
52
54
|
}) {
|
|
53
55
|
const done = steps.filter((step) => step.status === "done").length;
|
|
54
56
|
const current = steps.find((step) => step.status === "in_progress");
|
|
55
|
-
const [open,
|
|
57
|
+
const [open, toggle] = useLifecycleDisclosure(running);
|
|
56
58
|
|
|
57
59
|
return (
|
|
58
|
-
<div className="group
|
|
60
|
+
<div className="group">
|
|
59
61
|
<button
|
|
60
62
|
type="button"
|
|
61
|
-
onClick={
|
|
62
|
-
className=
|
|
63
|
+
onClick={toggle}
|
|
64
|
+
className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} w-full`}
|
|
63
65
|
aria-expanded={open}
|
|
64
66
|
>
|
|
65
|
-
<span className=
|
|
67
|
+
<span className={SECONDARY_DISCLOSURE_ICON}>
|
|
66
68
|
<WorkIcon Icon={ListChecks} />
|
|
67
69
|
</span>
|
|
68
70
|
<span className="min-w-0 flex-1">
|
|
69
|
-
<span className="flex min-w-0 items-center gap-2
|
|
71
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
70
72
|
<span className={`min-w-0 truncate ${running ? "cos-thinking-shimmer" : ""}`}>
|
|
71
73
|
Plan · {done}/{steps.length}
|
|
72
74
|
</span>
|
|
73
75
|
<CaretRight
|
|
74
|
-
size={
|
|
75
|
-
weight="bold"
|
|
76
|
+
size={16}
|
|
76
77
|
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
77
78
|
/>
|
|
78
79
|
</span>
|
|
@@ -84,7 +85,7 @@ export function PlanBlock({
|
|
|
84
85
|
</span>
|
|
85
86
|
</button>
|
|
86
87
|
{open && (
|
|
87
|
-
<div className="
|
|
88
|
+
<div className="mt-1 flex flex-col gap-2 rounded-xl border border-kumo-line bg-[rgba(0,0,0,0.03)] px-4 py-3">
|
|
88
89
|
{steps.map((step, index) => (
|
|
89
90
|
<div
|
|
90
91
|
key={`${step.text}-${index}`}
|
|
@@ -171,8 +172,7 @@ export function AskUserBlock({
|
|
|
171
172
|
// 首帧 questions 往往还是空数组,那之后每次 setAnswers 都在空数组上 map,
|
|
172
173
|
// 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
|
|
173
174
|
const [answers, setAnswers] = useState<DraftAnswer[]>([]);
|
|
174
|
-
const
|
|
175
|
-
const direction = useRef<"back" | "forward">("forward");
|
|
175
|
+
const contentId = useId();
|
|
176
176
|
const answerAt = (index: number): DraftAnswer =>
|
|
177
177
|
answers[index] ?? { selections: [], text: "" };
|
|
178
178
|
const toggle = (questionIndex: number, option: string, multi: boolean) =>
|
|
@@ -202,31 +202,43 @@ export function AskUserBlock({
|
|
|
202
202
|
// 在途只用于按钮转圈;权威状态永远是 part 的 state。
|
|
203
203
|
const [inFlight, setInFlight] = useState(false);
|
|
204
204
|
const outcome = askUserOutcome(call);
|
|
205
|
-
const
|
|
205
|
+
const [open, toggleDisclosure] = useLifecycleDisclosure(
|
|
206
|
+
outcome.kind === "pending",
|
|
207
|
+
);
|
|
208
|
+
const displayAnswerAt = (index: number): DraftAnswer => {
|
|
209
|
+
if (outcome.kind !== "answered") return answerAt(index);
|
|
210
|
+
const answer = outcome.answers[index];
|
|
211
|
+
return {
|
|
212
|
+
selections: answer?.selections ?? [],
|
|
213
|
+
text: answer?.text ?? "",
|
|
214
|
+
};
|
|
215
|
+
};
|
|
206
216
|
// 服务端已经结算 = 这张卡永久不可操作,不管结算成什么。
|
|
207
217
|
const resolved = outcome.kind !== "pending" || inFlight;
|
|
208
218
|
const valid = questions.length > 0 && questions.every(
|
|
209
219
|
(_question, index) =>
|
|
210
220
|
answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
|
|
211
221
|
);
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
?
|
|
224
|
-
:
|
|
222
|
+
const titleBase = questions.length === 1
|
|
223
|
+
? "Clarifying question"
|
|
224
|
+
: "Clarifying questions";
|
|
225
|
+
const disclosureTitle = outcome.kind === "pending"
|
|
226
|
+
? questions.length === 1 && questions[0]
|
|
227
|
+
? `${titleBase} — ${questions[0].prompt}`
|
|
228
|
+
: questions.length > 1
|
|
229
|
+
? `${titleBase} · ${questions.length} questions`
|
|
230
|
+
: titleBase
|
|
231
|
+
: outcome.kind === "answered"
|
|
232
|
+
? questions.length > 1
|
|
233
|
+
? `${titleBase} · ${questions.length} answered`
|
|
234
|
+
: `${titleBase} · Answered`
|
|
235
|
+
: `${titleBase} · Closed`;
|
|
236
|
+
const buttonLabel = inFlight ? "Submitting…" : "Submit";
|
|
225
237
|
const footnote = outcome.kind === "closed"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
? outcome.reason === "user_replied_freeform"
|
|
239
|
+
? "Answered in the chat instead."
|
|
240
|
+
: "Closed without an answer."
|
|
241
|
+
: "";
|
|
230
242
|
|
|
231
243
|
const submit = () => {
|
|
232
244
|
if (!onRespond) return;
|
|
@@ -242,205 +254,129 @@ export function AskUserBlock({
|
|
|
242
254
|
}, () => setInFlight(false));
|
|
243
255
|
};
|
|
244
256
|
|
|
245
|
-
const
|
|
257
|
+
const closedButton = (
|
|
246
258
|
<WorkshopButton
|
|
247
259
|
tone="primary"
|
|
248
260
|
disabled
|
|
249
261
|
className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
250
262
|
>
|
|
251
|
-
|
|
263
|
+
Closed
|
|
252
264
|
</WorkshopButton>
|
|
253
265
|
);
|
|
254
266
|
|
|
255
267
|
return (
|
|
256
|
-
// 收成 w-fit:短问题不该撑成一条横贯全栏的长条,长问题到 560px 再换行。
|
|
257
268
|
<div
|
|
258
|
-
className="w-
|
|
269
|
+
className="w-full"
|
|
259
270
|
data-part-type="ask_user"
|
|
260
271
|
data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
|
|
261
272
|
>
|
|
262
|
-
<div className="flex flex-col gap-3
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
);
|
|
305
|
-
return (
|
|
306
|
-
<li key={option} className="m-0 p-0">
|
|
307
|
-
<label
|
|
308
|
-
className={`flex min-h-8 items-center gap-2 rounded-lg border px-2.5 py-1.5 text-[13px] leading-[18px] tracking-[-0.25px] transition-colors duration-150 ease-out ${
|
|
309
|
-
resolved || !onRespond
|
|
310
|
-
? "cursor-default text-kumo-inactive"
|
|
311
|
-
: "cursor-pointer text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
312
|
-
} ${
|
|
313
|
-
selected
|
|
314
|
-
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
315
|
-
: "border-kumo-line bg-operation-base"
|
|
316
|
-
}`}
|
|
317
|
-
>
|
|
318
|
-
<input
|
|
319
|
-
type={currentQuestion.multi
|
|
320
|
-
? "checkbox"
|
|
321
|
-
: "radio"}
|
|
322
|
-
name={`${call.toolCallId}-${currentIndex}`}
|
|
323
|
-
checked={selected}
|
|
273
|
+
<div className="flex flex-col gap-3 py-2.5">
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
onClick={toggleDisclosure}
|
|
277
|
+
aria-expanded={open}
|
|
278
|
+
aria-controls={contentId}
|
|
279
|
+
title={disclosureTitle}
|
|
280
|
+
className={`${SECONDARY_DISCLOSURE_TITLE_WITHOUT_ICON} max-w-full`}
|
|
281
|
+
>
|
|
282
|
+
<span className="min-w-0 truncate">{disclosureTitle}</span>
|
|
283
|
+
<CaretRight
|
|
284
|
+
size={16}
|
|
285
|
+
className={`shrink-0 transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
286
|
+
/>
|
|
287
|
+
</button>
|
|
288
|
+
<div
|
|
289
|
+
id={contentId}
|
|
290
|
+
data-ask-user-content=""
|
|
291
|
+
className={open ? "flex flex-col gap-3" : "hidden"}
|
|
292
|
+
>
|
|
293
|
+
{outcome.kind !== "closed" ? (
|
|
294
|
+
<>
|
|
295
|
+
<div className="flex flex-col gap-4">
|
|
296
|
+
{questions.map((question, questionIndex) => {
|
|
297
|
+
const answer = displayAnswerAt(questionIndex);
|
|
298
|
+
return (
|
|
299
|
+
<div
|
|
300
|
+
key={`${question.prompt}:${questionIndex}`}
|
|
301
|
+
className="flex flex-col items-start gap-2"
|
|
302
|
+
>
|
|
303
|
+
<p className="m-0 text-[14px] leading-[1.4] font-normal text-black">
|
|
304
|
+
{question.prompt}
|
|
305
|
+
</p>
|
|
306
|
+
{question.options.length > 0 && (
|
|
307
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
308
|
+
{question.options.map((option) => {
|
|
309
|
+
const selected = answer.selections.includes(option);
|
|
310
|
+
return (
|
|
311
|
+
<button
|
|
312
|
+
key={option}
|
|
313
|
+
type="button"
|
|
314
|
+
aria-pressed={selected}
|
|
324
315
|
disabled={resolved || !onRespond}
|
|
325
|
-
|
|
326
|
-
toggle(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
<span>
|
|
343
|
-
{currentQuestion.options.length > 0 ? "Other" : "Answer"}
|
|
344
|
-
</span>
|
|
345
|
-
<input
|
|
346
|
-
type="text"
|
|
347
|
-
value={currentAnswer.text}
|
|
348
|
-
maxLength={2_000}
|
|
349
|
-
disabled={resolved || !onRespond}
|
|
350
|
-
aria-label={`自定义回答:${currentQuestion.prompt}`}
|
|
351
|
-
placeholder={currentQuestion.options.length > 0
|
|
352
|
-
? "Add a custom answer"
|
|
353
|
-
: "Type your answer"}
|
|
354
|
-
onChange={(event) =>
|
|
355
|
-
setText(currentIndex, event.currentTarget.value)}
|
|
356
|
-
className="h-8 rounded-lg border border-kumo-line bg-kumo-base px-2.5 text-[13px] text-kumo-default outline-none placeholder:text-kumo-inactive focus:border-kumo-brand focus:ring-2 focus:ring-kumo-brand/20 disabled:cursor-default"
|
|
357
|
-
/>
|
|
358
|
-
</label>
|
|
359
|
-
)}
|
|
360
|
-
</div>
|
|
361
|
-
</div>
|
|
362
|
-
)}
|
|
363
|
-
<div className="flex items-center gap-2">
|
|
364
|
-
{questions.length > 1 && (
|
|
365
|
-
<WorkshopButton
|
|
366
|
-
disabled={currentIndex === 0 || resolved}
|
|
367
|
-
onClick={() => {
|
|
368
|
-
direction.current = "back";
|
|
369
|
-
setCurrent((index) => Math.max(0, index - 1));
|
|
370
|
-
}}
|
|
371
|
-
className="!h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
372
|
-
>
|
|
373
|
-
<ArrowLeft size={13} weight="bold" />
|
|
374
|
-
Back
|
|
375
|
-
</WorkshopButton>
|
|
376
|
-
)}
|
|
377
|
-
<WorkshopButton
|
|
378
|
-
tone="primary"
|
|
379
|
-
disabled={
|
|
380
|
-
resolved || !onRespond ||
|
|
381
|
-
(questions.length > 1 && !isLast ? !currentValid : !valid)
|
|
382
|
-
}
|
|
383
|
-
onClick={() => {
|
|
384
|
-
if (questions.length > 1 && !isLast) {
|
|
385
|
-
direction.current = "forward";
|
|
386
|
-
setCurrent((index) =>
|
|
387
|
-
Math.min(questions.length - 1, index + 1)
|
|
388
|
-
);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
submit();
|
|
392
|
-
}}
|
|
393
|
-
className="ml-auto !h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
394
|
-
>
|
|
395
|
-
{questions.length > 1 && !isLast ? (
|
|
396
|
-
<>
|
|
397
|
-
Next
|
|
398
|
-
<ArrowRight size={13} weight="bold" />
|
|
399
|
-
</>
|
|
400
|
-
) : buttonLabel}
|
|
401
|
-
</WorkshopButton>
|
|
402
|
-
</div>
|
|
403
|
-
</>
|
|
404
|
-
) : (
|
|
405
|
-
<>
|
|
406
|
-
{outcome.kind === "answered" &&
|
|
407
|
-
questions.map((question, questionIndex) => (
|
|
408
|
-
<div
|
|
409
|
-
key={`${question.prompt}:${questionIndex}`}
|
|
410
|
-
className="flex flex-col gap-1.5"
|
|
411
|
-
>
|
|
412
|
-
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
413
|
-
{question.prompt}
|
|
414
|
-
</p>
|
|
415
|
-
{settledAnswers[questionIndex] && (
|
|
416
|
-
<div className="flex flex-col gap-1 text-[12px] leading-4 text-kumo-inactive">
|
|
417
|
-
{settledAnswers[questionIndex]!.selections.length > 0 && (
|
|
418
|
-
<p className="m-0">
|
|
419
|
-
Answered: {settledAnswers[questionIndex]!.selections
|
|
420
|
-
.join(" / ")}
|
|
421
|
-
</p>
|
|
316
|
+
onClick={() =>
|
|
317
|
+
toggle(questionIndex, option, question.multi)}
|
|
318
|
+
className={`inline-flex w-fit items-center justify-center rounded-lg border px-4 py-2 text-left text-[14px] leading-[1.4] font-normal text-[rgba(0,0,0,0.7)] transition-[background,border-color] duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff9938]/30 ${
|
|
319
|
+
selected
|
|
320
|
+
? `${resolved || !onRespond ? "cursor-default" : "cursor-pointer"} border-[#ff9938] bg-[#fff]`
|
|
321
|
+
: resolved
|
|
322
|
+
? "cursor-default border-[rgba(0,0,0,0.07)] bg-[#fff]"
|
|
323
|
+
: !onRespond
|
|
324
|
+
? "cursor-default border-[rgba(0,0,0,0.07)] bg-[#fff] opacity-60"
|
|
325
|
+
: "cursor-pointer border-[rgba(0,0,0,0.07)] bg-[#fff] hover:border-transparent hover:bg-[linear-gradient(#fff,#fff)_padding-box,linear-gradient(90deg,#ff9938,#ffe46c)_border-box]"
|
|
326
|
+
}`}
|
|
327
|
+
>
|
|
328
|
+
{option}
|
|
329
|
+
</button>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
</div>
|
|
422
333
|
)}
|
|
423
|
-
{
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
?
|
|
427
|
-
|
|
428
|
-
|
|
334
|
+
{question.allowCustom && (
|
|
335
|
+
<label className="flex w-full flex-col gap-2">
|
|
336
|
+
<span className="m-0 text-[14px] leading-[1.4] font-normal text-black">
|
|
337
|
+
{question.options.length > 0 ? "Other" : "Answer"}
|
|
338
|
+
</span>
|
|
339
|
+
<input
|
|
340
|
+
type="text"
|
|
341
|
+
value={answer.text}
|
|
342
|
+
maxLength={2_000}
|
|
343
|
+
disabled={resolved || !onRespond}
|
|
344
|
+
aria-label={`自定义回答:${question.prompt}`}
|
|
345
|
+
placeholder={question.options.length > 0
|
|
346
|
+
? "Add a custom answer"
|
|
347
|
+
: "Type your answer"}
|
|
348
|
+
onChange={(event) =>
|
|
349
|
+
setText(questionIndex, event.currentTarget.value)}
|
|
350
|
+
className="rounded-lg border border-[rgba(0,0,0,0.07)] bg-[#fff] px-4 py-2 text-[14px] leading-[1.4] font-normal text-[rgba(0,0,0,0.7)] outline-none placeholder:text-kumo-inactive focus:border-[#ff9938] focus:ring-2 focus:ring-[#ff9938]/30 disabled:cursor-default"
|
|
351
|
+
/>
|
|
352
|
+
</label>
|
|
429
353
|
)}
|
|
430
354
|
</div>
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
|
|
355
|
+
);
|
|
356
|
+
})}
|
|
357
|
+
</div>
|
|
358
|
+
{outcome.kind === "pending" && (
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
disabled={resolved || !onRespond || !valid}
|
|
362
|
+
onClick={submit}
|
|
363
|
+
className="inline-flex h-10 min-w-[120px] self-start items-center justify-center rounded-lg bg-gradient-to-r from-[#ff9938] to-[#ffe46c] px-5 py-2.5 text-[14px] leading-5 font-medium text-black transition-opacity duration-150 ease-out hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff9938]/30 disabled:cursor-default disabled:opacity-50"
|
|
364
|
+
>
|
|
365
|
+
{buttonLabel}
|
|
366
|
+
</button>
|
|
367
|
+
)}
|
|
368
|
+
</>
|
|
369
|
+
) : (
|
|
434
370
|
<div className="flex items-center gap-3">
|
|
435
371
|
{footnote && (
|
|
436
372
|
<p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
|
|
437
373
|
{footnote}
|
|
438
374
|
</p>
|
|
439
375
|
)}
|
|
440
|
-
{
|
|
376
|
+
{closedButton}
|
|
441
377
|
</div>
|
|
442
|
-
|
|
443
|
-
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
444
380
|
</div>
|
|
445
381
|
</div>
|
|
446
382
|
);
|
|
@@ -813,34 +749,35 @@ function StatusDot({ status }: { status: ParallelTool["status"] }) {
|
|
|
813
749
|
export function ParallelBlock({
|
|
814
750
|
label,
|
|
815
751
|
tools,
|
|
752
|
+
running = tools.some((tool) => tool.status === "running"),
|
|
816
753
|
}: {
|
|
817
754
|
label: string;
|
|
818
755
|
tools: ParallelTool[];
|
|
756
|
+
running?: boolean;
|
|
819
757
|
}) {
|
|
820
758
|
const done = tools.filter((tool) => tool.status === "done").length;
|
|
821
|
-
const [open,
|
|
759
|
+
const [open, toggle] = useLifecycleDisclosure(running);
|
|
822
760
|
return (
|
|
823
761
|
<div className="max-w-[860px]">
|
|
824
762
|
<button
|
|
825
763
|
type="button"
|
|
826
|
-
onClick={
|
|
827
|
-
className=
|
|
764
|
+
onClick={toggle}
|
|
765
|
+
className={SECONDARY_DISCLOSURE_TITLE_WITH_ICON}
|
|
828
766
|
aria-expanded={open}
|
|
829
767
|
>
|
|
830
|
-
<span className=
|
|
768
|
+
<span className={SECONDARY_DISCLOSURE_ICON}>
|
|
831
769
|
<WorkIcon Icon={ListChecks} />
|
|
832
770
|
</span>
|
|
833
771
|
<span className="min-w-0 truncate">
|
|
834
772
|
{label} · {done}/{tools.length}
|
|
835
773
|
</span>
|
|
836
774
|
<CaretRight
|
|
837
|
-
size={
|
|
838
|
-
weight="bold"
|
|
775
|
+
size={16}
|
|
839
776
|
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
840
777
|
/>
|
|
841
778
|
</button>
|
|
842
779
|
{open && (
|
|
843
|
-
<div className="
|
|
780
|
+
<div className="mt-1 space-y-1">
|
|
844
781
|
{tools.map((tool, index) => (
|
|
845
782
|
<div
|
|
846
783
|
key={`${tool.label}-${index}`}
|
|
@@ -861,30 +798,35 @@ export function ParallelBlock({
|
|
|
861
798
|
);
|
|
862
799
|
}
|
|
863
800
|
|
|
864
|
-
export function SubAgentsBlock({
|
|
865
|
-
|
|
801
|
+
export function SubAgentsBlock({
|
|
802
|
+
agents,
|
|
803
|
+
running = agents.some((agent) => agent.status === "running"),
|
|
804
|
+
}: {
|
|
805
|
+
agents: SubAgentView[];
|
|
806
|
+
running?: boolean;
|
|
807
|
+
}) {
|
|
808
|
+
const [open, toggle] = useLifecycleDisclosure(running);
|
|
866
809
|
return (
|
|
867
810
|
<div className="max-w-[860px]">
|
|
868
811
|
<button
|
|
869
812
|
type="button"
|
|
870
|
-
onClick={
|
|
871
|
-
className=
|
|
813
|
+
onClick={toggle}
|
|
814
|
+
className={SECONDARY_DISCLOSURE_TITLE_WITH_ICON}
|
|
872
815
|
aria-expanded={open}
|
|
873
816
|
>
|
|
874
|
-
<span className=
|
|
817
|
+
<span className={SECONDARY_DISCLOSURE_ICON}>
|
|
875
818
|
<WorkIcon Icon={UsersThree} />
|
|
876
819
|
</span>
|
|
877
820
|
<span className="min-w-0 truncate">
|
|
878
821
|
{agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
|
|
879
822
|
</span>
|
|
880
823
|
<CaretRight
|
|
881
|
-
size={
|
|
882
|
-
weight="bold"
|
|
824
|
+
size={16}
|
|
883
825
|
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
884
826
|
/>
|
|
885
827
|
</button>
|
|
886
828
|
{open && (
|
|
887
|
-
<div className="
|
|
829
|
+
<div className="mt-1 space-y-1">
|
|
888
830
|
{agents.map((agent, index) => (
|
|
889
831
|
<div
|
|
890
832
|
key={`${agent.name}-${index}`}
|
|
@@ -920,31 +862,30 @@ export function ErrorBlock({
|
|
|
920
862
|
body?: string;
|
|
921
863
|
onRetry?: () => void;
|
|
922
864
|
}) {
|
|
923
|
-
const [expanded,
|
|
865
|
+
const [expanded, toggle] = useLifecycleDisclosure(false);
|
|
924
866
|
return (
|
|
925
867
|
<div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
926
|
-
<div className="flex w-full items-center gap-2
|
|
868
|
+
<div className="flex w-full items-center gap-2">
|
|
927
869
|
<button
|
|
928
870
|
type="button"
|
|
929
|
-
onClick={
|
|
930
|
-
className=
|
|
871
|
+
onClick={toggle}
|
|
872
|
+
className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} flex-1`}
|
|
931
873
|
aria-expanded={expanded}
|
|
932
874
|
>
|
|
933
875
|
<span
|
|
934
|
-
className=
|
|
876
|
+
className={`${SECONDARY_DISCLOSURE_ICON} text-kumo-danger`}
|
|
935
877
|
aria-hidden="true"
|
|
936
878
|
>
|
|
937
|
-
<WarningCircle size={
|
|
879
|
+
<WarningCircle size={15} weight="fill" />
|
|
938
880
|
</span>
|
|
939
881
|
<span className="flex min-w-0 flex-1 items-center gap-1">
|
|
940
882
|
<span className="min-w-0 truncate">
|
|
941
|
-
<span
|
|
942
|
-
<span
|
|
883
|
+
<span>{title}: </span>
|
|
884
|
+
<span>{body}</span>
|
|
943
885
|
</span>
|
|
944
886
|
{body && (
|
|
945
887
|
<CaretRight
|
|
946
|
-
size={
|
|
947
|
-
weight="bold"
|
|
888
|
+
size={16}
|
|
948
889
|
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${expanded ? "rotate-90" : ""}`}
|
|
949
890
|
/>
|
|
950
891
|
)}
|
|
@@ -963,7 +904,7 @@ export function ErrorBlock({
|
|
|
963
904
|
)}
|
|
964
905
|
</div>
|
|
965
906
|
{expanded && body && (
|
|
966
|
-
<div className="
|
|
907
|
+
<div className="mt-1">
|
|
967
908
|
<pre className="max-h-48 overflow-auto rounded-xl border border-kumo-line/70 bg-kumo-base p-3 font-mono text-[12px] leading-[18px] text-kumo-subtle whitespace-pre-wrap">
|
|
968
909
|
{body}
|
|
969
910
|
</pre>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
|
|
2
2
|
import { motion, useReducedMotion } from "motion/react";
|
|
3
|
-
import { memo, useId,
|
|
3
|
+
import { memo, useId, type ReactNode } from "react";
|
|
4
4
|
import {
|
|
5
5
|
MarkdownMessage,
|
|
6
6
|
type CloudOsUrlResolver,
|
|
@@ -13,12 +13,25 @@ import {
|
|
|
13
13
|
type PhosphorIcon,
|
|
14
14
|
type ToolCallGroup,
|
|
15
15
|
} from "./tool-presentation";
|
|
16
|
+
import { useLifecycleDisclosure } from "./use-lifecycle-disclosure";
|
|
16
17
|
|
|
17
18
|
const thinkingSpark = new URL(
|
|
18
19
|
"../assets/thinking-spark.svg",
|
|
19
20
|
import.meta.url,
|
|
20
21
|
).href;
|
|
21
22
|
|
|
23
|
+
const SECONDARY_DISCLOSURE_TITLE_BASE =
|
|
24
|
+
"flex min-w-0 cursor-pointer gap-2 rounded-xl 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 active:scale-[0.995]";
|
|
25
|
+
|
|
26
|
+
export const SECONDARY_DISCLOSURE_TITLE_WITH_ICON =
|
|
27
|
+
`${SECONDARY_DISCLOSURE_TITLE_BASE} items-start px-1.5 py-1`;
|
|
28
|
+
|
|
29
|
+
export const SECONDARY_DISCLOSURE_TITLE_WITHOUT_ICON =
|
|
30
|
+
`${SECONDARY_DISCLOSURE_TITLE_BASE} items-center py-1 pr-1.5`;
|
|
31
|
+
|
|
32
|
+
export const SECONDARY_DISCLOSURE_ICON =
|
|
33
|
+
"mt-0.5 flex size-4 shrink-0 items-center justify-center";
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
|
|
24
37
|
* ToolGroupRow / ThinkingTraceRow。渲染 UIMessage 归一化后的 CloudOsToolCall。
|
|
@@ -150,13 +163,13 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
150
163
|
<button
|
|
151
164
|
type="button"
|
|
152
165
|
onClick={() => onToggle(key)}
|
|
153
|
-
className=
|
|
166
|
+
className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} w-full`}
|
|
154
167
|
aria-expanded={open}
|
|
155
168
|
>
|
|
156
|
-
<span className=
|
|
169
|
+
<span className={SECONDARY_DISCLOSURE_ICON}>
|
|
157
170
|
<WorkIcon Icon={Icon} />
|
|
158
171
|
</span>
|
|
159
|
-
<span className="flex min-w-0 flex-1 items-center gap-2
|
|
172
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
160
173
|
<span className="min-w-0 truncate">{label}</span>
|
|
161
174
|
{tc.failed && (
|
|
162
175
|
<span className="flex-shrink-0 rounded-full bg-kumo-danger-tint px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.04em] text-kumo-danger">
|
|
@@ -164,14 +177,13 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
164
177
|
</span>
|
|
165
178
|
)}
|
|
166
179
|
<CaretRight
|
|
167
|
-
size={
|
|
168
|
-
weight="bold"
|
|
180
|
+
size={16}
|
|
169
181
|
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
170
182
|
/>
|
|
171
183
|
</span>
|
|
172
184
|
</button>
|
|
173
185
|
{open && (
|
|
174
|
-
<div className="
|
|
186
|
+
<div className="mt-1 space-y-3">
|
|
175
187
|
<ToolCallDetails toolCall={tc} />
|
|
176
188
|
</div>
|
|
177
189
|
)}
|
|
@@ -179,6 +191,27 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
179
191
|
);
|
|
180
192
|
});
|
|
181
193
|
|
|
194
|
+
function ThinkingContent({
|
|
195
|
+
id,
|
|
196
|
+
text,
|
|
197
|
+
resolveUrl,
|
|
198
|
+
className = "",
|
|
199
|
+
}: {
|
|
200
|
+
id?: string;
|
|
201
|
+
text: string;
|
|
202
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
203
|
+
className?: string;
|
|
204
|
+
}) {
|
|
205
|
+
return (
|
|
206
|
+
<div
|
|
207
|
+
id={id}
|
|
208
|
+
className={`cos-markdown max-h-[240px] overflow-y-auto rounded-xl border border-[rgba(0,0,0,0.07)] bg-[#F2F2F2] px-4 py-3 text-[14px] leading-[1.4] text-[rgba(0,0,0,0.7)] ${className}`}
|
|
209
|
+
>
|
|
210
|
+
<MarkdownMessage message={text} resolveUrl={resolveUrl} />
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
182
215
|
export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
183
216
|
reasoning,
|
|
184
217
|
running,
|
|
@@ -186,7 +219,7 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
|
186
219
|
reasoning: string;
|
|
187
220
|
running: boolean;
|
|
188
221
|
}) {
|
|
189
|
-
const [open,
|
|
222
|
+
const [open, toggle] = useLifecycleDisclosure(running);
|
|
190
223
|
const contentId = useId();
|
|
191
224
|
const summary = reasoning.replace(/\s+/g, " ").trim();
|
|
192
225
|
|
|
@@ -198,22 +231,22 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
|
198
231
|
>
|
|
199
232
|
<button
|
|
200
233
|
type="button"
|
|
201
|
-
onClick={
|
|
202
|
-
className=
|
|
234
|
+
onClick={toggle}
|
|
235
|
+
className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} group w-fit max-w-[80%]`}
|
|
203
236
|
aria-expanded={open}
|
|
204
237
|
aria-controls={contentId}
|
|
205
238
|
>
|
|
206
|
-
<span className=
|
|
239
|
+
<span className={`${SECONDARY_DISCLOSURE_ICON} relative`} aria-hidden="true">
|
|
207
240
|
<Atom
|
|
208
|
-
size={
|
|
241
|
+
size={15}
|
|
209
242
|
className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
|
|
210
243
|
/>
|
|
211
244
|
<CaretDown
|
|
212
|
-
size={
|
|
245
|
+
size={15}
|
|
213
246
|
className={`absolute opacity-0 transition-[opacity,transform] group-hover:opacity-100 group-focus-visible:opacity-100 ${open ? "" : "-rotate-90"}`}
|
|
214
247
|
/>
|
|
215
248
|
</span>
|
|
216
|
-
<span className="flex-shrink-0
|
|
249
|
+
<span className="flex-shrink-0">Think</span>
|
|
217
250
|
<span aria-hidden="true">·</span>
|
|
218
251
|
<span
|
|
219
252
|
className="min-w-0 flex-1 truncate"
|
|
@@ -223,12 +256,11 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
|
223
256
|
</span>
|
|
224
257
|
</button>
|
|
225
258
|
{open && (
|
|
226
|
-
<
|
|
259
|
+
<ThinkingContent
|
|
227
260
|
id={contentId}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
</div>
|
|
261
|
+
text={reasoning}
|
|
262
|
+
className="w-full"
|
|
263
|
+
/>
|
|
232
264
|
)}
|
|
233
265
|
</div>
|
|
234
266
|
);
|
|
@@ -327,7 +359,7 @@ export const WorkTraceDisclosure = memo(function WorkTraceDisclosure({
|
|
|
327
359
|
|
|
328
360
|
return (
|
|
329
361
|
<div data-agent-work="" data-state={open ? "open" : "closed"}>
|
|
330
|
-
<div className="w-full
|
|
362
|
+
<div className="w-full pb-2">
|
|
331
363
|
<button
|
|
332
364
|
type="button"
|
|
333
365
|
onClick={onToggle}
|
|
@@ -336,33 +368,35 @@ export const WorkTraceDisclosure = memo(function WorkTraceDisclosure({
|
|
|
336
368
|
aria-label={label}
|
|
337
369
|
className="flex min-w-0 items-center gap-2 text-left text-[14px] leading-[1.4] text-cos-work-description transition-colors duration-150 ease-out hover:text-cos-work-description-subtle focus-visible:text-cos-work-description-subtle focus-visible:outline-none"
|
|
338
370
|
>
|
|
339
|
-
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
[
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
371
|
+
{!completed && (
|
|
372
|
+
<span className="grid size-4 shrink-0 place-items-center" aria-hidden="true">
|
|
373
|
+
<motion.img
|
|
374
|
+
src={thinkingSpark}
|
|
375
|
+
alt=""
|
|
376
|
+
className="size-[12.24px]"
|
|
377
|
+
initial={{ rotate: 0 }}
|
|
378
|
+
animate={
|
|
379
|
+
reduceMotion ? { rotate: 0 } : { rotate: [0, 360, -360, -360] }
|
|
380
|
+
}
|
|
381
|
+
transition={
|
|
382
|
+
reduceMotion
|
|
383
|
+
? { duration: 0 }
|
|
384
|
+
: {
|
|
385
|
+
rotate: {
|
|
386
|
+
duration: 1.68,
|
|
387
|
+
times: [0, 0.8368, 0.8373, 1],
|
|
388
|
+
ease: [
|
|
389
|
+
[0.5, 0, 0.5, 1],
|
|
390
|
+
[0.5, 0, 0.5, 1],
|
|
391
|
+
"linear",
|
|
392
|
+
],
|
|
393
|
+
repeat: Infinity,
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/>
|
|
398
|
+
</span>
|
|
399
|
+
)}
|
|
366
400
|
<span className="min-w-0 truncate">
|
|
367
401
|
<span>{`${verb}${duration}${activity ? `, ${activity.leading}` : ""}`}</span>
|
|
368
402
|
{activity?.emphasis && (
|
|
@@ -391,12 +425,14 @@ export const WorkDescriptionRow = memo(function WorkDescriptionRow({
|
|
|
391
425
|
resolveUrl,
|
|
392
426
|
}: {
|
|
393
427
|
text: string;
|
|
428
|
+
running?: boolean;
|
|
394
429
|
resolveUrl?: CloudOsUrlResolver;
|
|
395
430
|
}) {
|
|
396
431
|
return (
|
|
397
|
-
<
|
|
398
|
-
|
|
399
|
-
|
|
432
|
+
<ThinkingContent
|
|
433
|
+
text={text}
|
|
434
|
+
resolveUrl={resolveUrl}
|
|
435
|
+
/>
|
|
400
436
|
);
|
|
401
437
|
});
|
|
402
438
|
|
|
@@ -417,10 +453,10 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
417
453
|
<button
|
|
418
454
|
type="button"
|
|
419
455
|
onClick={() => onToggle(group.key)}
|
|
420
|
-
className=
|
|
456
|
+
className={SECONDARY_DISCLOSURE_TITLE_WITH_ICON}
|
|
421
457
|
aria-expanded={open}
|
|
422
458
|
>
|
|
423
|
-
<span className=
|
|
459
|
+
<span className={SECONDARY_DISCLOSURE_ICON}>
|
|
424
460
|
<WorkIcon Icon={group.Icon} />
|
|
425
461
|
</span>
|
|
426
462
|
<span className="min-w-0 flex-1">
|
|
@@ -448,7 +484,7 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
448
484
|
</span>
|
|
449
485
|
</button>
|
|
450
486
|
{open && (
|
|
451
|
-
<div className="mt-3
|
|
487
|
+
<div className="mt-3">
|
|
452
488
|
<ToolGroupDetails calls={group.calls} />
|
|
453
489
|
</div>
|
|
454
490
|
)}
|
|
@@ -636,6 +636,8 @@ function assistantBlocks(
|
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
const USER_STOP_REASON = "Stopped by user";
|
|
639
|
+
const REQUEST_UNAVAILABLE_ERROR =
|
|
640
|
+
"We’re unable to complete your request right now. Please try again in a few moments.";
|
|
639
641
|
|
|
640
642
|
function terminalOf(
|
|
641
643
|
message: UIMessage,
|
|
@@ -650,7 +652,9 @@ function terminalOf(
|
|
|
650
652
|
if (metadata.turnStatus === "error" || metadata.error) {
|
|
651
653
|
return {
|
|
652
654
|
kind: "error",
|
|
653
|
-
title:
|
|
655
|
+
title: metadata.error === REQUEST_UNAVAILABLE_ERROR
|
|
656
|
+
? "Unable to complete your request"
|
|
657
|
+
: "Error",
|
|
654
658
|
body: metadata.error ?? "The response failed.",
|
|
655
659
|
};
|
|
656
660
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type DisclosurePolicy = "always-open" | "lifecycle";
|
|
4
|
+
|
|
5
|
+
export const DISCLOSURE_POLICY: DisclosurePolicy = "lifecycle";
|
|
6
|
+
|
|
7
|
+
export function reconcileExpandedWithAutoOpen(
|
|
8
|
+
expanded: ReadonlySet<string>,
|
|
9
|
+
previousAutoOpenKeys: ReadonlySet<string>,
|
|
10
|
+
autoOpenKeys: ReadonlySet<string>,
|
|
11
|
+
): ReadonlySet<string> {
|
|
12
|
+
const next = new Set(expanded);
|
|
13
|
+
for (const key of previousAutoOpenKeys) {
|
|
14
|
+
if (!autoOpenKeys.has(key)) next.delete(key);
|
|
15
|
+
}
|
|
16
|
+
for (const key of autoOpenKeys) {
|
|
17
|
+
if (!previousAutoOpenKeys.has(key)) next.add(key);
|
|
18
|
+
}
|
|
19
|
+
if (next.size !== expanded.size) return next;
|
|
20
|
+
return [...next].every((key) => expanded.has(key)) ? expanded : next;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useDisclosureExpandedKeys(
|
|
24
|
+
disclosureKeys: ReadonlySet<string>,
|
|
25
|
+
runningKeys: ReadonlySet<string>,
|
|
26
|
+
) {
|
|
27
|
+
const autoOpenKeys =
|
|
28
|
+
DISCLOSURE_POLICY === "always-open" ? disclosureKeys : runningKeys;
|
|
29
|
+
const [expanded, setExpanded] = useState<ReadonlySet<string>>(autoOpenKeys);
|
|
30
|
+
const previousAutoOpenKeys = useRef(autoOpenKeys);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const previous = previousAutoOpenKeys.current;
|
|
34
|
+
previousAutoOpenKeys.current = autoOpenKeys;
|
|
35
|
+
setExpanded((current) =>
|
|
36
|
+
reconcileExpandedWithAutoOpen(current, previous, autoOpenKeys),
|
|
37
|
+
);
|
|
38
|
+
}, [autoOpenKeys]);
|
|
39
|
+
|
|
40
|
+
const toggle = useCallback((key: string) => {
|
|
41
|
+
setExpanded((current) => {
|
|
42
|
+
const next = new Set(current);
|
|
43
|
+
if (next.has(key)) next.delete(key);
|
|
44
|
+
else next.add(key);
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return [expanded, toggle] as const;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** User choice persists while the policy-derived automatic state is unchanged. */
|
|
53
|
+
export function useLifecycleDisclosure(running: boolean) {
|
|
54
|
+
const autoOpen = DISCLOSURE_POLICY === "always-open" || running;
|
|
55
|
+
const [state, setState] = useState(() => ({ autoOpen, open: autoOpen }));
|
|
56
|
+
const open = state.autoOpen === autoOpen ? state.open : autoOpen;
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setState((current) =>
|
|
60
|
+
current.autoOpen === autoOpen ? current : { autoOpen, open: autoOpen }
|
|
61
|
+
);
|
|
62
|
+
}, [autoOpen]);
|
|
63
|
+
|
|
64
|
+
return [open, () => setState({ autoOpen, open: !open })] as const;
|
|
65
|
+
}
|