@springbrand/message-panel 0.1.3-alpha.34 → 0.1.3-alpha.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cloud-os/README.md +2 -1
- package/cloud-os/assets/thinking-star.svg +9 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +262 -77
- package/cloud-os/chat/code-runner.tsx +106 -0
- package/cloud-os/chat/range.ts +8 -0
- package/cloud-os/chat/rich-blocks.tsx +161 -260
- package/cloud-os/chat/surfaces.tsx +90 -0
- package/cloud-os/chat/tool-call.tsx +94 -0
- package/cloud-os/chat/tool-presentation.ts +3 -11
- package/cloud-os/chat/tool-rows.tsx +145 -141
- package/cloud-os/chat/tool-timeline.tsx +123 -0
- package/cloud-os/chat/transcript-model.ts +92 -15
- package/cloud-os/chat/web-search.tsx +135 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +1 -1
- package/cloud-os/index.ts +10 -0
- package/cloud-os/primitives/collapsible.tsx +21 -0
- package/cloud-os/styles/cloud-os.css +18 -0
- package/package.json +3 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronRightIcon, type LucideIcon } from "lucide-react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import {
|
|
6
|
+
Collapsible,
|
|
7
|
+
CollapsibleContent,
|
|
8
|
+
CollapsibleTrigger,
|
|
9
|
+
} from "../primitives/collapsible";
|
|
10
|
+
import { cn } from "../internal/cn";
|
|
11
|
+
import { ShimmerLabel, SwapLabel } from "./surfaces";
|
|
12
|
+
import { take } from "./range";
|
|
13
|
+
|
|
14
|
+
export interface TimelineStep {
|
|
15
|
+
verb: string;
|
|
16
|
+
chip: string;
|
|
17
|
+
icon: LucideIcon;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TimelineStat {
|
|
21
|
+
file: string;
|
|
22
|
+
added?: number;
|
|
23
|
+
removed?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ToolTimelineProps {
|
|
27
|
+
steps: readonly TimelineStep[];
|
|
28
|
+
visibleSteps: number;
|
|
29
|
+
streaming: boolean;
|
|
30
|
+
open: boolean;
|
|
31
|
+
onOpenChange: (open: boolean) => void;
|
|
32
|
+
restingLabel: string;
|
|
33
|
+
activeLabel: string;
|
|
34
|
+
stats: TimelineStat[];
|
|
35
|
+
className?: string;
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ToolTimeline({
|
|
40
|
+
steps,
|
|
41
|
+
visibleSteps,
|
|
42
|
+
streaming,
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
restingLabel,
|
|
46
|
+
activeLabel,
|
|
47
|
+
stats,
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
}: ToolTimelineProps) {
|
|
51
|
+
return (
|
|
52
|
+
<Collapsible
|
|
53
|
+
data-slot="tool-timeline"
|
|
54
|
+
open={open}
|
|
55
|
+
onOpenChange={onOpenChange}
|
|
56
|
+
className={cn("w-full max-w-sm", className)}
|
|
57
|
+
>
|
|
58
|
+
<CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-1.5 rounded-md py-1 text-[13.5px] transition-colors outline-none">
|
|
59
|
+
<ChevronRightIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-90 group-data-panel-open/trigger:rotate-90 motion-reduce:transition-none" />
|
|
60
|
+
<SwapLabel
|
|
61
|
+
active={streaming ? 0 : 1}
|
|
62
|
+
className="text-start tabular-nums"
|
|
63
|
+
>
|
|
64
|
+
<ShimmerLabel
|
|
65
|
+
active={streaming}
|
|
66
|
+
className="relative inline-block leading-none"
|
|
67
|
+
>
|
|
68
|
+
{activeLabel}
|
|
69
|
+
</ShimmerLabel>
|
|
70
|
+
<>{restingLabel}</>
|
|
71
|
+
</SwapLabel>
|
|
72
|
+
</CollapsibleTrigger>
|
|
73
|
+
<CollapsibleContent className="outline-none">
|
|
74
|
+
<div className="flex flex-col gap-2.5 ps-4 pt-2.5">
|
|
75
|
+
{children ?? take(steps, visibleSteps).map((step, index, shown) => {
|
|
76
|
+
const Icon = step.icon;
|
|
77
|
+
const active = streaming && index === shown.length - 1;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
key={step.chip}
|
|
82
|
+
className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-foreground/55 flex items-center gap-2 text-[13.5px] duration-300"
|
|
83
|
+
>
|
|
84
|
+
<Icon className="text-foreground/35 size-3.5 shrink-0" />
|
|
85
|
+
<ShimmerLabel
|
|
86
|
+
active={active}
|
|
87
|
+
className="relative inline-block leading-none"
|
|
88
|
+
>
|
|
89
|
+
{step.verb}
|
|
90
|
+
</ShimmerLabel>
|
|
91
|
+
<span className="bg-foreground/[0.06] text-foreground/70 rounded-md px-1.5 py-0.5 font-mono text-[11px]">
|
|
92
|
+
{step.chip}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
{stats.length > 0 && (
|
|
98
|
+
<div className="flex flex-wrap gap-1.5 pt-1">
|
|
99
|
+
{stats.map((stat) => (
|
|
100
|
+
<span
|
|
101
|
+
key={stat.file}
|
|
102
|
+
className="bg-foreground/[0.06] text-foreground/70 inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-[11px]"
|
|
103
|
+
>
|
|
104
|
+
<span>{stat.file}</span>
|
|
105
|
+
{stat.added !== undefined && (
|
|
106
|
+
<span className="text-emerald-600 dark:text-emerald-400">
|
|
107
|
+
+{stat.added}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
{stat.removed !== undefined && (
|
|
111
|
+
<span className="text-red-600 dark:text-red-400">
|
|
112
|
+
−{stat.removed}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</span>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</CollapsibleContent>
|
|
121
|
+
</Collapsible>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -28,6 +28,7 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
|
28
28
|
error?: string;
|
|
29
29
|
interruptedByUser?: boolean;
|
|
30
30
|
turnDurationMs?: number;
|
|
31
|
+
turnId?: string;
|
|
31
32
|
turnStartedAt?: number;
|
|
32
33
|
turnStatus?: string;
|
|
33
34
|
requestedCapabilities?: unknown;
|
|
@@ -77,7 +78,7 @@ export type AssistantBlock =
|
|
|
77
78
|
text: string;
|
|
78
79
|
running: boolean;
|
|
79
80
|
}
|
|
80
|
-
| { kind: "text"; key: string; text: string }
|
|
81
|
+
| { kind: "text"; key: string; text: string; final: boolean }
|
|
81
82
|
| { kind: "toolGroup"; key: string; group: ToolCallGroup }
|
|
82
83
|
| { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
|
|
83
84
|
// 答案就在 call.output 里(`ask_user` 是 client-settled tool,用户点选后
|
|
@@ -143,6 +144,7 @@ export type CloudOsEntry =
|
|
|
143
144
|
type: "assistant";
|
|
144
145
|
key: string;
|
|
145
146
|
messageId: string;
|
|
147
|
+
turnId: string;
|
|
146
148
|
blocks: AssistantBlock[];
|
|
147
149
|
copyText: string;
|
|
148
150
|
timestamp?: number;
|
|
@@ -416,7 +418,7 @@ function assistantBlocks(
|
|
|
416
418
|
const text = String(record.text ?? "").trim();
|
|
417
419
|
if (!text) return;
|
|
418
420
|
flush();
|
|
419
|
-
blocks.push({ kind: "text", key, text });
|
|
421
|
+
blocks.push({ kind: "text", key, text, final: false });
|
|
420
422
|
return;
|
|
421
423
|
}
|
|
422
424
|
|
|
@@ -622,6 +624,7 @@ export function buildCloudOsEntries({
|
|
|
622
624
|
approvalIdOf = () => undefined,
|
|
623
625
|
}: BuildEntriesOptions): CloudOsEntry[] {
|
|
624
626
|
const entries: CloudOsEntry[] = [];
|
|
627
|
+
let fallbackTurnId = "turn:initial";
|
|
625
628
|
const lastAssistantIndex = (() => {
|
|
626
629
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
627
630
|
if (messages[index].role === "assistant") return index;
|
|
@@ -655,6 +658,9 @@ export function buildCloudOsEntries({
|
|
|
655
658
|
};
|
|
656
659
|
});
|
|
657
660
|
if (!text && attachments.length === 0) return;
|
|
661
|
+
fallbackTurnId = typeof metadata.turnId === "string" && metadata.turnId
|
|
662
|
+
? metadata.turnId
|
|
663
|
+
: `turn:user:${message.id}`;
|
|
658
664
|
entries.push({
|
|
659
665
|
type: "user",
|
|
660
666
|
key: `user-${message.id}-${index}`,
|
|
@@ -682,6 +688,9 @@ export function buildCloudOsEntries({
|
|
|
682
688
|
type: "assistant",
|
|
683
689
|
key: `assistant-${message.id}-${index}`,
|
|
684
690
|
messageId: message.id,
|
|
691
|
+
turnId: typeof metadata.turnId === "string" && metadata.turnId
|
|
692
|
+
? metadata.turnId
|
|
693
|
+
: fallbackTurnId,
|
|
685
694
|
blocks,
|
|
686
695
|
copyText: messageText(message),
|
|
687
696
|
timestamp:
|
|
@@ -691,7 +700,70 @@ export function buildCloudOsEntries({
|
|
|
691
700
|
});
|
|
692
701
|
});
|
|
693
702
|
|
|
694
|
-
return
|
|
703
|
+
return markFinalTextBlocks(mergeConsecutiveAssistantTurns(
|
|
704
|
+
dropSupersededActionPresentations(dropSupersededPlans(entries)),
|
|
705
|
+
));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function isFinalOutputBlock(block: AssistantBlock): boolean {
|
|
709
|
+
return block.kind === "suggestions" ||
|
|
710
|
+
block.kind === "image" ||
|
|
711
|
+
block.kind === "file" ||
|
|
712
|
+
(block.kind === "actionPresentation" && block.presentation.state === "ready");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isWorkBoundaryBlock(block: AssistantBlock): boolean {
|
|
716
|
+
return block.kind !== "text" && !isFinalOutputBlock(block);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function markFinalTextBlocks(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
720
|
+
return entries.map((entry) => {
|
|
721
|
+
if (entry.type !== "assistant") return entry;
|
|
722
|
+
|
|
723
|
+
const finalOutputBoundary = entry.blocks.findLastIndex(isFinalOutputBlock);
|
|
724
|
+
// Provider cleanup after final output must not pull the answer back into Work.
|
|
725
|
+
const searchBefore = finalOutputBoundary === -1
|
|
726
|
+
? entry.blocks.length
|
|
727
|
+
: finalOutputBoundary;
|
|
728
|
+
let lastWorkBoundary = -1;
|
|
729
|
+
for (let index = searchBefore - 1; index >= 0; index -= 1) {
|
|
730
|
+
if (!isWorkBoundaryBlock(entry.blocks[index]!)) continue;
|
|
731
|
+
lastWorkBoundary = index;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
...entry,
|
|
737
|
+
blocks: entry.blocks.map((block, index) =>
|
|
738
|
+
block.kind === "text"
|
|
739
|
+
? { ...block, final: index > lastWorkBoundary }
|
|
740
|
+
: block
|
|
741
|
+
),
|
|
742
|
+
};
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function mergeConsecutiveAssistantTurns(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
747
|
+
return entries.reduce<CloudOsEntry[]>((merged, entry) => {
|
|
748
|
+
const previous = merged.at(-1);
|
|
749
|
+
if (
|
|
750
|
+
entry.type !== "assistant" ||
|
|
751
|
+
previous?.type !== "assistant" ||
|
|
752
|
+
previous.turnId !== entry.turnId
|
|
753
|
+
) {
|
|
754
|
+
merged.push(entry);
|
|
755
|
+
return merged;
|
|
756
|
+
}
|
|
757
|
+
merged[merged.length - 1] = {
|
|
758
|
+
...previous,
|
|
759
|
+
messageId: entry.messageId,
|
|
760
|
+
blocks: [...previous.blocks, ...entry.blocks],
|
|
761
|
+
copyText: [previous.copyText, entry.copyText].filter(Boolean).join("\n\n"),
|
|
762
|
+
timestamp: entry.timestamp ?? previous.timestamp,
|
|
763
|
+
terminal: entry.terminal ?? previous.terminal,
|
|
764
|
+
};
|
|
765
|
+
return merged;
|
|
766
|
+
}, []);
|
|
695
767
|
}
|
|
696
768
|
|
|
697
769
|
function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
@@ -741,31 +813,31 @@ function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntr
|
|
|
741
813
|
}
|
|
742
814
|
|
|
743
815
|
/**
|
|
744
|
-
* update_plan 是**整份快照覆盖**,不是增量事件 ——
|
|
745
|
-
* 完整计划。原样按序渲染会把同一份计划画四遍(0/3、1/3、2/3、3/3)
|
|
746
|
-
*
|
|
816
|
+
* update_plan 是**整份快照覆盖**,不是增量事件 —— 一次 Turn 里每推进一步就会再发一份
|
|
817
|
+
* 完整计划。原样按序渲染会把同一份计划画四遍(0/3、1/3、2/3、3/3),只有该 Turn 的
|
|
818
|
+
* 最后一份是当前事实。所以每个 turnId 各自保留最后一份计划快照。
|
|
747
819
|
*/
|
|
748
820
|
function dropSupersededPlans(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
749
|
-
|
|
750
|
-
let lastPlanBlock = -1;
|
|
821
|
+
const lastPlanByTurn = new Map<string, { entryIndex: number; blockIndex: number }>();
|
|
751
822
|
entries.forEach((entry, entryIndex) => {
|
|
752
823
|
if (entry.type !== "assistant") return;
|
|
753
824
|
entry.blocks.forEach((block, blockIndex) => {
|
|
754
825
|
if (block.kind !== "plan") return;
|
|
755
|
-
|
|
756
|
-
lastPlanBlock = blockIndex;
|
|
826
|
+
lastPlanByTurn.set(entry.turnId, { entryIndex, blockIndex });
|
|
757
827
|
});
|
|
758
828
|
});
|
|
759
|
-
if (
|
|
829
|
+
if (lastPlanByTurn.size === 0) return entries;
|
|
760
830
|
|
|
761
831
|
return entries.map((entry, entryIndex) =>
|
|
762
832
|
entry.type === "assistant"
|
|
763
833
|
? {
|
|
764
834
|
...entry,
|
|
765
835
|
blocks: entry.blocks.filter(
|
|
766
|
-
(block, blockIndex) =>
|
|
767
|
-
block.kind !== "plan"
|
|
768
|
-
|
|
836
|
+
(block, blockIndex) => {
|
|
837
|
+
if (block.kind !== "plan") return true;
|
|
838
|
+
const last = lastPlanByTurn.get(entry.turnId);
|
|
839
|
+
return last?.entryIndex === entryIndex && last.blockIndex === blockIndex;
|
|
840
|
+
},
|
|
769
841
|
),
|
|
770
842
|
}
|
|
771
843
|
: entry,
|
|
@@ -876,6 +948,8 @@ export function deriveTurnActivity(
|
|
|
876
948
|
options: {
|
|
877
949
|
isActive: boolean;
|
|
878
950
|
isRecovering?: boolean;
|
|
951
|
+
recoveryAttempt?: number;
|
|
952
|
+
recoveryMax?: number;
|
|
879
953
|
recoveryStatusLabel?: string;
|
|
880
954
|
awaitingApproval?: boolean;
|
|
881
955
|
hasPendingSteer?: boolean;
|
|
@@ -884,7 +958,10 @@ export function deriveTurnActivity(
|
|
|
884
958
|
if (options.isRecovering) {
|
|
885
959
|
return {
|
|
886
960
|
state: "connecting",
|
|
887
|
-
label: options.recoveryStatusLabel ??
|
|
961
|
+
label: options.recoveryStatusLabel ??
|
|
962
|
+
(options.recoveryAttempt === undefined || options.recoveryMax === undefined
|
|
963
|
+
? "Recovering model response…"
|
|
964
|
+
: `Recovering model response ${options.recoveryAttempt}/${options.recoveryMax}…`),
|
|
888
965
|
};
|
|
889
966
|
}
|
|
890
967
|
if (!options.isActive) return null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
import { SearchIcon } from "lucide-react";
|
|
5
|
+
import { cn } from "../internal/cn";
|
|
6
|
+
import { field, mono, ShimmerLabel } from "./surfaces";
|
|
7
|
+
import { take } from "./range";
|
|
8
|
+
import type { CloudOsToolCall } from "./tool-presentation";
|
|
9
|
+
|
|
10
|
+
/** Adapted from assistant-ui's MIT-licensed Web Search element. */
|
|
11
|
+
export interface WebSearchResult {
|
|
12
|
+
title: string;
|
|
13
|
+
domain: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function WebSearch({
|
|
17
|
+
query,
|
|
18
|
+
results,
|
|
19
|
+
visibleResults,
|
|
20
|
+
searching,
|
|
21
|
+
cycle,
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: Omit<
|
|
25
|
+
ComponentProps<"div">,
|
|
26
|
+
"children" | "results"
|
|
27
|
+
> & {
|
|
28
|
+
query: string;
|
|
29
|
+
results: readonly WebSearchResult[];
|
|
30
|
+
visibleResults: number;
|
|
31
|
+
searching: boolean;
|
|
32
|
+
cycle: number;
|
|
33
|
+
}) {
|
|
34
|
+
const revealedResults = take(results, visibleResults);
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
data-slot="web-search"
|
|
38
|
+
className={cn("flex w-full max-w-sm flex-col gap-2.5", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
<span
|
|
42
|
+
className={cn(
|
|
43
|
+
field,
|
|
44
|
+
"text-foreground/70 inline-flex w-fit items-center gap-1.5 rounded-full px-3.5 py-2 text-xs",
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<SearchIcon className="text-foreground/40 size-3" />
|
|
48
|
+
{query}
|
|
49
|
+
</span>
|
|
50
|
+
<div className="text-foreground/45 text-xs">
|
|
51
|
+
{searching ? (
|
|
52
|
+
<ShimmerLabel className="relative inline-block leading-none">
|
|
53
|
+
Searching
|
|
54
|
+
</ShimmerLabel>
|
|
55
|
+
) : (
|
|
56
|
+
<span className="fade-in animate-in duration-300">
|
|
57
|
+
Read {results.length} {results.length === 1 ? "source" : "sources"}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
<div className="flex min-h-[5.75rem] max-h-48 flex-col overflow-y-auto overscroll-contain pr-1">
|
|
62
|
+
{revealedResults.map((result, index) => (
|
|
63
|
+
<div
|
|
64
|
+
key={`${cycle}-${result.domain}-${index}`}
|
|
65
|
+
className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both hover:bg-foreground/[0.03] -mx-2.5 flex items-center gap-2.5 rounded-xl px-2.5 py-1.5 transition-colors duration-300"
|
|
66
|
+
>
|
|
67
|
+
<span className="bg-foreground/[0.06] text-foreground/45 flex size-4 shrink-0 items-center justify-center rounded text-[9px] font-medium">
|
|
68
|
+
{result.domain.charAt(0).toUpperCase()}
|
|
69
|
+
</span>
|
|
70
|
+
<span className="text-foreground/90 min-w-0 flex-1 truncate text-[13.5px]">
|
|
71
|
+
{result.title}
|
|
72
|
+
</span>
|
|
73
|
+
<span className={cn(mono, "text-foreground/35 shrink-0")}>
|
|
74
|
+
{result.domain}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type UnknownRecord = Record<string, unknown>;
|
|
84
|
+
|
|
85
|
+
function recordOf(value: unknown): UnknownRecord {
|
|
86
|
+
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
87
|
+
? value as UnknownRecord
|
|
88
|
+
: {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function webSearchResults(output: unknown): WebSearchResult[] {
|
|
92
|
+
const record = recordOf(output);
|
|
93
|
+
const candidates = [record.sources, record.searchResults]
|
|
94
|
+
.flatMap((value) => Array.isArray(value) ? value : []);
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
return candidates.flatMap((value) => {
|
|
97
|
+
const result = recordOf(value);
|
|
98
|
+
if (typeof result.url !== "string") return [];
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(result.url);
|
|
101
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return [];
|
|
102
|
+
if (seen.has(url.href)) return [];
|
|
103
|
+
seen.add(url.href);
|
|
104
|
+
return [{
|
|
105
|
+
title: typeof result.title === "string" && result.title.trim()
|
|
106
|
+
? result.title.trim()
|
|
107
|
+
: url.hostname,
|
|
108
|
+
domain: url.hostname,
|
|
109
|
+
}];
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function WebSearchToolCall({
|
|
117
|
+
toolCall,
|
|
118
|
+
}: {
|
|
119
|
+
toolCall: CloudOsToolCall;
|
|
120
|
+
}) {
|
|
121
|
+
const query = typeof toolCall.input.query === "string" &&
|
|
122
|
+
toolCall.input.query.trim()
|
|
123
|
+
? toolCall.input.query.trim()
|
|
124
|
+
: "Web search";
|
|
125
|
+
const results = webSearchResults(toolCall.output);
|
|
126
|
+
return (
|
|
127
|
+
<WebSearch
|
|
128
|
+
query={query}
|
|
129
|
+
results={results}
|
|
130
|
+
visibleResults={toolCall.running ? 0 : results.length}
|
|
131
|
+
searching={toolCall.running}
|
|
132
|
+
cycle={0}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -320,7 +320,7 @@ function CloudOsChatInputContent({
|
|
|
320
320
|
|
|
321
321
|
return (
|
|
322
322
|
// isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
|
|
323
|
-
<div className="cos-chat-input-root relative isolate px-4
|
|
323
|
+
<div className="cos-chat-input-root relative isolate px-4 pb-4">
|
|
324
324
|
<ComposerTriggerPopover
|
|
325
325
|
char="/"
|
|
326
326
|
{...slash}
|
package/cloud-os/index.ts
CHANGED
|
@@ -71,6 +71,16 @@ export {
|
|
|
71
71
|
ToolGroupRow,
|
|
72
72
|
WorkIcon,
|
|
73
73
|
} from "./chat/tool-rows";
|
|
74
|
+
export { ToolCall } from "./chat/tool-call";
|
|
75
|
+
export type { ToolCallProps } from "./chat/tool-call";
|
|
76
|
+
export { CodeRunner } from "./chat/code-runner";
|
|
77
|
+
export type { RunState } from "./chat/code-runner";
|
|
78
|
+
export { ToolTimeline } from "./chat/tool-timeline";
|
|
79
|
+
export type {
|
|
80
|
+
TimelineStat,
|
|
81
|
+
TimelineStep,
|
|
82
|
+
ToolTimelineProps,
|
|
83
|
+
} from "./chat/tool-timeline";
|
|
74
84
|
export {
|
|
75
85
|
ApprovalBlock,
|
|
76
86
|
AskUserBlock,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
|
4
|
+
|
|
5
|
+
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
|
6
|
+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
|
|
10
|
+
return (
|
|
11
|
+
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
|
|
16
|
+
return (
|
|
17
|
+
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
|
@@ -557,6 +557,24 @@
|
|
|
557
557
|
flex: none;
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
/* Trace 新块进入时轻推上浮;已有块更新不会重复播放。 */
|
|
561
|
+
.cos-trace-chain > * {
|
|
562
|
+
animation: cos-trace-enter 180ms cubic-bezier(0.32, 0.72, 0, 1) both;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@keyframes cos-trace-enter {
|
|
566
|
+
from {
|
|
567
|
+
opacity: 0;
|
|
568
|
+
transform: translateY(6px);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@media (prefers-reduced-motion: reduce) {
|
|
573
|
+
.cos-trace-chain > * {
|
|
574
|
+
animation: none;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
560
578
|
/* ── 流式占位:Thinking 扫光 ──────────────────────────────────────────────── */
|
|
561
579
|
.cos-thinking-shimmer {
|
|
562
580
|
color: var(--text-color-kumo-inactive);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/message-panel",
|
|
3
|
-
"version": "0.1.3-alpha.
|
|
3
|
+
"version": "0.1.3-alpha.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@assistant-ui/react": "^0.15.1",
|
|
28
|
+
"@base-ui/react": "^1.7.0",
|
|
28
29
|
"@phosphor-icons/react": "^2.1.10",
|
|
29
30
|
"@streamdown/cjk": "^1.0.3",
|
|
30
31
|
"@streamdown/code": "^1.1.1",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"@types/react-dom": "^19.2.3",
|
|
44
45
|
"@vitejs/plugin-react": "^6.0.3",
|
|
45
46
|
"tailwindcss": "^4.3.2",
|
|
47
|
+
"tw-animate-css": "^1.4.0",
|
|
46
48
|
"typescript": "^7.0.2",
|
|
47
49
|
"vite": "^8.1.4"
|
|
48
50
|
},
|