@springbrand/message-panel 0.1.3-alpha.33 → 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.
@@ -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
+ }
@@ -27,8 +27,8 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
27
27
  createdAt?: number;
28
28
  error?: string;
29
29
  interruptedByUser?: boolean;
30
- turnId?: string;
31
30
  turnDurationMs?: number;
31
+ turnId?: string;
32
32
  turnStartedAt?: number;
33
33
  turnStatus?: string;
34
34
  requestedCapabilities?: unknown;
@@ -78,7 +78,7 @@ export type AssistantBlock =
78
78
  text: string;
79
79
  running: boolean;
80
80
  }
81
- | { kind: "text"; key: string; text: string }
81
+ | { kind: "text"; key: string; text: string; final: boolean }
82
82
  | { kind: "toolGroup"; key: string; group: ToolCallGroup }
83
83
  | { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
84
84
  // 答案就在 call.output 里(`ask_user` 是 client-settled tool,用户点选后
@@ -144,10 +144,9 @@ export type CloudOsEntry =
144
144
  type: "assistant";
145
145
  key: string;
146
146
  messageId: string;
147
- turnId?: string;
147
+ turnId: string;
148
148
  blocks: AssistantBlock[];
149
149
  copyText: string;
150
- durationMs?: number;
151
150
  timestamp?: number;
152
151
  terminal?: { kind: "interrupted" | "error"; title: string; body?: string };
153
152
  };
@@ -171,10 +170,6 @@ function nonNegativeNumber(value: unknown): number | undefined {
171
170
  : undefined;
172
171
  }
173
172
 
174
- function nonEmptyString(value: unknown): string | undefined {
175
- return typeof value === "string" && value.trim() ? value : undefined;
176
- }
177
-
178
173
  export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
179
174
  return recordOf(message.metadata) as CloudOsMessageMetadata;
180
175
  }
@@ -229,54 +224,6 @@ export function formatFullTimestamp(timestamp: number): string {
229
224
  });
230
225
  }
231
226
 
