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

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.
Files changed (41) hide show
  1. package/cloud-os/README.md +6 -6
  2. package/cloud-os/assets/followup-arrow.svg +3 -0
  3. package/cloud-os/assets/loading-corner.svg +3 -0
  4. package/cloud-os/assets/loading-mark.svg +16 -0
  5. package/cloud-os/assets/loading-spark.svg +3 -0
  6. package/cloud-os/assets/model-selected.svg +5 -0
  7. package/cloud-os/capability-chip.tsx +35 -0
  8. package/cloud-os/chat/activity-indicator.tsx +29 -0
  9. package/cloud-os/chat/cloud-os-chat-messages.tsx +130 -70
  10. package/cloud-os/chat/markdown-message.tsx +85 -40
  11. package/cloud-os/chat/rich-blocks.tsx +494 -190
  12. package/cloud-os/chat/tool-presentation.ts +67 -6
  13. package/cloud-os/chat/tool-rows.tsx +87 -43
  14. package/cloud-os/chat/transcript-model.ts +215 -23
  15. package/cloud-os/composer/cloud-os-chat-input.tsx +242 -127
  16. package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
  17. package/cloud-os/file-view.tsx +266 -0
  18. package/cloud-os/index.ts +29 -2
  19. package/cloud-os/layout/cloud-os-workspace-split.tsx +107 -24
  20. package/cloud-os/primitives/workshop-controls.tsx +2 -2
  21. package/cloud-os/styles/cloud-os.css +93 -19
  22. package/demo/camel-chat-showcase.tsx +21 -13
  23. package/demo/chat-scenarios.ts +100 -40
  24. package/demo/cloud-os-chat-showcase.tsx +51 -17
  25. package/demo/fixtures.ts +4 -5
  26. package/demo/message-panel-gallery.tsx +16 -2
  27. package/package.json +3 -4
  28. package/src/camel/camel-chat-messages.tsx +82 -77
  29. package/src/camel/camel-prompt-input.tsx +2 -1
  30. package/src/camel/camel-tool-presentation.tsx +19 -15
  31. package/src/camel/camel-turn.ts +1 -17
  32. package/src/chat-summary-panel.tsx +1 -1
  33. package/src/composer/chat-composer.tsx +2 -1
  34. package/src/composer/composer-trigger-popover.tsx +1 -1
  35. package/src/composer/composer.tsx +2 -1
  36. package/src/composer/index.ts +1 -0
  37. package/src/composer/key-rules.ts +12 -0
  38. package/src/contracts.ts +0 -2
  39. package/src/message-panel.tsx +0 -23
  40. package/src/parts/index.tsx +102 -63
  41. package/src/styles/index.css +4 -1
@@ -46,10 +46,10 @@ pnpm dev:cloud-os # http://localhost:9992
46
46
  shadcn 主题正面冲突。
47
47
  4. **作用域化主题**。只有 kumo 命名空间的 token 进 `@theme`;通用 token(字体 /
48
48
  圆角 / 字号 / 缓动)挂在 `.cloud-os-root` 上靠继承生效,宿主主题分毫不动。
49
- 5. **删掉的东西**:capsule(URL → 资源胶囊)、行内 slash picker、ComposerMirror、
50
- GatekeeperModal、Yjs 草稿 / 「接受改动」那一整套。它们都直挂 gadgets 的 capnweb
51
- RPC,UIMessage 协议下没有对应物。slash 命令退成「+」菜单里的一组条目 —— 能力
52
- 保住,但没有边打字边过滤的行内补全。
49
+ 5. **删掉的东西**:capsule(URL → 资源胶囊)、ComposerMirror、GatekeeperModal、
50
+ Yjs 草稿 / 「接受改动」那一整套。它们都直挂 gadgets 的 capnweb RPC,
51
+ UIMessage 协议下没有对应物。命令补全使用 package 内的 assistant-ui
52
+ `ComposerTriggerPopover`,宿主只传命令数据。
53
53
  6. **窄栏适配**。授权卡 / 提问卡 / 定时卡在上游是「图标 + 正文 + 右侧动作」一行到底;
