@springbrand/message-panel 0.1.3-alpha.1 → 0.1.3-alpha.3
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/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +632 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +672 -0
- package/cloud-os/chat/tool-presentation.ts +584 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +672 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +541 -0
- package/cloud-os/index.ts +107 -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 +242 -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 +573 -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 +471 -0
- package/demo/index.ts +2 -0
- package/package.json +16 -4
- package/src/camel/camel-chat-messages.tsx +41 -18
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -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/styles/index.css +4 -1
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bell,
|
|
3
|
+
CaretRight,
|
|
4
|
+
Check,
|
|
5
|
+
CircleNotch,
|
|
6
|
+
ListChecks,
|
|
7
|
+
ShieldCheck,
|
|
8
|
+
UsersThree,
|
|
9
|
+
WarningCircle,
|
|
10
|
+
} from "@phosphor-icons/react";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { Tooltip } from "../primitives/tooltip";
|
|
13
|
+
import { WorkshopButton } from "../primitives/workshop-controls";
|
|
14
|
+
import { MarkdownMessage } from "./markdown-message";
|
|
15
|
+
import { WorkIcon } from "./tool-rows";
|
|
16
|
+
import type {
|
|
17
|
+
ParallelTool,
|
|
18
|
+
PlanStep,
|
|
19
|
+
SubAgentView,
|
|
20
|
+
} from "./transcript-model";
|
|
21
|
+
import { askUserOutcome, type CloudOsToolCall } from "./tool-presentation";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
|
|
25
|
+
* 后续建议、定时任务、并行/子 Agent、原位授权。视觉语言全部沿用 cloud-os:
|
|
26
|
+
* 折叠工作行用 `px-1.5 py-1` 的行式布局,卡片用
|
|
27
|
+
* `themed-surface-inset rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
|
|
28
|
+
* 授权卡沿用 renderActionCard 的 blocking callout(brand/40 边 + brand/10 底)。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// ── 计划快照 ────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export function PlanBlock({
|
|
34
|
+
steps,
|
|
35
|
+
running,
|
|
36
|
+
}: {
|
|
37
|
+
steps: PlanStep[];
|
|
38
|
+
running: boolean;
|
|
39
|
+
}) {
|
|
40
|
+
const done = steps.filter((step) => step.status === "done").length;
|
|
41
|
+
const current = steps.find((step) => step.status === "in_progress");
|
|
42
|
+
const [open, setOpen] = useState(true);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="group -ml-0.5">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={() => setOpen((value) => !value)}
|
|
49
|
+
className="flex w-full cursor-pointer items-center gap-3 rounded-xl px-1.5 py-1 text-left text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.995]"
|
|
50
|
+
aria-expanded={open}
|
|
51
|
+
>
|
|
52
|
+
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
53
|
+
<WorkIcon Icon={ListChecks} />
|
|
54
|
+
</span>
|
|
55
|
+
<span className="min-w-0 flex-1">
|
|
56
|
+
<span className="flex min-w-0 items-center gap-2 text-[14px] leading-5 tracking-[-0.25px]">
|
|
57
|
+
<span className={`min-w-0 truncate ${running ? "cos-thinking-shimmer" : ""}`}>
|
|
58
|
+
Plan · {done}/{steps.length}
|
|
59
|
+
</span>
|
|
60
|
+
<CaretRight
|
|
61
|
+
size={13}
|
|
62
|
+
weight="bold"
|
|
63
|
+
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
64
|
+
/>
|
|
65
|
+
</span>
|
|
66
|
+
{!open && current && (
|
|
67
|
+
<span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
|
|
68
|
+
{current.text}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
</span>
|
|
72
|
+
</button>
|
|
73
|
+
{open && (
|
|
74
|
+
<div className="themed-surface-inset ml-8 mt-1 space-y-1.5 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3">
|
|
75
|
+
{steps.map((step, index) => (
|
|
76
|
+
<div
|
|
77
|
+
key={`${step.text}-${index}`}
|
|
78
|
+
className="flex items-start gap-2.5 text-[13px] leading-[19px] tracking-[-0.25px]"
|
|
79
|
+
>
|
|
80
|
+
<span
|
|
81
|
+
className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center"
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
>
|
|
84
|
+
{step.status === "done" ? (
|
|
85
|
+
<Check size={12} weight="bold" className="text-kumo-success" />
|
|
86
|
+
) : step.status === "in_progress" ? (
|
|
87
|
+
<CircleNotch
|
|
88
|
+
size={12}
|
|
89
|
+
weight="bold"
|
|
90
|
+
className="animate-spin text-kumo-brand motion-reduce:animate-none"
|
|
91
|
+
/>
|
|
92
|
+
) : (
|
|
93
|
+
<span className="h-1.5 w-1.5 rounded-full bg-kumo-fill" />
|
|
94
|
+
)}
|
|
95
|
+
</span>
|
|
96
|
+
<span
|
|
97
|
+
className={
|
|
98
|
+
step.status === "done"
|
|
99
|
+
? "min-w-0 flex-1 text-kumo-inactive line-through"
|
|
100
|
+
: step.status === "in_progress"
|
|
101
|
+
? "min-w-0 flex-1 font-medium text-kumo-default"
|
|
102
|
+
: "min-w-0 flex-1 text-kumo-subtle"
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
{step.text}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 原位提问 ────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
interface AskQuestion {
|
|
118
|
+
prompt: string;
|
|
119
|
+
options: string[];
|
|
120
|
+
multi: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function questionsOf(input: Record<string, unknown>): AskQuestion[] {
|
|
124
|
+
if (Array.isArray(input.questions)) {
|
|
125
|
+
return input.questions.map((item) => {
|
|
126
|
+
const question = (item ?? {}) as Record<string, unknown>;
|
|
127
|
+
return {
|
|
128
|
+
prompt: String(question.prompt ?? question.question ?? "Choose an option"),
|
|
129
|
+
options: Array.isArray(question.options)
|
|
130
|
+
? question.options.filter((o): o is string => typeof o === "string")
|
|
131
|
+
: [],
|
|
132
|
+
multi: question.kind === "multi" || question.multiSelect === true,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (typeof input.question === "string") {
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
prompt: input.question,
|
|
140
|
+
options: Array.isArray(input.options)
|
|
141
|
+
? input.options.filter((o): o is string => typeof o === "string")
|
|
142
|
+
: [],
|
|
143
|
+
multi: input.multiSelect === true,
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function AskUserBlock({
|
|
151
|
+
call,
|
|
152
|
+
onRespond,
|
|
153
|
+
}: {
|
|
154
|
+
call: CloudOsToolCall;
|
|
155
|
+
/** 缺省(宿主没接)时选项不可点 —— 形状对齐 ScheduleBlock 的 onOpen。 */
|
|
156
|
+
onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
|
|
157
|
+
}) {
|
|
158
|
+
const questions = questionsOf(call.input);
|
|
159
|
+
// 不能用 `useState(() => questions.map(() => []))` 初始化:工具入参是**流式**到达的,
|
|
160
|
+
// 首帧 questions 往往还是空数组,那之后每次 setSelections 都在空数组上 map,
|
|
161
|
+
// 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
|
|
162
|
+
const [selections, setSelections] = useState<string[][]>([]);
|
|
163
|
+
const selectionAt = (index: number) => selections[index] ?? [];
|
|
164
|
+
const toggle = (questionIndex: number, option: string, multi: boolean) =>
|
|
165
|
+
setSelections((current) => {
|
|
166
|
+
const next = questions.map((_, index) => current[index] ?? []);
|
|
167
|
+
const values = next[questionIndex] ?? [];
|
|
168
|
+
next[questionIndex] = !multi
|
|
169
|
+
? [option]
|
|
170
|
+
: values.includes(option)
|
|
171
|
+
? values.filter((value) => value !== option)
|
|
172
|
+
: [...values, option];
|
|
173
|
+
return next;
|
|
174
|
+
});
|
|
175
|
+
// 在途只用于按钮转圈;权威状态永远是 part 的 state。
|
|
176
|
+
const [inFlight, setInFlight] = useState(false);
|
|
177
|
+
const outcome = askUserOutcome(call);
|
|
178
|
+
const answer = outcome.kind === "answered" ? outcome : null;
|
|
179
|
+
const answeredSelections = new Set(answer?.selections ?? []);
|
|
180
|
+
const answeredText = [
|
|
181
|
+
...(answer?.selections ?? []),
|
|
182
|
+
...(answer?.text.trim() ? [answer.text.trim()] : []),
|
|
183
|
+
].join(" / ");
|
|
184
|
+
// 服务端已经结算 = 这张卡永久不可操作,不管结算成什么。
|
|
185
|
+
const resolved = outcome.kind !== "pending" || inFlight;
|
|
186
|
+
const valid = questions.length > 0 && questions.every(
|
|
187
|
+
(question, index) =>
|
|
188
|
+
question.options.length === 0 || selectionAt(index).length > 0,
|
|
189
|
+
);
|
|
190
|
+
const buttonLabel = outcome.kind === "answered"
|
|
191
|
+
? "Submitted"
|
|
192
|
+
: outcome.kind === "closed"
|
|
193
|
+
? "Closed"
|
|
194
|
+
: inFlight
|
|
195
|
+
? "Submitting…"
|
|
196
|
+
: "Submit";
|
|
197
|
+
const footnote = outcome.kind === "answered"
|
|
198
|
+
? answeredText && `Answered: ${answeredText}`
|
|
199
|
+
: outcome.kind === "closed"
|
|
200
|
+
? outcome.reason === "user_replied_freeform"
|
|
201
|
+
? "Answered in the chat instead."
|
|
202
|
+
: "Closed without an answer."
|
|
203
|
+
: "";
|
|
204
|
+
|
|
205
|
+
// 提交按钮固定在卡片右下角,和脚注同一行 —— 脚注不再单独占一行,多问题时
|
|
206
|
+
// 按钮也不会卡在某一题的选项中间。选项没到(流式首帧 questions 为空)时这行
|
|
207
|
+
// 照样渲染,否则用户看到的是一张空卡。
|
|
208
|
+
const submit = (
|
|
209
|
+
<WorkshopButton
|
|
210
|
+
tone="primary"
|
|
211
|
+
disabled={resolved || !valid || !onRespond}
|
|
212
|
+
onClick={() => {
|
|
213
|
+
if (!onRespond) return;
|
|
214
|
+
setInFlight(true);
|
|
215
|
+
// 失败就把按钮放回可点:权威状态由 part 决定,这里只管别把用户锁死。
|
|
216
|
+
void onRespond(call.toolCallId, {
|
|
217
|
+
selections: questions.flatMap((_, index) => selectionAt(index)),
|
|
218
|
+
text: "",
|
|
219
|
+
}).then((ok) => {
|
|
220
|
+
if (!ok) setInFlight(false);
|
|
221
|
+
});
|
|
222
|
+
}}
|
|
223
|
+
className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
224
|
+
>
|
|
225
|
+
{buttonLabel}
|
|
226
|
+
</WorkshopButton>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
// 收成 w-fit:短问题不该撑成一条横贯全栏的长条,长问题到 560px 再换行。
|
|
231
|
+
<div className="w-fit min-w-[15rem] max-w-[560px]">
|
|
232
|
+
{/* 组内(问题↔选项)10px、组间(题与题、题与按钮行)12px —— 组间必须比组内
|
|
233
|
+
松,否则多问题时上一题的选项会看起来像下一题的。
|
|
234
|
+
用 flex+gap 而不是 space-y-*:Tailwind v4 的 space-y 选择器裹在
|
|
235
|
+
`:where()` 里,特异性被抹平,会被子元素自己的 `m-0` 压掉 → 不生效。 */}
|
|
236
|
+
<div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-kumo-base px-3 py-2.5">
|
|
237
|
+
{questions.map((question, questionIndex) => (
|
|
238
|
+
<div
|
|
239
|
+
key={`${question.prompt}:${questionIndex}`}
|
|
240
|
+
className="flex flex-col gap-2.5"
|
|
241
|
+
>
|
|
242
|
+
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
243
|
+
{question.prompt}
|
|
244
|
+
</p>
|
|
245
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
246
|
+
{question.options.map((option) => {
|
|
247
|
+
const selected = answer
|
|
248
|
+
? answeredSelections.has(option)
|
|
249
|
+
: selectionAt(questionIndex).includes(option);
|
|
250
|
+
return (
|
|
251
|
+
<button
|
|
252
|
+
key={option}
|
|
253
|
+
type="button"
|
|
254
|
+
disabled={resolved || !onRespond}
|
|
255
|
+
onClick={() => toggle(questionIndex, option, question.multi)}
|
|
256
|
+
className={`inline-flex h-7 cursor-pointer items-center rounded-lg border px-2.5 text-[13px] leading-none tracking-[-0.25px] transition-colors duration-150 ease-out disabled:cursor-default ${
|
|
257
|
+
selected
|
|
258
|
+
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
259
|
+
: "border-kumo-line bg-kumo-base text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
260
|
+
}`}
|
|
261
|
+
>
|
|
262
|
+
{option}
|
|
263
|
+
</button>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
))}
|
|
269
|
+
<div className="flex items-center gap-3">
|
|
270
|
+
{footnote && (
|
|
271
|
+
<p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
|
|
272
|
+
{footnote}
|
|
273
|
+
</p>
|
|
274
|
+
)}
|
|
275
|
+
{submit}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── 后续建议 ────────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
export function SuggestionsBlock({
|
|
285
|
+
items,
|
|
286
|
+
onSuggestion,
|
|
287
|
+
}: {
|
|
288
|
+
items: string[];
|
|
289
|
+
onSuggestion: (text: string) => void;
|
|
290
|
+
}) {
|
|
291
|
+
return (
|
|
292
|
+
<div className="max-w-[860px] space-y-2 pt-1">
|
|
293
|
+
<div className="flex items-center gap-2 px-1.5 text-[13px] leading-[18px] text-kumo-success">
|
|
294
|
+
<Check size={14} weight="bold" />
|
|
295
|
+
<span>Task complete</span>
|
|
296
|
+
</div>
|
|
297
|
+
<div className="flex flex-wrap gap-1.5 px-1.5">
|
|
298
|
+
{items.map((item) => (
|
|
299
|
+
<button
|
|
300
|
+
key={item}
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={() => onSuggestion(item)}
|
|
303
|
+
className="inline-flex h-7 cursor-pointer items-center rounded-full border border-kumo-line bg-kumo-base px-3 text-[13px] leading-none tracking-[-0.25px] text-kumo-subtle transition-colors duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-none active:scale-[0.98]"
|
|
304
|
+
>
|
|
305
|
+
{item}
|
|
306
|
+
</button>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── 定时任务 ────────────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
function scheduleDuration(seconds: unknown): string {
|
|
316
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
for (const [size, unit] of [
|
|
320
|
+
[86_400, "day"],
|
|
321
|
+
[3_600, "hour"],
|
|
322
|
+
[60, "minute"],
|
|
323
|
+
] as const) {
|
|
324
|
+
if (seconds % size === 0) {
|
|
325
|
+
const value = seconds / size;
|
|
326
|
+
return `${value} ${unit}${value === 1 ? "" : "s"}`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function scheduleCadence(input: Record<string, unknown>): string {
|
|
333
|
+
const trigger = (input.trigger ?? {}) as Record<string, unknown>;
|
|
334
|
+
let cadence = "";
|
|
335
|
+
if (trigger.kind === "once-in") cadence = `In ${scheduleDuration(trigger.seconds)}`;
|
|
336
|
+
else if (trigger.kind === "once-at" && typeof trigger.at === "string") {
|
|
337
|
+
cadence = `Once at ${trigger.at}`;
|
|
338
|
+
} else if (trigger.kind === "interval") {
|
|
339
|
+
cadence = `Every ${scheduleDuration(trigger.seconds)}`;
|
|
340
|
+
} else if (trigger.kind === "cron" && typeof trigger.cron === "string") {
|
|
341
|
+
cadence = `Cron ${trigger.cron}`;
|
|
342
|
+
}
|
|
343
|
+
if (!cadence) cadence = String(input.cadence ?? input.schedule ?? "Scheduled");
|
|
344
|
+
const timezone =
|
|
345
|
+
typeof input.timezone === "string"
|
|
346
|
+
? input.timezone
|
|
347
|
+
: typeof input.tz === "string"
|
|
348
|
+
? input.tz
|
|
349
|
+
: "";
|
|
350
|
+
return timezone ? `${cadence} · ${timezone}` : cadence;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id 即任务在 SchedulerAgent 里的主键。 */
|
|
354
|
+
function scheduleIdOf(output: unknown): string {
|
|
355
|
+
if (typeof output !== "object" || output === null) return "";
|
|
356
|
+
const id = (output as Record<string, unknown>).id;
|
|
357
|
+
return typeof id === "string" ? id : "";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function ScheduleBlock({
|
|
361
|
+
call,
|
|
362
|
+
onOpen,
|
|
363
|
+
}: {
|
|
364
|
+
call: CloudOsToolCall;
|
|
365
|
+
/** 有 id 且宿主给了回调时,整张卡变成跳到「已安排」详情的按钮。 */
|
|
366
|
+
onOpen?: (scheduleId: string) => void;
|
|
367
|
+
}) {
|
|
368
|
+
const state = call.failed ? "failed" : call.running ? "running" : "scheduled";
|
|
369
|
+
const scheduleId = state === "scheduled" ? scheduleIdOf(call.output) : "";
|
|
370
|
+
const clickable = Boolean(scheduleId && onOpen);
|
|
371
|
+
const Card = clickable ? "button" : "div";
|
|
372
|
+
return (
|
|
373
|
+
<div className="max-w-[860px]">
|
|
374
|
+
<Card
|
|
375
|
+
{...(clickable
|
|
376
|
+
? {
|
|
377
|
+
type: "button" as const,
|
|
378
|
+
onClick: () => onOpen?.(scheduleId),
|
|
379
|
+
"aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
|
|
380
|
+
}
|
|
381
|
+
: {})}
|
|
382
|
+
className={`w-full rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3 text-left${
|
|
383
|
+
clickable
|
|
384
|
+
? " cursor-pointer transition-colors duration-150 ease-out hover:bg-kumo-tint focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/50"
|
|
385
|
+
: ""
|
|
386
|
+
}`}
|
|
387
|
+
>
|
|
388
|
+
<div className="flex flex-wrap items-start gap-3">
|
|
389
|
+
<span
|
|
390
|
+
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-kumo-tint text-kumo-subtle"
|
|
391
|
+
aria-hidden="true"
|
|
392
|
+
>
|
|
393
|
+
<Bell size={18} />
|
|
394
|
+
</span>
|
|
395
|
+
<div className="min-w-[10rem] flex-1">
|
|
396
|
+
<span className="block truncate text-[14px] leading-5 font-medium tracking-[-0.25px] text-kumo-default">
|
|
397
|
+
{String(call.input.label ?? call.input.title ?? "Scheduled task")}
|
|
398
|
+
</span>
|
|
399
|
+
<p className="mt-1 mb-0 text-[12px] leading-4 text-kumo-inactive">
|
|
400
|
+
{scheduleCadence(call.input)}
|
|
401
|
+
</p>
|
|
402
|
+
</div>
|
|
403
|
+
<span
|
|
404
|
+
className={`ml-auto flex flex-shrink-0 items-center gap-1 self-center text-[12px] leading-4 font-medium ${
|
|
405
|
+
state === "failed"
|
|
406
|
+
? "text-kumo-danger"
|
|
407
|
+
: state === "running"
|
|
408
|
+
? "text-kumo-brand"
|
|
409
|
+
: "text-kumo-success"
|
|
410
|
+
}`}
|
|
411
|
+
>
|
|
412
|
+
{state === "failed" ? (
|
|
413
|
+
<WarningCircle size={13} weight="fill" />
|
|
414
|
+
) : state === "running" ? (
|
|
415
|
+
<CircleNotch
|
|
416
|
+
size={13}
|
|
417
|
+
weight="bold"
|
|
418
|
+
className="animate-spin motion-reduce:animate-none"
|
|
419
|
+
/>
|
|
420
|
+
) : (
|
|
421
|
+
<Check size={13} weight="bold" />
|
|
422
|
+
)}
|
|
423
|
+
{state === "failed"
|
|
424
|
+
? "Failed"
|
|
425
|
+
: state === "running"
|
|
426
|
+
? "Scheduling…"
|
|
427
|
+
: "Scheduled"}
|
|
428
|
+
</span>
|
|
429
|
+
</div>
|
|
430
|
+
</Card>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── 原位授权(照搬 renderActionCard 的 blocking callout)────────────────────
|
|
436
|
+
|
|
437
|
+
export type ApprovalDecision = "deny" | "allow_once" | "allow_level";
|
|
438
|
+
|
|
439
|
+
export function ApprovalBlock({
|
|
440
|
+
call,
|
|
441
|
+
approvalId,
|
|
442
|
+
disabled = false,
|
|
443
|
+
onApprove,
|
|
444
|
+
}: {
|
|
445
|
+
call: CloudOsToolCall;
|
|
446
|
+
approvalId: string;
|
|
447
|
+
disabled?: boolean;
|
|
448
|
+
onApprove: (approvalId: string, decision: ApprovalDecision) => void;
|
|
449
|
+
}) {
|
|
450
|
+
const title = `${call.toolName} needs approval`;
|
|
451
|
+
const description =
|
|
452
|
+
Object.keys(call.input).length > 0
|
|
453
|
+
? "```json\n" + JSON.stringify(call.input, null, 2) + "\n```"
|
|
454
|
+
: "This action pauses the turn until you decide.";
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
458
|
+
{/* 原文件的 blocking callout 是「图标 + 正文 + 右侧动作」一行到底。聊天栏默认
|
|
459
|
+
只有 420px 宽,一行放不下三段,正文会被挤成 0 宽 —— 所以外层允许换行,
|
|
460
|
+
动作组在挤不下时整体落到下一行右对齐。 */}
|
|
461
|
+
<div className="rounded-2xl border border-kumo-brand/40 bg-kumo-brand/10 px-4 py-3">
|
|
462
|
+
<div className="flex flex-wrap items-start gap-3">
|
|
463
|
+
<span
|
|
464
|
+
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-kumo-tint text-kumo-brand"
|
|
465
|
+
aria-hidden="true"
|
|
466
|
+
>
|
|
467
|
+
<ShieldCheck size={20} weight="fill" />
|
|
468
|
+
</span>
|
|
469
|
+
<div className="min-w-[12rem] flex-1">
|
|
470
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
|
|
471
|
+
<span className="min-w-0 truncate font-medium text-kumo-default">
|
|
472
|
+
{title}
|
|
473
|
+
</span>
|
|
474
|
+
</div>
|
|
475
|
+
<div className="cos-chat-panel cos-markdown mt-1 max-h-[200px] overflow-y-auto pr-1 text-[13px] leading-[18px] text-kumo-subtle">
|
|
476
|
+
<MarkdownMessage message={description} />
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
<div className="ml-auto flex flex-wrap items-center justify-end gap-1 self-center">
|
|
480
|
+
<Tooltip content="Reject this action and let the agent continue without it." asChild>
|
|
481
|
+
<WorkshopButton
|
|
482
|
+
tone="secondary"
|
|
483
|
+
disabled={disabled}
|
|
484
|
+
onClick={() => onApprove(approvalId, "deny")}
|
|
485
|
+
className="!h-8 !rounded-lg text-[12px]"
|
|
486
|
+
>
|
|
487
|
+
Deny
|
|
488
|
+
</WorkshopButton>
|
|
489
|
+
</Tooltip>
|
|
490
|
+
<Tooltip content="Allow just this call." asChild>
|
|
491
|
+
<WorkshopButton
|
|
492
|
+
tone="secondary"
|
|
493
|
+
disabled={disabled}
|
|
494
|
+
onClick={() => onApprove(approvalId, "allow_once")}
|
|
495
|
+
className="!h-8 !rounded-lg text-[12px]"
|
|
496
|
+
>
|
|
497
|
+
Allow once
|
|
498
|
+
</WorkshopButton>
|
|
499
|
+
</Tooltip>
|
|
500
|
+
<Tooltip content="Allow every action at this execution level, without future prompts." asChild>
|
|
501
|
+
<WorkshopButton
|
|
502
|
+
tone="primary"
|
|
503
|
+
disabled={disabled}
|
|
504
|
+
onClick={() => onApprove(approvalId, "allow_level")}
|
|
505
|
+
className="!h-8 gap-1 !rounded-lg text-[12px]"
|
|
506
|
+
>
|
|
507
|
+
<Check size={11} weight="bold" />
|
|
508
|
+
Allow level
|
|
509
|
+
</WorkshopButton>
|
|
510
|
+
</Tooltip>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ── 并行 / 子 Agent ─────────────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
function StatusDot({ status }: { status: ParallelTool["status"] }) {
|
|
521
|
+
return (
|
|
522
|
+
<span
|
|
523
|
+
className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${
|
|
524
|
+
status === "failed"
|
|
525
|
+
? "bg-kumo-danger"
|
|
526
|
+
: status === "running"
|
|
527
|
+
? "animate-pulse bg-kumo-brand motion-reduce:animate-none"
|
|
528
|
+
: status === "done"
|
|
529
|
+
? "bg-kumo-success"
|
|
530
|
+
: "bg-kumo-fill"
|
|
531
|
+
}`}
|
|
532
|
+
aria-hidden="true"
|
|
533
|
+
/>
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function ParallelBlock({
|
|
538
|
+
label,
|
|
539
|
+
tools,
|
|
540
|
+
}: {
|
|
541
|
+
label: string;
|
|
542
|
+
tools: ParallelTool[];
|
|
543
|
+
}) {
|
|
544
|
+
const done = tools.filter((tool) => tool.status === "done").length;
|
|
545
|
+
return (
|
|
546
|
+
<div className="max-w-[860px]">
|
|
547
|
+
<div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
548
|
+
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
549
|
+
<WorkIcon Icon={ListChecks} />
|
|
550
|
+
</span>
|
|
551
|
+
<span className="min-w-0 truncate">
|
|
552
|
+
{label} · {done}/{tools.length}
|
|
553
|
+
</span>
|
|
554
|
+
</div>
|
|
555
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
556
|
+
{tools.map((tool, index) => (
|
|
557
|
+
<div
|
|
558
|
+
key={`${tool.label}-${index}`}
|
|
559
|
+
className="flex items-center gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle"
|
|
560
|
+
>
|
|
561
|
+
<StatusDot status={tool.status} />
|
|
562
|
+
<span className="min-w-0 flex-1 truncate">{tool.label}</span>
|
|
563
|
+
{tool.meta && (
|
|
564
|
+
<span className="flex-shrink-0 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
565
|
+
{tool.meta}
|
|
566
|
+
</span>
|
|
567
|
+
)}
|
|
568
|
+
</div>
|
|
569
|
+
))}
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
|
|
576
|
+
return (
|
|
577
|
+
<div className="max-w-[860px]">
|
|
578
|
+
<div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
579
|
+
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
580
|
+
<WorkIcon Icon={UsersThree} />
|
|
581
|
+
</span>
|
|
582
|
+
<span className="min-w-0 truncate">
|
|
583
|
+
{agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
|
|
584
|
+
</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
587
|
+
{agents.map((agent, index) => (
|
|
588
|
+
<div
|
|
589
|
+
key={`${agent.name}-${index}`}
|
|
590
|
+
className="flex items-start gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px]"
|
|
591
|
+
>
|
|
592
|
+
<span className="mt-1.5 flex">
|
|
593
|
+
<StatusDot status={agent.status} />
|
|
594
|
+
</span>
|
|
595
|
+
<span className="min-w-0 flex-1">
|
|
596
|
+
<span className="block truncate font-medium text-kumo-default">
|
|
597
|
+
{agent.name}
|
|
598
|
+
</span>
|
|
599
|
+
{agent.summary && (
|
|
600
|
+
<span className="block text-kumo-subtle">{agent.summary}</span>
|
|
601
|
+
)}
|
|
602
|
+
</span>
|
|
603
|
+
</div>
|
|
604
|
+
))}
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ── 错误行(照搬 msg.type === "error" 分支)─────────────────────────────────
|
|
611
|
+
|
|
612
|
+
export function ErrorBlock({
|
|
613
|
+
title,
|
|
614
|
+
body,
|
|
615
|
+
onRetry,
|
|
616
|
+
}: {
|
|
617
|
+
title: string;
|
|
618
|
+
body?: string;
|
|
619
|
+
onRetry?: () => void;
|
|
620
|
+
}) {
|
|
621
|
+
const [expanded, setExpanded] = useState(false);
|
|
622
|
+
return (
|
|
623
|
+
<div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
624
|
+
<div className="flex w-full items-center gap-2 px-1.5 py-1">
|
|
625
|
+
<button
|
|
626
|
+
type="button"
|
|
627
|
+
onClick={() => setExpanded((value) => !value)}
|
|
628
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-md text-left transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.995]"
|
|
629
|
+
aria-expanded={expanded}
|
|
630
|
+
>
|
|
631
|
+
<span
|
|
632
|
+
className="flex h-5 w-5 flex-shrink-0 items-center justify-center text-kumo-danger"
|
|
633
|
+
aria-hidden="true"
|
|
634
|
+
>
|
|
635
|
+
<WarningCircle size={16} weight="fill" />
|
|
636
|
+
</span>
|
|
637
|
+
<span className="flex min-w-0 flex-1 items-center gap-1">
|
|
638
|
+
<span className="min-w-0 truncate">
|
|
639
|
+
<span className="font-medium text-kumo-danger">{title}: </span>
|
|
640
|
+
<span className="text-kumo-subtle">{body}</span>
|
|
641
|
+
</span>
|
|
642
|
+
{body && (
|
|
643
|
+
<CaretRight
|
|
644
|
+
size={13}
|
|
645
|
+
weight="bold"
|
|
646
|
+
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${expanded ? "rotate-90" : ""}`}
|
|
647
|
+
/>
|
|
648
|
+
)}
|
|
649
|
+
</span>
|
|
650
|
+
</button>
|
|
651
|
+
{onRetry && (
|
|
652
|
+
<Tooltip content="Retry the last action." asChild>
|
|
653
|
+
<button
|
|
654
|
+
type="button"
|
|
655
|
+
onClick={onRetry}
|
|
656
|
+
className="flex flex-shrink-0 cursor-pointer items-center gap-1 rounded-md px-1 py-0.5 text-[13px] leading-4 font-medium text-kumo-default transition-[color,opacity,transform] duration-150 ease-out hover:text-kumo-default-hover focus-visible:text-kumo-default-hover focus-visible:outline-none active:scale-[0.98]"
|
|
657
|
+
>
|
|
658
|
+
Retry
|
|
659
|
+
</button>
|
|
660
|
+
</Tooltip>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
{expanded && body && (
|
|
664
|
+
<div className="ml-8 mt-1">
|
|
665
|
+
<pre className="max-h-48 overflow-auto rounded-xl border border-kumo-line/70 bg-kumo-base p-3 font-mono text-[12px] leading-[18px] text-kumo-subtle whitespace-pre-wrap">
|
|
666
|
+
{body}
|
|
667
|
+
</pre>
|
|
668
|
+
</div>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
);
|
|
672
|
+
}
|