@springbrand/message-panel 0.1.3-alpha.0 → 0.1.3-alpha.10
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 +71 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +660 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +787 -0
- package/cloud-os/chat/tool-presentation.ts +586 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +780 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +686 -0
- package/cloud-os/file-view.tsx +258 -0
- package/cloud-os/index.ts +123 -0
- package/cloud-os/internal/cn.ts +6 -0
- package/cloud-os/internal/theme-context.tsx +16 -0
- package/cloud-os/layout/cloud-os-root.tsx +34 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +228 -0
- package/cloud-os/primitives/dropdown-menu.tsx +82 -0
- package/cloud-os/primitives/tooltip.tsx +68 -0
- package/cloud-os/primitives/workshop-controls.tsx +114 -0
- package/cloud-os/styles/cloud-os.css +553 -0
- package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
- package/demo/camel-chat-showcase.tsx +13 -917
- package/demo/chat-scenarios.ts +926 -0
- package/demo/cloud-os-chat-showcase.tsx +481 -0
- package/demo/index.ts +2 -0
- package/package.json +17 -5
- package/src/camel/camel-chat-messages.tsx +55 -20
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/camel/camel-turn.ts +21 -3
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -1
- package/src/composer/index.ts +1 -0
- package/src/composer/key-rules.ts +12 -0
- package/src/message.tsx +0 -8
- package/src/parts/plan.ts +3 -2
- package/src/styles/index.css +4 -1
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import type { OrbState } from "thinking-orbs";
|
|
3
|
+
import {
|
|
4
|
+
askUserOutcome,
|
|
5
|
+
buildToolCallGroups,
|
|
6
|
+
toCloudOsToolCall,
|
|
7
|
+
type CloudOsToolCall,
|
|
8
|
+
type CloudOsToolKind,
|
|
9
|
+
type ToolCallGroup,
|
|
10
|
+
} from "./tool-presentation";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* UIMessage → cloud-os 展示条目。
|
|
14
|
+
*
|
|
15
|
+
* cloudflare-os-main 的 `buildChatDisplayEntries` 吃的是 gadgets 自己的
|
|
16
|
+
* `AiChatMessage`(离散消息类型 + 独立的 toolCalls 数组)。UIMessage 的事实是
|
|
17
|
+
* **一条 assistant 消息里一个有序 parts 数组**,所以这里把「消息类型分派」换成
|
|
18
|
+
* 「按 parts 顺序切块」:连续的工具 part 攒成一个 ToolCallGroup,遇到非工具
|
|
19
|
+
* part 就 flush。原文件的行间距规则(rhythmTopClass)照搬。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
type UnknownRecord = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
25
|
+
authorDisplayName?: string;
|
|
26
|
+
completedAt?: number;
|
|
27
|
+
createdAt?: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
interruptedByUser?: boolean;
|
|
30
|
+
turnDurationMs?: number;
|
|
31
|
+
turnStartedAt?: number;
|
|
32
|
+
turnStatus?: string;
|
|
33
|
+
requestedCapabilities?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CloudOsRequestedCapability {
|
|
37
|
+
kind: "skill" | "plan";
|
|
38
|
+
name: string;
|
|
39
|
+
label: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CloudOsAttachment {
|
|
43
|
+
url: string;
|
|
44
|
+
filename?: string;
|
|
45
|
+
mediaType?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PlanStep {
|
|
49
|
+
text: string;
|
|
50
|
+
status: "pending" | "in_progress" | "done";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AssistantBlock =
|
|
54
|
+
| { kind: "reasoning"; key: string; text: string }
|
|
55
|
+
| { kind: "text"; key: string; text: string }
|
|
56
|
+
| { kind: "toolGroup"; key: string; group: ToolCallGroup }
|
|
57
|
+
| { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
|
|
58
|
+
// 答案就在 call.output 里(`ask_user` 是 client-settled tool,用户点选后
|
|
59
|
+
// 经 respondToolInteraction 回写成这次调用的结果)。曾经靠扫下一条用户消息
|
|
60
|
+
// 推断,那是有损且会误认无关消息的,已彻底删除。
|
|
61
|
+
// `pending` 是这张卡还在等人 —— 卡片据此决定能不能点,活动指示器据此决定
|
|
62
|
+
// 该不该出现,同一条判据(askUserOutcome)算一次。
|
|
63
|
+
| { kind: "askUser"; key: string; call: CloudOsToolCall; pending: boolean }
|
|
64
|
+
| { kind: "suggestions"; key: string; items: string[] }
|
|
65
|
+
| { kind: "schedule"; key: string; call: CloudOsToolCall }
|
|
66
|
+
| { kind: "approval"; key: string; call: CloudOsToolCall; approvalId: string }
|
|
67
|
+
| { kind: "parallel"; key: string; label: string; tools: ParallelTool[] }
|
|
68
|
+
| { kind: "subagents"; key: string; agents: SubAgentView[] }
|
|
69
|
+
| {
|
|
70
|
+
kind: "image";
|
|
71
|
+
key: string;
|
|
72
|
+
src: string;
|
|
73
|
+
alt: string;
|
|
74
|
+
mediaType?: string;
|
|
75
|
+
}
|
|
76
|
+
| { kind: "file"; key: string; file: CloudOsAttachment }
|
|
77
|
+
| { kind: "error"; key: string; title: string; body?: string };
|
|
78
|
+
|
|
79
|
+
export interface ParallelTool {
|
|
80
|
+
label: string;
|
|
81
|
+
status: "pending" | "running" | "done" | "failed";
|
|
82
|
+
meta?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SubAgentView {
|
|
86
|
+
name: string;
|
|
87
|
+
summary?: string;
|
|
88
|
+
status: "pending" | "running" | "done" | "failed";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type CloudOsEntry =
|
|
92
|
+
| {
|
|
93
|
+
type: "user";
|
|
94
|
+
key: string;
|
|
95
|
+
messageId: string;
|
|
96
|
+
text: string;
|
|
97
|
+
attachments: CloudOsAttachment[];
|
|
98
|
+
capabilities: CloudOsRequestedCapability[];
|
|
99
|
+
authorName?: string;
|
|
100
|
+
timestamp?: number;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
type: "slashCommand";
|
|
104
|
+
key: string;
|
|
105
|
+
messageId: string;
|
|
106
|
+
text: string;
|
|
107
|
+
timestamp?: number;
|
|
108
|
+
}
|
|
109
|
+
| { type: "localCommandOutput"; key: string; messageId: string; text: string }
|
|
110
|
+
| { type: "notice"; key: string; messageId: string; text: string }
|
|
111
|
+
| { type: "compactionBoundary"; key: string; messageId: string; summary: string }
|
|
112
|
+
| {
|
|
113
|
+
type: "assistant";
|
|
114
|
+
key: string;
|
|
115
|
+
messageId: string;
|
|
116
|
+
blocks: AssistantBlock[];
|
|
117
|
+
copyText: string;
|
|
118
|
+
timestamp?: number;
|
|
119
|
+
terminal?: { kind: "interrupted" | "error"; title: string; body?: string };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ── 小工具 ──────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function recordOf(value: unknown): UnknownRecord {
|
|
125
|
+
return value != null && typeof value === "object"
|
|
126
|
+
? (value as UnknownRecord)
|
|
127
|
+
: {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dataOf(part: unknown): UnknownRecord {
|
|
131
|
+
const record = recordOf(part);
|
|
132
|
+
return recordOf(record.data ?? record.input ?? record);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function nonNegativeNumber(value: unknown): number | undefined {
|
|
136
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
137
|
+
? value
|
|
138
|
+
: undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
|
|
142
|
+
return recordOf(message.metadata) as CloudOsMessageMetadata;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function requestedCapabilitiesOf(
|
|
146
|
+
metadata: CloudOsMessageMetadata,
|
|
147
|
+
): CloudOsRequestedCapability[] {
|
|
148
|
+
if (!Array.isArray(metadata.requestedCapabilities)) return [];
|
|
149
|
+
const seen = new Set<string>();
|
|
150
|
+
return metadata.requestedCapabilities.flatMap((value) => {
|
|
151
|
+
const capability = recordOf(value);
|
|
152
|
+
if (
|
|
153
|
+
(capability.kind !== "skill" && capability.kind !== "plan") ||
|
|
154
|
+
typeof capability.name !== "string" ||
|
|
155
|
+
!capability.name.trim() ||
|
|
156
|
+
typeof capability.label !== "string" ||
|
|
157
|
+
!capability.label.trim()
|
|
158
|
+
) return [];
|
|
159
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
160
|
+
if (seen.has(key)) return [];
|
|
161
|
+
seen.add(key);
|
|
162
|
+
return [{
|
|
163
|
+
kind: capability.kind,
|
|
164
|
+
name: capability.name,
|
|
165
|
+
label: capability.label,
|
|
166
|
+
}];
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function messageText(message: UIMessage): string {
|
|
171
|
+
return message.parts
|
|
172
|
+
.filter(
|
|
173
|
+
(part): part is { type: "text"; text: string } =>
|
|
174
|
+
part.type === "text" && typeof part.text === "string" &&
|
|
175
|
+
part.text.trim().length > 0,
|
|
176
|
+
)
|
|
177
|
+
.map((part) => part.text)
|
|
178
|
+
.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatClockTime(timestamp: number): string {
|
|
182
|
+
return new Date(timestamp).toLocaleTimeString([], {
|
|
183
|
+
hour: "2-digit",
|
|
184
|
+
minute: "2-digit",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function formatFullTimestamp(timestamp: number): string {
|
|
189
|
+
return new Date(timestamp).toLocaleString([], {
|
|
190
|
+
dateStyle: "medium",
|
|
191
|
+
timeStyle: "short",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function toolNameOf(part: unknown): string | null {
|
|
196
|
+
const type = String(recordOf(part).type ?? "");
|
|
197
|
+
if (type === "dynamic-tool") {
|
|
198
|
+
const name = recordOf(part).toolName;
|
|
199
|
+
return typeof name === "string" ? name : "dynamic";
|
|
200
|
+
}
|
|
201
|
+
return type.startsWith("tool-") ? type.slice(5) : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dataKindOf(part: unknown): string | null {
|
|
205
|
+
const type = String(recordOf(part).type ?? "");
|
|
206
|
+
return type.startsWith("data-") ? type.slice(5).replaceAll("-", "_") : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function planStepsOf(input: UnknownRecord): PlanStep[] {
|
|
210
|
+
const raw = Array.isArray(input.steps)
|
|
211
|
+
? input.steps
|
|
212
|
+
: Array.isArray(input.todos)
|
|
213
|
+
? input.todos
|
|
214
|
+
: [];
|
|
215
|
+
return raw.map((item) => {
|
|
216
|
+
const step = recordOf(item);
|
|
217
|
+
const status = String(step.status ?? "pending");
|
|
218
|
+
return {
|
|
219
|
+
text: String(step.text ?? step.content ?? step.activeForm ?? "Untitled step"),
|
|
220
|
+
status:
|
|
221
|
+
status === "in_progress"
|
|
222
|
+
? "in_progress"
|
|
223
|
+
: status === "done" || status === "completed"
|
|
224
|
+
? "done"
|
|
225
|
+
: "pending",
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function stringList(value: unknown): string[] {
|
|
231
|
+
return Array.isArray(value)
|
|
232
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
233
|
+
: [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeStatus(value: unknown): ParallelTool["status"] {
|
|
237
|
+
const status = String(value ?? "");
|
|
238
|
+
if (status === "running" || status === "done" || status === "failed") return status;
|
|
239
|
+
return "pending";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── 独立消息(非 assistant 回合)判定 ────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
function standaloneEntry(
|
|
245
|
+
message: UIMessage,
|
|
246
|
+
index: number,
|
|
247
|
+
): CloudOsEntry | null {
|
|
248
|
+
const metadata = cloudOsMetadata(message);
|
|
249
|
+
for (const part of message.parts) {
|
|
250
|
+
const kind = dataKindOf(part);
|
|
251
|
+
if (kind === null) continue;
|
|
252
|
+
const data = dataOf(part);
|
|
253
|
+
const text = String(data.text ?? data.command ?? data.output ?? "").trim();
|
|
254
|
+
if (kind === "slash_command") {
|
|
255
|
+
return {
|
|
256
|
+
type: "slashCommand",
|
|
257
|
+
key: `${message.id}-${index}`,
|
|
258
|
+
messageId: message.id,
|
|
259
|
+
text,
|
|
260
|
+
timestamp: nonNegativeNumber(metadata.createdAt),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (kind === "local_command_output") {
|
|
264
|
+
return {
|
|
265
|
+
type: "localCommandOutput",
|
|
266
|
+
key: `${message.id}-${index}`,
|
|
267
|
+
messageId: message.id,
|
|
268
|
+
text,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (kind === "compact_summary") {
|
|
272
|
+
return {
|
|
273
|
+
type: "compactionBoundary",
|
|
274
|
+
key: `${message.id}-${index}`,
|
|
275
|
+
messageId: message.id,
|
|
276
|
+
summary: text,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (kind === "notice") {
|
|
280
|
+
return {
|
|
281
|
+
type: "notice",
|
|
282
|
+
key: `${message.id}-${index}`,
|
|
283
|
+
messageId: message.id,
|
|
284
|
+
text,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── assistant 回合切块 ──────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function assistantBlocks(
|
|
294
|
+
message: UIMessage,
|
|
295
|
+
isActive: boolean,
|
|
296
|
+
showThinkingTraces: boolean,
|
|
297
|
+
approvalIdOf: (part: unknown) => string | undefined,
|
|
298
|
+
): AssistantBlock[] {
|
|
299
|
+
const blocks: AssistantBlock[] = [];
|
|
300
|
+
let pendingCalls: CloudOsToolCall[] = [];
|
|
301
|
+
|
|
302
|
+
const flush = () => {
|
|
303
|
+
if (pendingCalls.length === 0) return;
|
|
304
|
+
for (const group of buildToolCallGroups(pendingCalls)) {
|
|
305
|
+
blocks.push({ kind: "toolGroup", key: group.key, group });
|
|
306
|
+
}
|
|
307
|
+
pendingCalls = [];
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
message.parts.forEach((part, partIndex) => {
|
|
311
|
+
const key = `${message.id}:${partIndex}`;
|
|
312
|
+
const record = recordOf(part);
|
|
313
|
+
const type = String(record.type ?? "");
|
|
314
|
+
|
|
315
|
+
if (type === "step-start") return;
|
|
316
|
+
|
|
317
|
+
if (type === "text") {
|
|
318
|
+
const text = String(record.text ?? "").trim();
|
|
319
|
+
if (!text) return;
|
|
320
|
+
flush();
|
|
321
|
+
blocks.push({ kind: "text", key, text });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (type === "reasoning") {
|
|
326
|
+
const text = String(record.text ?? "").trim();
|
|
327
|
+
if (!text || !showThinkingTraces) return;
|
|
328
|
+
flush();
|
|
329
|
+
blocks.push({ kind: "reasoning", key, text });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const dataKind = dataKindOf(part);
|
|
334
|
+
if (dataKind === "error") {
|
|
335
|
+
const data = dataOf(part);
|
|
336
|
+
flush();
|
|
337
|
+
blocks.push({
|
|
338
|
+
kind: "error",
|
|
339
|
+
key,
|
|
340
|
+
title: String(data.title ?? "Error"),
|
|
341
|
+
body: String(data.body ?? data.message ?? "") || undefined,
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (dataKind === "parallel") {
|
|
346
|
+
const data = dataOf(part);
|
|
347
|
+
flush();
|
|
348
|
+
blocks.push({
|
|
349
|
+
kind: "parallel",
|
|
350
|
+
key,
|
|
351
|
+
label: String(data.label ?? "Parallel tasks"),
|
|
352
|
+
tools: (Array.isArray(data.tools) ? data.tools : []).map((item) => {
|
|
353
|
+
const tool = recordOf(item);
|
|
354
|
+
return {
|
|
355
|
+
label: String(tool.label ?? "Task"),
|
|
356
|
+
status: normalizeStatus(tool.status),
|
|
357
|
+
meta: typeof tool.meta === "string" ? tool.meta : undefined,
|
|
358
|
+
};
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (dataKind === "subagents") {
|
|
364
|
+
const data = dataOf(part);
|
|
365
|
+
flush();
|
|
366
|
+
blocks.push({
|
|
367
|
+
kind: "subagents",
|
|
368
|
+
key,
|
|
369
|
+
agents: (Array.isArray(data.agents) ? data.agents : []).map((item) => {
|
|
370
|
+
const agent = recordOf(item);
|
|
371
|
+
return {
|
|
372
|
+
name: String(agent.name ?? "Sub-agent"),
|
|
373
|
+
summary:
|
|
374
|
+
typeof agent.summary === "string" && agent.summary
|
|
375
|
+
? agent.summary
|
|
376
|
+
: undefined,
|
|
377
|
+
status: normalizeStatus(agent.status),
|
|
378
|
+
};
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (dataKind === "image" || type === "file") {
|
|
384
|
+
const data = dataOf(part);
|
|
385
|
+
const src = String(data.src ?? data.url ?? record.url ?? "");
|
|
386
|
+
const mediaType = String(record.mediaType ?? data.mediaType ?? "");
|
|
387
|
+
if (!src) return;
|
|
388
|
+
|
|
389
|
+
flush();
|
|
390
|
+
const filename = String(
|
|
391
|
+
data.alt ?? data.filename ?? record.filename ?? "Attachment",
|
|
392
|
+
);
|
|
393
|
+
if (dataKind === "image" || mediaType.startsWith("image/")) {
|
|
394
|
+
blocks.push({
|
|
395
|
+
kind: "image",
|
|
396
|
+
key,
|
|
397
|
+
src,
|
|
398
|
+
alt: filename,
|
|
399
|
+
mediaType: mediaType || undefined,
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
blocks.push({
|
|
405
|
+
kind: "file",
|
|
406
|
+
key,
|
|
407
|
+
file: {
|
|
408
|
+
url: src,
|
|
409
|
+
filename,
|
|
410
|
+
mediaType: mediaType || undefined,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const toolName = toolNameOf(part);
|
|
417
|
+
if (toolName === null) return;
|
|
418
|
+
|
|
419
|
+
const call = toCloudOsToolCall(part, key, isActive);
|
|
420
|
+
|
|
421
|
+
if (call.awaitingApproval) {
|
|
422
|
+
const approvalId = approvalIdOf(part);
|
|
423
|
+
if (approvalId !== undefined) {
|
|
424
|
+
flush();
|
|
425
|
+
blocks.push({ kind: "approval", key, call, approvalId });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (call.kind === "tasks") {
|
|
431
|
+
const steps = planStepsOf(call.input);
|
|
432
|
+
if (steps.length > 0) {
|
|
433
|
+
flush();
|
|
434
|
+
blocks.push({ kind: "plan", key, steps, running: call.running });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (call.kind === "ask-user") {
|
|
440
|
+
flush();
|
|
441
|
+
blocks.push({
|
|
442
|
+
kind: "askUser",
|
|
443
|
+
key,
|
|
444
|
+
call,
|
|
445
|
+
pending: askUserOutcome(call).kind === "pending",
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (call.kind === "suggestions") {
|
|
451
|
+
const items = stringList(call.input.items ?? dataOf(part).items);
|
|
452
|
+
if (items.length > 0) {
|
|
453
|
+
flush();
|
|
454
|
+
blocks.push({ kind: "suggestions", key, items });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (call.kind === "schedule" && call.toolName === "schedule") {
|
|
460
|
+
flush();
|
|
461
|
+
blocks.push({ kind: "schedule", key, call });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
pendingCalls.push(call);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
flush();
|
|
469
|
+
return blocks;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const USER_STOP_REASON = "Stopped by user";
|
|
473
|
+
|
|
474
|
+
function terminalOf(
|
|
475
|
+
message: UIMessage,
|
|
476
|
+
): { kind: "interrupted" | "error"; title: string; body?: string } | undefined {
|
|
477
|
+
const metadata = cloudOsMetadata(message);
|
|
478
|
+
if (metadata.interruptedByUser === true) {
|
|
479
|
+
return { kind: "interrupted", title: USER_STOP_REASON };
|
|
480
|
+
}
|
|
481
|
+
if (metadata.turnStatus === "aborted" && metadata.error === USER_STOP_REASON) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
if (metadata.turnStatus === "error" || metadata.error) {
|
|
485
|
+
return {
|
|
486
|
+
kind: "error",
|
|
487
|
+
title: "Error",
|
|
488
|
+
body: metadata.error ?? "The response failed.",
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export interface BuildEntriesOptions {
|
|
495
|
+
messages: readonly UIMessage[];
|
|
496
|
+
/** streaming/submitted 时最后一条 assistant 消息才算「活着」。 */
|
|
497
|
+
isActive: boolean;
|
|
498
|
+
showThinkingTraces: boolean;
|
|
499
|
+
/** 把某个 part 映射成待授权 id;返回 undefined 表示这条不走原位授权。 */
|
|
500
|
+
approvalIdOf?: (part: unknown) => string | undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function buildCloudOsEntries({
|
|
504
|
+
messages,
|
|
505
|
+
isActive,
|
|
506
|
+
showThinkingTraces,
|
|
507
|
+
approvalIdOf = () => undefined,
|
|
508
|
+
}: BuildEntriesOptions): CloudOsEntry[] {
|
|
509
|
+
const entries: CloudOsEntry[] = [];
|
|
510
|
+
const lastAssistantIndex = (() => {
|
|
511
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
512
|
+
if (messages[index].role === "assistant") return index;
|
|
513
|
+
}
|
|
514
|
+
return -1;
|
|
515
|
+
})();
|
|
516
|
+
|
|
517
|
+
messages.forEach((message, index) => {
|
|
518
|
+
if (message.role === "system") return;
|
|
519
|
+
|
|
520
|
+
const standalone = standaloneEntry(message, index);
|
|
521
|
+
if (standalone) {
|
|
522
|
+
entries.push(standalone);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const metadata = cloudOsMetadata(message);
|
|
527
|
+
|
|
528
|
+
if (message.role === "user") {
|
|
529
|
+
const text = messageText(message);
|
|
530
|
+
const attachments = message.parts
|
|
531
|
+
.filter((part) => part.type === "file")
|
|
532
|
+
.map((part) => {
|
|
533
|
+
const record = recordOf(part);
|
|
534
|
+
return {
|
|
535
|
+
url: String(record.url ?? ""),
|
|
536
|
+
filename:
|
|
537
|
+
typeof record.filename === "string" ? record.filename : undefined,
|
|
538
|
+
mediaType:
|
|
539
|
+
typeof record.mediaType === "string" ? record.mediaType : undefined,
|
|
540
|
+
};
|
|
541
|
+
});
|
|
542
|
+
if (!text && attachments.length === 0) return;
|
|
543
|
+
entries.push({
|
|
544
|
+
type: "user",
|
|
545
|
+
key: `user-${message.id}-${index}`,
|
|
546
|
+
messageId: message.id,
|
|
547
|
+
text,
|
|
548
|
+
attachments,
|
|
549
|
+
capabilities: requestedCapabilitiesOf(metadata),
|
|
550
|
+
authorName: metadata.authorDisplayName,
|
|
551
|
+
timestamp:
|
|
552
|
+
nonNegativeNumber(metadata.createdAt) ??
|
|
553
|
+
nonNegativeNumber(metadata.completedAt),
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const blocks = assistantBlocks(
|
|
559
|
+
message,
|
|
560
|
+
isActive && index === lastAssistantIndex,
|
|
561
|
+
showThinkingTraces,
|
|
562
|
+
approvalIdOf,
|
|
563
|
+
);
|
|
564
|
+
const terminal = terminalOf(message);
|
|
565
|
+
if (blocks.length === 0 && terminal === undefined) return;
|
|
566
|
+
entries.push({
|
|
567
|
+
type: "assistant",
|
|
568
|
+
key: `assistant-${message.id}-${index}`,
|
|
569
|
+
messageId: message.id,
|
|
570
|
+
blocks,
|
|
571
|
+
copyText: messageText(message),
|
|
572
|
+
timestamp:
|
|
573
|
+
nonNegativeNumber(metadata.completedAt) ??
|
|
574
|
+
nonNegativeNumber(metadata.createdAt),
|
|
575
|
+
terminal,
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
return dropSupersededPlans(entries);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* update_plan 是**整份快照覆盖**,不是增量事件 —— 一次任务里每推进一步就会再发一份
|
|
584
|
+
* 完整计划。原样按序渲染会把同一份计划画四遍(0/3、1/3、2/3、3/3),只有最后那份
|
|
585
|
+
* 是当前事实。所以整条会话里只保留最后一份计划快照。
|
|
586
|
+
*/
|
|
587
|
+
function dropSupersededPlans(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
588
|
+
let lastPlanEntry = -1;
|
|
589
|
+
let lastPlanBlock = -1;
|
|
590
|
+
entries.forEach((entry, entryIndex) => {
|
|
591
|
+
if (entry.type !== "assistant") return;
|
|
592
|
+
entry.blocks.forEach((block, blockIndex) => {
|
|
593
|
+
if (block.kind !== "plan") return;
|
|
594
|
+
lastPlanEntry = entryIndex;
|
|
595
|
+
lastPlanBlock = blockIndex;
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
if (lastPlanEntry === -1) return entries;
|
|
599
|
+
|
|
600
|
+
return entries.map((entry, entryIndex) =>
|
|
601
|
+
entry.type === "assistant"
|
|
602
|
+
? {
|
|
603
|
+
...entry,
|
|
604
|
+
blocks: entry.blocks.filter(
|
|
605
|
+
(block, blockIndex) =>
|
|
606
|
+
block.kind !== "plan" ||
|
|
607
|
+
(entryIndex === lastPlanEntry && blockIndex === lastPlanBlock),
|
|
608
|
+
),
|
|
609
|
+
}
|
|
610
|
+
: entry,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ── 活动指示(本回合此刻在干什么)────────────────────────────────────────────
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* 底部活动指示器要显示的动画与文案。
|
|
618
|
+
*/
|
|
619
|
+
export interface CloudOsActivity {
|
|
620
|
+
state: OrbState;
|
|
621
|
+
label: string;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const SEARCHING_TOOL_KINDS: ReadonlySet<CloudOsToolKind> = new Set([
|
|
625
|
+
"read",
|
|
626
|
+
"glob",
|
|
627
|
+
"grep",
|
|
628
|
+
"list",
|
|
629
|
+
"web-search",
|
|
630
|
+
"web-fetch",
|
|
631
|
+
]);
|
|
632
|
+
const SHAPING_TOOL_KINDS: ReadonlySet<CloudOsToolKind> = new Set([
|
|
633
|
+
"write",
|
|
634
|
+
"edit",
|
|
635
|
+
"tasks",
|
|
636
|
+
"create-agent",
|
|
637
|
+
"publish-extension",
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
function activityForTool(call: CloudOsToolCall | undefined): CloudOsActivity {
|
|
641
|
+
if (!call) return { state: "working", label: "Working…" };
|
|
642
|
+
const name = call.toolName.replaceAll("-", "_").toLowerCase();
|
|
643
|
+
if (call.kind === "agent") {
|
|
644
|
+
return {
|
|
645
|
+
state: "weaving",
|
|
646
|
+
label: call.running ? "Coordinating agents…" : "Combining results…",
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (SEARCHING_TOOL_KINDS.has(call.kind)) {
|
|
650
|
+
return {
|
|
651
|
+
state: "searching",
|
|
652
|
+
label: call.running ? "Searching…" : "Reading results…",
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (SHAPING_TOOL_KINDS.has(call.kind)) {
|
|
656
|
+
return { state: "shaping", label: "Shaping result…" };
|
|
657
|
+
}
|
|
658
|
+
if (call.kind === "skill" || name === "activate_skill") {
|
|
659
|
+
return { state: "working", label: "Loading skill…" };
|
|
660
|
+
}
|
|
661
|
+
if (name === "run_skill_script") {
|
|
662
|
+
return { state: "working", label: "Running skill…" };
|
|
663
|
+
}
|
|
664
|
+
return { state: "working", label: "Working…" };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function activityForTail(block: AssistantBlock | undefined): CloudOsActivity {
|
|
668
|
+
if (!block) return { state: "solving", label: "Thinking…" };
|
|
669
|
+
switch (block.kind) {
|
|
670
|
+
case "reasoning":
|
|
671
|
+
return { state: "solving", label: "Reasoning…" };
|
|
672
|
+
case "text":
|
|
673
|
+
case "suggestions":
|
|
674
|
+
return { state: "composing", label: "Composing answer…" };
|
|
675
|
+
case "toolGroup":
|
|
676
|
+
return activityForTool(block.group.calls.at(-1));
|
|
677
|
+
case "plan":
|
|
678
|
+
case "image":
|
|
679
|
+
case "file":
|
|
680
|
+
return { state: "shaping", label: "Shaping result…" };
|
|
681
|
+
case "askUser":
|
|
682
|
+
return block.pending
|
|
683
|
+
? { state: "listening", label: "Waiting for your answer…" }
|
|
684
|
+
: { state: "solving", label: "Picking up your answer…" };
|
|
685
|
+
case "approval":
|
|
686
|
+
return { state: "breathing", label: "Waiting for approval…" };
|
|
687
|
+
case "schedule":
|
|
688
|
+
return block.call.awaitingApproval
|
|
689
|
+
? { state: "breathing", label: "Waiting for approval…" }
|
|
690
|
+
: { state: "working", label: "Working…" };
|
|
691
|
+
case "parallel":
|
|
692
|
+
return {
|
|
693
|
+
state: "weaving",
|
|
694
|
+
label: block.tools.some((tool) => tool.status === "running")
|
|
695
|
+
? "Coordinating tasks…"
|
|
696
|
+
: "Combining results…",
|
|
697
|
+
};
|
|
698
|
+
case "subagents":
|
|
699
|
+
return {
|
|
700
|
+
state: "weaving",
|
|
701
|
+
label: block.agents.some((agent) => agent.status === "running")
|
|
702
|
+
? "Coordinating agents…"
|
|
703
|
+
: "Combining results…",
|
|
704
|
+
};
|
|
705
|
+
default:
|
|
706
|
+
return { state: "working", label: "Working…" };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* 回合活着时始终返回一个 Orb 状态;只有回合终止时才返回 `null`。
|
|
712
|
+
*/
|
|
713
|
+
export function deriveTurnActivity(
|
|
714
|
+
entries: readonly CloudOsEntry[],
|
|
715
|
+
options: {
|
|
716
|
+
isActive: boolean;
|
|
717
|
+
isRecovering?: boolean;
|
|
718
|
+
recoveryStatusLabel?: string;
|
|
719
|
+
awaitingApproval?: boolean;
|
|
720
|
+
hasPendingSteer?: boolean;
|
|
721
|
+
},
|
|
722
|
+
): CloudOsActivity | null {
|
|
723
|
+
if (options.isRecovering) {
|
|
724
|
+
return {
|
|
725
|
+
state: "connecting",
|
|
726
|
+
label: options.recoveryStatusLabel ?? "Reconnecting…",
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (!options.isActive) return null;
|
|
730
|
+
if (options.awaitingApproval) {
|
|
731
|
+
return { state: "breathing", label: "Waiting for approval…" };
|
|
732
|
+
}
|
|
733
|
+
if (options.hasPendingSteer) {
|
|
734
|
+
return {
|
|
735
|
+
state: "breathing",
|
|
736
|
+
label: "Waiting for the current step to finish…",
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const tail = entries.at(-1) ?? null;
|
|
740
|
+
const block = tail?.type === "assistant" ? tail.blocks.at(-1) : undefined;
|
|
741
|
+
return activityForTail(block);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ── 行间距节奏(照搬原 rhythmTopClass)──────────────────────────────────────
|
|
745
|
+
|
|
746
|
+
function isUserEntry(entry: CloudOsEntry): boolean {
|
|
747
|
+
return entry.type === "user" || entry.type === "slashCommand";
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function startsWithWorkRow(entry: CloudOsEntry): boolean {
|
|
751
|
+
return (
|
|
752
|
+
entry.type === "assistant" &&
|
|
753
|
+
entry.blocks[0] !== undefined &&
|
|
754
|
+
entry.blocks[0].kind === "toolGroup"
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function endsInWorkRow(entry: CloudOsEntry): boolean {
|
|
759
|
+
return (
|
|
760
|
+
entry.type === "assistant" &&
|
|
761
|
+
entry.blocks.at(-1) !== undefined &&
|
|
762
|
+
entry.blocks.at(-1)!.kind === "toolGroup"
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* 相邻两条之间的上间距。原则(原文件注释):用户消息前后留最大的气口;
|
|
768
|
+
* 两条工作行之间贴紧;其余取中间档。
|
|
769
|
+
*/
|
|
770
|
+
export function rhythmTopClass(
|
|
771
|
+
previous: CloudOsEntry | null,
|
|
772
|
+
entry: CloudOsEntry,
|
|
773
|
+
): string {
|
|
774
|
+
if (!previous) return "";
|
|
775
|
+
if (isUserEntry(entry)) return "mt-8";
|
|
776
|
+
if (isUserEntry(previous)) return "mt-5";
|
|
777
|
+
if (endsInWorkRow(previous) && startsWithWorkRow(entry)) return "mt-2";
|
|
778
|
+
if (previous.type === "notice" || entry.type === "notice") return "mt-4";
|
|
779
|
+
return "mt-4";
|
|
780
|
+
}
|