@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.31
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
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ArrowLeft,
|
|
3
|
+
ArrowRight,
|
|
2
4
|
Bell,
|
|
3
5
|
CaretRight,
|
|
4
6
|
Check,
|
|
7
|
+
CheckCircle,
|
|
5
8
|
CircleNotch,
|
|
9
|
+
Clock,
|
|
10
|
+
Key,
|
|
6
11
|
ListChecks,
|
|
7
|
-
ShieldCheck,
|
|
8
12
|
UsersThree,
|
|
9
13
|
WarningCircle,
|
|
10
14
|
} from "@phosphor-icons/react";
|
|
11
|
-
import { useState } from "react";
|
|
15
|
+
import { useRef, useState } from "react";
|
|
12
16
|
import { Tooltip } from "../primitives/tooltip";
|
|
13
17
|
import { WorkshopButton } from "../primitives/workshop-controls";
|
|
14
18
|
import { MarkdownMessage } from "./markdown-message";
|
|
@@ -18,13 +22,18 @@ import type {
|
|
|
18
22
|
PlanStep,
|
|
19
23
|
SubAgentView,
|
|
20
24
|
} from "./transcript-model";
|
|
21
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
askUserOutcome,
|
|
27
|
+
humanizeToolName,
|
|
28
|
+
safeStringify,
|
|
29
|
+
type CloudOsToolCall,
|
|
30
|
+
} from "./tool-presentation";
|
|
22
31
|
|
|
23
32
|
/**
|
|
24
33
|
* UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
|
|
25
34
|
* 后续建议、定时任务、并行/子 Agent、原位授权。视觉语言全部沿用 cloud-os:
|
|
26
35
|
* 折叠工作行用 `px-1.5 py-1` 的行式布局,卡片用
|
|
27
|
-
* `
|
|
36
|
+
* `rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
|
|
28
37
|
* 授权卡沿用 renderActionCard 的 blocking callout(brand/40 边 + brand/10 底)。
|
|
29
38
|
*/
|
|
30
39
|
|
|
@@ -33,13 +42,17 @@ import { askUserOutcome, type CloudOsToolCall } from "./tool-presentation";
|
|
|
33
42
|
export function PlanBlock({
|
|
34
43
|
steps,
|
|
35
44
|
running,
|
|
45
|
+
defaultOpen = true,
|
|
46
|
+
showCollapsedSummary = true,
|
|
36
47
|
}: {
|
|
37
48
|
steps: PlanStep[];
|
|
38
49
|
running: boolean;
|
|
50
|
+
defaultOpen?: boolean;
|
|
51
|
+
showCollapsedSummary?: boolean;
|
|
39
52
|
}) {
|
|
40
53
|
const done = steps.filter((step) => step.status === "done").length;
|
|
41
54
|
const current = steps.find((step) => step.status === "in_progress");
|
|
42
|
-
const [open, setOpen] = useState(
|
|
55
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
43
56
|
|
|
44
57
|
return (
|
|
45
58
|
<div className="group -ml-0.5">
|
|
@@ -63,7 +76,7 @@ export function PlanBlock({
|
|
|
63
76
|
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
64
77
|
/>
|
|
65
78
|
</span>
|
|
66
|
-
{!open && current && (
|
|
79
|
+
{!open && showCollapsedSummary && current && (
|
|
67
80
|
<span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
|
|
68
81
|
{current.text}
|
|
69
82
|
</span>
|
|
@@ -71,35 +84,34 @@ export function PlanBlock({
|
|
|
71
84
|
</span>
|
|
72
85
|
</button>
|
|
73
86
|
{open && (
|
|
74
|
-
<div className="
|
|
87
|
+
<div className="ml-8 mt-1 flex flex-col gap-2 rounded-xl border border-kumo-line bg-[rgba(0,0,0,0.03)] px-4 py-3">
|
|
75
88
|
{steps.map((step, index) => (
|
|
76
89
|
<div
|
|
77
90
|
key={`${step.text}-${index}`}
|
|
78
|
-
className="flex items-
|
|
91
|
+
className="flex items-center gap-2 text-[14px] leading-[1.4] font-normal tracking-normal"
|
|
79
92
|
>
|
|
80
93
|
<span
|
|
81
|
-
className="
|
|
94
|
+
className="flex size-4 flex-shrink-0 items-center justify-center"
|
|
82
95
|
aria-hidden="true"
|
|
83
96
|
>
|
|
84
97
|
{step.status === "done" ? (
|
|
85
|
-
<
|
|
98
|
+
<CheckCircle size={16} weight="fill" className="text-[#00bf75]" />
|
|
86
99
|
) : step.status === "in_progress" ? (
|
|
87
100
|
<CircleNotch
|
|
88
|
-
size={
|
|
89
|
-
|
|
90
|
-
className="animate-spin text-kumo-brand motion-reduce:animate-none"
|
|
101
|
+
size={16}
|
|
102
|
+
className="animate-spin text-[#ff9938] motion-reduce:animate-none"
|
|
91
103
|
/>
|
|
92
104
|
) : (
|
|
93
|
-
<span className="h-1.5 w-1.5 rounded-full bg-kumo-
|
|
105
|
+
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--text-color-kumo-default)] opacity-20" />
|
|
94
106
|
)}
|
|
95
107
|
</span>
|
|
96
108
|
<span
|
|
97
109
|
className={
|
|
98
110
|
step.status === "done"
|
|
99
|
-
? "min-w-0 flex-1 text-kumo-
|
|
111
|
+
? "min-w-0 flex-1 text-kumo-default/30 line-through"
|
|
100
112
|
: step.status === "in_progress"
|
|
101
|
-
? "min-w-0 flex-1
|
|
102
|
-
: "min-w-0 flex-1 text-kumo-
|
|
113
|
+
? "min-w-0 flex-1 text-kumo-default"
|
|
114
|
+
: "min-w-0 flex-1 text-kumo-default/70"
|
|
103
115
|
}
|
|
104
116
|
>
|
|
105
117
|
{step.text}
|
|
@@ -118,33 +130,32 @@ interface AskQuestion {
|
|
|
118
130
|
prompt: string;
|
|
119
131
|
options: string[];
|
|
120
132
|
multi: boolean;
|
|
133
|
+
allowCustom: boolean;
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
function questionsOf(input: Record<string, unknown>): AskQuestion[] {
|
|
124
|
-
if (Array.isArray(input.questions))
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
return [];
|
|
137
|
+
if (!Array.isArray(input.questions)) return [];
|
|
138
|
+
return input.questions.map((item) => {
|
|
139
|
+
const question = (item ?? {}) as Record<string, unknown>;
|
|
140
|
+
const options = Array.isArray(question.options)
|
|
141
|
+
? question.options.filter((option): option is string =>
|
|
142
|
+
typeof option === "string"
|
|
143
|
+
)
|
|
144
|
+
: [];
|
|
145
|
+
return {
|
|
146
|
+
prompt: typeof question.question === "string"
|
|
147
|
+
? question.question
|
|
148
|
+
: "Choose an option",
|
|
149
|
+
options,
|
|
150
|
+
multi: question.multiSelect === true,
|
|
151
|
+
allowCustom: question.allowCustom === true || options.length === 0,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface DraftAnswer {
|
|
157
|
+
selections: string[];
|
|
158
|
+
text: string;
|
|
148
159
|
}
|
|
149
160
|
|
|
150
161
|
export function AskUserBlock({
|
|
@@ -156,37 +167,54 @@ export function AskUserBlock({
|
|
|
156
167
|
onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
|
|
157
168
|
}) {
|
|
158
169
|
const questions = questionsOf(call.input);
|
|
159
|
-
// 不能用 `useState(() => questions.map(
|
|
160
|
-
// 首帧 questions 往往还是空数组,那之后每次
|
|
170
|
+
// 不能用 `useState(() => questions.map(...))` 初始化:工具入参是**流式**到达的,
|
|
171
|
+
// 首帧 questions 往往还是空数组,那之后每次 setAnswers 都在空数组上 map,
|
|
161
172
|
// 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
|
|
162
|
-
const [
|
|
163
|
-
const
|
|
173
|
+
const [answers, setAnswers] = useState<DraftAnswer[]>([]);
|
|
174
|
+
const [current, setCurrent] = useState(0);
|
|
175
|
+
const direction = useRef<"back" | "forward">("forward");
|
|
176
|
+
const answerAt = (index: number): DraftAnswer =>
|
|
177
|
+
answers[index] ?? { selections: [], text: "" };
|
|
164
178
|
const toggle = (questionIndex: number, option: string, multi: boolean) =>
|
|
165
|
-
|
|
166
|
-
const next = questions.map((_, index) =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
setAnswers((current) => {
|
|
180
|
+
const next = questions.map((_, index) =>
|
|
181
|
+
current[index] ?? { selections: [], text: "" }
|
|
182
|
+
);
|
|
183
|
+
const answer = next[questionIndex]!;
|
|
184
|
+
next[questionIndex] = {
|
|
185
|
+
...answer,
|
|
186
|
+
selections: !multi
|
|
187
|
+
? [option]
|
|
188
|
+
: answer.selections.includes(option)
|
|
189
|
+
? answer.selections.filter((value) => value !== option)
|
|
190
|
+
: [...answer.selections, option],
|
|
191
|
+
};
|
|
192
|
+
return next;
|
|
193
|
+
});
|
|
194
|
+
const setText = (questionIndex: number, text: string) =>
|
|
195
|
+
setAnswers((current) => {
|
|
196
|
+
const next = questions.map((_, index) =>
|
|
197
|
+
current[index] ?? { selections: [], text: "" }
|
|
198
|
+
);
|
|
199
|
+
next[questionIndex] = { ...next[questionIndex]!, text };
|
|
173
200
|
return next;
|
|
174
201
|
});
|
|
175
202
|
// 在途只用于按钮转圈;权威状态永远是 part 的 state。
|
|
176
203
|
const [inFlight, setInFlight] = useState(false);
|
|
177
204
|
const outcome = askUserOutcome(call);
|
|
178
|
-
const
|
|
179
|
-
const answeredSelections = new Set(answer?.selections ?? []);
|
|
180
|
-
const answeredText = [
|
|
181
|
-
...(answer?.selections ?? []),
|
|
182
|
-
...(answer?.text.trim() ? [answer.text.trim()] : []),
|
|
183
|
-
].join(" / ");
|
|
205
|
+
const settledAnswers = outcome.kind === "answered" ? outcome.answers : [];
|
|
184
206
|
// 服务端已经结算 = 这张卡永久不可操作,不管结算成什么。
|
|
185
207
|
const resolved = outcome.kind !== "pending" || inFlight;
|
|
186
208
|
const valid = questions.length > 0 && questions.every(
|
|
187
|
-
(
|
|
188
|
-
|
|
209
|
+
(_question, index) =>
|
|
210
|
+
answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
|
|
189
211
|
);
|
|
212
|
+
const currentIndex = Math.min(current, Math.max(questions.length - 1, 0));
|
|
213
|
+
const currentQuestion = questions[currentIndex];
|
|
214
|
+
const currentAnswer = answerAt(currentIndex);
|
|
215
|
+
const currentValid = currentAnswer.selections.length > 0 ||
|
|
216
|
+
Boolean(currentAnswer.text.trim());
|
|
217
|
+
const isLast = currentIndex === questions.length - 1;
|
|
190
218
|
const buttonLabel = outcome.kind === "answered"
|
|
191
219
|
? "Submitted"
|
|
192
220
|
: outcome.kind === "closed"
|
|
@@ -194,32 +222,30 @@ export function AskUserBlock({
|
|
|
194
222
|
: inFlight
|
|
195
223
|
? "Submitting…"
|
|
196
224
|
: "Submit";
|
|
197
|
-
const footnote = outcome.kind === "
|
|
198
|
-
? answeredText && `Answered: ${answeredText}`
|
|
199
|
-
: outcome.kind === "closed"
|
|
225
|
+
const footnote = outcome.kind === "closed"
|
|
200
226
|
? outcome.reason === "user_replied_freeform"
|
|
201
227
|
? "Answered in the chat instead."
|
|
202
228
|
: "Closed without an answer."
|
|
203
229
|
: "";
|
|
204
230
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
231
|
+
const submit = () => {
|
|
232
|
+
if (!onRespond) return;
|
|
233
|
+
setInFlight(true);
|
|
234
|
+
// 失败就把按钮放回可点:权威状态由 part 决定,这里只管别把用户锁死。
|
|
235
|
+
void onRespond(call.toolCallId, {
|
|
236
|
+
answers: questions.map((_, index) => ({
|
|
237
|
+
selections: answerAt(index).selections,
|
|
238
|
+
text: answerAt(index).text.trim(),
|
|
239
|
+
})),
|
|
240
|
+
}).then((ok) => {
|
|
241
|
+
if (!ok) setInFlight(false);
|
|
242
|
+
}, () => setInFlight(false));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const settledButton = (
|
|
209
246
|
<WorkshopButton
|
|
210
247
|
tone="primary"
|
|
211
|
-
disabled
|
|
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
|
-
}}
|
|
248
|
+
disabled
|
|
223
249
|
className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
224
250
|
>
|
|
225
251
|
{buttonLabel}
|
|
@@ -228,52 +254,193 @@ export function AskUserBlock({
|
|
|
228
254
|
|
|
229
255
|
return (
|
|
230
256
|
// 收成 w-fit:短问题不该撑成一条横贯全栏的长条,长问题到 560px 再换行。
|
|
231
|
-
<div
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-
|
|
237
|
-
{
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
257
|
+
<div
|
|
258
|
+
className="w-fit min-w-[15rem] max-w-[560px]"
|
|
259
|
+
data-part-type="ask_user"
|
|
260
|
+
data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
|
|
261
|
+
>
|
|
262
|
+
<div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-operation-base px-3 py-2.5">
|
|
263
|
+
{outcome.kind === "pending" ? (
|
|
264
|
+
<>
|
|
265
|
+
{questions.length > 1 && (
|
|
266
|
+
<div className="flex flex-col gap-2">
|
|
267
|
+
<div className="flex items-center justify-between text-[11px] font-medium leading-4 text-kumo-inactive">
|
|
268
|
+
<span>Clarifying questions</span>
|
|
269
|
+
<span className="tabular-nums">
|
|
270
|
+
{currentIndex + 1}/{questions.length}
|
|
271
|
+
</span>
|
|
272
|
+
</div>
|
|
273
|
+
<div className="flex gap-1" aria-hidden="true">
|
|
274
|
+
{questions.map((question, index) => (
|
|
275
|
+
<span
|
|
276
|
+
key={`${question.prompt}:${index}`}
|
|
277
|
+
className={`h-1 flex-1 rounded-full ${
|
|
278
|
+
index < currentIndex
|
|
279
|
+
? "bg-kumo-brand"
|
|
280
|
+
: index === currentIndex
|
|
281
|
+
? "bg-kumo-brand/60"
|
|
282
|
+
: "bg-kumo-fill"
|
|
283
|
+
}`}
|
|
284
|
+
/>
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
{currentQuestion && (
|
|
290
|
+
<div className="overflow-hidden">
|
|
291
|
+
<div
|
|
292
|
+
key={currentIndex}
|
|
293
|
+
className="cos-ask-user-question flex flex-col gap-2.5"
|
|
294
|
+
data-direction={direction.current}
|
|
295
|
+
>
|
|
296
|
+
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
297
|
+
{currentQuestion.prompt}
|
|
298
|
+
</p>
|
|
299
|
+
{currentQuestion.options.length > 0 && (
|
|
300
|
+
<ul className="m-0 flex list-none flex-col gap-1.5 p-0">
|
|
301
|
+
{currentQuestion.options.map((option) => {
|
|
302
|
+
const selected = currentAnswer.selections.includes(
|
|
303
|
+
option,
|
|
304
|
+
);
|
|
305
|
+
return (
|
|
306
|
+
<li key={option} className="m-0 p-0">
|
|
307
|
+
<label
|
|
308
|
+
className={`flex min-h-8 items-center gap-2 rounded-lg border px-2.5 py-1.5 text-[13px] leading-[18px] tracking-[-0.25px] transition-colors duration-150 ease-out ${
|
|
309
|
+
resolved || !onRespond
|
|
310
|
+
? "cursor-default text-kumo-inactive"
|
|
311
|
+
: "cursor-pointer text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
312
|
+
} ${
|
|
313
|
+
selected
|
|
314
|
+
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
315
|
+
: "border-kumo-line bg-operation-base"
|
|
316
|
+
}`}
|
|
317
|
+
>
|
|
318
|
+
<input
|
|
319
|
+
type={currentQuestion.multi
|
|
320
|
+
? "checkbox"
|
|
321
|
+
: "radio"}
|
|
322
|
+
name={`${call.toolCallId}-${currentIndex}`}
|
|
323
|
+
checked={selected}
|
|
324
|
+
disabled={resolved || !onRespond}
|
|
325
|
+
onChange={() =>
|
|
326
|
+
toggle(
|
|
327
|
+
currentIndex,
|
|
328
|
+
option,
|
|
329
|
+
currentQuestion.multi,
|
|
330
|
+
)}
|
|
331
|
+
className="h-4 w-4 shrink-0 accent-[var(--color-kumo-brand)]"
|
|
332
|
+
/>
|
|
333
|
+
<span>{option}</span>
|
|
334
|
+
</label>
|
|
335
|
+
</li>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
</ul>
|
|
339
|
+
)}
|
|
340
|
+
{currentQuestion.allowCustom && (
|
|
341
|
+
<label className="flex flex-col gap-1 text-[12px] leading-4 text-kumo-subtle">
|
|
342
|
+
<span>
|
|
343
|
+
{currentQuestion.options.length > 0 ? "Other" : "Answer"}
|
|
344
|
+
</span>
|
|
345
|
+
<input
|
|
346
|
+
type="text"
|
|
347
|
+
value={currentAnswer.text}
|
|
348
|
+
maxLength={2_000}
|
|
349
|
+
disabled={resolved || !onRespond}
|
|
350
|
+
aria-label={`自定义回答:${currentQuestion.prompt}`}
|
|
351
|
+
placeholder={currentQuestion.options.length > 0
|
|
352
|
+
? "Add a custom answer"
|
|
353
|
+
: "Type your answer"}
|
|
354
|
+
onChange={(event) =>
|
|
355
|
+
setText(currentIndex, event.currentTarget.value)}
|
|
356
|
+
className="h-8 rounded-lg border border-kumo-line bg-kumo-base px-2.5 text-[13px] text-kumo-default outline-none placeholder:text-kumo-inactive focus:border-kumo-brand focus:ring-2 focus:ring-kumo-brand/20 disabled:cursor-default"
|
|
357
|
+
/>
|
|
358
|
+
</label>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
<div className="flex items-center gap-2">
|
|
364
|
+
{questions.length > 1 && (
|
|
365
|
+
<WorkshopButton
|
|
366
|
+
disabled={currentIndex === 0 || resolved}
|
|
367
|
+
onClick={() => {
|
|
368
|
+
direction.current = "back";
|
|
369
|
+
setCurrent((index) => Math.max(0, index - 1));
|
|
370
|
+
}}
|
|
371
|
+
className="!h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
372
|
+
>
|
|
373
|
+
<ArrowLeft size={13} weight="bold" />
|
|
374
|
+
Back
|
|
375
|
+
</WorkshopButton>
|
|
376
|
+
)}
|
|
377
|
+
<WorkshopButton
|
|
378
|
+
tone="primary"
|
|
379
|
+
disabled={
|
|
380
|
+
resolved || !onRespond ||
|
|
381
|
+
(questions.length > 1 && !isLast ? !currentValid : !valid)
|
|
382
|
+
}
|
|
383
|
+
onClick={() => {
|
|
384
|
+
if (questions.length > 1 && !isLast) {
|
|
385
|
+
direction.current = "forward";
|
|
386
|
+
setCurrent((index) =>
|
|
387
|
+
Math.min(questions.length - 1, index + 1)
|
|
388
|
+
);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
submit();
|
|
392
|
+
}}
|
|
393
|
+
className="ml-auto !h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
394
|
+
>
|
|
395
|
+
{questions.length > 1 && !isLast ? (
|
|
396
|
+
<>
|
|
397
|
+
Next
|
|
398
|
+
<ArrowRight size={13} weight="bold" />
|
|
399
|
+
</>
|
|
400
|
+
) : buttonLabel}
|
|
401
|
+
</WorkshopButton>
|
|
266
402
|
</div>
|
|
267
|
-
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
403
|
+
</>
|
|
404
|
+
) : (
|
|
405
|
+
<>
|
|
406
|
+
{outcome.kind === "answered" &&
|
|
407
|
+
questions.map((question, questionIndex) => (
|
|
408
|
+
<div
|
|
409
|
+
key={`${question.prompt}:${questionIndex}`}
|
|
410
|
+
className="flex flex-col gap-1.5"
|
|
411
|
+
>
|
|
412
|
+
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
413
|
+
{question.prompt}
|
|
414
|
+
</p>
|
|
415
|
+
{settledAnswers[questionIndex] && (
|
|
416
|
+
<div className="flex flex-col gap-1 text-[12px] leading-4 text-kumo-inactive">
|
|
417
|
+
{settledAnswers[questionIndex]!.selections.length > 0 && (
|
|
418
|
+
<p className="m-0">
|
|
419
|
+
Answered: {settledAnswers[questionIndex]!.selections
|
|
420
|
+
.join(" / ")}
|
|
421
|
+
</p>
|
|
422
|
+
)}
|
|
423
|
+
{settledAnswers[questionIndex]!.text.trim() && (
|
|
424
|
+
<p className="m-0">
|
|
425
|
+
{settledAnswers[questionIndex]!.selections.length > 0
|
|
426
|
+
? `补充:${settledAnswers[questionIndex]!.text.trim()}`
|
|
427
|
+
: `Answered: ${settledAnswers[questionIndex]!.text.trim()}`}
|
|
428
|
+
</p>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
))}
|
|
434
|
+
<div className="flex items-center gap-3">
|
|
435
|
+
{footnote && (
|
|
436
|
+
<p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
|
|
437
|
+
{footnote}
|
|
438
|
+
</p>
|
|
439
|
+
)}
|
|
440
|
+
{settledButton}
|
|
441
|
+
</div>
|
|
442
|
+
</>
|
|
443
|
+
)}
|
|
277
444
|
</div>
|
|
278
445
|
</div>
|
|
279
446
|
);
|
|
@@ -289,23 +456,26 @@ export function SuggestionsBlock({
|
|
|
289
456
|
onSuggestion: (text: string) => void;
|
|
290
457
|
}) {
|
|
291
458
|
return (
|
|
292
|
-
<div className="max-w-[860px]
|
|
293
|
-
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
459
|
+
<div className="flex max-w-[860px] flex-col items-start gap-3">
|
|
460
|
+
{items.map((item) => (
|
|
461
|
+
<button
|
|
462
|
+
key={item}
|
|
463
|
+
type="button"
|
|
464
|
+
onClick={() => onSuggestion(item)}
|
|
465
|
+
className="inline-flex cursor-pointer items-center gap-2 text-[14px] leading-[1.4] text-kumo-inactive transition-colors duration-150 ease-out hover:text-kumo-brand focus-visible:text-kumo-brand focus-visible:outline-none active:scale-[0.98]"
|
|
466
|
+
>
|
|
467
|
+
<span className="grid size-5 flex-shrink-0 place-items-center" aria-hidden="true">
|
|
468
|
+
<span className="rotate-180 -scale-y-100">
|
|
469
|
+
<img
|
|
470
|
+
src={new URL("../assets/followup-arrow.svg", import.meta.url).href}
|
|
471
|
+
alt=""
|
|
472
|
+
className="h-[8.33333px] w-[11.6667px]"
|
|
473
|
+
/>
|
|
474
|
+
</span>
|
|
475
|
+
</span>
|
|
476
|
+
<span className="whitespace-nowrap">{item}</span>
|
|
477
|
+
</button>
|
|
478
|
+
))}
|
|
309
479
|
</div>
|
|
310
480
|
);
|
|
311
481
|
}
|
|
@@ -350,13 +520,73 @@ function scheduleCadence(input: Record<string, unknown>): string {
|
|
|
350
520
|
return timezone ? `${cadence} · ${timezone}` : cadence;
|
|
351
521
|
}
|
|
352
522
|
|
|
353
|
-
/** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id
|
|
523
|
+
/** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id 即 Inbox Schedule Definition 主键。 */
|
|
354
524
|
function scheduleIdOf(output: unknown): string {
|
|
355
525
|
if (typeof output !== "object" || output === null) return "";
|
|
356
526
|
const id = (output as Record<string, unknown>).id;
|
|
357
527
|
return typeof id === "string" ? id : "";
|
|
358
528
|
}
|
|
359
529
|
|
|
530
|
+
export function ScheduleConfirmation({
|
|
531
|
+
input,
|
|
532
|
+
disabled = false,
|
|
533
|
+
onCancel,
|
|
534
|
+
onConfirm,
|
|
535
|
+
}: {
|
|
536
|
+
input: Record<string, unknown>;
|
|
537
|
+
disabled?: boolean;
|
|
538
|
+
onCancel: () => void;
|
|
539
|
+
onConfirm: () => void;
|
|
540
|
+
}) {
|
|
541
|
+
const name = String(input.label ?? input.title ?? "Scheduled task");
|
|
542
|
+
const prompt = typeof input.prompt === "string" && input.prompt.trim()
|
|
543
|
+
? input.prompt.trim()
|
|
544
|
+
: "Run the scheduled task";
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<section
|
|
548
|
+
data-slot="schedule-card"
|
|
549
|
+
aria-label={`Confirm scheduled task: ${name}`}
|
|
550
|
+
className="flex w-full max-w-sm flex-col gap-3 rounded-2xl border border-kumo-line/70 bg-operation-base p-4 text-kumo-default shadow-[0_1px_2px_rgba(0,0,0,0.04),0_12px_32px_-16px_rgba(0,0,0,0.12)]"
|
|
551
|
+
>
|
|
552
|
+
<div className="flex items-center gap-2.5">
|
|
553
|
+
<span
|
|
554
|
+
className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-kumo-fill/70 text-kumo-subtle"
|
|
555
|
+
aria-hidden="true"
|
|
556
|
+
>
|
|
557
|
+
<Clock size={14} />
|
|
558
|
+
</span>
|
|
559
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
560
|
+
<span className="truncate text-[13.5px] leading-[18px] font-medium">
|
|
561
|
+
{name}
|
|
562
|
+
</span>
|
|
563
|
+
<span className="truncate font-mono text-[11px] leading-4 tracking-tight text-kumo-inactive">
|
|
564
|
+
{scheduleCadence(input)}
|
|
565
|
+
</span>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<div className="flex items-start gap-2 rounded-xl bg-kumo-fill/50 px-3 py-2">
|
|
570
|
+
<span className="shrink-0 font-mono text-[11px] leading-[18px] tracking-tight text-kumo-inactive">
|
|
571
|
+
task
|
|
572
|
+
</span>
|
|
573
|
+
<span className="min-w-0 flex-1 text-[13px] leading-[18px] text-kumo-subtle">
|
|
574
|
+
{prompt}
|
|
575
|
+
</span>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div className="flex items-center justify-end gap-2">
|
|
579
|
+
<WorkshopButton disabled={disabled} onClick={onCancel}>
|
|
580
|
+
Cancel
|
|
581
|
+
</WorkshopButton>
|
|
582
|
+
<WorkshopButton tone="primary" disabled={disabled} onClick={onConfirm}>
|
|
583
|
+
{disabled ? "Confirming…" : "Confirm schedule"}
|
|
584
|
+
</WorkshopButton>
|
|
585
|
+
</div>
|
|
586
|
+
</section>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
360
590
|
export function ScheduleBlock({
|
|
361
591
|
call,
|
|
362
592
|
onOpen,
|
|
@@ -365,6 +595,8 @@ export function ScheduleBlock({
|
|
|
365
595
|
/** 有 id 且宿主给了回调时,整张卡变成跳到「已安排」详情的按钮。 */
|
|
366
596
|
onOpen?: (scheduleId: string) => void;
|
|
367
597
|
}) {
|
|
598
|
+
// 待确认时由宿主把持久化审批渲染在输入框上方;这里不重复画第二张卡。
|
|
599
|
+
if (call.awaitingApproval) return null;
|
|
368
600
|
const state = call.failed ? "failed" : call.running ? "running" : "scheduled";
|
|
369
601
|
const scheduleId = state === "scheduled" ? scheduleIdOf(call.output) : "";
|
|
370
602
|
const clickable = Boolean(scheduleId && onOpen);
|
|
@@ -379,7 +611,7 @@ export function ScheduleBlock({
|
|
|
379
611
|
"aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
|
|
380
612
|
}
|
|
381
613
|
: {})}
|
|
382
|
-
className={`w-full rounded-2xl border border-kumo-line bg-
|
|
614
|
+
className={`w-full rounded-2xl border border-kumo-line bg-operation-base px-4 py-3 text-left${
|
|
383
615
|
clickable
|
|
384
616
|
? " 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
617
|
: ""
|
|
@@ -432,10 +664,109 @@ export function ScheduleBlock({
|
|
|
432
664
|
);
|
|
433
665
|
}
|
|
434
666
|
|
|
435
|
-
// ── 原位授权
|
|
667
|
+
// ── 原位授权 ──────────────────────────────────────────────
|
|
436
668
|
|
|
437
669
|
export type ApprovalDecision = "deny" | "allow_once" | "allow_level";
|
|
438
670
|
|
|
671
|
+
export function PermissionGrant({
|
|
672
|
+
capability,
|
|
673
|
+
requester,
|
|
674
|
+
reach,
|
|
675
|
+
disabled = false,
|
|
676
|
+
onGrant,
|
|
677
|
+
"aria-label": ariaLabel = `${capability} permission request`,
|
|
678
|
+
}: {
|
|
679
|
+
capability: string;
|
|
680
|
+
requester: string;
|
|
681
|
+
reach: readonly string[];
|
|
682
|
+
disabled?: boolean;
|
|
683
|
+
onGrant: (decision: ApprovalDecision) => void;
|
|
684
|
+
"aria-label"?: string;
|
|
685
|
+
}) {
|
|
686
|
+
const actionClassName =
|
|
687
|
+
"inline-flex h-10 w-[120px] items-center justify-center rounded-lg border border-kumo-line px-4 text-[14px] leading-[1.4] font-medium transition-[background-color,color,opacity,transform] duration-150 ease-out active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-ring/40 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100";
|
|
688
|
+
|
|
689
|
+
return (
|
|
690
|
+
<section
|
|
691
|
+
aria-label={ariaLabel}
|
|
692
|
+
className="w-full rounded-xl border border-kumo-line bg-operation-base p-4 text-kumo-default"
|
|
693
|
+
>
|
|
694
|
+
<div className="flex items-center gap-2">
|
|
695
|
+
<span
|
|
696
|
+
className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-kumo-fill text-kumo-subtle"
|
|
697
|
+
aria-hidden="true"
|
|
698
|
+
>
|
|
699
|
+
<Key size={14} weight="bold" />
|
|
700
|
+
</span>
|
|
701
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
702
|
+
<span className="truncate text-[14px] leading-[1.4] font-medium">
|
|
703
|
+
{capability}
|
|
704
|
+
</span>
|
|
705
|
+
<span className="truncate text-[12px] leading-4 text-kumo-inactive">
|
|
706
|
+
requested by {requester}
|
|
707
|
+
</span>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<div className="mt-4 flex max-h-[200px] flex-col gap-2 overflow-y-auto">
|
|
712
|
+
<span className="text-[12px] leading-4 text-kumo-inactive">
|
|
713
|
+
this grants
|
|
714
|
+
</span>
|
|
715
|
+
{reach.map((item, index) => (
|
|
716
|
+
<span
|
|
717
|
+
key={`${index}:${item}`}
|
|
718
|
+
className="flex items-baseline gap-2 text-[14px] leading-[1.4] text-kumo-subtle"
|
|
719
|
+
>
|
|
720
|
+
<span aria-hidden="true">•</span>
|
|
721
|
+
<span className="min-w-0 break-words whitespace-pre-wrap">{item}</span>
|
|
722
|
+
</span>
|
|
723
|
+
))}
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
727
|
+
<Tooltip
|
|
728
|
+
content="Allow every action at this execution level, without future prompts."
|
|
729
|
+
asChild
|
|
730
|
+
>
|
|
731
|
+
<button
|
|
732
|
+
type="button"
|
|
733
|
+
disabled={disabled}
|
|
734
|
+
onClick={() => onGrant("allow_level")}
|
|
735
|
+
className={`${actionClassName} border-0 bg-[linear-gradient(90deg,#ffd077_0%,#ff9938_100%)] text-kumo-inverse enabled:hover:brightness-95`}
|
|
736
|
+
>
|
|
737
|
+
Allow level
|
|
738
|
+
</button>
|
|
739
|
+
</Tooltip>
|
|
740
|
+
<button
|
|
741
|
+
type="button"
|
|
742
|
+
disabled={disabled}
|
|
743
|
+
onClick={() => onGrant("allow_once")}
|
|
744
|
+
className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
|
|
745
|
+
>
|
|
746
|
+
Allow once
|
|
747
|
+
</button>
|
|
748
|
+
<button
|
|
749
|
+
type="button"
|
|
750
|
+
disabled={disabled}
|
|
751
|
+
onClick={() => onGrant("deny")}
|
|
752
|
+
className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
|
|
753
|
+
>
|
|
754
|
+
Deny
|
|
755
|
+
</button>
|
|
756
|
+
</div>
|
|
757
|
+
</section>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function approvalReach(call: CloudOsToolCall): string[] {
|
|
762
|
+
const entries = Object.entries(call.input);
|
|
763
|
+
return entries.length > 0
|
|
764
|
+
? entries.map(
|
|
765
|
+
([name, value]) => `${humanizeToolName(name)}: ${safeStringify(value)}`,
|
|
766
|
+
)
|
|
767
|
+
: ["Run this tool without arguments"];
|
|
768
|
+
}
|
|
769
|
+
|
|
439
770
|
export function ApprovalBlock({
|
|
440
771
|
call,
|
|
441
772
|
approvalId,
|
|
@@ -447,70 +778,15 @@ export function ApprovalBlock({
|
|
|
447
778
|
disabled?: boolean;
|
|
448
779
|
onApprove: (approvalId: string, decision: ApprovalDecision) => void;
|
|
449
780
|
}) {
|
|
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
781
|
return (
|
|
457
782
|
<div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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>
|
|
783
|
+
<PermissionGrant
|
|
784
|
+
capability={humanizeToolName(call.toolName)}
|
|
785
|
+
requester="the agent"
|
|
786
|
+
reach={approvalReach(call)}
|
|
787
|
+
disabled={disabled}
|
|
788
|
+
onGrant={(decision) => onApprove(approvalId, decision)}
|
|
789
|
+
/>
|
|
514
790
|
</div>
|
|
515
791
|
);
|
|
516
792
|
}
|
|
@@ -542,67 +818,93 @@ export function ParallelBlock({
|
|
|
542
818
|
tools: ParallelTool[];
|
|
543
819
|
}) {
|
|
544
820
|
const done = tools.filter((tool) => tool.status === "done").length;
|
|
821
|
+
const [open, setOpen] = useState(false);
|
|
545
822
|
return (
|
|
546
823
|
<div className="max-w-[860px]">
|
|
547
|
-
<
|
|
824
|
+
<button
|
|
825
|
+
type="button"
|
|
826
|
+
onClick={() => setOpen((value) => !value)}
|
|
827
|
+
className="flex cursor-pointer items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
|
|
828
|
+
aria-expanded={open}
|
|
829
|
+
>
|
|
548
830
|
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
549
831
|
<WorkIcon Icon={ListChecks} />
|
|
550
832
|
</span>
|
|
551
833
|
<span className="min-w-0 truncate">
|
|
552
834
|
{label} · {done}/{tools.length}
|
|
553
835
|
</span>
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
836
|
+
<CaretRight
|
|
837
|
+
size={13}
|
|
838
|
+
weight="bold"
|
|
839
|
+
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
840
|
+
/>
|
|
841
|
+
</button>
|
|
842
|
+
{open && (
|
|
843
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
844
|
+
{tools.map((tool, index) => (
|
|
845
|
+
<div
|
|
846
|
+
key={`${tool.label}-${index}`}
|
|
847
|
+
className="flex items-center gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle"
|
|
848
|
+
>
|
|
849
|
+
<StatusDot status={tool.status} />
|
|
850
|
+
<span className="min-w-0 flex-1 truncate">{tool.label}</span>
|
|
851
|
+
{tool.meta && (
|
|
852
|
+
<span className="flex-shrink-0 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
853
|
+
{tool.meta}
|
|
854
|
+
</span>
|
|
855
|
+
)}
|
|
856
|
+
</div>
|
|
857
|
+
))}
|
|
858
|
+
</div>
|
|
859
|
+
)}
|
|
571
860
|
</div>
|
|
572
861
|
);
|
|
573
862
|
}
|
|
574
863
|
|
|
575
864
|
export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
|
|
865
|
+
const [open, setOpen] = useState(false);
|
|
576
866
|
return (
|
|
577
867
|
<div className="max-w-[860px]">
|
|
578
|
-
<
|
|
868
|
+
<button
|
|
869
|
+
type="button"
|
|
870
|
+
onClick={() => setOpen((value) => !value)}
|
|
871
|
+
className="flex cursor-pointer items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none"
|
|
872
|
+
aria-expanded={open}
|
|
873
|
+
>
|
|
579
874
|
<span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
|
580
875
|
<WorkIcon Icon={UsersThree} />
|
|
581
876
|
</span>
|
|
582
877
|
<span className="min-w-0 truncate">
|
|
583
878
|
{agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
|
|
584
879
|
</span>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
880
|
+
<CaretRight
|
|
881
|
+
size={13}
|
|
882
|
+
weight="bold"
|
|
883
|
+
className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
|
|
884
|
+
/>
|
|
885
|
+
</button>
|
|
886
|
+
{open && (
|
|
887
|
+
<div className="ml-8 mt-1 space-y-1">
|
|
888
|
+
{agents.map((agent, index) => (
|
|
889
|
+
<div
|
|
890
|
+
key={`${agent.name}-${index}`}
|
|
891
|
+
className="flex items-start gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px]"
|
|
892
|
+
>
|
|
893
|
+
<span className="mt-1.5 flex">
|
|
894
|
+
<StatusDot status={agent.status} />
|
|
598
895
|
</span>
|
|
599
|
-
|
|
600
|
-
<span className="block text-kumo-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
896
|
+
<span className="min-w-0 flex-1">
|
|
897
|
+
<span className="block truncate font-medium text-kumo-default">
|
|
898
|
+
{agent.name}
|
|
899
|
+
</span>
|
|
900
|
+
{agent.summary && (
|
|
901
|
+
<span className="block text-kumo-subtle">{agent.summary}</span>
|
|
902
|
+
)}
|
|
903
|
+
</span>
|
|
904
|
+
</div>
|
|
905
|
+
))}
|
|
906
|
+
</div>
|
|
907
|
+
)}
|
|
606
908
|
</div>
|
|
607
909
|
);
|
|
608
910
|
}
|