@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.30
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 +6 -6
- package/cloud-os/assets/followup-arrow.svg +3 -0
- package/cloud-os/assets/loading-corner.svg +3 -0
- package/cloud-os/assets/loading-mark.svg +16 -0
- package/cloud-os/assets/loading-spark.svg +3 -0
- package/cloud-os/assets/model-selected.svg +5 -0
- package/cloud-os/assets/thinking-spark.svg +9 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +2 -2
- package/cloud-os/chat/cloud-os-chat-messages.tsx +374 -63
- package/cloud-os/chat/image-generation.tsx +76 -0
- package/cloud-os/chat/loading-state.tsx +25 -0
- package/cloud-os/chat/markdown-message.tsx +144 -41
- package/cloud-os/chat/rich-blocks.tsx +548 -246
- package/cloud-os/chat/tool-presentation.ts +35 -16
- package/cloud-os/chat/tool-rows.tsx +330 -85
- package/cloud-os/chat/transcript-model.ts +428 -56
- package/cloud-os/composer/cloud-os-chat-input.tsx +228 -142
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +441 -0
- package/cloud-os/index.ts +35 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +58 -15
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +100 -20
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +223 -43
- package/demo/cloud-os-chat-showcase.tsx +47 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +3 -4
- package/src/camel/camel-chat-messages.tsx +78 -78
- package/src/camel/camel-tool-presentation.tsx +19 -15
- package/src/camel/camel-turn.ts +1 -17
- package/src/composer/composer-attachments.tsx +1 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -2
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +1 -24
- package/src/parts/index.tsx +103 -64
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
buildToolCallGroups,
|
|
6
6
|
toCloudOsToolCall,
|
|
7
7
|
type CloudOsToolCall,
|
|
8
|
+
type CloudOsToolKind,
|
|
8
9
|
type ToolCallGroup,
|
|
9
10
|
} from "./tool-presentation";
|
|
10
11
|
|
|
@@ -26,9 +27,17 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
|
26
27
|
createdAt?: number;
|
|
27
28
|
error?: string;
|
|
28
29
|
interruptedByUser?: boolean;
|
|
30
|
+
turnId?: string;
|
|
29
31
|
turnDurationMs?: number;
|
|
30
32
|
turnStartedAt?: number;
|
|
31
33
|
turnStatus?: string;
|
|
34
|
+
requestedCapabilities?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CloudOsRequestedCapability {
|
|
38
|
+
kind: "skill" | "tool" | "plan";
|
|
39
|
+
name: string;
|
|
40
|
+
label: string;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export interface CloudOsAttachment {
|
|
@@ -37,13 +46,38 @@ export interface CloudOsAttachment {
|
|
|
37
46
|
mediaType?: string;
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
interface CloudOsActionPresentationFile {
|
|
50
|
+
type: "file";
|
|
51
|
+
url: string;
|
|
52
|
+
mediaType?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CloudOsActionProduces {
|
|
56
|
+
type: "image" | "audio" | "video" | "document" | "archive" | "data" | "other";
|
|
57
|
+
mediaType?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CloudOsActionPresentation {
|
|
61
|
+
type: "action-execution";
|
|
62
|
+
executionId: string;
|
|
63
|
+
state: "running" | "ready" | "failed" | "outcome_unknown";
|
|
64
|
+
failureCode?: "insufficient_credits";
|
|
65
|
+
produces?: CloudOsActionProduces;
|
|
66
|
+
content: CloudOsActionPresentationFile[];
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
export interface PlanStep {
|
|
41
70
|
text: string;
|
|
42
71
|
status: "pending" | "in_progress" | "done";
|
|
43
72
|
}
|
|
44
73
|
|
|
45
74
|
export type AssistantBlock =
|
|
46
|
-
| {
|
|
75
|
+
| {
|
|
76
|
+
kind: "reasoning";
|
|
77
|
+
key: string;
|
|
78
|
+
text: string;
|
|
79
|
+
running: boolean;
|
|
80
|
+
}
|
|
47
81
|
| { kind: "text"; key: string; text: string }
|
|
48
82
|
| { kind: "toolGroup"; key: string; group: ToolCallGroup }
|
|
49
83
|
| { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
|
|
@@ -58,7 +92,19 @@ export type AssistantBlock =
|
|
|
58
92
|
| { kind: "approval"; key: string; call: CloudOsToolCall; approvalId: string }
|
|
59
93
|
| { kind: "parallel"; key: string; label: string; tools: ParallelTool[] }
|
|
60
94
|
| { kind: "subagents"; key: string; agents: SubAgentView[] }
|
|
61
|
-
| {
|
|
95
|
+
| {
|
|
96
|
+
kind: "image";
|
|
97
|
+
key: string;
|
|
98
|
+
src: string;
|
|
99
|
+
alt: string;
|
|
100
|
+
mediaType?: string;
|
|
101
|
+
}
|
|
102
|
+
| { kind: "file"; key: string; file: CloudOsAttachment }
|
|
103
|
+
| {
|
|
104
|
+
kind: "actionPresentation";
|
|
105
|
+
key: string;
|
|
106
|
+
presentation: CloudOsActionPresentation;
|
|
107
|
+
}
|
|
62
108
|
| { kind: "error"; key: string; title: string; body?: string };
|
|
63
109
|
|
|
64
110
|
export interface ParallelTool {
|
|
@@ -80,6 +126,7 @@ export type CloudOsEntry =
|
|
|
80
126
|
messageId: string;
|
|
81
127
|
text: string;
|
|
82
128
|
attachments: CloudOsAttachment[];
|
|
129
|
+
capabilities: CloudOsRequestedCapability[];
|
|
83
130
|
authorName?: string;
|
|
84
131
|
timestamp?: number;
|
|
85
132
|
}
|
|
@@ -97,8 +144,10 @@ export type CloudOsEntry =
|
|
|
97
144
|
type: "assistant";
|
|
98
145
|
key: string;
|
|
99
146
|
messageId: string;
|
|
147
|
+
turnId?: string;
|
|
100
148
|
blocks: AssistantBlock[];
|
|
101
149
|
copyText: string;
|
|
150
|
+
durationMs?: number;
|
|
102
151
|
timestamp?: number;
|
|
103
152
|
terminal?: { kind: "interrupted" | "error"; title: string; body?: string };
|
|
104
153
|
};
|
|
@@ -122,10 +171,39 @@ function nonNegativeNumber(value: unknown): number | undefined {
|
|
|
122
171
|
: undefined;
|
|
123
172
|
}
|
|
124
173
|
|
|
174
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
175
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
125
178
|
export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
|
|
126
179
|
return recordOf(message.metadata) as CloudOsMessageMetadata;
|
|
127
180
|
}
|
|
128
181
|
|
|
182
|
+
export function requestedCapabilitiesOf(
|
|
183
|
+
metadata: CloudOsMessageMetadata,
|
|
184
|
+
): CloudOsRequestedCapability[] {
|
|
185
|
+
if (!Array.isArray(metadata.requestedCapabilities)) return [];
|
|
186
|
+
const seen = new Set<string>();
|
|
187
|
+
return metadata.requestedCapabilities.flatMap((value) => {
|
|
188
|
+
const capability = recordOf(value);
|
|
189
|
+
if (
|
|
190
|
+
(capability.kind !== "skill" && capability.kind !== "tool" && capability.kind !== "plan") ||
|
|
191
|
+
typeof capability.name !== "string" ||
|
|
192
|
+
!capability.name.trim() ||
|
|
193
|
+
typeof capability.label !== "string" ||
|
|
194
|
+
!capability.label.trim()
|
|
195
|
+
) return [];
|
|
196
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
197
|
+
if (seen.has(key)) return [];
|
|
198
|
+
seen.add(key);
|
|
199
|
+
return [{
|
|
200
|
+
kind: capability.kind,
|
|
201
|
+
name: capability.name,
|
|
202
|
+
label: capability.label,
|
|
203
|
+
}];
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
129
207
|
export function messageText(message: UIMessage): string {
|
|
130
208
|
return message.parts
|
|
131
209
|
.filter(
|
|
@@ -151,6 +229,54 @@ export function formatFullTimestamp(timestamp: number): string {
|
|
|
151
229
|
});
|
|
152
230
|
}
|
|
153
231
|
|
|
232
|
+
function turnDuration(metadata: CloudOsMessageMetadata): number | undefined {
|
|
233
|
+
const explicit = nonNegativeNumber(metadata.turnDurationMs);
|
|
234
|
+
if (explicit !== undefined) return explicit;
|
|
235
|
+
const startedAt = nonNegativeNumber(metadata.turnStartedAt);
|
|
236
|
+
const completedAt = nonNegativeNumber(metadata.completedAt);
|
|
237
|
+
return startedAt !== undefined && completedAt !== undefined
|
|
238
|
+
? Math.max(0, completedAt - startedAt)
|
|
239
|
+
: undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isWorkBlock(block: AssistantBlock): boolean {
|
|
243
|
+
return (
|
|
244
|
+
block.kind === "reasoning" ||
|
|
245
|
+
block.kind === "toolGroup" ||
|
|
246
|
+
block.kind === "plan" ||
|
|
247
|
+
block.kind === "parallel" ||
|
|
248
|
+
block.kind === "subagents"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 把一条 assistant 消息拆成可折叠的工作过程与始终可见的最终内容。
|
|
254
|
+
* 最后一段工作之前的 text 是过程旁白;最后一次工作之后的 text 才是正文。
|
|
255
|
+
*/
|
|
256
|
+
export function partitionAssistantBlocks(blocks: readonly AssistantBlock[]): {
|
|
257
|
+
work: AssistantBlock[];
|
|
258
|
+
content: AssistantBlock[];
|
|
259
|
+
} {
|
|
260
|
+
let lastWorkIndex = -1;
|
|
261
|
+
blocks.forEach((block, index) => {
|
|
262
|
+
if (isWorkBlock(block)) lastWorkIndex = index;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const work: AssistantBlock[] = [];
|
|
266
|
+
const content: AssistantBlock[] = [];
|
|
267
|
+
blocks.forEach((block, index) => {
|
|
268
|
+
if (
|
|
269
|
+
isWorkBlock(block) ||
|
|
270
|
+
(block.kind === "text" && index < lastWorkIndex)
|
|
271
|
+
) {
|
|
272
|
+
work.push(block);
|
|
273
|
+
} else {
|
|
274
|
+
content.push(block);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return { work, content };
|
|
278
|
+
}
|
|
279
|
+
|
|
154
280
|
function toolNameOf(part: unknown): string | null {
|
|
155
281
|
const type = String(recordOf(part).type ?? "");
|
|
156
282
|
if (type === "dynamic-tool") {
|
|
@@ -198,6 +324,74 @@ function normalizeStatus(value: unknown): ParallelTool["status"] {
|
|
|
198
324
|
return "pending";
|
|
199
325
|
}
|
|
200
326
|
|
|
327
|
+
function actionPresentationOf(value: unknown): CloudOsActionPresentation | null {
|
|
328
|
+
const presentation = recordOf(recordOf(value).presentation);
|
|
329
|
+
const state = presentation.state;
|
|
330
|
+
if (
|
|
331
|
+
presentation.type !== "action-execution" ||
|
|
332
|
+
typeof presentation.executionId !== "string" ||
|
|
333
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
334
|
+
presentation.executionId,
|
|
335
|
+
) ||
|
|
336
|
+
(state !== "running" &&
|
|
337
|
+
state !== "ready" &&
|
|
338
|
+
state !== "failed" &&
|
|
339
|
+
state !== "outcome_unknown") ||
|
|
340
|
+
!Array.isArray(presentation.content)
|
|
341
|
+
) return null;
|
|
342
|
+
|
|
343
|
+
let produces: CloudOsActionProduces | undefined;
|
|
344
|
+
if (presentation.produces !== undefined) {
|
|
345
|
+
const hint = recordOf(presentation.produces);
|
|
346
|
+
if (
|
|
347
|
+
!["image", "audio", "video", "document", "archive", "data", "other"].includes(
|
|
348
|
+
String(hint.type),
|
|
349
|
+
) ||
|
|
350
|
+
(hint.mediaType !== undefined &&
|
|
351
|
+
(typeof hint.mediaType !== "string" || !/^[^\s/]+\/[^\s/]+$/.test(hint.mediaType)))
|
|
352
|
+
) return null;
|
|
353
|
+
produces = {
|
|
354
|
+
type: hint.type as CloudOsActionProduces["type"],
|
|
355
|
+
...(hint.mediaType === undefined ? {} : { mediaType: hint.mediaType as string }),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (state !== "running" && produces) return null;
|
|
359
|
+
|
|
360
|
+
const content: CloudOsActionPresentationFile[] = [];
|
|
361
|
+
for (const value of presentation.content) {
|
|
362
|
+
const file = recordOf(value);
|
|
363
|
+
if (file.type !== "file" || typeof file.url !== "string") return null;
|
|
364
|
+
try {
|
|
365
|
+
if (new URL(file.url).protocol !== "https:") return null;
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (
|
|
370
|
+
file.mediaType !== undefined &&
|
|
371
|
+
(typeof file.mediaType !== "string" || !file.mediaType.trim())
|
|
372
|
+
) return null;
|
|
373
|
+
content.push({
|
|
374
|
+
type: "file" as const,
|
|
375
|
+
url: file.url,
|
|
376
|
+
...(file.mediaType !== undefined
|
|
377
|
+
? { mediaType: file.mediaType }
|
|
378
|
+
: {}),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (state !== "ready" && content.length > 0) return null;
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
type: "action-execution",
|
|
385
|
+
executionId: presentation.executionId,
|
|
386
|
+
state,
|
|
387
|
+
...(state === "failed" && presentation.failureCode === "insufficient_credits"
|
|
388
|
+
? { failureCode: presentation.failureCode }
|
|
389
|
+
: {}),
|
|
390
|
+
...(produces ? { produces } : {}),
|
|
391
|
+
content,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
201
395
|
// ── 独立消息(非 assistant 回合)判定 ────────────────────────────────────────
|
|
202
396
|
|
|
203
397
|
function standaloneEntry(
|
|
@@ -285,7 +479,12 @@ function assistantBlocks(
|
|
|
285
479
|
const text = String(record.text ?? "").trim();
|
|
286
480
|
if (!text || !showThinkingTraces) return;
|
|
287
481
|
flush();
|
|
288
|
-
blocks.push({
|
|
482
|
+
blocks.push({
|
|
483
|
+
kind: "reasoning",
|
|
484
|
+
key,
|
|
485
|
+
text,
|
|
486
|
+
running: record.state === "streaming",
|
|
487
|
+
});
|
|
289
488
|
return;
|
|
290
489
|
}
|
|
291
490
|
|
|
@@ -343,15 +542,32 @@ function assistantBlocks(
|
|
|
343
542
|
const data = dataOf(part);
|
|
344
543
|
const src = String(data.src ?? data.url ?? record.url ?? "");
|
|
345
544
|
const mediaType = String(record.mediaType ?? data.mediaType ?? "");
|
|
346
|
-
if (src
|
|
347
|
-
|
|
545
|
+
if (!src) return;
|
|
546
|
+
|
|
547
|
+
flush();
|
|
548
|
+
const filename = String(
|
|
549
|
+
data.alt ?? data.filename ?? record.filename ?? "Attachment",
|
|
550
|
+
);
|
|
551
|
+
if (dataKind === "image" || mediaType.startsWith("image/")) {
|
|
348
552
|
blocks.push({
|
|
349
553
|
kind: "image",
|
|
350
554
|
key,
|
|
351
555
|
src,
|
|
352
|
-
alt:
|
|
556
|
+
alt: filename,
|
|
557
|
+
mediaType: mediaType || undefined,
|
|
353
558
|
});
|
|
559
|
+
return;
|
|
354
560
|
}
|
|
561
|
+
|
|
562
|
+
blocks.push({
|
|
563
|
+
kind: "file",
|
|
564
|
+
key,
|
|
565
|
+
file: {
|
|
566
|
+
url: src,
|
|
567
|
+
filename,
|
|
568
|
+
mediaType: mediaType || undefined,
|
|
569
|
+
},
|
|
570
|
+
});
|
|
355
571
|
return;
|
|
356
572
|
}
|
|
357
573
|
|
|
@@ -359,6 +575,13 @@ function assistantBlocks(
|
|
|
359
575
|
if (toolName === null) return;
|
|
360
576
|
|
|
361
577
|
const call = toCloudOsToolCall(part, key, isActive);
|
|
578
|
+
const presentation = actionPresentationOf(call.output);
|
|
579
|
+
if (presentation) {
|
|
580
|
+
pendingCalls.push(call);
|
|
581
|
+
flush();
|
|
582
|
+
blocks.push({ kind: "actionPresentation", key, presentation });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
362
585
|
|
|
363
586
|
if (call.awaitingApproval) {
|
|
364
587
|
const approvalId = approvalIdOf(part);
|
|
@@ -379,6 +602,7 @@ function assistantBlocks(
|
|
|
379
602
|
}
|
|
380
603
|
|
|
381
604
|
if (call.kind === "ask-user") {
|
|
605
|
+
if (call.failed) return;
|
|
382
606
|
flush();
|
|
383
607
|
blocks.push({
|
|
384
608
|
kind: "askUser",
|
|
@@ -455,6 +679,9 @@ export function buildCloudOsEntries({
|
|
|
455
679
|
}
|
|
456
680
|
return -1;
|
|
457
681
|
})();
|
|
682
|
+
const activeTurnId = isActive && lastAssistantIndex >= 0
|
|
683
|
+
? nonEmptyString(cloudOsMetadata(messages[lastAssistantIndex]!).turnId)
|
|
684
|
+
: undefined;
|
|
458
685
|
|
|
459
686
|
messages.forEach((message, index) => {
|
|
460
687
|
if (message.role === "system") return;
|
|
@@ -488,6 +715,7 @@ export function buildCloudOsEntries({
|
|
|
488
715
|
messageId: message.id,
|
|
489
716
|
text,
|
|
490
717
|
attachments,
|
|
718
|
+
capabilities: requestedCapabilitiesOf(metadata),
|
|
491
719
|
authorName: metadata.authorDisplayName,
|
|
492
720
|
timestamp:
|
|
493
721
|
nonNegativeNumber(metadata.createdAt) ??
|
|
@@ -508,8 +736,10 @@ export function buildCloudOsEntries({
|
|
|
508
736
|
type: "assistant",
|
|
509
737
|
key: `assistant-${message.id}-${index}`,
|
|
510
738
|
messageId: message.id,
|
|
739
|
+
turnId: nonEmptyString(metadata.turnId),
|
|
511
740
|
blocks,
|
|
512
741
|
copyText: messageText(message),
|
|
742
|
+
durationMs: turnDuration(metadata),
|
|
513
743
|
timestamp:
|
|
514
744
|
nonNegativeNumber(metadata.completedAt) ??
|
|
515
745
|
nonNegativeNumber(metadata.createdAt),
|
|
@@ -517,7 +747,100 @@ export function buildCloudOsEntries({
|
|
|
517
747
|
});
|
|
518
748
|
});
|
|
519
749
|
|
|
520
|
-
return
|
|
750
|
+
return aggregateCompletedTurnWork(
|
|
751
|
+
dropSupersededActionPresentations(dropSupersededPlans(entries)),
|
|
752
|
+
activeTurnId,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Collapse every completed Turn's assistant segments into one display entry. */
|
|
757
|
+
function aggregateCompletedTurnWork(
|
|
758
|
+
entries: CloudOsEntry[],
|
|
759
|
+
activeTurnId: string | undefined,
|
|
760
|
+
): CloudOsEntry[] {
|
|
761
|
+
const indexesByTurn = new Map<string, number[]>();
|
|
762
|
+
entries.forEach((entry, index) => {
|
|
763
|
+
if (entry.type !== "assistant" || !entry.turnId) return;
|
|
764
|
+
const indexes = indexesByTurn.get(entry.turnId) ?? [];
|
|
765
|
+
indexes.push(index);
|
|
766
|
+
indexesByTurn.set(entry.turnId, indexes);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const hidden = new Set<number>();
|
|
770
|
+
const replacements = new Map<number, CloudOsEntry>();
|
|
771
|
+
for (const [turnId, indexes] of indexesByTurn) {
|
|
772
|
+
if (turnId === activeTurnId || indexes.length < 2) continue;
|
|
773
|
+
const assistantEntries = indexes.map(
|
|
774
|
+
(index) => entries[index] as Extract<CloudOsEntry, { type: "assistant" }>,
|
|
775
|
+
);
|
|
776
|
+
const anchorIndex = indexes[indexes.length - 1]!;
|
|
777
|
+
const anchor = assistantEntries[assistantEntries.length - 1]!;
|
|
778
|
+
const durations = assistantEntries.flatMap((entry) =>
|
|
779
|
+
entry.durationMs === undefined ? [] : [entry.durationMs]
|
|
780
|
+
);
|
|
781
|
+
const timestamps = assistantEntries.flatMap((entry) =>
|
|
782
|
+
entry.timestamp === undefined ? [] : [entry.timestamp]
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
indexes.slice(0, -1).forEach((index) => hidden.add(index));
|
|
786
|
+
replacements.set(anchorIndex, {
|
|
787
|
+
...anchor,
|
|
788
|
+
key: `assistant-turn-${turnId}`,
|
|
789
|
+
blocks: assistantEntries.flatMap((entry) => entry.blocks),
|
|
790
|
+
...(durations.length > 0 ? { durationMs: Math.max(...durations) } : {}),
|
|
791
|
+
...(timestamps.length > 0 ? { timestamp: Math.max(...timestamps) } : {}),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return entries.flatMap((entry, index) =>
|
|
796
|
+
hidden.has(index) ? [] : [replacements.get(index) ?? entry]
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
801
|
+
const snapshots = new Map<string, {
|
|
802
|
+
entryIndex: number;
|
|
803
|
+
blockIndex: number;
|
|
804
|
+
presentation: CloudOsActionPresentation;
|
|
805
|
+
}>();
|
|
806
|
+
entries.forEach((entry, entryIndex) => {
|
|
807
|
+
if (entry.type !== "assistant") return;
|
|
808
|
+
entry.blocks.forEach((block, blockIndex) => {
|
|
809
|
+
if (block.kind !== "actionPresentation") return;
|
|
810
|
+
const current = snapshots.get(block.presentation.executionId);
|
|
811
|
+
if (!current) {
|
|
812
|
+
snapshots.set(block.presentation.executionId, {
|
|
813
|
+
entryIndex,
|
|
814
|
+
blockIndex,
|
|
815
|
+
presentation: block.presentation,
|
|
816
|
+
});
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (block.presentation.state !== "running") {
|
|
820
|
+
current.presentation = block.presentation;
|
|
821
|
+
} else if (current.presentation.state === "running") {
|
|
822
|
+
current.presentation = {
|
|
823
|
+
...block.presentation,
|
|
824
|
+
produces: block.presentation.produces ?? current.presentation.produces,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
return entries.map((entry, entryIndex) =>
|
|
831
|
+
entry.type !== "assistant"
|
|
832
|
+
? entry
|
|
833
|
+
: {
|
|
834
|
+
...entry,
|
|
835
|
+
blocks: entry.blocks.flatMap<AssistantBlock>((block, blockIndex) => {
|
|
836
|
+
if (block.kind !== "actionPresentation") return [block];
|
|
837
|
+
const snapshot = snapshots.get(block.presentation.executionId)!;
|
|
838
|
+
return snapshot.entryIndex === entryIndex && snapshot.blockIndex === blockIndex
|
|
839
|
+
? [{ ...block, presentation: snapshot.presentation }]
|
|
840
|
+
: [];
|
|
841
|
+
}),
|
|
842
|
+
}
|
|
843
|
+
);
|
|
521
844
|
}
|
|
522
845
|
|
|
523
846
|
/**
|
|
@@ -562,75 +885,124 @@ export interface CloudOsActivity {
|
|
|
562
885
|
label: string;
|
|
563
886
|
}
|
|
564
887
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
888
|
+
const SEARCHING_TOOL_KINDS: ReadonlySet<CloudOsToolKind> = new Set([
|
|
889
|
+
"read",
|
|
890
|
+
"glob",
|
|
891
|
+
"grep",
|
|
892
|
+
"list",
|
|
893
|
+
"web-search",
|
|
894
|
+
"web-fetch",
|
|
895
|
+
]);
|
|
896
|
+
const SHAPING_TOOL_KINDS: ReadonlySet<CloudOsToolKind> = new Set([
|
|
897
|
+
"write",
|
|
898
|
+
"edit",
|
|
899
|
+
"tasks",
|
|
900
|
+
"create-agent",
|
|
901
|
+
"publish-extension",
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
function activityForTool(call: CloudOsToolCall | undefined): CloudOsActivity {
|
|
905
|
+
if (!call) return { state: "working", label: "Working…" };
|
|
906
|
+
const name = call.toolName.replaceAll("-", "_").toLowerCase();
|
|
907
|
+
if (call.kind === "agent") {
|
|
908
|
+
return {
|
|
909
|
+
state: "weaving",
|
|
910
|
+
label: call.running ? "Coordinating agents…" : "Combining results…",
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
if (SEARCHING_TOOL_KINDS.has(call.kind)) {
|
|
914
|
+
return {
|
|
915
|
+
state: "searching",
|
|
916
|
+
label: call.running ? "Searching…" : "Reading results…",
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
if (SHAPING_TOOL_KINDS.has(call.kind)) {
|
|
920
|
+
return { state: "shaping", label: "Shaping result…" };
|
|
921
|
+
}
|
|
922
|
+
if (call.kind === "skill" || name === "activate_skill") {
|
|
923
|
+
return { state: "working", label: "Loading skill…" };
|
|
924
|
+
}
|
|
925
|
+
if (name === "run_skill_script") {
|
|
926
|
+
return { state: "working", label: "Running skill…" };
|
|
927
|
+
}
|
|
928
|
+
return { state: "working", label: "Working…" };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function activityForTail(block: AssistantBlock | undefined): CloudOsActivity {
|
|
932
|
+
if (!block) return { state: "solving", label: "Thinking…" };
|
|
573
933
|
switch (block.kind) {
|
|
574
|
-
case "text":
|
|
575
934
|
case "reasoning":
|
|
576
|
-
return
|
|
935
|
+
return { state: "solving", label: "Reasoning…" };
|
|
936
|
+
case "text":
|
|
937
|
+
case "suggestions":
|
|
938
|
+
return { state: "composing", label: "Composing answer…" };
|
|
577
939
|
case "toolGroup":
|
|
578
|
-
return block.group.
|
|
940
|
+
return activityForTool(block.group.calls.at(-1));
|
|
579
941
|
case "plan":
|
|
580
|
-
|
|
942
|
+
case "image":
|
|
943
|
+
case "file":
|
|
944
|
+
return { state: "shaping", label: "Shaping result…" };
|
|
581
945
|
case "askUser":
|
|
582
|
-
return block.pending
|
|
583
|
-
|
|
946
|
+
return block.pending
|
|
947
|
+
? { state: "listening", label: "Waiting for your answer…" }
|
|
948
|
+
: { state: "solving", label: "Picking up your answer…" };
|
|
584
949
|
case "approval":
|
|
585
|
-
return
|
|
950
|
+
return { state: "breathing", label: "Waiting for approval…" };
|
|
951
|
+
case "schedule":
|
|
952
|
+
return block.call.awaitingApproval
|
|
953
|
+
? { state: "breathing", label: "Waiting for approval…" }
|
|
954
|
+
: { state: "working", label: "Working…" };
|
|
586
955
|
case "parallel":
|
|
587
|
-
return
|
|
956
|
+
return {
|
|
957
|
+
state: "weaving",
|
|
958
|
+
label: block.tools.some((tool) => tool.status === "running")
|
|
959
|
+
? "Coordinating tasks…"
|
|
960
|
+
: "Combining results…",
|
|
961
|
+
};
|
|
588
962
|
case "subagents":
|
|
589
|
-
return
|
|
963
|
+
return {
|
|
964
|
+
state: "weaving",
|
|
965
|
+
label: block.agents.some((agent) => agent.status === "running")
|
|
966
|
+
? "Coordinating agents…"
|
|
967
|
+
: "Combining results…",
|
|
968
|
+
};
|
|
590
969
|
default:
|
|
591
|
-
return
|
|
970
|
+
return { state: "working", label: "Working…" };
|
|
592
971
|
}
|
|
593
972
|
}
|
|
594
973
|
|
|
595
|
-
// 已结算的尾巴 → 此刻真正在发生的事。措辞刻意只说得出口的那部分:
|
|
596
|
-
// 工具跑完了,模型在读它的输出;卡答完了,模型在接你的答案。
|
|
597
|
-
function activityForTail(block: AssistantBlock | undefined): CloudOsActivity {
|
|
598
|
-
if (!block) return { state: "solving", label: "Thinking…" };
|
|
599
|
-
if (block.kind === "askUser") {
|
|
600
|
-
return { state: "solving", label: "Picking up your answer…" };
|
|
601
|
-
}
|
|
602
|
-
if (block.kind === "toolGroup") {
|
|
603
|
-
const kind = block.group.calls.at(-1)?.kind;
|
|
604
|
-
return kind !== undefined &&
|
|
605
|
-
["web-search", "web-fetch", "grep", "glob", "list", "read"].includes(
|
|
606
|
-
kind,
|
|
607
|
-
)
|
|
608
|
-
? { state: "searching", label: "Reading results…" }
|
|
609
|
-
: { state: "working", label: "Working…" };
|
|
610
|
-
}
|
|
611
|
-
return { state: "working", label: "Working…" };
|
|
612
|
-
}
|
|
613
|
-
|
|
614
974
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* @remarks
|
|
618
|
-
* 判据是「**尾巴有没有活口**」,不是「本回合有没有出过内容」。后者曾经是这里的
|
|
619
|
-
* 写法,于是答完 `ask_user`(卡片变成 Submitted,读起来就是"结束了")到模型下一
|
|
620
|
-
* 个 chunk 之间整屏没有任何在跑的迹象 —— 而那段恰好是最长的一次 TTFT,看起来
|
|
621
|
-
* 就是卡死。凡是尾巴为**已结算卡片**的断档都是同一个洞,这里一并补上。
|
|
975
|
+
* 回合活着时始终返回一个 Orb 状态;只有回合终止时才返回 `null`。
|
|
622
976
|
*/
|
|
623
977
|
export function deriveTurnActivity(
|
|
624
978
|
entries: readonly CloudOsEntry[],
|
|
625
|
-
options: {
|
|
979
|
+
options: {
|
|
980
|
+
isActive: boolean;
|
|
981
|
+
isRecovering?: boolean;
|
|
982
|
+
recoveryStatusLabel?: string;
|
|
983
|
+
awaitingApproval?: boolean;
|
|
984
|
+
hasPendingSteer?: boolean;
|
|
985
|
+
},
|
|
626
986
|
): CloudOsActivity | null {
|
|
987
|
+
if (options.isRecovering) {
|
|
988
|
+
return {
|
|
989
|
+
state: "connecting",
|
|
990
|
+
label: options.recoveryStatusLabel ?? "Reconnecting…",
|
|
991
|
+
};
|
|
992
|
+
}
|
|
627
993
|
if (!options.isActive) return null;
|
|
994
|
+
if (options.awaitingApproval) {
|
|
995
|
+
return { state: "breathing", label: "Waiting for approval…" };
|
|
996
|
+
}
|
|
997
|
+
if (options.hasPendingSteer) {
|
|
998
|
+
return {
|
|
999
|
+
state: "breathing",
|
|
1000
|
+
label: "Waiting for the current step to finish…",
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
628
1003
|
const tail = entries.at(-1) ?? null;
|
|
629
1004
|
const block = tail?.type === "assistant" ? tail.blocks.at(-1) : undefined;
|
|
630
|
-
|
|
631
|
-
return options.hasPendingSteer
|
|
632
|
-
? { state: "solving", label: "Waiting for the current step to finish…" }
|
|
633
|
-
: activityForTail(block);
|
|
1005
|
+
return activityForTail(block);
|
|
634
1006
|
}
|
|
635
1007
|
|
|
636
1008
|
// ── 行间距节奏(照搬原 rhythmTopClass)──────────────────────────────────────
|