232
- function turnDuration(metadata: CloudOsMessageMetadata): number | undefined {
233
- const explicit = nonNegativeNumber(metadata.turnDurationMs);
234
- if (explicit !== undefined) return explicit;
235
- const startedAt = nonNegativeNumber(metadata.turnStartedAt);
236
- const completedAt = nonNegativeNumber(metadata.completedAt);
237
- return startedAt !== undefined && completedAt !== undefined
238
- ? Math.max(0, completedAt - startedAt)
239
- : undefined;
240
- }
241
-
242
- function isWorkBlock(block: AssistantBlock): boolean {
243
- return (
244
- block.kind === "reasoning" ||
245
- block.kind === "toolGroup" ||
246
- block.kind === "plan" ||
247
- block.kind === "parallel" ||
248
- block.kind === "subagents"
249
- );
250
- }
251
-
252
- /**
253
- * 把一条 assistant 消息拆成可折叠的工作过程与始终可见的最终内容。
254
- * 最后一段工作之前的 text 是过程旁白;最后一次工作之后的 text 才是正文。
255
- */
256
- export function partitionAssistantBlocks(blocks: readonly AssistantBlock[]): {
257
- work: AssistantBlock[];
258
- content: AssistantBlock[];
259
- } {
260
- let lastWorkIndex = -1;
261
- blocks.forEach((block, index) => {
262
- if (isWorkBlock(block)) lastWorkIndex = index;
263
- });
264
-
265
- const work: AssistantBlock[] = [];
266
- const content: AssistantBlock[] = [];
267
- blocks.forEach((block, index) => {
268
- if (
269
- isWorkBlock(block) ||
270
- (block.kind === "text" && index < lastWorkIndex)
271
- ) {
272
- work.push(block);
273
- } else {
274
- content.push(block);
275
- }
276
- });
277
- return { work, content };
278
- }
279
-
280
227
  function toolNameOf(part: unknown): string | null {
281
228
  const type = String(recordOf(part).type ?? "");
282
229
  if (type === "dynamic-tool") {
@@ -471,7 +418,7 @@ function assistantBlocks(
471
418
  const text = String(record.text ?? "").trim();
472
419
  if (!text) return;
473
420
  flush();
474
- blocks.push({ kind: "text", key, text });
421
+ blocks.push({ kind: "text", key, text, final: false });
475
422
  return;
476
423
  }
477
424
 
@@ -677,15 +624,13 @@ export function buildCloudOsEntries({
677
624
  approvalIdOf = () => undefined,
678
625
  }: BuildEntriesOptions): CloudOsEntry[] {
679
626
  const entries: CloudOsEntry[] = [];
627
+ let fallbackTurnId = "turn:initial";
680
628
  const lastAssistantIndex = (() => {
681
629
  for (let index = messages.length - 1; index >= 0; index -= 1) {
682
630
  if (messages[index].role === "assistant") return index;
683
631
  }
684
632
  return -1;
685
633
  })();
686
- const activeTurnId = isActive && lastAssistantIndex >= 0
687
- ? nonEmptyString(cloudOsMetadata(messages[lastAssistantIndex]!).turnId)
688
- : undefined;
689
634
 
690
635
  messages.forEach((message, index) => {
691
636
  if (message.role === "system") return;
@@ -713,6 +658,9 @@ export function buildCloudOsEntries({
713
658
  };
714
659
  });
715
660
  if (!text && attachments.length === 0) return;
661
+ fallbackTurnId = typeof metadata.turnId === "string" && metadata.turnId
662
+ ? metadata.turnId
663
+ : `turn:user:${message.id}`;
716
664
  entries.push({
717
665
  type: "user",
718
666
  key: `user-${message.id}-${index}`,
@@ -740,10 +688,11 @@ export function buildCloudOsEntries({
740
688
  type: "assistant",
741
689
  key: `assistant-${message.id}-${index}`,
742
690
  messageId: message.id,
743
- turnId: nonEmptyString(metadata.turnId),
691
+ turnId: typeof metadata.turnId === "string" && metadata.turnId
692
+ ? metadata.turnId
693
+ : fallbackTurnId,
744
694
  blocks,
745
695
  copyText: messageText(message),
746
- durationMs: turnDuration(metadata),
747
696
  timestamp:
748
697
  nonNegativeNumber(metadata.completedAt) ??
749
698
  nonNegativeNumber(metadata.createdAt),
@@ -751,54 +700,70 @@ export function buildCloudOsEntries({
751
700
  });
752
701
  });
753
702
 
754
- return aggregateCompletedTurnWork(
703
+ return markFinalTextBlocks(mergeConsecutiveAssistantTurns(
755
704
  dropSupersededActionPresentations(dropSupersededPlans(entries)),
756
- activeTurnId,
757
- );
705
+ ));
758
706
  }
759
707
 
760
- /** Collapse every completed Turn's assistant segments into one display entry. */
761
- function aggregateCompletedTurnWork(
762
- entries: CloudOsEntry[],
763
- activeTurnId: string | undefined,
764
- ): CloudOsEntry[] {
765
- const indexesByTurn = new Map<string, number[]>();
766
- entries.forEach((entry, index) => {
767
- if (entry.type !== "assistant" || !entry.turnId) return;
768
- const indexes = indexesByTurn.get(entry.turnId) ?? [];
769
- indexes.push(index);
770
- indexesByTurn.set(entry.turnId, indexes);
771
- });
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
+ }
772
714
 
773
- const hidden = new Set<number>();
774
- const replacements = new Map<number, CloudOsEntry>();
775
- for (const [turnId, indexes] of indexesByTurn) {
776
- if (turnId === activeTurnId || indexes.length < 2) continue;
777
- const assistantEntries = indexes.map(
778
- (index) => entries[index] as Extract<CloudOsEntry, { type: "assistant" }>,
779
- );
780
- const anchorIndex = indexes[indexes.length - 1]!;
781
- const anchor = assistantEntries[assistantEntries.length - 1]!;
782
- const durations = assistantEntries.flatMap((entry) =>
783
- entry.durationMs === undefined ? [] : [entry.durationMs]
784
- );
785
- const timestamps = assistantEntries.flatMap((entry) =>
786
- entry.timestamp === undefined ? [] : [entry.timestamp]
787
- );
715
+ function isWorkBoundaryBlock(block: AssistantBlock): boolean {
716
+ return block.kind !== "text" && !isFinalOutputBlock(block);
717
+ }
788
718
 
789
- indexes.slice(0, -1).forEach((index) => hidden.add(index));
790
- replacements.set(anchorIndex, {
791
- ...anchor,
792
- key: `assistant-turn-${turnId}`,
793
- blocks: assistantEntries.flatMap((entry) => entry.blocks),
794
- ...(durations.length > 0 ? { durationMs: Math.max(...durations) } : {}),
795
- ...(timestamps.length > 0 ? { timestamp: Math.max(...timestamps) } : {}),
796
- });
797
- }
719
+ function markFinalTextBlocks(entries: CloudOsEntry[]): CloudOsEntry[] {
720
+ return entries.map((entry) => {
721
+ if (entry.type !== "assistant") return entry;
798
722
 
799
- return entries.flatMap((entry, index) =>
800
- hidden.has(index) ? [] : [replacements.get(index) ?? entry]
801
- );
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
+ }, []);
802
767
  }
803
768
 
804
769
  function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntry[] {
@@ -848,31 +813,31 @@ function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntr
848
813
  }
849
814
 
850
815
  /**
851
- * update_plan 是**整份快照覆盖**,不是增量事件 —— 一次任务里每推进一步就会再发一份
852
- * 完整计划。原样按序渲染会把同一份计划画四遍(0/3、1/3、2/3、3/3),只有最后那份
853
- * 是当前事实。所以整条会话里只保留最后一份计划快照。
816
+ * update_plan 是**整份快照覆盖**,不是增量事件 —— 一次 Turn 里每推进一步就会再发一份
817
+ * 完整计划。原样按序渲染会把同一份计划画四遍(0/3、1/3、2/3、3/3),只有该 Turn 的
818
+ * 最后一份是当前事实。所以每个 turnId 各自保留最后一份计划快照。
854
819
  */
855
820
  function dropSupersededPlans(entries: CloudOsEntry[]): CloudOsEntry[] {
856
- let lastPlanEntry = -1;
857
- let lastPlanBlock = -1;
821
+ const lastPlanByTurn = new Map<string, { entryIndex: number; blockIndex: number }>();
858
822
  entries.forEach((entry, entryIndex) => {
859
823
  if (entry.type !== "assistant") return;
860
824
  entry.blocks.forEach((block, blockIndex) => {
861
825
  if (block.kind !== "plan") return;
862
- lastPlanEntry = entryIndex;
863
- lastPlanBlock = blockIndex;
826
+ lastPlanByTurn.set(entry.turnId, { entryIndex, blockIndex });
864
827
  });
865
828
  });
866
- if (lastPlanEntry === -1) return entries;
829
+ if (lastPlanByTurn.size === 0) return entries;
867
830
 
868
831
  return entries.map((entry, entryIndex) =>
869
832
  entry.type === "assistant"
870
833
  ? {
871
834
  ...entry,
872
835
  blocks: entry.blocks.filter(
873
- (block, blockIndex) =>
874
- block.kind !== "plan" ||
875
- (entryIndex === lastPlanEntry && blockIndex === lastPlanBlock),
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
+ },
876
841
  ),
877
842
  }
878
843
  : entry,
@@ -983,6 +948,8 @@ export function deriveTurnActivity(
983
948
  options: {
984
949
  isActive: boolean;
985
950
  isRecovering?: boolean;
951
+ recoveryAttempt?: number;
952
+ recoveryMax?: number;
986
953
  recoveryStatusLabel?: string;
987
954
  awaitingApproval?: boolean;
988
955
  hasPendingSteer?: boolean;
@@ -991,7 +958,10 @@ export function deriveTurnActivity(
991
958
  if (options.isRecovering) {
992
959
  return {
993
960
  state: "connecting",
994
- label: options.recoveryStatusLabel ?? "Reconnecting…",
961
+ label: options.recoveryStatusLabel ??
962
+ (options.recoveryAttempt === undefined || options.recoveryMax === undefined
963
+ ? "Recovering model response…"
964
+ : `Recovering model response ${options.recoveryAttempt}/${options.recoveryMax}…`),
995
965
  };
996
966
  }
997
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 py-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
@@ -68,13 +68,19 @@ export type {
68
68
  export {
69
69
  ThinkingTraceRow,
70
70
  ToolCallDetails,
71
- ToolGroupDetails,
72
71
  ToolGroupRow,
73
- WorkDescriptionRow,
74
72
  WorkIcon,
75
- WorkTraceDisclosure,
76
- formatWorkDuration,
77
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";
78
84
  export {
79
85
  ApprovalBlock,
80
86
  AskUserBlock,
@@ -98,7 +104,6 @@ export {
98
104
  formatClockTime,
99
105
  formatFullTimestamp,
100
106
  messageText,
101
- partitionAssistantBlocks,
102
107
  requestedCapabilitiesOf,
103
108
  rhythmTopClass,
104
109
  } from "./chat/transcript-model";
@@ -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 };