@springbrand/message-panel 0.1.3-alpha.2 → 0.1.3-alpha.4

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.
@@ -1,5 +1,7 @@
1
1
  import type { UIMessage } from "ai";
2
+ import type { OrbState } from "thinking-orbs";
2
3
  import {
4
+ askUserOutcome,
3
5
  buildToolCallGroups,
4
6
  toCloudOsToolCall,
5
7
  type CloudOsToolCall,
@@ -27,6 +29,13 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
27
29
  turnDurationMs?: number;
28
30
  turnStartedAt?: number;
29
31
  turnStatus?: string;
32
+ requestedCapabilities?: unknown;
33
+ }
34
+
35
+ export interface CloudOsRequestedCapability {
36
+ kind: "skill" | "plan";
37
+ name: string;
38
+ label: string;
30
39
  }
31
40
 
32
41
  export interface CloudOsAttachment {
@@ -45,9 +54,12 @@ export type AssistantBlock =
45
54
  | { kind: "text"; key: string; text: string }
46
55
  | { kind: "toolGroup"; key: string; group: ToolCallGroup }
47
56
  | { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
48
- // answeredText 取的是**这条消息之后的第一条用户直答**,不是工具 output ——
49
- // ask_user output 往往只是 `{ asked: true }` 之类的回执。
50
- | { kind: "askUser"; key: string; call: CloudOsToolCall; answeredText?: string }
57
+ // 答案就在 call.output 里(`ask_user` 是 client-settled tool,用户点选后
58
+ // respondToolInteraction 回写成这次调用的结果)。曾经靠扫下一条用户消息
59
+ // 推断,那是有损且会误认无关消息的,已彻底删除。
60
+ // `pending` 是这张卡还在等人 —— 卡片据此决定能不能点,活动指示器据此决定
61
+ // 该不该出现,同一条判据(askUserOutcome)算一次。
62
+ | { kind: "askUser"; key: string; call: CloudOsToolCall; pending: boolean }
51
63
  | { kind: "suggestions"; key: string; items: string[] }
52
64
  | { kind: "schedule"; key: string; call: CloudOsToolCall }
53
65
  | { kind: "approval"; key: string; call: CloudOsToolCall; approvalId: string }
@@ -75,6 +87,7 @@ export type CloudOsEntry =
75
87
  messageId: string;
76
88
  text: string;
77
89
  attachments: CloudOsAttachment[];
90
+ capabilities: CloudOsRequestedCapability[];
78
91
  authorName?: string;
79
92
  timestamp?: number;
80
93
  }
@@ -121,6 +134,31 @@ export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
121
134
  return recordOf(message.metadata) as CloudOsMessageMetadata;
122
135
  }
123
136
 