54
54
  聊天栏默认只有 420px,一行放不下会把正文挤成 0 宽,所以改成允许换行。
55
55
 
@@ -57,8 +57,8 @@ pnpm dev:cloud-os # http://localhost:9992
57
57
 
58
58
  - `update_plan` 是**整份快照覆盖**,不是增量事件。整条会话只保留最后一份计划快照
59
59
  (`dropSupersededPlans`),否则同一份计划会被画上四遍。
60
- - `ask_user` 的「已回答」取的是**这条消息之后的第一条用户直答**,不是工具 output ——
61
- 后者往往只是 `{ asked: true }` 之类的回执。
60
+ - `ask_user` 的「已回答」只取该 Tool Part 的 `output.answers[]`;
61
+ 不扫描后续用户消息,问题与答案按数组下标一一对应。
62
62
  - `approval-requested` 的工具是否渲染成原位授权卡,由宿主的 `approvalIdOf` 决定;
63
63
  返回 `undefined` 就退回普通工具行(本仓的持久化审批走输入框上方那条队列)。
64
64
 
@@ -0,0 +1,3 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="11.6667" height="8.33333" viewBox="0 0 11.6667 8.33333" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path id="Vector (Stroke)" d="M3.92575 0.159107C4.14614 -0.0530669 4.50354 -0.0530047 4.72403 0.159107C4.94441 0.371284 4.94435 0.715363 4.72403 0.927642L1.92639 3.62317H11.1021C11.4139 3.62317 11.6667 3.8665 11.6667 4.16667C11.6667 4.46683 11.4139 4.71016 11.1021 4.71016H1.92639L4.72403 7.40569C4.94435 7.61797 4.94441 7.96205 4.72403 8.17423C4.50354 8.38634 4.14614 8.3864 3.92575 8.17423L0.165189 4.55093C-0.0550631 4.33872 -0.0550631 3.99461 0.165189 3.7824L3.92575 0.159107Z" fill="black" fill-opacity="0.5"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="21.8891" height="21.9254" viewBox="0 0 21.8891 21.9254" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path id="Vector" d="M11.6017 6.11877H0.752769C0.337056 6.11877 3.87367e-05 5.78171 3.87367e-05 5.36611L0 0.849598C0 0.433872 0.337004 0.0968018 0.75273 0.0968018H11.5558L15.6704 0.00163975C19.0193 -0.0757287 21.7878 2.59451 21.8311 5.94405L21.8891 10.4301V21.1726C21.8891 21.5883 21.552 21.9254 21.1363 21.9254H16.6199C16.2042 21.9254 15.8671 21.5883 15.8671 21.1726V10.3695L15.7581 7.55744C15.7267 6.74739 15.0595 6.10781 14.249 6.11039L11.6017 6.11877Z" fill="#FF9938"/>
3
+ </svg>
@@ -0,0 +1,16 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="37.5518" height="37.4985" viewBox="0 0 37.5518 37.4985" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g id="Group" filter="url(#filter0_f_0_4)">
3
+ <path id="Vector" d="M31.8173 8.87482C32.1113 9.16882 32.1113 9.64541 31.8173 9.93941L24.16 17.5967H35.009C35.4247 17.5967 35.7616 17.9338 35.7616 18.3494L35.7618 22.8659C35.7618 23.2816 35.4247 23.6185 35.009 23.6185H24.2059L31.8173 31.2299C32.1113 31.5239 32.1113 32.0005 31.8173 32.2945L28.6236 35.488C28.3298 35.782 27.8532 35.782 27.5592 35.488L22.4352 30.3641C22.2939 30.2227 22.2146 30.0309 22.2148 29.831L22.216 28.8867L22.1335 21.8329C22.0894 18.0581 18.9682 15.0475 15.1943 15.1395L8.62867 15.2995L7.68452 15.3007C7.48456 15.3009 7.29271 15.2216 7.15132 15.0802L2.0105 9.93941C1.71653 9.64541 1.71653 9.16882 2.0105 8.87482L5.20408 5.68127C5.49804 5.38731 5.97465 5.38731 6.26861 5.68127L13.8727 13.2853V2.54284C13.8727 2.12712 14.2096 1.79009 14.6254 1.79008L19.1417 1.79002C19.5575 1.79002 19.8945 2.12707 19.8945 2.54279V13.3458L27.5592 5.68127C27.8532 5.38731 28.3298 5.38731 28.6236 5.68127L31.8173 8.87482Z" fill="url(#paint0_linear_0_4)"/>
4
+ </g>
5
+ <defs>
6
+ <filter id="filter0_f_0_4" x="-1.86265e-09" y="0" width="37.5518" height="37.4985" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
7
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
8
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
9
+ <feGaussianBlur stdDeviation="0.895012" result="effect1_foregroundBlur_0_4"/>
10
+ </filter>
11
+ <linearGradient id="paint0_linear_0_4" x1="30.2205" y1="7.27806" x2="22.5471" y2="31.3091" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#FFD078"/>
13
+ <stop offset="1" stop-color="#FF9938"/>
14
+ </linearGradient>
15
+ </defs>
16
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="9.81853" height="9.8186" viewBox="0 0 9.81853 9.8186" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path id="Vector" d="M4.67857 0.881903C5.85442 -0.293968 7.76085 -0.293968 8.93672 0.881903C10.1125 2.05777 10.1125 3.96426 8.93672 5.14L4.25811 9.8186L0 5.5605L4.67857 0.881903Z" fill="#FF9938"/>
3
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g id="Icon Variants">
3
+ <path id="Subtract" d="M8.00033 1.33333C11.6821 1.33351 14.6663 4.31854 14.6663 8.00033C14.6662 11.682 11.682 14.6662 8.00033 14.6663C4.31854 14.6663 1.33351 11.6821 1.33333 8.00033C1.33333 4.31843 4.31843 1.33333 8.00033 1.33333ZM11.138 6.19466C10.8776 5.93464 10.4549 5.93442 10.1947 6.19466L7.33236 9.05697L6.13802 7.86165C5.87764 7.60163 5.4549 7.60141 5.19466 7.86165C4.93469 8.1218 4.93502 8.54369 5.19466 8.80404L6.86165 10.471C7.12195 10.7313 7.54367 10.7312 7.80404 10.471L11.138 7.13802C11.3984 6.87767 11.3984 6.45501 11.138 6.19466Z" fill="#FF9938"/>
4
+ </g>
5
+ </svg>
@@ -0,0 +1,35 @@
1
+ import { Diamond, X } from "@phosphor-icons/react";
2
+
3
+ export interface CapabilityChipProps {
4
+ kind: "skill" | "plan";
5
+ label: string;
6
+ removable?: boolean;
7
+ onRemove?: () => void;
8
+ }
9
+
10
+ export function CapabilityChip({
11
+ kind,
12
+ label,
13
+ removable = false,
14
+ onRemove,
15
+ }: CapabilityChipProps) {
16
+ return (
17
+ <span
18
+ data-capability-kind={kind}
19
+ className="inline-flex max-w-full items-center gap-1 rounded-full border border-kumo-line bg-kumo-elevated px-2 py-1 text-[12px] leading-4 text-kumo-subtle"
20
+ >
21
+ <Diamond size={11} weight="duotone" className="shrink-0 text-kumo-brand" />
22
+ <span className="truncate">{label}</span>
23
+ {removable && onRemove && (
24
+ <button
25
+ type="button"
26
+ onClick={onRemove}
27
+ aria-label={`Remove ${label}`}
28
+ className="-mr-1 grid size-5 shrink-0 cursor-pointer place-items-center rounded-full text-kumo-inactive hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
29
+ >
30
+ <X size={10} weight="bold" />
31
+ </button>
32
+ )}
33
+ </span>
34
+ );
35
+ }
@@ -0,0 +1,29 @@
1
+ import { ThinkingOrb } from "thinking-orbs";
2
+ import { useCloudOsMode } from "../internal/theme-context";
3
+ import type { CloudOsActivity } from "./transcript-model";
4
+
5
+ /**
6
+ * 回合存活期间持续显示的全局活动指示。
7
+ *
8
+ * 用 thinking-orbs 的 canvas 球而不是一行 shimmer 文字:动画本身就是「还活着」
9
+ * 的信号,文案只负责说清在干什么(`deriveTurnActivity` 给出的九种之一)。
10
+ *
11
+ * 和 `src/status.tsx` 的 `ThinkingIndicator` 是同一个上游球、同一套 role/aria
12
+ * 形状,但**不共用实现** —— cloud-os 不 import `../src`(见 cloud-os/README.md,
13
+ * 由 check-chat-presentation-boundary.mjs 守着),文案也走 cloud-os 自己的
14
+ * 工具种类表,不是 src 那套工具名正则。
15
+ */
16
+ export function CloudOsActivityIndicator({ state, label }: CloudOsActivity) {
17
+ // 必须显式传 theme,不能用它的 `auto`:auto 先扫祖先的 data-theme / .dark / .light,
18
+ // 而本仓的浅色**是「没有 .dark」**,扫不到任何标记 → 它回落到
19
+ // prefers-color-scheme,也就是**操作系统**的深浅。系统深色 + 应用浅色时,
20
+ // 球会在浅底上画浅墨,几乎看不见。cloud-os 子树的深浅事实源是这个 context
21
+ // (CloudOsRoot 从宿主的 useThemeMode() 拿到),直接用它。
22
+ const mode = useCloudOsMode();
23
+ return (
24
+ <div className="cos-activity" role="status" aria-label={label}>
25
+ <ThinkingOrb state={state} size={20} theme={mode} aria-hidden="true" />
26
+ <span>{label}</span>
27
+ </div>
28
+ );
29
+ }
@@ -5,15 +5,16 @@ import {
5
5
  CaretRight,
6
6
  Check,
7
7
  Copy,
8
- File as FileIcon,
9
8
  Terminal,
10
9
  } from "@phosphor-icons/react";
