@springbrand/message-panel 0.1.3-alpha.0 → 0.1.3-alpha.2
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/chat/cloud-os-chat-messages.tsx +606 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +640 -0
- package/cloud-os/chat/tool-presentation.ts +542 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +599 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +532 -0
- package/cloud-os/index.ts +103 -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 +202 -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 +559 -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 +470 -0
- package/demo/index.ts +2 -0
- package/package.json +16 -4
- package/src/camel/camel-chat-messages.tsx +50 -20
- package/src/camel/camel-turn.ts +21 -3
- package/src/message.tsx +0 -8
- package/src/parts/plan.ts +3 -2
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Blueprint,
|
|
3
|
+
BracketsCurly,
|
|
4
|
+
Brain,
|
|
5
|
+
Clock,
|
|
6
|
+
Code,
|
|
7
|
+
File as FileIcon,
|
|
8
|
+
FolderOpen,
|
|
9
|
+
Globe,
|
|
10
|
+
LinkSimple,
|
|
11
|
+
ListChecks,
|
|
12
|
+
MagnifyingGlass,
|
|
13
|
+
PencilSimple,
|
|
14
|
+
Plus,
|
|
15
|
+
Question,
|
|
16
|
+
Robot,
|
|
17
|
+
ShieldCheck,
|
|
18
|
+
Sparkle,
|
|
19
|
+
Terminal,
|
|
20
|
+
UsersThree,
|
|
21
|
+
} from "@phosphor-icons/react";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* cloud-os 的工具行文案层。
|
|
25
|
+
*
|
|
26
|
+
* cloudflare-os-main 里这块(getToolCallSummary / describeToolCallCount /
|
|
27
|
+
* getToolIcon / buildToolCallGroups)是**穷举 gadgets 自己那十几个工具名**的
|
|
28
|
+
* switch。UIMessage 协议下工具名是开放集合,穷举不成立,所以这里按「规范化
|
|
29
|
+
* 工具种类」重写查表:动词表、复数表、图标表逐条对应原文件的同名函数,
|
|
30
|
+
* 分组算法(buildToolCallGroups)则是逐行搬过来的。
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export type PhosphorIcon = typeof MagnifyingGlass;
|
|
34
|
+
|
|
35
|
+
type UnknownRecord = Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
export type CloudOsToolKind =
|
|
38
|
+
| "read"
|
|
39
|
+
| "write"
|
|
40
|
+
| "edit"
|
|
41
|
+
| "bash"
|
|
42
|
+
| "glob"
|
|
43
|
+
| "grep"
|
|
44
|
+
| "list"
|
|
45
|
+
| "web-search"
|
|
46
|
+
| "web-fetch"
|
|
47
|
+
| "javascript"
|
|
48
|
+
| "tasks"
|
|
49
|
+
| "agent"
|
|
50
|
+
| "skill"
|
|
51
|
+
| "ask-user"
|
|
52
|
+
| "suggestions"
|
|
53
|
+
| "schedule"
|
|
54
|
+
| "create-agent"
|
|
55
|
+
| "publish-extension"
|
|
56
|
+
| "mcp"
|
|
57
|
+
| "generic";
|
|
58
|
+
|
|
59
|
+
export interface CloudOsToolCall {
|
|
60
|
+
/** 稳定 id:展开态跨流式 → 落库要保持。 */
|
|
61
|
+
toolCallId: string;
|
|
62
|
+
toolName: string;
|
|
63
|
+
kind: CloudOsToolKind;
|
|
64
|
+
input: UnknownRecord;
|
|
65
|
+
output: unknown;
|
|
66
|
+
outputText: string;
|
|
67
|
+
errorText: string;
|
|
68
|
+
/** UIMessage 的 part.state 原值。 */
|
|
69
|
+
state: string;
|
|
70
|
+
running: boolean;
|
|
71
|
+
failed: boolean;
|
|
72
|
+
/** 等待授权中(approval-requested)。 */
|
|
73
|
+
awaitingApproval: boolean;
|
|
74
|
+
raw: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ToolCallGroup {
|
|
78
|
+
key: string;
|
|
79
|
+
Icon: PhosphorIcon;
|
|
80
|
+
label: string;
|
|
81
|
+
detailLines: string[];
|
|
82
|
+
calls: CloudOsToolCall[];
|
|
83
|
+
hasError: boolean;
|
|
84
|
+
hasRunning: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── 基础工具 ────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function recordOf(value: unknown): UnknownRecord {
|
|
90
|
+
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
91
|
+
? (value as UnknownRecord)
|
|
92
|
+
: {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stringField(record: UnknownRecord, ...keys: string[]): string {
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const value = record[key];
|
|
98
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function safeStringify(value: unknown): string {
|
|
104
|
+
if (typeof value === "string") return value;
|
|
105
|
+
try {
|
|
106
|
+
return JSON.stringify(value, null, 2);
|
|
107
|
+
} catch {
|
|
108
|
+
return String(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeOutputText(value: unknown): string {
|
|
113
|
+
if (value == null) return "";
|
|
114
|
+
if (typeof value === "string") return value;
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
return value
|
|
117
|
+
.map((item) => {
|
|
118
|
+
const record = recordOf(item);
|
|
119
|
+
return typeof record.text === "string" ? record.text : safeStringify(item);
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join("\n\n");
|
|
123
|
+
}
|
|
124
|
+
const record = recordOf(value);
|
|
125
|
+
if (typeof record.text === "string") return record.text;
|
|
126
|
+
if (typeof record.content === "string") return record.content;
|
|
127
|
+
if (Array.isArray(record.content)) return normalizeOutputText(record.content);
|
|
128
|
+
return safeStringify(value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function filenameOf(path: string): string {
|
|
132
|
+
const trimmed = path.trim().replace(/\/+$/, "");
|
|
133
|
+
return trimmed.split("/").pop() || trimmed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hostnameOf(url: string): string {
|
|
137
|
+
try {
|
|
138
|
+
return new URL(url).host;
|
|
139
|
+
} catch {
|
|
140
|
+
return url;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function truncate(value: string, maxLength: number): string {
|
|
145
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function humanize(name: string): string {
|
|
149
|
+
const spaced = name
|
|
150
|
+
.replace(/^mcp__/, "")
|
|
151
|
+
.replace(/__/g, " ")
|
|
152
|
+
.replace(/[_-]+/g, " ")
|
|
153
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
154
|
+
.trim();
|
|
155
|
+
return spaced
|
|
156
|
+
? spaced.replace(/\b\w/g, (character) => character.toUpperCase())
|
|
157
|
+
: "Tool";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function lowerFirst(text: string): string {
|
|
161
|
+
return text ? text[0].toLowerCase() + text.slice(1) : text;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function pluralize(
|
|
165
|
+
count: number,
|
|
166
|
+
singular: string,
|
|
167
|
+
plural = `${singular}s`,
|
|
168
|
+
): string {
|
|
169
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatTimes(count: number): string {
|
|
173
|
+
return pluralize(count, "time");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── 工具名 → 规范种类 ───────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export function toolNameOfPart(part: unknown): string | null {
|
|
179
|
+
const record = recordOf(part);
|
|
180
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
181
|
+
if (type === "dynamic-tool") {
|
|
182
|
+
return typeof record.toolName === "string" ? record.toolName : "dynamic";
|
|
183
|
+
}
|
|
184
|
+
return type.startsWith("tool-") ? type.slice(5) : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function canonicalToolKind(name: string): CloudOsToolKind {
|
|
188
|
+
const normalized = name.trim().replaceAll("-", "_").toLowerCase();
|
|
189
|
+
if (normalized.startsWith("mcp__")) return "mcp";
|
|
190
|
+
if (["read", "read_file", "read_text_file", "read_document"].includes(normalized)) {
|
|
191
|
+
return "read";
|
|
192
|
+
}
|
|
193
|
+
if (["write", "write_file", "create_file", "save_file"].includes(normalized)) {
|
|
194
|
+
return "write";
|
|
195
|
+
}
|
|
196
|
+
if (["edit", "edit_file", "apply_patch", "patch_file"].includes(normalized)) {
|
|
197
|
+
return "edit";
|
|
198
|
+
}
|
|
199
|
+
if (
|
|
200
|
+
["bash", "shell", "exec", "exec_command", "run_command", "terminal"].includes(
|
|
201
|
+
normalized,
|
|
202
|
+
)
|
|
203
|
+
) {
|
|
204
|
+
return "bash";
|
|
205
|
+
}
|
|
206
|
+
if (["glob", "find", "find_file", "find_files"].includes(normalized)) return "glob";
|
|
207
|
+
if (["grep", "rg", "search_files", "search_code"].includes(normalized)) return "grep";
|
|
208
|
+
if (["ls", "list_directory", "list_files"].includes(normalized)) return "list";
|
|
209
|
+
if (["web_search", "search_web", "search_query"].includes(normalized)) {
|
|
210
|
+
return "web-search";
|
|
211
|
+
}
|
|
212
|
+
if (["web_fetch", "fetch_url", "open_url", "fetch_page"].includes(normalized)) {
|
|
213
|
+
return "web-fetch";
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
["javascript", "js_exec", "node_repl", "execute_javascript", "execute_code"].includes(
|
|
217
|
+
normalized,
|
|
218
|
+
)
|
|
219
|
+
) {
|
|
220
|
+
return "javascript";
|
|
221
|
+
}
|
|
222
|
+
if (["todo_write", "update_todo", "update_plan"].includes(normalized)) return "tasks";
|
|
223
|
+
if (
|
|
224
|
+
[
|
|
225
|
+
"task",
|
|
226
|
+
"agent",
|
|
227
|
+
"run_agent",
|
|
228
|
+
"run_temporary_agent",
|
|
229
|
+
"subagents",
|
|
230
|
+
"fanout",
|
|
231
|
+
"extract",
|
|
232
|
+
"dispatch_background",
|
|
233
|
+
"explore",
|
|
234
|
+
"research",
|
|
235
|
+
].includes(normalized)
|
|
236
|
+
) {
|
|
237
|
+
return "agent";
|
|
238
|
+
}
|
|
239
|
+
if (["skill", "read_skill", "read_skill_resource"].includes(normalized)) return "skill";
|
|
240
|
+
if (["ask_user", "ask_user_question", "request_user_input"].includes(normalized)) {
|
|
241
|
+
return "ask-user";
|
|
242
|
+
}
|
|
243
|
+
if (["suggest_followups", "suggestions"].includes(normalized)) return "suggestions";
|
|
244
|
+
if (["schedule", "cancel_schedule"].includes(normalized)) return "schedule";
|
|
245
|
+
if (normalized === "create_agent") return "create-agent";
|
|
246
|
+
if (normalized === "publish_extension") return "publish-extension";
|
|
247
|
+
return "generic";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── 图标(对应原 getToolIcon)────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
const KIND_ICONS: Record<CloudOsToolKind, PhosphorIcon> = {
|
|
253
|
+
read: FileIcon,
|
|
254
|
+
write: FileIcon,
|
|
255
|
+
edit: PencilSimple,
|
|
256
|
+
bash: Terminal,
|
|
257
|
+
glob: MagnifyingGlass,
|
|
258
|
+
grep: MagnifyingGlass,
|
|
259
|
+
list: FolderOpen,
|
|
260
|
+
"web-search": MagnifyingGlass,
|
|
261
|
+
"web-fetch": Globe,
|
|
262
|
+
javascript: Code,
|
|
263
|
+
tasks: ListChecks,
|
|
264
|
+
agent: UsersThree,
|
|
265
|
+
skill: Sparkle,
|
|
266
|
+
"ask-user": Question,
|
|
267
|
+
suggestions: Sparkle,
|
|
268
|
+
schedule: Clock,
|
|
269
|
+
"create-agent": Robot,
|
|
270
|
+
"publish-extension": Blueprint,
|
|
271
|
+
mcp: LinkSimple,
|
|
272
|
+
generic: BracketsCurly,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export function getToolIcon(kind: CloudOsToolKind | undefined): PhosphorIcon {
|
|
276
|
+
return kind ? KIND_ICONS[kind] : Question;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const ReasoningIcon = Brain;
|
|
280
|
+
export const ApprovalIcon = ShieldCheck;
|
|
281
|
+
export const CreateIcon = Plus;
|
|
282
|
+
|
|
283
|
+
// ── 单条摘要(对应原 getToolCallSummary)──────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
export function getToolCallSummary(call: CloudOsToolCall): {
|
|
286
|
+
verb: string;
|
|
287
|
+
target?: string;
|
|
288
|
+
} {
|
|
289
|
+
const { kind, input, toolName, running } = call;
|
|
290
|
+
const path = stringField(input, "file_path", "path", "filename", "target");
|
|
291
|
+
const filename = path ? filenameOf(path) : "";
|
|
292
|
+
switch (kind) {
|
|
293
|
+
case "read":
|
|
294
|
+
return { verb: running ? "Reading" : "Read", target: filename || undefined };
|
|
295
|
+
case "write":
|
|
296
|
+
return { verb: running ? "Writing" : "Wrote", target: filename || undefined };
|
|
297
|
+
case "edit":
|
|
298
|
+
return { verb: running ? "Editing" : "Edited", target: filename || undefined };
|
|
299
|
+
case "bash": {
|
|
300
|
+
const command =
|
|
301
|
+
stringField(input, "description") || stringField(input, "command", "cmd");
|
|
302
|
+
return {
|
|
303
|
+
verb: running ? "Running" : "Ran",
|
|
304
|
+
target: command ? truncate(command, 60) : undefined,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
case "glob":
|
|
308
|
+
case "grep": {
|
|
309
|
+
const pattern = stringField(input, "pattern", "glob", "query");
|
|
310
|
+
return {
|
|
311
|
+
verb: running ? "Searching" : "Searched",
|
|
312
|
+
target: pattern ? truncate(pattern, 40) : undefined,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
case "list":
|
|
316
|
+
return { verb: running ? "Listing" : "Listed", target: filename || undefined };
|
|
317
|
+
case "web-search": {
|
|
318
|
+
const query = stringField(input, "query", "q");
|
|
319
|
+
return {
|
|
320
|
+
verb: running ? "Searching web" : "Searched web",
|
|
321
|
+
target: query ? truncate(query, 50) : undefined,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
case "web-fetch": {
|
|
325
|
+
const url = stringField(input, "url");
|
|
326
|
+
return {
|
|
327
|
+
verb: running ? "Fetching" : "Fetched",
|
|
328
|
+
target: url ? hostnameOf(url) : undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
case "javascript": {
|
|
332
|
+
const firstLine = stringField(input, "code")
|
|
333
|
+
.split("\n")
|
|
334
|
+
.map((line) => line.trim())
|
|
335
|
+
.find((line) => line.length > 0);
|
|
336
|
+
return {
|
|
337
|
+
verb: running ? "Running code" : "Ran code",
|
|
338
|
+
target: firstLine ? truncate(firstLine, 60) : undefined,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
case "tasks":
|
|
342
|
+
return { verb: running ? "Updating plan" : "Updated plan" };
|
|
343
|
+
case "agent": {
|
|
344
|
+
const name =
|
|
345
|
+
stringField(input, "subagentName", "agent", "name", "agentType") ||
|
|
346
|
+
humanize(toolName);
|
|
347
|
+
return { verb: running ? "Running" : "Ran", target: name };
|
|
348
|
+
}
|
|
349
|
+
case "skill": {
|
|
350
|
+
const skill = stringField(input, "skill", "name") || filename;
|
|
351
|
+
return {
|
|
352
|
+
verb: running ? "Reading instructions" : "Read instructions",
|
|
353
|
+
target: skill || undefined,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
case "ask-user":
|
|
357
|
+
return { verb: running ? "Asking" : "Asked a question" };
|
|
358
|
+
case "suggestions":
|
|
359
|
+
return { verb: "Suggested follow-ups" };
|
|
360
|
+
case "schedule": {
|
|
361
|
+
const label = stringField(input, "label", "title");
|
|
362
|
+
return {
|
|
363
|
+
verb: toolName === "cancel_schedule" ? "Cancelled schedule" : "Scheduled",
|
|
364
|
+
target: label || undefined,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
case "create-agent":
|
|
368
|
+
return {
|
|
369
|
+
verb: running ? "Creating agent" : "Created agent",
|
|
370
|
+
target: stringField(input, "name") || undefined,
|
|
371
|
+
};
|
|
372
|
+
case "publish-extension":
|
|
373
|
+
return {
|
|
374
|
+
verb: running ? "Publishing extension" : "Published extension",
|
|
375
|
+
target: stringField(input, "name") || undefined,
|
|
376
|
+
};
|
|
377
|
+
case "mcp": {
|
|
378
|
+
const [, server = "", ...rest] = toolName.split("__");
|
|
379
|
+
return {
|
|
380
|
+
verb: running ? "Calling" : "Called",
|
|
381
|
+
target: rest.length ? `${humanize(rest.join(" "))} · ${humanize(server)}` : undefined,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
case "generic":
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
const displayName = humanize(toolName);
|
|
388
|
+
return { verb: running ? `Using ${displayName}` : `Used ${displayName}` };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── 计数摘要(对应原 describeToolCallCount)──────────────────────────────────
|
|
392
|
+
|
|
393
|
+
export function describeToolCallCount(
|
|
394
|
+
kind: CloudOsToolKind,
|
|
395
|
+
toolName: string,
|
|
396
|
+
count: number,
|
|
397
|
+
): string {
|
|
398
|
+
switch (kind) {
|
|
399
|
+
case "read":
|
|
400
|
+
return `Read ${pluralize(count, "file")}`;
|
|
401
|
+
case "write":
|
|
402
|
+
return `Wrote ${pluralize(count, "file")}`;
|
|
403
|
+
case "edit":
|
|
404
|
+
return count === 1 ? "Made 1 edit" : `Made ${count} edits`;
|
|
405
|
+
case "bash":
|
|
406
|
+
return count === 1 ? "Ran a command" : `Ran ${pluralize(count, "command")}`;
|
|
407
|
+
case "glob":
|
|
408
|
+
case "grep":
|
|
409
|
+
return `Ran ${pluralize(count, "search")}`;
|
|
410
|
+
case "list":
|
|
411
|
+
return `Listed ${pluralize(count, "directory", "directories")}`;
|
|
412
|
+
case "web-search":
|
|
413
|
+
return count === 1 ? "Searched web" : `Searched web ${formatTimes(count)}`;
|
|
414
|
+
case "web-fetch":
|
|
415
|
+
return `Fetched ${pluralize(count, "page")}`;
|
|
416
|
+
case "javascript":
|
|
417
|
+
return count === 1 ? "Ran code" : `Ran code ${formatTimes(count)}`;
|
|
418
|
+
case "tasks":
|
|
419
|
+
return count === 1 ? "Updated plan" : `Updated plan ${formatTimes(count)}`;
|
|
420
|
+
case "agent":
|
|
421
|
+
return `Ran ${pluralize(count, "sub-agent")}`;
|
|
422
|
+
case "skill":
|
|
423
|
+
return `Read ${pluralize(count, "instruction set")}`;
|
|
424
|
+
case "ask-user":
|
|
425
|
+
return count === 1 ? "Asked a question" : `Asked ${count} questions`;
|
|
426
|
+
case "suggestions":
|
|
427
|
+
return "Suggested follow-ups";
|
|
428
|
+
case "schedule":
|
|
429
|
+
return `Managed ${pluralize(count, "schedule")}`;
|
|
430
|
+
case "create-agent":
|
|
431
|
+
return `Created ${pluralize(count, "agent")}`;
|
|
432
|
+
case "publish-extension":
|
|
433
|
+
return `Published ${pluralize(count, "extension")}`;
|
|
434
|
+
case "mcp":
|
|
435
|
+
case "generic":
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
const displayName = humanize(toolName);
|
|
439
|
+
return count === 1 ? `Used ${displayName}` : `Used ${displayName} ${formatTimes(count)}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── UIMessage part → CloudOsToolCall ────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
const RUNNING_STATES = new Set([
|
|
445
|
+
"input-streaming",
|
|
446
|
+
"input-available",
|
|
447
|
+
"approval-responded",
|
|
448
|
+
"running",
|
|
449
|
+
"",
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
export function toCloudOsToolCall(
|
|
453
|
+
part: unknown,
|
|
454
|
+
fallbackId: string,
|
|
455
|
+
isActive: boolean,
|
|
456
|
+
): CloudOsToolCall {
|
|
457
|
+
const record = recordOf(part);
|
|
458
|
+
const toolName = toolNameOfPart(part) ?? stringField(record, "name") ?? "tool";
|
|
459
|
+
const data = recordOf(record.data);
|
|
460
|
+
const input =
|
|
461
|
+
record.input != null
|
|
462
|
+
? recordOf(record.input)
|
|
463
|
+
: data.input != null
|
|
464
|
+
? recordOf(data.input)
|
|
465
|
+
: data;
|
|
466
|
+
const output = record.output ?? data.output ?? record.result;
|
|
467
|
+
const errorText = stringField(record, "errorText", "error");
|
|
468
|
+
const state = stringField(record, "state", "status");
|
|
469
|
+
const failed =
|
|
470
|
+
state === "output-error" ||
|
|
471
|
+
state === "output-denied" ||
|
|
472
|
+
state === "failed" ||
|
|
473
|
+
Boolean(errorText);
|
|
474
|
+
const awaitingApproval = state === "approval-requested";
|
|
475
|
+
return {
|
|
476
|
+
toolCallId:
|
|
477
|
+
stringField(record, "toolCallId", "id") || fallbackId,
|
|
478
|
+
toolName,
|
|
479
|
+
kind: canonicalToolKind(toolName),
|
|
480
|
+
input,
|
|
481
|
+
output,
|
|
482
|
+
outputText: normalizeOutputText(output),
|
|
483
|
+
errorText,
|
|
484
|
+
state,
|
|
485
|
+
running: isActive && !failed && RUNNING_STATES.has(state),
|
|
486
|
+
failed,
|
|
487
|
+
awaitingApproval,
|
|
488
|
+
raw: part,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── 分组(逐行对应原 buildToolCallGroups)────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
export function buildToolCallGroups(calls: CloudOsToolCall[]): ToolCallGroup[] {
|
|
495
|
+
if (calls.length === 0) return [];
|
|
496
|
+
|
|
497
|
+
const distinctKinds = Array.from(new Set(calls.map((call) => call.kind)));
|
|
498
|
+
const detailLines = Array.from(
|
|
499
|
+
new Set(
|
|
500
|
+
calls
|
|
501
|
+
.map((call) => getToolCallSummary(call).target)
|
|
502
|
+
.filter((target): target is string => Boolean(target)),
|
|
503
|
+
),
|
|
504
|
+
);
|
|
505
|
+
const labelParts: string[] = [];
|
|
506
|
+
|
|
507
|
+
if (calls.length === 1) {
|
|
508
|
+
const summary = getToolCallSummary(calls[0]);
|
|
509
|
+
labelParts.push(`${summary.verb}${summary.target ? ` ${summary.target}` : ""}`);
|
|
510
|
+
} else if (distinctKinds.length === 1) {
|
|
511
|
+
const summary = getToolCallSummary(calls[0]);
|
|
512
|
+
labelParts.push(
|
|
513
|
+
detailLines.length === 1 && summary.target
|
|
514
|
+
? `${summary.verb} ${summary.target}`
|
|
515
|
+
: describeToolCallCount(calls[0].kind, calls[0].toolName, calls.length),
|
|
516
|
+
);
|
|
517
|
+
} else if (distinctKinds.length <= 3) {
|
|
518
|
+
labelParts.push(
|
|
519
|
+
...distinctKinds.map((kind) => {
|
|
520
|
+
const ofKind = calls.filter((call) => call.kind === kind);
|
|
521
|
+
return describeToolCallCount(kind, ofKind[0].toolName, ofKind.length);
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
} else {
|
|
525
|
+
labelParts.push(`${calls.length} tool calls`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return [
|
|
529
|
+
{
|
|
530
|
+
// 用第一条 work item 的 id 作 key:展开态在「流式 → 已落库」之间要活下来。
|
|
531
|
+
key: `group-${calls[0].toolCallId}`,
|
|
532
|
+
Icon: getToolIcon(calls[0].kind),
|
|
533
|
+
label: labelParts
|
|
534
|
+
.map((part, index) => (index === 0 ? part : lowerFirst(part)))
|
|
535
|
+
.join(", "),
|
|
536
|
+
detailLines,
|
|
537
|
+
calls,
|
|
538
|
+
hasError: calls.some((call) => call.failed),
|
|
539
|
+
hasRunning: calls.some((call) => call.running),
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
}
|