@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.
- 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/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +130 -70
- package/cloud-os/chat/markdown-message.tsx +85 -40
- package/cloud-os/chat/rich-blocks.tsx +494 -190
- package/cloud-os/chat/tool-presentation.ts +67 -6
- package/cloud-os/chat/tool-rows.tsx +87 -43
- package/cloud-os/chat/transcript-model.ts +215 -23
- package/cloud-os/composer/cloud-os-chat-input.tsx +242 -127
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +266 -0
- package/cloud-os/index.ts +29 -2
- package/cloud-os/layout/cloud-os-workspace-split.tsx +107 -24
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +93 -19
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +100 -40
- package/demo/cloud-os-chat-showcase.tsx +51 -17
- 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 +82 -77
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/camel/camel-tool-presentation.tsx +19 -15
- package/src/camel/camel-turn.ts +1 -17
- 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/contracts.ts +0 -2
- package/src/message-panel.tsx +0 -23
- package/src/parts/index.tsx +102 -63
- package/src/styles/index.css +4 -1
|
@@ -145,7 +145,7 @@ function truncate(value: string, maxLength: number): string {
|
|
|
145
145
|
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function
|
|
148
|
+
export function humanizeToolName(name: string): string {
|
|
149
149
|
const spaced = name
|
|
150
150
|
.replace(/^mcp__/, "")
|
|
151
151
|
.replace(/__/g, " ")
|
|
@@ -298,7 +298,8 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
298
298
|
return { verb: running ? "Editing" : "Edited", target: filename || undefined };
|
|
299
299
|
case "bash": {
|
|
300
300
|
const command =
|
|
301
|
-
stringField(input, "description") ||
|
|
301
|
+
stringField(input, "description") ||
|
|
302
|
+
stringField(input, "script", "command", "cmd");
|
|
302
303
|
return {
|
|
303
304
|
verb: running ? "Running" : "Ran",
|
|
304
305
|
target: command ? truncate(command, 60) : undefined,
|
|
@@ -343,7 +344,7 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
343
344
|
case "agent": {
|
|
344
345
|
const name =
|
|
345
346
|
stringField(input, "subagentName", "agent", "name", "agentType") ||
|
|
346
|
-
|
|
347
|
+
humanizeToolName(toolName);
|
|
347
348
|
return { verb: running ? "Running" : "Ran", target: name };
|
|
348
349
|
}
|
|
349
350
|
case "skill": {
|
|
@@ -378,13 +379,15 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
378
379
|
const [, server = "", ...rest] = toolName.split("__");
|
|
379
380
|
return {
|
|
380
381
|
verb: running ? "Calling" : "Called",
|
|
381
|
-
target: rest.length
|
|
382
|
+
target: rest.length
|
|
383
|
+
? `${humanizeToolName(rest.join(" "))} · ${humanizeToolName(server)}`
|
|
384
|
+
: undefined,
|
|
382
385
|
};
|
|
383
386
|
}
|
|
384
387
|
case "generic":
|
|
385
388
|
break;
|
|
386
389
|
}
|
|
387
|
-
const displayName =
|
|
390
|
+
const displayName = humanizeToolName(toolName);
|
|
388
391
|
return { verb: running ? `Using ${displayName}` : `Used ${displayName}` };
|
|
389
392
|
}
|
|
390
393
|
|
|
@@ -435,7 +438,7 @@ export function describeToolCallCount(
|
|
|
435
438
|
case "generic":
|
|
436
439
|
break;
|
|
437
440
|
}
|
|
438
|
-
const displayName =
|
|
441
|
+
const displayName = humanizeToolName(toolName);
|
|
439
442
|
return count === 1 ? `Used ${displayName}` : `Used ${displayName} ${formatTimes(count)}`;
|
|
440
443
|
}
|
|
441
444
|
|
|
@@ -489,6 +492,64 @@ export function toCloudOsToolCall(
|
|
|
489
492
|
};
|
|
490
493
|
}
|
|
491
494
|
|
|
495
|
+
// ── ask_user 的终局 ─────────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* `ask_user` 这次调用的三种终局,全部只看它自己的 state 和 output。
|
|
499
|
+
*
|
|
500
|
+
* - `pending`:还等着用户操作,选项可点。**只有 input 态才算 pending** ——
|
|
501
|
+
* 服务端一旦结算,这次 interaction 就不再接受响应。
|
|
502
|
+
* - `answered`:`ask_user` 的 settle 写了按问题对应的 `{ answers }`。
|
|
503
|
+
* - `closed`:被结算掉了但不是一个答案 —— 用户改成直接发言(`{ cancelled: true }`)、
|
|
504
|
+
* Turn 提前结束、或工具出错。必须和 pending 分开,否则卡片会留一个点了拿
|
|
505
|
+
* `{ ok: false }` 的假 Submit,读起来就是「点了没反应」。
|
|
506
|
+
*
|
|
507
|
+
* 判据放在这里而不是卡片里:卡片(能不能点)和底部活动指示器(还在不在等人)
|
|
508
|
+
* 都要用它,两处各判一次迟早会分叉。
|
|
509
|
+
*/
|
|
510
|
+
export type AskUserOutcome =
|
|
511
|
+
| { kind: "pending" }
|
|
512
|
+
| {
|
|
513
|
+
kind: "answered";
|
|
514
|
+
answers: Array<{
|
|
515
|
+
question: string;
|
|
516
|
+
selections: string[];
|
|
517
|
+
text: string;
|
|
518
|
+
}>;
|
|
519
|
+
}
|
|
520
|
+
| { kind: "closed"; reason: "user_replied_freeform" | "other" };
|
|
521
|
+
|
|
522
|
+
export function askUserOutcome(call: CloudOsToolCall): AskUserOutcome {
|
|
523
|
+
if (call.state === "input-streaming" || call.state === "input-available") {
|
|
524
|
+
return { kind: "pending" };
|
|
525
|
+
}
|
|
526
|
+
const record = recordOf(call.output);
|
|
527
|
+
if (Array.isArray(record.answers) && record.answers.length > 0) {
|
|
528
|
+
const answers = record.answers.map(recordOf);
|
|
529
|
+
if (answers.every((answer) =>
|
|
530
|
+
typeof answer.question === "string" &&
|
|
531
|
+
Array.isArray(answer.selections) &&
|
|
532
|
+
answer.selections.every((value) => typeof value === "string") &&
|
|
533
|
+
(answer.text === undefined || typeof answer.text === "string")
|
|
534
|
+
)) {
|
|
535
|
+
return {
|
|
536
|
+
kind: "answered",
|
|
537
|
+
answers: answers.map((answer) => ({
|
|
538
|
+
question: answer.question as string,
|
|
539
|
+
selections: answer.selections as string[],
|
|
540
|
+
text: typeof answer.text === "string" ? answer.text : "",
|
|
541
|
+
})),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
kind: "closed",
|
|
547
|
+
reason: record.reason === "user_replied_freeform"
|
|
548
|
+
? "user_replied_freeform"
|
|
549
|
+
: "other",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
492
553
|
// ── 分组(逐行对应原 buildToolCallGroups)────────────────────────────────────
|
|
493
554
|
|
|
494
555
|
export function buildToolCallGroups(calls: CloudOsToolCall[]): ToolCallGroup[] {
|
|
@@ -1,40 +1,60 @@
|
|
|
1
|
-
import { CaretRight } from "@phosphor-icons/react";
|
|
2
|
-
import { memo } from "react";
|
|
1
|
+
import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
|
|
2
|
+
import { memo, useId, useState } from "react";
|
|
3
3
|
import { MarkdownMessage } from "./markdown-message";
|
|
4
4
|
import {
|
|
5
5
|
getToolCallSummary,
|
|
6
6
|
getToolIcon,
|
|
7
7
|
normalizeOutputText,
|
|
8
|
-
safeStringify,
|
|
9
8
|
type CloudOsToolCall,
|
|
10
9
|
type PhosphorIcon,
|
|
11
10
|
type ToolCallGroup,
|
|
12
11
|
} from "./tool-presentation";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
* 工作行(work rows)
|
|
16
|
-
*
|
|
17
|
-
* className 一个字没改;改的只有取数——从 gadgets 的 AiToolCall 换成
|
|
18
|
-
* UIMessage 归一化后的 CloudOsToolCall。
|
|
14
|
+
* 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
|
|
15
|
+
* ToolGroupRow / ThinkingTraceRow。渲染 UIMessage 归一化后的 CloudOsToolCall。
|
|
19
16
|
*/
|
|
20
17
|
|
|
21
18
|
export function WorkIcon({ Icon }: { Icon: PhosphorIcon }) {
|
|
22
19
|
return <Icon size={15} className="text-kumo-inactive" />;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
function
|
|
26
|
-
if (
|
|
22
|
+
function displayOutput(value: unknown, fallback: string): string {
|
|
23
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
const record = value as Record<string, unknown>;
|
|
27
|
+
if (typeof record.summary === "string" && record.summary.trim()) {
|
|
28
|
+
return record.summary.trim();
|
|
29
|
+
}
|
|
30
|
+
const displayValue = (item: unknown): string =>
|
|
31
|
+
item != null && typeof item === "object"
|
|
32
|
+
? Object.entries(item).map(([key, child]) => `${key}: ${displayValue(child)}`).join("\n")
|
|
33
|
+
: String(item ?? "");
|
|
34
|
+
return displayValue(record);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function OutputPre({ value }: { value: string }) {
|
|
27
38
|
return (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
<div
|
|
40
|
+
className="rounded-xl border border-kumo-line/70 bg-kumo-elevated/45 px-4 py-3 text-kumo-subtle"
|
|
41
|
+
data-output-card=""
|
|
42
|
+
>
|
|
43
|
+
<div className="flex items-center justify-between gap-3 text-[14px] font-medium leading-5">
|
|
44
|
+
<span>Output</span>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={() => navigator.clipboard.writeText(value)}
|
|
48
|
+
className="flex h-4 w-4 items-center justify-center text-kumo-subtle transition-colors hover:text-kumo-default focus-visible:outline-none focus-visible:text-kumo-default"
|
|
49
|
+
aria-label="Copy output"
|
|
50
|
+
>
|
|
51
|
+
<Copy size={16} />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
<pre className="mt-2 max-h-56 overflow-auto border-t border-kumo-line/70 pt-3 font-sans text-[14px] font-normal leading-5 whitespace-pre-wrap">
|
|
35
55
|
{value}
|
|
36
56
|
</pre>
|
|
37
|
-
|
|
57
|
+
</div>
|
|
38
58
|
);
|
|
39
59
|
}
|
|
40
60
|
|
|
@@ -43,14 +63,7 @@ export const ToolCallDetails = memo(function ToolCallDetails({
|
|
|
43
63
|
}: {
|
|
44
64
|
toolCall: CloudOsToolCall;
|
|
45
65
|
}) {
|
|
46
|
-
const
|
|
47
|
-
typeof tc.input.code === "string"
|
|
48
|
-
? tc.input.code
|
|
49
|
-
: typeof tc.input.command === "string"
|
|
50
|
-
? tc.input.command
|
|
51
|
-
: "";
|
|
52
|
-
const inputText =
|
|
53
|
-
Object.keys(tc.input).length > 0 ? safeStringify(tc.input) : "";
|
|
66
|
+
const output = displayOutput(tc.output, tc.outputText);
|
|
54
67
|
return (
|
|
55
68
|
<div className="space-y-2">
|
|
56
69
|
{tc.errorText && (
|
|
@@ -58,18 +71,8 @@ export const ToolCallDetails = memo(function ToolCallDetails({
|
|
|
58
71
|
{tc.errorText}
|
|
59
72
|
</pre>
|
|
60
73
|
)}
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
<OutputPre label="Code" value={code} />
|
|
64
|
-
<OutputPre label="Output" value={tc.outputText} />
|
|
65
|
-
</>
|
|
66
|
-
) : (
|
|
67
|
-
<>
|
|
68
|
-
<OutputPre value={inputText} />
|
|
69
|
-
{tc.outputText && <OutputPre label="Output" value={tc.outputText} />}
|
|
70
|
-
</>
|
|
71
|
-
)}
|
|
72
|
-
{!code && !inputText && !tc.outputText && !tc.errorText && (
|
|
74
|
+
{output && <OutputPre value={output} />}
|
|
75
|
+
{!output && !tc.errorText && (
|
|
73
76
|
<p className="m-0 text-[12px] leading-4 text-kumo-inactive">
|
|
74
77
|
No additional data
|
|
75
78
|
</p>
|
|
@@ -118,7 +121,7 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
118
121
|
</span>
|
|
119
122
|
</button>
|
|
120
123
|
{open && (
|
|
121
|
-
<div className="
|
|
124
|
+
<div className="ml-8 mt-1 space-y-3">
|
|
122
125
|
<ToolCallDetails toolCall={tc} />
|
|
123
126
|
</div>
|
|
124
127
|
)}
|
|
@@ -128,14 +131,55 @@ export const NestedToolCallRow = memo(function NestedToolCallRow({
|
|
|
128
131
|
|
|
129
132
|
export const ThinkingTraceRow = memo(function ThinkingTraceRow({
|
|
130
133
|
reasoning,
|
|
134
|
+
running,
|
|
131
135
|
}: {
|
|
132
136
|
reasoning: string;
|
|
137
|
+
running: boolean;
|
|
133
138
|
}) {
|
|
139
|
+
const [open, setOpen] = useState(false);
|
|
140
|
+
const contentId = useId();
|
|
141
|
+
const summary = reasoning.replace(/\s+/g, " ").trim();
|
|
142
|
+
|
|
134
143
|
return (
|
|
135
|
-
<div
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
<div
|
|
145
|
+
className="flex min-w-0 flex-col items-start gap-2"
|
|
146
|
+
data-thinking-trace=""
|
|
147
|
+
data-state={open ? "open" : "closed"}
|
|
148
|
+
>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => setOpen((current) => !current)}
|
|
152
|
+
className="group -ml-0.5 flex w-fit max-w-[80%] min-w-0 items-center gap-1.5 rounded-xl px-1.5 py-1 text-left text-[14px] leading-5 text-kumo-inactive transition-colors hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
|
|
153
|
+
aria-expanded={open}
|
|
154
|
+
aria-controls={contentId}
|
|
155
|
+
>
|
|
156
|
+
<span className="relative grid size-4 flex-shrink-0 place-items-center" aria-hidden="true">
|
|
157
|
+
<Atom
|
|
158
|
+
size={14}
|
|
159
|
+
className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
|
|
160
|
+
/>
|
|
161
|
+
<CaretDown
|
|
162
|
+
size={14}
|
|
163
|
+
className={`absolute opacity-0 transition-[opacity,transform] group-hover:opacity-100 group-focus-visible:opacity-100 ${open ? "" : "-rotate-90"}`}
|
|
164
|
+
/>
|
|
165
|
+
</span>
|
|
166
|
+
<span className="flex-shrink-0 text-kumo-subtle">Think</span>
|
|
167
|
+
<span aria-hidden="true">·</span>
|
|
168
|
+
<span
|
|
169
|
+
className="min-w-0 flex-1 truncate"
|
|
170
|
+
style={running ? { direction: "rtl" } : undefined}
|
|
171
|
+
>
|
|
172
|
+
<bdi dir="auto">{summary}</bdi>
|
|
173
|
+
</span>
|
|
174
|
+
</button>
|
|
175
|
+
{open && (
|
|
176
|
+
<div
|
|
177
|
+
id={contentId}
|
|
178
|
+
className="cos-markdown w-full rounded-xl border border-kumo-line bg-kumo-elevated/45 px-4 py-3 text-[14px] leading-[1.4] text-kumo-default/70"
|
|
179
|
+
>
|
|
180
|
+
<MarkdownMessage message={reasoning} />
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
139
183
|
</div>
|
|
140
184
|
);
|
|
141
185
|
});
|
|
@@ -189,7 +233,7 @@ export const ToolGroupRow = memo(function ToolGroupRow({
|
|
|
189
233
|
</button>
|
|
190
234
|
{open &&
|
|
191
235
|
(group.calls.length === 1 ? (
|
|
192
|
-
<div className="
|
|
236
|
+
<div className="ml-8 mt-1 space-y-3">
|
|
193
237
|
<ToolCallDetails toolCall={group.calls[0]} />
|
|
194
238
|
</div>
|
|
195
239
|
) : (
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
+
import type { OrbState } from "thinking-orbs";
|
|
2
3
|
import {
|
|
4
|
+
askUserOutcome,
|
|
3
5
|
buildToolCallGroups,
|
|
4
6
|
toCloudOsToolCall,
|
|
5
7
|
type CloudOsToolCall,
|
|
8
|
+
type CloudOsToolKind,
|
|
6
9
|
type ToolCallGroup,
|
|
7
10
|
} from "./tool-presentation";
|
|
8
11
|
|
|
@@ -27,6 +30,13 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
|
27
30
|
turnDurationMs?: number;
|
|
28
31
|
turnStartedAt?: number;
|
|
29
32
|
turnStatus?: string;
|
|
33
|
+
requestedCapabilities?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CloudOsRequestedCapability {
|
|
37
|
+
kind: "skill" | "plan";
|
|
38
|
+
name: string;
|
|
39
|
+
label: string;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
export interface CloudOsAttachment {
|
|
@@ -41,19 +51,34 @@ export interface PlanStep {
|
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
export type AssistantBlock =
|
|
44
|
-
| {
|
|
54
|
+
| {
|
|
55
|
+
kind: "reasoning";
|
|
56
|
+
key: string;
|
|
57
|
+
text: string;
|
|
58
|
+
running: boolean;
|
|
59
|
+
}
|
|
45
60
|
| { kind: "text"; key: string; text: string }
|
|
46
61
|
| { kind: "toolGroup"; key: string; group: ToolCallGroup }
|
|
47
62
|
| { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
63
|
+
// 答案就在 call.output 里(`ask_user` 是 client-settled tool,用户点选后
|
|
64
|
+
// 经 respondToolInteraction 回写成这次调用的结果)。曾经靠扫下一条用户消息
|
|
65
|
+
// 推断,那是有损且会误认无关消息的,已彻底删除。
|
|
66
|
+
// `pending` 是这张卡还在等人 —— 卡片据此决定能不能点,活动指示器据此决定
|
|
67
|
+
// 该不该出现,同一条判据(askUserOutcome)算一次。
|
|
68
|
+
| { kind: "askUser"; key: string; call: CloudOsToolCall; pending: boolean }
|
|
51
69
|
| { kind: "suggestions"; key: string; items: string[] }
|
|
52
70
|
| { kind: "schedule"; key: string; call: CloudOsToolCall }
|
|
53
71
|
| { kind: "approval"; key: string; call: CloudOsToolCall; approvalId: string }
|
|
54
72
|
| { kind: "parallel"; key: string; label: string; tools: ParallelTool[] }
|
|
55
73
|
| { kind: "subagents"; key: string; agents: SubAgentView[] }
|
|
56
|
-
| {
|
|
74
|
+
| {
|
|
75
|
+
kind: "image";
|
|
76
|
+
key: string;
|
|
77
|
+
src: string;
|
|
78
|
+
alt: string;
|
|
79
|
+
mediaType?: string;
|
|
80
|
+
}
|
|
81
|
+
| { kind: "file"; key: string; file: CloudOsAttachment }
|
|
57
82
|
| { kind: "error"; key: string; title: string; body?: string };
|
|
58
83
|
|
|
59
84
|
export interface ParallelTool {
|
|
@@ -75,6 +100,7 @@ export type CloudOsEntry =
|
|
|
75
100
|
messageId: string;
|
|
76
101
|
text: string;
|
|
77
102
|
attachments: CloudOsAttachment[];
|
|
103
|
+
capabilities: CloudOsRequestedCapability[];
|
|
78
104
|
authorName?: string;
|
|
79
105
|
timestamp?: number;
|
|
80
106
|
}
|
|
@@ -121,6 +147,31 @@ export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
|
|
|
121
147
|
return recordOf(message.metadata) as CloudOsMessageMetadata;
|
|
122
148
|
}
|
|
123
149
|
|
|
150
|
+
export function requestedCapabilitiesOf(
|
|
151
|
+
metadata: CloudOsMessageMetadata,
|
|
152
|
+
): CloudOsRequestedCapability[] {
|
|
153
|
+
if (!Array.isArray(metadata.requestedCapabilities)) return [];
|
|
154
|
+
const seen = new Set<string>();
|
|
155
|
+
return metadata.requestedCapabilities.flatMap((value) => {
|
|
156
|
+
const capability = recordOf(value);
|
|
157
|
+
if (
|
|
158
|
+
(capability.kind !== "skill" && capability.kind !== "plan") ||
|
|
159
|
+
typeof capability.name !== "string" ||
|
|
160
|
+
!capability.name.trim() ||
|
|
161
|
+
typeof capability.label !== "string" ||
|
|
162
|
+
!capability.label.trim()
|
|
163
|
+
) return [];
|
|
164
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
165
|
+
if (seen.has(key)) return [];
|
|
166
|
+
seen.add(key);
|
|
167
|
+
return [{
|
|
168
|
+
kind: capability.kind,
|
|
169
|
+
name: capability.name,
|
|
170
|
+
label: capability.label,
|
|
171
|
+
}];
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
124
175
|
export function messageText(message: UIMessage): string {
|
|
125
176
|
return message.parts
|
|
126
177
|
.filter(
|
|
@@ -248,7 +299,6 @@ function assistantBlocks(
|
|
|
248
299
|
message: UIMessage,
|
|
249
300
|
isActive: boolean,
|
|
250
301
|
showThinkingTraces: boolean,
|
|
251
|
-
replyText: string | undefined,
|
|
252
302
|
approvalIdOf: (part: unknown) => string | undefined,
|
|
253
303
|
): AssistantBlock[] {
|
|
254
304
|
const blocks: AssistantBlock[] = [];
|
|
@@ -281,7 +331,12 @@ function assistantBlocks(
|
|
|
281
331
|
const text = String(record.text ?? "").trim();
|
|
282
332
|
if (!text || !showThinkingTraces) return;
|
|
283
333
|
flush();
|
|
284
|
-
blocks.push({
|
|
334
|
+
blocks.push({
|
|
335
|
+
kind: "reasoning",
|
|
336
|
+
key,
|
|
337
|
+
text,
|
|
338
|
+
running: record.state === "streaming",
|
|
339
|
+
});
|
|
285
340
|
return;
|
|
286
341
|
}
|
|
287
342
|
|
|
@@ -339,15 +394,32 @@ function assistantBlocks(
|
|
|
339
394
|
const data = dataOf(part);
|
|
340
395
|
const src = String(data.src ?? data.url ?? record.url ?? "");
|
|
341
396
|
const mediaType = String(record.mediaType ?? data.mediaType ?? "");
|
|
342
|
-
if (src
|
|
343
|
-
|
|
397
|
+
if (!src) return;
|
|
398
|
+
|
|
399
|
+
flush();
|
|
400
|
+
const filename = String(
|
|
401
|
+
data.alt ?? data.filename ?? record.filename ?? "Attachment",
|
|
402
|
+
);
|
|
403
|
+
if (dataKind === "image" || mediaType.startsWith("image/")) {
|
|
344
404
|
blocks.push({
|
|
345
405
|
kind: "image",
|
|
346
406
|
key,
|
|
347
407
|
src,
|
|
348
|
-
alt:
|
|
408
|
+
alt: filename,
|
|
409
|
+
mediaType: mediaType || undefined,
|
|
349
410
|
});
|
|
411
|
+
return;
|
|
350
412
|
}
|
|
413
|
+
|
|
414
|
+
blocks.push({
|
|
415
|
+
kind: "file",
|
|
416
|
+
key,
|
|
417
|
+
file: {
|
|
418
|
+
url: src,
|
|
419
|
+
filename,
|
|
420
|
+
mediaType: mediaType || undefined,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
351
423
|
return;
|
|
352
424
|
}
|
|
353
425
|
|
|
@@ -375,15 +447,13 @@ function assistantBlocks(
|
|
|
375
447
|
}
|
|
376
448
|
|
|
377
449
|
if (call.kind === "ask-user") {
|
|
450
|
+
if (call.failed) return;
|
|
378
451
|
flush();
|
|
379
452
|
blocks.push({
|
|
380
453
|
kind: "askUser",
|
|
381
454
|
key,
|
|
382
455
|
call,
|
|
383
|
-
|
|
384
|
-
replyText?.trim() ||
|
|
385
|
-
(typeof call.output === "string" ? call.output.trim() : "") ||
|
|
386
|
-
undefined,
|
|
456
|
+
pending: askUserOutcome(call).kind === "pending",
|
|
387
457
|
});
|
|
388
458
|
return;
|
|
389
459
|
}
|
|
@@ -432,13 +502,6 @@ function terminalOf(
|
|
|
432
502
|
return undefined;
|
|
433
503
|
}
|
|
434
504
|
|
|
435
|
-
function isDirectUserMessage(message: UIMessage): boolean {
|
|
436
|
-
return (
|
|
437
|
-
message.role === "user" &&
|
|
438
|
-
!message.parts.some((part) => dataKindOf(part) !== null)
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
505
|
export interface BuildEntriesOptions {
|
|
443
506
|
messages: readonly UIMessage[];
|
|
444
507
|
/** streaming/submitted 时最后一条 assistant 消息才算「活着」。 */
|
|
@@ -494,6 +557,7 @@ export function buildCloudOsEntries({
|
|
|
494
557
|
messageId: message.id,
|
|
495
558
|
text,
|
|
496
559
|
attachments,
|
|
560
|
+
capabilities: requestedCapabilitiesOf(metadata),
|
|
497
561
|
authorName: metadata.authorDisplayName,
|
|
498
562
|
timestamp:
|
|
499
563
|
nonNegativeNumber(metadata.createdAt) ??
|
|
@@ -502,12 +566,10 @@ export function buildCloudOsEntries({
|
|
|
502
566
|
return;
|
|
503
567
|
}
|
|
504
568
|
|
|
505
|
-
const reply = messages.slice(index + 1).find(isDirectUserMessage);
|
|
506
569
|
const blocks = assistantBlocks(
|
|
507
570
|
message,
|
|
508
571
|
isActive && index === lastAssistantIndex,
|
|
509
572
|
showThinkingTraces,
|
|
510
|
-
reply ? messageText(reply).trim() || undefined : undefined,
|
|
511
573
|
approvalIdOf,
|
|
512
574
|
);
|
|
513
575
|
const terminal = terminalOf(message);
|
|
@@ -560,6 +622,136 @@ function dropSupersededPlans(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
|
560
622
|
);
|
|
561
623
|
}
|
|
562
624
|
|
|
625
|
+
// ── 活动指示(本回合此刻在干什么)────────────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* 底部活动指示器要显示的动画与文案。
|
|
629
|
+
*/
|
|
630
|
+
export interface CloudOsActivity {
|
|
631
|
+
state: OrbState;
|
|
632
|
+
label: string;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const SEARCHING_TOOL_KINDS: ReadonlySet<CloudOsToolKind> = new Set([
|
|
636
|
+
"read",
|
|
637
|
+
"glob",
|
|
638
|
+
"grep",
|
|
639
|
+
"list",
|
|
640
|
+
"web-search",
|
|
641
|
+
"web-fetch",
|
|
642
|
+
]);
|
|
643
|
+
const SHAPING_TOOL_KINDS: ReadonlySet<CloudOsToolKind> = new Set([
|
|
644
|
+
"write",
|
|
645
|
+
"edit",
|
|
646
|
+
"tasks",
|
|
647
|
+
"create-agent",
|
|
648
|
+
"publish-extension",
|
|
649
|
+
]);
|
|
650
|
+
|
|
651
|
+
function activityForTool(call: CloudOsToolCall | undefined): CloudOsActivity {
|
|
652
|
+
if (!call) return { state: "working", label: "Working…" };
|
|
653
|
+
const name = call.toolName.replaceAll("-", "_").toLowerCase();
|
|
654
|
+
if (call.kind === "agent") {
|
|
655
|
+
return {
|
|
656
|
+
state: "weaving",
|
|
657
|
+
label: call.running ? "Coordinating agents…" : "Combining results…",
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (SEARCHING_TOOL_KINDS.has(call.kind)) {
|
|
661
|
+
return {
|
|
662
|
+
state: "searching",
|
|
663
|
+
label: call.running ? "Searching…" : "Reading results…",
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (SHAPING_TOOL_KINDS.has(call.kind)) {
|
|
667
|
+
return { state: "shaping", label: "Shaping result…" };
|
|
668
|
+
}
|
|
669
|
+
if (call.kind === "skill" || name === "activate_skill") {
|
|
670
|
+
return { state: "working", label: "Loading skill…" };
|
|
671
|
+
}
|
|
672
|
+
if (name === "run_skill_script") {
|
|
673
|
+
return { state: "working", label: "Running skill…" };
|
|
674
|
+
}
|
|
675
|
+
return { state: "working", label: "Working…" };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function activityForTail(block: AssistantBlock | undefined): CloudOsActivity {
|
|
679
|
+
if (!block) return { state: "solving", label: "Thinking…" };
|
|
680
|
+
switch (block.kind) {
|
|
681
|
+
case "reasoning":
|
|
682
|
+
return { state: "solving", label: "Reasoning…" };
|
|
683
|
+
case "text":
|
|
684
|
+
case "suggestions":
|
|
685
|
+
return { state: "composing", label: "Composing answer…" };
|
|
686
|
+
case "toolGroup":
|
|
687
|
+
return activityForTool(block.group.calls.at(-1));
|
|
688
|
+
case "plan":
|
|
689
|
+
case "image":
|
|
690
|
+
case "file":
|
|
691
|
+
return { state: "shaping", label: "Shaping result…" };
|
|
692
|
+
case "askUser":
|
|
693
|
+
return block.pending
|
|
694
|
+
? { state: "listening", label: "Waiting for your answer…" }
|
|
695
|
+
: { state: "solving", label: "Picking up your answer…" };
|
|
696
|
+
case "approval":
|
|
697
|
+
return { state: "breathing", label: "Waiting for approval…" };
|
|
698
|
+
case "schedule":
|
|
699
|
+
return block.call.awaitingApproval
|
|
700
|
+
? { state: "breathing", label: "Waiting for approval…" }
|
|
701
|
+
: { state: "working", label: "Working…" };
|
|
702
|
+
case "parallel":
|
|
703
|
+
return {
|
|
704
|
+
state: "weaving",
|
|
705
|
+
label: block.tools.some((tool) => tool.status === "running")
|
|
706
|
+
? "Coordinating tasks…"
|
|
707
|
+
: "Combining results…",
|
|
708
|
+
};
|
|
709
|
+
case "subagents":
|
|
710
|
+
return {
|
|
711
|
+
state: "weaving",
|
|
712
|
+
label: block.agents.some((agent) => agent.status === "running")
|
|
713
|
+
? "Coordinating agents…"
|
|
714
|
+
: "Combining results…",
|
|
715
|
+
};
|
|
716
|
+
default:
|
|
717
|
+
return { state: "working", label: "Working…" };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* 回合活着时始终返回一个 Orb 状态;只有回合终止时才返回 `null`。
|
|
723
|
+
*/
|
|
724
|
+
export function deriveTurnActivity(
|
|
725
|
+
entries: readonly CloudOsEntry[],
|
|
726
|
+
options: {
|
|
727
|
+
isActive: boolean;
|
|
728
|
+
isRecovering?: boolean;
|
|
729
|
+
recoveryStatusLabel?: string;
|
|
730
|
+
awaitingApproval?: boolean;
|
|
731
|
+
hasPendingSteer?: boolean;
|
|
732
|
+
},
|
|
733
|
+
): CloudOsActivity | null {
|
|
734
|
+
if (options.isRecovering) {
|
|
735
|
+
return {
|
|
736
|
+
state: "connecting",
|
|
737
|
+
label: options.recoveryStatusLabel ?? "Reconnecting…",
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (!options.isActive) return null;
|
|
741
|
+
if (options.awaitingApproval) {
|
|
742
|
+
return { state: "breathing", label: "Waiting for approval…" };
|
|
743
|
+
}
|
|
744
|
+
if (options.hasPendingSteer) {
|
|
745
|
+
return {
|
|
746
|
+
state: "breathing",
|
|
747
|
+
label: "Waiting for the current step to finish…",
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const tail = entries.at(-1) ?? null;
|
|
751
|
+
const block = tail?.type === "assistant" ? tail.blocks.at(-1) : undefined;
|
|
752
|
+
return activityForTail(block);
|
|
753
|
+
}
|
|
754
|
+
|
|
563
755
|
// ── 行间距节奏(照搬原 rhythmTopClass)──────────────────────────────────────
|
|
564
756
|
|
|
565
757
|
function isUserEntry(entry: CloudOsEntry): boolean {
|