@springbrand/message-panel 0.1.3-alpha.1 → 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 +36 -18
- package/src/message.tsx +0 -8
|
@@ -0,0 +1,640 @@
|
|
|
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 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
|
+
answeredText,
|
|
153
|
+
onAnswer,
|
|
154
|
+
}: {
|
|
155
|
+
call: CloudOsToolCall;
|
|
156
|
+
answeredText?: string;
|
|
157
|
+
onAnswer: (payload: unknown) => void;
|
|
158
|
+
}) {
|
|
159
|
+
const questions = questionsOf(call.input);
|
|
160
|
+
const [selections, setSelections] = useState<string[][]>(() =>
|
|
161
|
+
questions.map(() => []),
|
|
162
|
+
);
|
|
163
|
+
const [submitted, setSubmitted] = useState(false);
|
|
164
|
+
const external = answeredText?.trim() ?? "";
|
|
165
|
+
const externalSelections = new Set(
|
|
166
|
+
external.split("、").map((value) => value.trim()).filter(Boolean),
|
|
167
|
+
);
|
|
168
|
+
const resolved = submitted || Boolean(external);
|
|
169
|
+
const valid = questions.every(
|
|
170
|
+
(question, index) =>
|
|
171
|
+
question.options.length === 0 || (selections[index]?.length ?? 0) > 0,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="max-w-[860px]">
|
|
176
|
+
<div className="rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3">
|
|
177
|
+
{/* 同 ApprovalBlock:窄栏下让提交按钮整体换行,别把问题正文挤没。 */}
|
|
178
|
+
<div className="flex flex-wrap items-start gap-3">
|
|
179
|
+
<span
|
|
180
|
+
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-kumo-tint text-kumo-brand"
|
|
181
|
+
aria-hidden="true"
|
|
182
|
+
>
|
|
183
|
+
<Bell size={18} weight="fill" />
|
|
184
|
+
</span>
|
|
185
|
+
<div className="min-w-[12rem] flex-1 space-y-2.5">
|
|
186
|
+
{questions.map((question, questionIndex) => (
|
|
187
|
+
<div key={`${question.prompt}:${questionIndex}`} className="space-y-1.5">
|
|
188
|
+
<p className="m-0 text-[14px] leading-5 font-medium tracking-[-0.25px] text-kumo-default">
|
|
189
|
+
{question.prompt}
|
|
190
|
+
</p>
|
|
191
|
+
<div className="flex flex-wrap gap-1.5">
|
|
192
|
+
{question.options.map((option) => {
|
|
193
|
+
const selected = external
|
|
194
|
+
? externalSelections.has(option)
|
|
195
|
+
: selections[questionIndex]?.includes(option);
|
|
196
|
+
return (
|
|
197
|
+
<button
|
|
198
|
+
key={option}
|
|
199
|
+
type="button"
|
|
200
|
+
disabled={resolved}
|
|
201
|
+
onClick={() =>
|
|
202
|
+
setSelections((current) =>
|
|
203
|
+
current.map((values, index) => {
|
|
204
|
+
if (index !== questionIndex) return values;
|
|
205
|
+
if (!question.multi) return [option];
|
|
206
|
+
return values.includes(option)
|
|
207
|
+
? values.filter((value) => value !== option)
|
|
208
|
+
: [...values, option];
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
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 ${
|
|
213
|
+
selected
|
|
214
|
+
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
215
|
+
: "border-kumo-line bg-kumo-base text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
216
|
+
}`}
|
|
217
|
+
>
|
|
218
|
+
{option}
|
|
219
|
+
</button>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
<div className="ml-auto flex flex-shrink-0 items-center self-center">
|
|
227
|
+
<WorkshopButton
|
|
228
|
+
tone="primary"
|
|
229
|
+
disabled={resolved || !valid}
|
|
230
|
+
onClick={() => {
|
|
231
|
+
onAnswer({ selections, text: "" });
|
|
232
|
+
setSubmitted(true);
|
|
233
|
+
}}
|
|
234
|
+
className="!h-8 gap-1 !rounded-lg text-[12px]"
|
|
235
|
+
>
|
|
236
|
+
{resolved ? "Submitted" : "Submit"}
|
|
237
|
+
</WorkshopButton>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
{external && (
|
|
241
|
+
<p className="mt-2 mb-0 pl-12 text-[12px] leading-4 text-kumo-inactive">
|
|
242
|
+
Answered: {external}
|
|
243
|
+
</p>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── 后续建议 ────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
export function SuggestionsBlock({
|
|
253
|
+
items,
|
|
254
|
+
onSuggestion,
|
|
255
|
+
}: {
|
|
256
|
+
items: string[];
|
|
257
|
+
onSuggestion: (text: string) => void;
|
|
258
|
+
}) {
|
|
259
|
+
return (
|
|
260
|
+
<div className="max-w-[860px] space-y-2 pt-1">
|
|
261
|
+
<div className="flex items-center gap-2 px-1.5 text-[13px] leading-[18px] text-kumo-success">
|
|
262
|
+
<Check size={14} weight="bold" />
|
|
263
|
+
<span>Task complete</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="flex flex-wrap gap-1.5 px-1.5">
|
|
266
|
+
{items.map((item) => (
|
|
267
|
+
<button
|
|
268
|
+
key={item}
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => onSuggestion(item)}
|
|
271
|
+
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]"
|
|
272
|
+
>
|
|
273
|
+
{item}
|
|
274
|
+
</button>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── 定时任务 ────────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
function scheduleDuration(seconds: unknown): string {
|
|
284
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
for (const [size, unit] of [
|
|
288
|
+
[86_400, "day"],
|
|
289
|
+
[3_600, "hour"],
|
|
290
|
+
[60, "minute"],
|
|
291
|
+
] as const) {
|
|
292
|
+
if (seconds % size === 0) {
|
|
293
|
+
const value = seconds / size;
|
|
294
|
+
return `${value} ${unit}${value === 1 ? "" : "s"}`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function scheduleCadence(input: Record<string, unknown>): string {
|
|
301
|
+
const trigger = (input.trigger ?? {}) as Record<string, unknown>;
|
|
302
|
+
let cadence = "";
|
|
303
|
+
if (trigger.kind === "once-in") cadence = `In ${scheduleDuration(trigger.seconds)}`;
|
|
304
|
+
else if (trigger.kind === "once-at" && typeof trigger.at === "string") {
|
|
305
|
+
cadence = `Once at ${trigger.at}`;
|
|
306
|
+
} else if (trigger.kind === "interval") {
|
|
307
|
+
cadence = `Every ${scheduleDuration(trigger.seconds)}`;
|
|
308
|
+
} else if (trigger.kind === "cron" && typeof trigger.cron === "string") {
|
|
309
|
+
cadence = `Cron ${trigger.cron}`;
|
|
310
|
+
}
|
|
311
|
+
if (!cadence) cadence = String(input.cadence ?? input.schedule ?? "Scheduled");
|
|
312
|
+
const timezone =
|
|
313
|
+
typeof input.timezone === "string"
|
|
314
|
+
? input.timezone
|
|
315
|
+
: typeof input.tz === "string"
|
|
316
|
+
? input.tz
|
|
317
|
+
: "";
|
|
318
|
+
return timezone ? `${cadence} · ${timezone}` : cadence;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id 即任务在 SchedulerAgent 里的主键。 */
|
|
322
|
+
function scheduleIdOf(output: unknown): string {
|
|
323
|
+
if (typeof output !== "object" || output === null) return "";
|
|
324
|
+
const id = (output as Record<string, unknown>).id;
|
|
325
|
+
return typeof id === "string" ? id : "";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function ScheduleBlock({
|
|
329
|
+
call,
|
|
330
|
+
onOpen,
|
|
331
|
+
}: {
|
|
332
|
+
call: CloudOsToolCall;
|
|
333
|
+
/** 有 id 且宿主给了回调时,整张卡变成跳到「已安排」详情的按钮。 */
|
|
334
|
+
onOpen?: (scheduleId: string) => void;
|
|
335
|
+
}) {
|
|
336
|
+
const state = call.failed ? "failed" : call.running ? "running" : "scheduled";
|
|
337
|
+
const scheduleId = state === "scheduled" ? scheduleIdOf(call.output) : "";
|
|
338
|
+
const clickable = Boolean(scheduleId && onOpen);
|
|
339
|
+
const Card = clickable ? "button" : "div";
|
|
340
|
+
return (
|
|
341
|
+
<div className="max-w-[860px]">
|
|
342
|
+
<Card
|
|
343
|
+
{...(clickable
|
|
344
|
+
? {
|
|
345
|
+
type: "button" as const,
|
|
346
|
+
onClick: () => onOpen?.(scheduleId),
|
|
347
|
+
"aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
|
|
348
|
+
}
|
|
349
|
+
: {})}
|
|
350
|
+
className={`w-full rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3 text-left${
|
|
351
|
+
clickable
|
|
352
|
+
? " 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"
|
|
353
|
+
: ""
|
|
354
|
+
}`}
|
|
355
|
+
>
|
|
356
|
+
<div className="flex flex-wrap items-start gap-3">
|
|
357
|
+
<span
|
|
358
|
+
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-kumo-tint text-kumo-subtle"
|
|
359
|
+
aria-hidden="true"
|
|
360
|
+
>
|
|
361
|
+
<Bell size={18} />
|
|
362
|
+
</span>
|
|
363
|
+
<div className="min-w-[10rem] flex-1">
|
|
364
|
+
<span className="block truncate text-[14px] leading-5 font-medium tracking-[-0.25px] text-kumo-default">
|
|
365
|
+
{String(call.input.label ?? call.input.title ?? "Scheduled task")}
|
|
366
|
+
</span>
|
|
367
|
+
<p className="mt-1 mb-0 text-[12px] leading-4 text-kumo-inactive">
|
|
368
|
+
{scheduleCadence(call.input)}
|
|
369
|
+
</p>
|
|
370
|
+
</div>
|
|
371
|
+
<span
|
|
372
|
+
className={`ml-auto flex flex-shrink-0 items-center gap-1 self-center text-[12px] leading-4 font-medium ${
|
|
373
|
+
state === "failed"
|
|
374
|
+
? "text-kumo-danger"
|
|
375
|
+
: state === "running"
|
|
376
|
+
? "text-kumo-brand"
|
|
377
|
+
: "text-kumo-success"
|
|
378
|
+
}`}
|
|
379
|
+
>
|
|
380
|
+
{state === "failed" ? (
|
|
381
|
+
<WarningCircle size={13} weight="fill" />
|
|
382
|
+
) : state === "running" ? (
|
|
383
|
+
<CircleNotch
|
|
384
|
+
size={13}
|
|
385
|
+
weight="bold"
|
|
386
|
+
className="animate-spin motion-reduce:animate-none"
|
|
387
|
+
/>
|
|
388
|
+
) : (
|
|
389
|
+
<Check size={13} weight="bold" />
|
|
390
|
+
)}
|
|
391
|
+
{state === "failed"
|
|
392
|
+
? "Failed"
|
|
393
|
+
: state === "running"
|
|
394
|
+
? "Scheduling…"
|
|
395
|
+
: "Scheduled"}
|
|
396
|
+
</span>
|
|
397
|
+
</div>
|
|
398
|
+
</Card>
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── 原位授权(照搬 renderActionCard 的 blocking callout)────────────────────
|
|
404
|
+
|
|
405
|
+
export type ApprovalDecision = "deny" | "allow_once" | "allow_level";
|
|
406
|
+
|
|
407
|
+
export function ApprovalBlock({
|
|
408
|
+
call,
|
|
409
|
+
approvalId,
|
|
410
|
+
disabled = false,
|
|
411
|
+
onApprove,
|
|
412
|
+
}: {
|
|
413
|
+
call: CloudOsToolCall;
|
|
414
|
+
approvalId: string;
|
|
415
|
+
disabled?: boolean;
|
|
416
|
+
onApprove: (approvalId: string, decision: ApprovalDecision) => void;
|
|
417
|
+
}) {
|
|
418
|
+
const title = `${call.toolName} needs approval`;
|
|
419
|
+
const description =
|
|
420
|
+
Object.keys(call.input).length > 0
|
|
421
|
+
? "```json\n" + JSON.stringify(call.input, null, 2) + "\n```"
|
|
422
|
+
: "This action pauses the turn until you decide.";
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
426
|
+
{/* 原文件的 blocking callout 是「图标 + 正文 + 右侧动作」一行到底。聊天栏默认
|
|
427
|
+
只有 420px 宽,一行放不下三段,正文会被挤成 0 宽 —— 所以外层允许换行,
|
|
428
|
+
动作组在挤不下时整体落到下一行右对齐。 */}
|
|
429
|
+
<div className="rounded-2xl border border-kumo-brand/40 bg-kumo-brand/10 px-4 py-3">
|
|
430
|
+
<div className="flex flex-wrap items-start gap-3">
|
|
431
|
+
<span
|
|
432
|
+
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-kumo-tint text-kumo-brand"
|
|
433
|
+
aria-hidden="true"
|
|
434
|
+
>
|
|
435
|
+
<ShieldCheck size={20} weight="fill" />
|
|
436
|
+
</span>
|
|
437
|
+
<div className="min-w-[12rem] flex-1">
|
|
438
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
|
|
439
|
+
<span className="min-w-0 truncate font-medium text-kumo-default">
|
|
440
|
+
{title}
|
|
441
|
+
</span>
|
|
442
|
+
</div>
|
|
443
|
+
<div className="cos-chat-panel cos-markdown mt-1 max-h-[200px] overflow-y-auto pr-1 text-[13px] leading-[18px] text-kumo-subtle">
|
|
444
|
+
<MarkdownMessage message={description} />
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
<div className="ml-auto flex flex-wrap items-center justify-end gap-1 self-center">
|
|
448
|
+
<Tooltip content="Reject this action and let the agent continue without it." asChild>
|
|
449
|
+
<WorkshopButton
|
|
450
|
+
tone="secondary"
|
|
451
|
+
disabled={disabled}
|
|
452
|
+
onClick={() => onApprove(approvalId, "deny")}
|
|
453
|
+
className="!h-8 !rounded-lg text-[12px]"
|
|
454
|
+
>
|
|
455
|
+
Deny
|
|
456
|
+
</WorkshopButton>
|
|
457
|
+
</Tooltip>
|
|
458
|
+
<Tooltip content="Allow just this call." asChild>
|
|
459
|
+
<WorkshopButton
|
|
460
|
+
tone="secondary"
|
|
461
|
+
disabled={disabled}
|
|
462
|
+
onClick={() => onApprove(approvalId, "allow_once")}
|
|
463
|
+
className="!h-8 !rounded-lg text-[12px]"
|
|
464
|
+
>
|
|
465
|
+
Allow once
|
|
466
|
+
</WorkshopButton>
|
|
467
|
+
</Tooltip>
|
|
468
|
+
<Tooltip content="Allow every action at this execution level, without future prompts." asChild>
|
|
469
|
+
<WorkshopButton
|
|
470
|
+
tone="primary"
|
|
471
|
+
disabled={disabled}
|
|
472
|
+
onClick={() => onApprove(approvalId, "allow_level")}
|
|
473
|
+
className="!h-8 gap-1 !rounded-lg text-[12px]"
|
|
474
|
+
>
|
|
475
|
+
<Check size={11} weight="bold" />
|
|
476
|
+
Allow level
|
|
477
|
+
</WorkshopButton>
|
|
478
|
+
</Tooltip>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── 并行 / 子 Agent ─────────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
function StatusDot({ status }: { status: ParallelTool["status"] }) {
|
|
489
|
+
return (
|
|
490
|
+
<span
|
|
491
|
+
className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${
|
|
492
|
+
status === "failed"
|
|
493
|
+
? "bg-kumo-danger"
|
|
494
|
+
: status === "running"
|
|
495
|
+
? "animate-pulse bg-kumo-brand motion-reduce:animate-none"
|
|
496
|
+
: status === "done"
|
|
497
|
+
? "bg-kumo-success"
|
|
498
|
+
: "bg-kumo-fill"
|
|
499
|
+
}`}
|
|
500
|
+
aria-hidden="true"
|
|
501
|
+
/>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function ParallelBlock({
|
|
506
|
+
label,
|
|
507
|
+
tools,
|
|
508
|
+
}: {
|
|
509
|
+
label: string;
|
|
510
|
+
tools: ParallelTool[];
|
|
511
|
+
}) {
|
|
512
|
+
const done = tools.filter((tool) => tool.status === "done").length;
|
|
513
|
+
return (
|
|
514
|
+
<div className="max-w-[860px]">
|
|
515
|
+
<div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
516
|
+
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
517
|
+
<WorkIcon Icon={ListChecks} />
|
|
518
|
+
</span>
|
|
519
|
+
<span className="min-w-0 truncate">
|
|
520
|
+
{label} · {done}/{tools.length}
|
|
521
|
+
</span>
|
|
522
|
+
</div>
|
|
523
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
524
|
+
{tools.map((tool, index) => (
|
|
525
|
+
<div
|
|
526
|
+
key={`${tool.label}-${index}`}
|
|
527
|
+
className="flex items-center gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle"
|
|
528
|
+
>
|
|
529
|
+
<StatusDot status={tool.status} />
|
|
530
|
+
<span className="min-w-0 flex-1 truncate">{tool.label}</span>
|
|
531
|
+
{tool.meta && (
|
|
532
|
+
<span className="flex-shrink-0 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
533
|
+
{tool.meta}
|
|
534
|
+
</span>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
))}
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
|
|
544
|
+
return (
|
|
545
|
+
<div className="max-w-[860px]">
|
|
546
|
+
<div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
547
|
+
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
548
|
+
<WorkIcon Icon={UsersThree} />
|
|
549
|
+
</span>
|
|
550
|
+
<span className="min-w-0 truncate">
|
|
551
|
+
{agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
|
|
552
|
+
</span>
|
|
553
|
+
</div>
|
|
554
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
555
|
+
{agents.map((agent, index) => (
|
|
556
|
+
<div
|
|
557
|
+
key={`${agent.name}-${index}`}
|
|
558
|
+
className="flex items-start gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px]"
|
|
559
|
+
>
|
|
560
|
+
<span className="mt-1.5 flex">
|
|
561
|
+
<StatusDot status={agent.status} />
|
|
562
|
+
</span>
|
|
563
|
+
<span className="min-w-0 flex-1">
|
|
564
|
+
<span className="block truncate font-medium text-kumo-default">
|
|
565
|
+
{agent.name}
|
|
566
|
+
</span>
|
|
567
|
+
{agent.summary && (
|
|
568
|
+
<span className="block text-kumo-subtle">{agent.summary}</span>
|
|
569
|
+
)}
|
|
570
|
+
</span>
|
|
571
|
+
</div>
|
|
572
|
+
))}
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ── 错误行(照搬 msg.type === "error" 分支)─────────────────────────────────
|
|
579
|
+
|
|
580
|
+
export function ErrorBlock({
|
|
581
|
+
title,
|
|
582
|
+
body,
|
|
583
|
+
onRetry,
|
|
584
|
+
}: {
|
|
585
|
+
title: string;
|
|
586
|
+
body?: string;
|
|
587
|
+
onRetry?: () => void;
|
|
588
|
+
}) {
|
|
589
|
+
const [expanded, setExpanded] = useState(false);
|
|
590
|
+
return (
|
|
591
|
+
<div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
592
|
+
<div className="flex w-full items-center gap-2 px-1.5 py-1">
|
|
593
|
+
<button
|
|
594
|
+
type="button"
|
|
595
|
+
onClick={() => setExpanded((value) => !value)}
|
|
596
|
+
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]"
|
|
597
|
+
aria-expanded={expanded}
|
|
598
|
+
>
|
|
599
|
+
<span
|
|
600
|
+
className="flex h-5 w-5 flex-shrink-0 items-center justify-center text-kumo-danger"
|
|
601
|
+
aria-hidden="true"
|
|
602
|
+
>
|
|
603
|
+
<WarningCircle size={16} weight="fill" />
|
|
604
|
+
</span>
|
|
605
|
+
<span className="flex min-w-0 flex-1 items-center gap-1">
|
|
606
|
+
<span className="min-w-0 truncate">
|
|
607
|
+
<span className="font-medium text-kumo-danger">{title}: </span>
|
|
608
|
+
<span className="text-kumo-subtle">{body}</span>
|
|
609
|
+
</span>
|
|
610
|
+
{body && (
|
|
611
|
+
<CaretRight
|
|
612
|
+
size={13}
|
|
613
|
+
weight="bold"
|
|
614
|
+
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${expanded ? "rotate-90" : ""}`}
|
|
615
|
+
/>
|
|
616
|
+
)}
|
|
617
|
+
</span>
|
|
618
|
+
</button>
|
|
619
|
+
{onRetry && (
|
|
620
|
+
<Tooltip content="Retry the last action." asChild>
|
|
621
|
+
<button
|
|
622
|
+
type="button"
|
|
623
|
+
onClick={onRetry}
|
|
624
|
+
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]"
|
|
625
|
+
>
|
|
626
|
+
Retry
|
|
627
|
+
</button>
|
|
628
|
+
</Tooltip>
|
|
629
|
+
)}
|
|
630
|
+
</div>
|
|
631
|
+
{expanded && body && (
|
|
632
|
+
<div className="ml-8 mt-1">
|
|
633
|
+
<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">
|
|
634
|
+
{body}
|
|
635
|
+
</pre>
|
|
636
|
+
</div>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
);
|
|
640
|
+
}
|