11
10
  import {
11
+ Fragment,
12
12
  useCallback,
13
13
  useEffect,
14
14
  useMemo,
15
15
  useRef,
16
16
  useState,
17
+ type ReactNode,
17
18
  } from "react";
18
19
  import { Tooltip } from "../primitives/tooltip";
19
20
  import { WorkshopIconButton } from "../primitives/workshop-controls";
@@ -33,8 +34,15 @@ import {
33
34
  type ApprovalDecision,
34
35
  } from "./rich-blocks";
35
36
  import { ThinkingTraceRow, ToolGroupRow } from "./tool-rows";
37
+ import { CloudOsActivityIndicator } from "./activity-indicator";
38
+ import { CapabilityChip } from "../capability-chip";
39
+ import {
40
+ renderFileView,
41
+ type FileRenderer,
42
+ } from "../file-view";
36
43
  import {
37
44
  buildCloudOsEntries,
45
+ deriveTurnActivity,
38
46
  formatClockTime,
39
47
  formatFullTimestamp,
40
48
  rhythmTopClass,
@@ -46,9 +54,11 @@ export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
46
54
  export interface CloudOsChatMessagesProps {
47
55
  messages: readonly UIMessage[];
48
56
  status: CloudOsChatStatus;
49
- isHydrating?: boolean;
57
+ /** 权威 Turn 生命周期;提供时替代本地 status 推断。 */
58
+ turnActive?: boolean;
50
59
  isRecovering?: boolean;
51
60
  recoveryStatusLabel?: string;
61
+ awaitingApproval?: boolean;
52
62
  /** 有 pending steer 时,活动占位说明「正在等当前步骤收尾」。 */
53
63
  hasPendingSteer?: boolean;
54
64
  /** 首帧就带着待发消息:此时不显示空会话欢迎语。 */
@@ -58,6 +68,20 @@ export interface CloudOsChatMessagesProps {
58
68
  /** 把某个 part 映射成待授权 id;返回 undefined 表示不在此处渲染授权。 */
59
69
  approvalIdOf?: (part: unknown) => string | undefined;
60
70
  approvalsDisabled?: boolean;
71
+ /**
72
+ * 用宿主自己的卡片渲染某条待授权。返回 undefined 就退回内置的通用授权行。
73
+ *
74
+ * 存在的理由:授权行只认得工具名和入参,说不出「这次要花多少钱、买的是什么」。
75
+ * 需要在决定之前把这些摆到人面前的宿主,得自己画那张卡。
76
+ */
77
+ renderApproval?: (approval: {
78
+ approvalId: string;
79
+ toolName: string;
80
+ input: Record<string, unknown>;
81
+ disabled: boolean;
82
+ }) => ReactNode;
83
+ /** 接管消息里的文件视图;未提供时使用包内 FileView。 */
84
+ renderFile?: FileRenderer;
61
85
  /** 把消息里的 `sandbox:/workspace/...` 之类地址翻译成可访问 URL。 */
62
86
  resolveUrl?: CloudOsUrlResolver;
63
87
  /** 消息区宽度是否收敛到 920px 居中(GadgetEditor 里叫 constrainChatWidth)。 */
@@ -66,7 +90,11 @@ export interface CloudOsChatMessagesProps {
66
90
  onRetry?: () => void;
67
91
  onApprove?: (approvalId: string, decision: ApprovalDecision) => void;
68
92
  onSuggestion?: (text: string) => void;
69
- onAnswer?: (payload: unknown) => void;
93
+ /**
94
+ * 把交互卡片上的操作回写成那次工具调用的结果(缺省 → 卡片不可操作)。
95
+ * 卡片不再知道「答案怎么变成消息」—— 它只认自己的 toolCallId。
96
+ */
97
+ onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
70
98
  /** 点击定时任务卡片:宿主决定跳到哪(缺省 → 卡片不可点)。 */
71
99
  onOpenSchedule?: (scheduleId: string) => void;
72
100
  onCopy?: (text: string) => void;
@@ -98,43 +126,54 @@ function UserBubble({
98
126
  entry,
99
127
  onCopy,
100
128
  copied,
129
+ renderFile,
101
130
  resolveUrl,
102
131
  }: {
103
132
  entry: Extract<CloudOsEntry, { type: "user" }>;
104
133
  onCopy: (text: string) => void;
105
134
  copied: boolean;
135
+ renderFile?: FileRenderer;
106
136
  resolveUrl?: CloudOsUrlResolver;
107
137
  }) {
108
138
  return (
109
139
  <div className="group/message relative flex flex-col items-end">
140
+ {entry.capabilities.length > 0 && (
141
+ <div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap justify-end gap-1.5">
142
+ {entry.capabilities.map((capability) => (
143
+ <CapabilityChip
144
+ key={`${capability.kind}:${capability.name}`}
145
+ kind={capability.kind}
146
+ label={capability.label}
147
+ />
148
+ ))}
149
+ </div>
150
+ )}
110
151
  {entry.attachments.length > 0 && (
111
- <div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap justify-end gap-2">
112
- {entry.attachments.map((attachment, index) =>
113
- attachment.mediaType?.startsWith("image/") ? (
114
- <img
115
- key={`${attachment.url}:${index}`}
116
- src={attachment.url}
117
- alt={attachment.filename ?? "Attachment"}
118
- className="themed-thumbnail-shadow max-h-52 max-w-64 rounded-xl border border-kumo-line object-cover"
119
- />
152
+ <div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap items-start justify-end gap-2">
153
+ {entry.attachments.map((attachment, index) => {
154
+ const view = renderFileView(renderFile, {
155
+ file: {
156
+ ...attachment,
157
+ url: resolveUrl?.(attachment.url) ?? attachment.url,
158
+ },
159
+ placement: "user-message",
160
+ });
161
+ return attachment.mediaType?.startsWith("image/") ? (
162
+ <Fragment key={`${attachment.url}:${index}`}>{view}</Fragment>
120
163
  ) : (
121
164
  <span
122
165
  key={`${attachment.url}:${index}`}
123
- className="inline-flex items-center gap-1.5 rounded-lg border border-kumo-line bg-kumo-elevated px-2.5 py-1.5 text-[12px] leading-4 text-kumo-subtle"
166
+ className="flex basis-full justify-end"
124
167
  >
125
- <FileIcon size={13} className="text-kumo-inactive" />
126
- {attachment.filename ?? "Attachment"}
168
+ {view}
127
169
  </span>
128
- ),
129
- )}
170
+ );
171
+ })}
130
172
  </div>
131
173
  )}
132
174
  {entry.text && (
133
- <div className="themed-user-bubble-shadow cos-markdown w-fit max-w-[min(680px,78%)] rounded-[24px] rounded-br-lg border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default">
134
- {/* pre-wrap 让用户消息里的单个换行渲染成硬换行 */}
135
- <div className="whitespace-pre-wrap">
136
- <MarkdownMessage message={entry.text} resolveUrl={resolveUrl} />
137
- </div>
175
+ <div className="themed-user-bubble-shadow cos-markdown cos-user-markdown w-fit max-w-[min(680px,78%)] rounded-[12px] border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default">
176
+ <MarkdownMessage message={entry.text} resolveUrl={resolveUrl} />
138
177
  </div>
139
178
  )}
140
179
  <div className="mt-0.5 flex items-center justify-end gap-2 pr-1 text-[11px] leading-4 text-kumo-inactive opacity-0 transition-opacity duration-150 ease-out group-hover/message:opacity-100 group-focus-within/message:opacity-100">
@@ -164,22 +203,25 @@ function UserBubble({
164
203
  export function CloudOsChatMessages({
165
204
  messages,
166
205
  status,
167
- isHydrating = false,
206
+ turnActive,
168
207
  isRecovering = false,
169
208
  recoveryStatusLabel,
209
+ awaitingApproval = false,
170
210
  hasPendingSteer = false,
171
211
  startedWithPending = false,
172
212
  error,
173
213
  showThinkingTraces = true,
174
214
  approvalIdOf,
175
215
  approvalsDisabled = false,
216
+ renderApproval,
217
+ renderFile,
176
218
  resolveUrl,
177
219
  constrainWidth = true,
178
220
  emptyTitle = "What are we working on?",
179
221
  onRetry,
180
222
  onApprove,
181
223
  onSuggestion,
182
- onAnswer,
224
+ onRespond,
183
225
  onOpenSchedule,
184
226
  onCopy,
185
227
  }: CloudOsChatMessagesProps) {
@@ -193,7 +235,8 @@ export function CloudOsChatMessages({
193
235
  ReadonlySet<string>
194
236
  >(() => new Set());
195
237
  const { copied, copy } = useCopyAction();
196
- const isActive = status === "submitted" || status === "streaming";
238
+ const isActive =
239
+ turnActive ?? (status === "submitted" || status === "streaming");
197
240
 
198
241
  const entries = useMemo(
199
242
  () =>
@@ -246,14 +289,13 @@ export function CloudOsChatMessages({
246
289
  element.scrollTo({ top: element.scrollHeight });
247
290
  }, [isRecovering, messages, status, showScrollButton]);
248
291
 
249
- // 只有在「本回合还没吐出任何可见内容」时才显示 Thinking 占位;一旦有真实输出,
250
- // 输出自己会说话(照搬原文件的 showThinking 判据)。
251
- const lastEntry = entries.at(-1) ?? null;
252
- const awaitingFirstOutput =
253
- isActive &&
254
- (lastEntry === null ||
255
- lastEntry.type === "user" ||
256
- lastEntry.type === "slashCommand");
292
+ const activity = deriveTurnActivity(entries, {
293
+ isActive,
294
+ isRecovering,
295
+ recoveryStatusLabel,
296
+ awaitingApproval,
297
+ hasPendingSteer,
298
+ });
257
299
  const copyText = (text: string) => {
258
300
  copy(text);
259
301
  onCopy?.(text);
@@ -264,7 +306,7 @@ export function CloudOsChatMessages({
264
306
  className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-kumo-base"
265
307
  data-cloud-os-transcript=""
266
308
  data-status={status}
267
- aria-busy={isActive || isHydrating}
309
+ aria-busy={isActive || isRecovering}
268
310
  >
269
311
  <div
270
312
  ref={scrollRef}
@@ -279,7 +321,7 @@ export function CloudOsChatMessages({
279
321
  constrainWidth ? "mx-auto w-full max-w-[920px]" : ""
280
322
  }`}
281
323
  >
282
- {entries.length === 0 && !startedWithPending && !isHydrating && (
324
+ {entries.length === 0 && !startedWithPending && (
283
325
  <div className="flex min-h-56 items-center justify-center text-center text-[24px] leading-8 font-semibold tracking-[-0.4px] text-kumo-default">
284
326
  {emptyTitle}
285
327
  </div>
@@ -295,6 +337,7 @@ export function CloudOsChatMessages({
295
337
  entry={entry}
296
338
  onCopy={copyText}
297
339
  copied={copied === entry.text}
340
+ renderFile={renderFile}
298
341
  resolveUrl={resolveUrl}
299
342
  />
300
343
  </div>
@@ -304,7 +347,7 @@ export function CloudOsChatMessages({
304
347
  if (entry.type === "slashCommand") {
305
348
  return (
306
349
  <div key={entry.key} className={`${topClass} flex flex-col items-end`}>
307
- <div className="themed-user-bubble-shadow w-fit max-w-[min(680px,78%)] rounded-[24px] rounded-br-lg border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-brand">
350
+ <div className="themed-user-bubble-shadow w-fit max-w-[min(680px,78%)] rounded-[12px] border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-brand">
308
351
  <span className="whitespace-pre-wrap font-mono">{entry.text}</span>
309
352
  </div>
310
353
  </div>
@@ -395,9 +438,17 @@ export function CloudOsChatMessages({
395
438
  switch (block.kind) {
396
439
  case "reasoning":
397
440
  return (
398
- <ThinkingTraceRow key={block.key} reasoning={block.text} />
441
+ <ThinkingTraceRow
442
+ key={block.key}
443
+ reasoning={block.text}
444
+ running={block.running}
445
+ />
399
446
  );
400
- case "text":
447
+ case "text": {
448
+ const streaming =
449
+ isActive &&
450
+ entryIndex === entries.length - 1 &&
451
+ block === entry.blocks.at(-1);
401
452
  return (
402
453
  <div
403
454
  key={block.key}
@@ -406,9 +457,11 @@ export function CloudOsChatMessages({
406
457
  <MarkdownMessage
407
458
  message={block.text}
408
459
  resolveUrl={resolveUrl}
460
+ streaming={streaming}
409
461
  />
410
462
  </div>
411
463
  );
464
+ }
412
465
  case "toolGroup":
413
466
  return (
414
467
  <ToolGroupRow
@@ -432,8 +485,7 @@ export function CloudOsChatMessages({
432
485
  <AskUserBlock
433
486
  key={block.key}
434
487
  call={block.call}
435
- answeredText={block.answeredText}
436
- onAnswer={(payload) => onAnswer?.(payload)}
488
+ onRespond={onRespond}
437
489
  />
438
490
  );
439
491
  case "suggestions":
@@ -452,7 +504,18 @@ export function CloudOsChatMessages({
452
504
  onOpen={onOpenSchedule}
453
505
  />
454
506
  );
455
- case "approval":
507
+ case "approval": {
508
+ const custom = renderApproval?.({
509
+ approvalId: block.approvalId,
510
+ toolName: block.call.toolName,
511
+ input: block.call.input,
512
+ disabled: approvalsDisabled,
513
+ });
514
+ if (custom !== undefined && custom !== null) {
515
+ return (
516
+ <Fragment key={block.key}>{custom}</Fragment>
517
+ );
518
+ }
456
519
  return (
457
520
  <ApprovalBlock
458
521
  key={block.key}
@@ -462,6 +525,7 @@ export function CloudOsChatMessages({
462
525
  onApprove={(id, decision) => onApprove?.(id, decision)}
463
526
  />
464
527
  );
528
+ }
465
529
  case "parallel":
466
530
  return (
467
531
  <ParallelBlock
@@ -476,13 +540,29 @@ export function CloudOsChatMessages({
476
540
  );
477
541
  case "image":
478
542
  return (
479
- <figure key={block.key} className="my-1">
480
- <img
481
- src={resolveUrl?.(block.src) ?? block.src}
482
- alt={block.alt}
483
- className="max-h-[28rem] max-w-full rounded-xl border border-kumo-line object-contain"
484
- />
485
- </figure>
543
+ <Fragment key={block.key}>
544
+ {renderFileView(renderFile, {
545
+ file: {
546
+ url: resolveUrl?.(block.src) ?? block.src,
547
+ filename: block.alt,
548
+ mediaType: block.mediaType ?? "image/*",
549
+ },
550
+ placement: "assistant-message",
551
+ })}
552
+ </Fragment>
553
+ );
554
+ case "file":
555
+ return (
556
+ <Fragment key={block.key}>
557
+ {renderFileView(renderFile, {
558
+ file: {
559
+ ...block.file,
560
+ url:
561
+ resolveUrl?.(block.file.url) ?? block.file.url,
562
+ },
563
+ placement: "assistant-message",
564
+ })}
565
+ </Fragment>
486
566
  );
487
567
  case "error":
488
568
  return (
@@ -543,19 +623,9 @@ export function CloudOsChatMessages({
543
623
  );
544
624
  })}
545
625
 
546
- {isRecovering && (
547
- <div className="mt-4 flex max-w-[860px] items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px]">
548
- <span className="cos-thinking-shimmer">
549
- {recoveryStatusLabel ?? "Reconnecting…"}
550
- </span>
551
- </div>
552
- )}
553
-
554
- {awaitingFirstOutput && (
626
+ {activity && (
555
627
  <div className="mt-5 inline-flex px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px]">
556
- <span className="cos-thinking-shimmer">
557
- {hasPendingSteer ? "Waiting for the current step to finish…" : "Thinking"}
558
- </span>
628
+ <CloudOsActivityIndicator {...activity} />
559
629
  </div>
560
630
  )}
561
631
 
@@ -591,16 +661,6 @@ export function CloudOsChatMessages({
591
661
  <ArrowDown size={15} />
592
662
  </WorkshopIconButton>
593
663
 
594
- {isHydrating && (
595
- <div className="absolute inset-0 z-30 grid place-items-center bg-kumo-base/90">
596
- <div className="flex flex-col items-center gap-3">
597
- <div className="h-6 w-6 animate-spin rounded-full border-2 border-kumo-brand border-t-transparent" />
598
- <p className="m-0 text-[13px] leading-[18px] text-kumo-subtle">
599
- Loading conversation…
600
- </p>
601
- </div>
602
- </div>
603
- )}
604
664
  </div>
605
665
  );
606
666
  }