137
+ export function requestedCapabilitiesOf(
138
+ metadata: CloudOsMessageMetadata,
139
+ ): CloudOsRequestedCapability[] {
140
+ if (!Array.isArray(metadata.requestedCapabilities)) return [];
141
+ const seen = new Set<string>();
142
+ return metadata.requestedCapabilities.flatMap((value) => {
143
+ const capability = recordOf(value);
144
+ if (
145
+ (capability.kind !== "skill" && capability.kind !== "plan") ||
146
+ typeof capability.name !== "string" ||
147
+ !capability.name.trim() ||
148
+ typeof capability.label !== "string" ||
149
+ !capability.label.trim()
150
+ ) return [];
151
+ const key = `${capability.kind}:${capability.name}`;
152
+ if (seen.has(key)) return [];
153
+ seen.add(key);
154
+ return [{
155
+ kind: capability.kind,
156
+ name: capability.name,
157
+ label: capability.label,
158
+ }];
159
+ });
160
+ }
161
+
124
162
  export function messageText(message: UIMessage): string {
125
163
  return message.parts
126
164
  .filter(
@@ -248,7 +286,6 @@ function assistantBlocks(
248
286
  message: UIMessage,
249
287
  isActive: boolean,
250
288
  showThinkingTraces: boolean,
251
- replyText: string | undefined,
252
289
  approvalIdOf: (part: unknown) => string | undefined,
253
290
  ): AssistantBlock[] {
254
291
  const blocks: AssistantBlock[] = [];
@@ -380,10 +417,7 @@ function assistantBlocks(
380
417
  kind: "askUser",
381
418
  key,
382
419
  call,
383
- answeredText:
384
- replyText?.trim() ||
385
- (typeof call.output === "string" ? call.output.trim() : "") ||
386
- undefined,
420
+ pending: askUserOutcome(call).kind === "pending",
387
421
  });
388
422
  return;
389
423
  }
@@ -432,13 +466,6 @@ function terminalOf(
432
466
  return undefined;
433
467
  }
434
468
 
435
- function isDirectUserMessage(message: UIMessage): boolean {
436
- return (
437
- message.role === "user" &&
438
- !message.parts.some((part) => dataKindOf(part) !== null)
439
- );
440
- }
441
-
442
469
  export interface BuildEntriesOptions {
443
470
  messages: readonly UIMessage[];
444
471
  /** streaming/submitted 时最后一条 assistant 消息才算「活着」。 */
@@ -494,6 +521,7 @@ export function buildCloudOsEntries({
494
521
  messageId: message.id,
495
522
  text,
496
523
  attachments,
524
+ capabilities: requestedCapabilitiesOf(metadata),
497
525
  authorName: metadata.authorDisplayName,
498
526
  timestamp:
499
527
  nonNegativeNumber(metadata.createdAt) ??
@@ -502,12 +530,10 @@ export function buildCloudOsEntries({
502
530
  return;
503
531
  }
504
532
 
505
- const reply = messages.slice(index + 1).find(isDirectUserMessage);
506
533
  const blocks = assistantBlocks(
507
534
  message,
508
535
  isActive && index === lastAssistantIndex,
509
536
  showThinkingTraces,
510
- reply ? messageText(reply).trim() || undefined : undefined,
511
537
  approvalIdOf,
512
538
  );
513
539
  const terminal = terminalOf(message);
@@ -560,6 +586,87 @@ function dropSupersededPlans(entries: CloudOsEntry[]): CloudOsEntry[] {
560
586
  );
561
587
  }
562
588
 
589
+ // ── 活动指示(本回合此刻在干什么)────────────────────────────────────────────
590
+
591
+ /**
592
+ * 底部活动指示器要显示的动画与文案。
593
+ */
594
+ export interface CloudOsActivity {
595
+ state: OrbState;
596
+ label: string;
597
+ }
598
+
599
+ /**
600
+ * 尾巴是否**自己会说话** —— 有它在动,就不该再挂一个活动指示器。
601
+ *
602
+ * 正在流的正文/思考、还在跑的工具组或计划,以及一张**还等着你作答**的
603
+ * `ask_user` 卡都算:前三者自身就有动效,最后一个等的是人不是模型,这时候
604
+ * 显示「Thinking」是在撒谎。
605
+ */
606
+ function isLiveBlock(block: AssistantBlock): boolean {
607
+ switch (block.kind) {
608
+ case "text":
609
+ case "reasoning":
610
+ return true;
611
+ case "toolGroup":
612
+ return block.group.hasRunning;
613
+ case "plan":
614
+ return block.running;
615
+ case "askUser":
616
+ return block.pending;
617
+ // approval 块只在 awaitingApproval 时才建出来,所以它在场就是在等人。
618
+ case "approval":
619
+ return true;
620
+ case "parallel":
621
+ return block.tools.some((tool) => tool.status === "running");
622
+ case "subagents":
623
+ return block.agents.some((agent) => agent.status === "running");
624
+ default:
625
+ return false;
626
+ }
627
+ }
628
+
629
+ // 已结算的尾巴 → 此刻真正在发生的事。措辞刻意只说得出口的那部分:
630
+ // 工具跑完了,模型在读它的输出;卡答完了,模型在接你的答案。
631
+ function activityForTail(block: AssistantBlock | undefined): CloudOsActivity {
632
+ if (!block) return { state: "solving", label: "Thinking…" };
633
+ if (block.kind === "askUser") {
634
+ return { state: "solving", label: "Picking up your answer…" };
635
+ }
636
+ if (block.kind === "toolGroup") {
637
+ const kind = block.group.calls.at(-1)?.kind;
638
+ return kind !== undefined &&
639
+ ["web-search", "web-fetch", "grep", "glob", "list", "read"].includes(
640
+ kind,
641
+ )
642
+ ? { state: "searching", label: "Reading results…" }
643
+ : { state: "working", label: "Working…" };
644
+ }
645
+ return { state: "working", label: "Working…" };
646
+ }
647
+
648
+ /**
649
+ * 本回合此刻该显示什么活动;`null` = 尾巴自己会说话,别再叠一个。
650
+ *
651
+ * @remarks
652
+ * 判据是「**尾巴有没有活口**」,不是「本回合有没有出过内容」。后者曾经是这里的
653
+ * 写法,于是答完 `ask_user`(卡片变成 Submitted,读起来就是"结束了")到模型下一
654
+ * 个 chunk 之间整屏没有任何在跑的迹象 —— 而那段恰好是最长的一次 TTFT,看起来
655
+ * 就是卡死。凡是尾巴为**已结算卡片**的断档都是同一个洞,这里一并补上。
656
+ */
657
+ export function deriveTurnActivity(
658
+ entries: readonly CloudOsEntry[],
659
+ options: { isActive: boolean; hasPendingSteer?: boolean },
660
+ ): CloudOsActivity | null {
661
+ if (!options.isActive) return null;
662
+ const tail = entries.at(-1) ?? null;
663
+ const block = tail?.type === "assistant" ? tail.blocks.at(-1) : undefined;
664
+ if (block && isLiveBlock(block)) return null;
665
+ return options.hasPendingSteer
666
+ ? { state: "solving", label: "Waiting for the current step to finish…" }
667
+ : activityForTail(block);
668
+ }
669
+
563
670
  // ── 行间距节奏(照搬原 rhythmTopClass)──────────────────────────────────────
564
671
 
565
672
  function isUserEntry(entry: CloudOsEntry): boolean {
@@ -8,9 +8,21 @@ import {
8
8
  Terminal,
9
9
  X,
10
10
  } from "@phosphor-icons/react";
11
+ import {
12
+ AssistantRuntimeProvider,
13
+ ComposerPrimitive,
14
+ unstable_useSlashCommandAdapter,
15
+ unstable_useTriggerPopoverAriaProps,
16
+ unstable_useTriggerPopoverTriggers,
17
+ useExternalStoreRuntime,
18
+ type AssistantRuntime,
19
+ type ThreadMessage,
20
+ } from "@assistant-ui/react";
21
+ import { ComposerTriggerPopover } from "@springbrand/message-panel/composer";
11
22
  import {
12
23
  useCallback,
13
24
  useEffect,
25
+ useMemo,
14
26
  useRef,
15
27
  useState,
16
28
  type DragEvent as ReactDragEvent,
@@ -19,6 +31,12 @@ import {
19
31
  import { DropdownMenu } from "../primitives/dropdown-menu";
20
32
  import { Tooltip } from "../primitives/tooltip";
21
33
  import { WorkshopIconButton } from "../primitives/workshop-controls";
34
+ import { CapabilityChip } from "../capability-chip";
35
+
36
+ const isComposingKeyEvent = (event: {
37
+ isComposing?: boolean;
38
+ keyCode?: number;
39
+ }) => event.isComposing === true || event.keyCode === 229;
22
40
 
23
41
  /**
24
42
  * 从 cloudflare-os-main `ChatInterface.tsx` 的 `ChatInput` 拷来。
@@ -27,9 +45,10 @@ import { WorkshopIconButton } from "../primitives/workshop-controls";
27
45
  * 附件缩略图行、底部左「+ / 添加资源」右「模型 / 发送-停止」的布局、
28
46
  * draftUpdateBanner 插槽、blockedReason 占位文案、Enter 发送 / Shift+Enter 换行。
29
47
  *
30
- * 删掉:capsule(URL → 资源胶囊)、slash command picker、ComposerMirror、
31
- * GatekeeperModal —— 这四样都直挂 gadgets 的 capnweb RPC,在 UIMessage 协议下
32
- * 没有对应物。「添加资源」按钮保留成一个可选回调,宿主不给就不渲染。
48
+ * 删掉:capsule(URL → 资源胶囊)、ComposerMirror、GatekeeperModal —— 这三样都直挂
49
+ * gadgets 的 capnweb RPC,在 UIMessage 协议下没有对应物。「添加资源」按钮保留成一个
50
+ * 可选回调,宿主不给就不渲染。Command picker 使用 package 内的
51
+ * assistant-ui ComposerTriggerPopover。
33
52
  */
34
53
 
35
54
  export interface CloudOsModelOption {
@@ -38,10 +57,15 @@ export interface CloudOsModelOption {
38
57
  }
39
58
 
40
59
  export interface CloudOsComposerCommand {
60
+ prefix?: "/" | "$";
41
61
  name: string;
42
62
  description: string;
43
- /** 选中后填进输入框的起手文本。 */
44
- prompt: string;
63
+ }
64
+
65
+ export interface CloudOsCapabilityView {
66
+ kind: "skill" | "plan";
67
+ name: string;
68
+ label: string;
45
69
  }
46
70
 
47
71
  export interface CloudOsAttachmentView {
@@ -77,11 +101,11 @@ export interface CloudOsChatInputProps {
77
101
  * 传了就以它为准来决定显示发送键还是停止键。
78
102
  */
79
103
  turnActive?: boolean;
80
- /**
81
- * 可用的斜杠命令。cloudflare-os 原版是一个直连 RPC 的行内 picker;这里退成
82
- * 「+」菜单里的一组条目 —— 能力保住,但**没有**边打字边过滤的行内补全。
83
- */
104
+ /** 可用的斜杠命令,也会显示在「+」菜单中。 */
84
105
  commands?: readonly CloudOsComposerCommand[];
106
+ capabilities?: readonly CloudOsCapabilityView[];
107
+ onCommandSelect?: (command: CloudOsComposerCommand) => void;
108
+ onCapabilityRemove?: (kind: CloudOsCapabilityView["kind"], name: string) => void;
85
109
  /** 有值时渲染「添加资源」按钮。 */
86
110
  onAttachResource?: () => void;
87
111
  attachLabel?: string;
@@ -101,6 +125,10 @@ export interface CloudOsChatInputProps {
101
125
 
102
126
  const MAX_PENDING_ATTACHMENTS = 5;
103
127
 
128
+ function commandLabel(command: CloudOsComposerCommand): string {
129
+ return `${command.prefix ?? "/"}${command.name.replaceAll("-", " ")}`;
130
+ }
131
+
104
132
  function autoResizeTextarea(
105
133
  textarea: HTMLTextAreaElement,
106
134
  minRows: number,
@@ -118,7 +146,7 @@ function autoResizeTextarea(
118
146
  textarea.style.overflow = textarea.scrollHeight > maxH ? "auto" : "hidden";
119
147
  }
120
148
 
121
- export function CloudOsChatInput({
149
+ function CloudOsChatInputContent({
122
150
  value,
123
151
  onChange,
124
152
  onSubmit,
@@ -136,6 +164,9 @@ export function CloudOsChatInput({
136
164
  submissionBlocked = false,
137
165
  turnActive,
138
166
  commands = [],
167
+ capabilities = [],
168
+ onCommandSelect,
169
+ onCapabilityRemove,
139
170
  onAttachResource,
140
171
  attachLabel,
141
172
  showThinkingTraces,
@@ -147,7 +178,8 @@ export function CloudOsChatInput({
147
178
  maxRows = 6,
148
179
  autoFocus = false,
149
180
  footnote,
150
- }: CloudOsChatInputProps) {
181
+ runtime,
182
+ }: CloudOsChatInputProps & { runtime: AssistantRuntime }) {
151
183
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
152
184
  const attachmentInputRef = useRef<HTMLInputElement>(null);
153
185
  const dragDepthRef = useRef(0);
@@ -184,6 +216,73 @@ export function CloudOsChatInput({
184
216
  if (element) autoResizeTextarea(element, minRows, maxRows);
185
217
  }, [maxRows, minRows, value]);
186
218
 
219
+ const selectCommand = useCallback((
220
+ command: CloudOsComposerCommand,
221
+ clearTrigger: boolean,
222
+ ) => {
223
+ onCommandSelect?.(command);
224
+ if (clearTrigger) {
225
+ runtime.thread.composer.setText("");
226
+ onChange("");
227
+ }
228
+ requestAnimationFrame(() => textareaRef.current?.focus());
229
+ }, [onChange, onCommandSelect, runtime]);
230
+
231
+ const slashCommands = useMemo(
232
+ () => commands
233
+ .filter((command) => (command.prefix ?? "/") === "/")
234
+ .map((command) => ({
235
+ id: command.name,
236
+ label: commandLabel(command),
237
+ description: command.description,
238
+ execute: () => selectCommand(command, true),
239
+ })),
240
+ [commands, selectCommand],
241
+ );
242
+ const skillCommands = useMemo(
243
+ () => commands
244
+ .filter((command) => command.prefix === "$")
245
+ .map((command) => ({
246
+ id: command.name,
247
+ label: commandLabel(command),
248
+ description: command.description,
249
+ execute: () => selectCommand(command, true),
250
+ })),
251
+ [commands, selectCommand],
252
+ );
253
+ const slashAdapter = unstable_useSlashCommandAdapter({
254
+ commands: slashCommands,
255
+ removeOnExecute: true,
256
+ });
257
+ const skillAdapter = unstable_useSlashCommandAdapter({
258
+ commands: skillCommands,
259
+ removeOnExecute: true,
260
+ });
261
+ // assistant-ui only re-runs a TriggerPopover search when the adapter changes.
262
+ const slash = useMemo(() => ({
263
+ ...slashAdapter,
264
+ adapter: { ...slashAdapter.adapter },
265
+ }), [slashAdapter, slashCommands]);
266
+ const skills = useMemo(() => ({
267
+ ...skillAdapter,
268
+ adapter: { ...skillAdapter.adapter },
269
+ }), [skillAdapter, skillCommands]);
270
+ const triggers = unstable_useTriggerPopoverTriggers();
271
+ const triggerAria = unstable_useTriggerPopoverAriaProps();
272
+
273
+ const setTriggerCursor = useCallback(
274
+ (position: number) => {
275
+ for (const trigger of triggers.values()) {
276
+ trigger.resource.setCursorPosition(position);
277
+ }
278
+ },
279
+ [triggers],
280
+ );
281
+
282
+ useEffect(() => {
283
+ setTriggerCursor(textareaRef.current?.selectionStart ?? value.length);
284
+ }, [commands, setTriggerCursor, value]);
285
+
187
286
  const addFiles = useCallback(
188
287
  (files: readonly File[]) => {
189
288
  if (files.length === 0) return;
@@ -219,6 +318,20 @@ export function CloudOsChatInput({
219
318
  return (
220
319
  // isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
221
320
  <div className="cos-chat-input-root relative isolate px-4 py-4">
321
+ <ComposerTriggerPopover
322
+ char="/"
323
+ {...slash}
324
+ aria-label="Commands"
325
+ emptyItemsLabel="No matching commands"
326
+ className="!w-72 !border-kumo-line/70 !bg-kumo-base !text-kumo-default themed-floating-shadow-lg"
327
+ />
328
+ <ComposerTriggerPopover
329
+ char="$"
330
+ {...skills}
331
+ aria-label="Skills"
332
+ emptyItemsLabel="No matching skills"
333
+ className="!w-72 !border-kumo-line/70 !bg-kumo-base !text-kumo-default themed-floating-shadow-lg"
334
+ />
222
335
  <input
223
336
  ref={attachmentInputRef}
224
337
  type="file"
@@ -271,7 +384,22 @@ export function CloudOsChatInput({
271
384
 
272
385
  {banner}
273
386
 
274
- <div className="relative px-4 pb-1 pt-3">
387
+ <div className="relative flex flex-nowrap items-center gap-2 px-4 pb-1 pt-3">
388
+ {capabilities.length > 0 && (
389
+ <div className="flex max-w-[65%] shrink-0 flex-nowrap gap-1.5 overflow-x-auto">
390
+ {capabilities.map((capability) => (
391
+ <CapabilityChip
392
+ key={`${capability.kind}:${capability.name}`}
393
+ kind={capability.kind}
394
+ label={capability.label}
395
+ removable
396
+ onRemove={() =>
397
+ onCapabilityRemove?.(capability.kind, capability.name)
398
+ }
399
+ />
400
+ ))}
401
+ </div>
402
+ )}
275
403
  <textarea
276
404
  ref={(element) => {
277
405
  textareaRef.current = element;
@@ -289,9 +417,17 @@ export function CloudOsChatInput({
289
417
  : (placeholder ?? "Ask a follow-up…")
290
418
  }
291
419
  onChange={(event) => {
292
- onChange(event.target.value);
420
+ const next = event.target.value;
421
+ runtime.thread.composer.setText(next);
422
+ onChange(next);
423
+ setTriggerCursor(event.target.selectionStart ?? next.length);
293
424
  autoResizeTextarea(event.target, minRows, maxRows);
294
425
  }}
426
+ onSelect={(event) =>
427
+ setTriggerCursor(
428
+ event.currentTarget.selectionStart ?? value.length,
429
+ )
430
+ }
295
431
  onPaste={(event) => {
296
432
  const files = Array.from(event.clipboardData.items)
297
433
  .filter((item) => item.kind === "file")
@@ -303,13 +439,21 @@ export function CloudOsChatInput({
303
439
  }
304
440
  }}
305
441
  onKeyDown={(event) => {
306
- // Enter 发送(按住 Shift 换行)
307
- if (event.key === "Enter" && !event.shiftKey) {
442
+ for (const trigger of triggers.values()) {
443
+ if (trigger.resource.handleKeyDown(event)) return;
444
+ }
445
+ // Enter 发送(按住 Shift 换行); IME 组合中不发送
446
+ if (
447
+ event.key === "Enter" &&
448
+ !event.shiftKey &&
449
+ !isComposingKeyEvent(event.nativeEvent)
450
+ ) {
308
451
  event.preventDefault();
309
452
  if (canSend) onSubmit();
310
453
  }
311
454
  }}
312
- className="relative z-[1] w-full resize-none border-none bg-transparent p-0 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default outline-none placeholder:text-kumo-inactive disabled:cursor-not-allowed"
455
+ className="relative z-[1] min-w-0 flex-1 resize-none border-none bg-transparent p-0 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default outline-none placeholder:text-kumo-inactive disabled:cursor-not-allowed"
456
+ {...triggerAria}
313
457
  />
314
458
  </div>
315
459
 
@@ -395,20 +539,15 @@ export function CloudOsChatInput({
395
539
  <>
396
540
  {commands.map((command) => (
397
541
  <DropdownMenu.Item
398
- key={command.name}
399
- onClick={() => {
400
- onChange(command.prompt);
401
- requestAnimationFrame(() =>
402
- textareaRef.current?.focus(),
403
- );
404
- }}
542
+ key={`${command.prefix ?? "/"}${command.name}`}
543
+ onClick={() => selectCommand(command, false)}
405
544
  className="!h-auto rounded-xl !px-2 !py-1.5 text-[12px] leading-4 font-normal tracking-[-0.15px] text-kumo-subtle transition-colors data-highlighted:bg-kumo-tint/70 data-highlighted:text-kumo-default"
406
545
  >
407
546
  <span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
408
547
  <Terminal size={14} />
409
548
  </span>
410
549
  <span className="min-w-0 flex-1 truncate">
411
- /{command.name}
550
+ {commandLabel(command)}
412
551
  </span>
413
552
  </DropdownMenu.Item>
414
553
  ))}
@@ -530,3 +669,24 @@ export function CloudOsChatInput({
530
669
  </div>
531
670
  );
532
671
  }
672
+
673
+ const EMPTY_MESSAGES: readonly ThreadMessage[] = [];
674
+
675
+ export function CloudOsChatInput(props: CloudOsChatInputProps) {
676
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
677
+ messages: EMPTY_MESSAGES,
678
+ onNew: async () => undefined,
679
+ });
680
+
681
+ useEffect(() => {
682
+ runtime.thread.composer.setText(props.value);
683
+ }, [props.value, runtime]);
684
+
685
+ return (
686
+ <AssistantRuntimeProvider runtime={runtime}>
687
+ <ComposerPrimitive.Unstable_TriggerPopoverRoot>
688
+ <CloudOsChatInputContent {...props} runtime={runtime} />
689
+ </ComposerPrimitive.Unstable_TriggerPopoverRoot>
690
+ </AssistantRuntimeProvider>
691
+ );
692
+ }
package/cloud-os/index.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  */
12
12
 
13
13
  export { CloudOsRoot } from "./layout/cloud-os-root";
14
+ export { CapabilityChip } from "./capability-chip";
15
+ export type { CapabilityChipProps } from "./capability-chip";
14
16
  export { CloudOsWorkspaceSplit } from "./layout/cloud-os-workspace-split";
15
17
  export type { CloudOsWorkspaceSplitProps } from "./layout/cloud-os-workspace-split";
16
18
 
@@ -23,6 +25,7 @@ export type {
23
25
  export { CloudOsChatInput } from "./composer/cloud-os-chat-input";
24
26
  export type {
25
27
  CloudOsAttachmentView,
28
+ CloudOsCapabilityView,
26
29
  CloudOsChatInputProps,
27
30
  CloudOsComposerCommand,
28
31
  CloudOsModelOption,
@@ -61,18 +64,24 @@ export {
61
64
  } from "./chat/rich-blocks";
62
65
  export type { ApprovalDecision } from "./chat/rich-blocks";
63
66
 
67
+ export { CloudOsActivityIndicator } from "./chat/activity-indicator";
68
+
64
69
  export {
65
70
  buildCloudOsEntries,
66
71
  cloudOsMetadata,
72
+ deriveTurnActivity,
67
73
  formatClockTime,
68
74
  formatFullTimestamp,
69
75
  messageText,
76
+ requestedCapabilitiesOf,
70
77
  rhythmTopClass,
71
78
  } from "./chat/transcript-model";
72
79
  export type {
73
80
  AssistantBlock,
81
+ CloudOsActivity,
74
82
  CloudOsAttachment,
75
83
  CloudOsEntry,
84
+ CloudOsRequestedCapability,
76
85
  ParallelTool,
77
86
  PlanStep,
78
87
  SubAgentView,