@springbrand/message-panel 0.1.3-alpha.2 → 0.1.3-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cloud-os/README.md +6 -6
- package/cloud-os/assets/followup-arrow.svg +3 -0
- package/cloud-os/assets/loading-corner.svg +3 -0
- package/cloud-os/assets/loading-mark.svg +16 -0
- package/cloud-os/assets/loading-spark.svg +3 -0
- package/cloud-os/assets/model-selected.svg +5 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +123 -69
- package/cloud-os/chat/markdown-message.tsx +82 -40
- package/cloud-os/chat/rich-blocks.tsx +494 -190
- package/cloud-os/chat/tool-presentation.ts +67 -6
- package/cloud-os/chat/tool-rows.tsx +87 -43
- package/cloud-os/chat/transcript-model.ts +215 -23
- package/cloud-os/composer/cloud-os-chat-input.tsx +242 -127
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +266 -0
- package/cloud-os/index.ts +29 -2
- package/cloud-os/layout/cloud-os-workspace-split.tsx +107 -24
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +93 -19
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +100 -40
- package/demo/cloud-os-chat-showcase.tsx +51 -17
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +3 -4
- package/src/camel/camel-chat-messages.tsx +82 -77
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/camel/camel-tool-presentation.tsx +19 -15
- package/src/camel/camel-turn.ts +1 -17
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -1
- package/src/composer/index.ts +1 -0
- package/src/composer/key-rules.ts +12 -0
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +0 -23
- package/src/parts/index.tsx +102 -63
- package/src/styles/index.css +4 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ArrowLeft,
|
|
3
|
+
ArrowRight,
|
|
2
4
|
Bell,
|
|
3
5
|
CaretRight,
|
|
4
6
|
Check,
|
|
5
7
|
CircleNotch,
|
|
8
|
+
Clock,
|
|
9
|
+
Key,
|
|
6
10
|
ListChecks,
|
|
7
|
-
ShieldCheck,
|
|
8
11
|
UsersThree,
|
|
9
12
|
WarningCircle,
|
|
10
13
|
} from "@phosphor-icons/react";
|
|
11
|
-
import { useState } from "react";
|
|
14
|
+
import { useRef, useState } from "react";
|
|
12
15
|
import { Tooltip } from "../primitives/tooltip";
|
|
13
16
|
import { WorkshopButton } from "../primitives/workshop-controls";
|
|
14
17
|
import { MarkdownMessage } from "./markdown-message";
|
|
@@ -18,7 +21,12 @@ import type {
|
|
|
18
21
|
PlanStep,
|
|
19
22
|
SubAgentView,
|
|
20
23
|
} from "./transcript-model";
|
|
21
|
-
import
|
|
24
|
+
import {
|
|
25
|
+
askUserOutcome,
|
|
26
|
+
humanizeToolName,
|
|
27
|
+
safeStringify,
|
|
28
|
+
type CloudOsToolCall,
|
|
29
|
+
} from "./tool-presentation";
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
|
|
@@ -118,129 +126,316 @@ interface AskQuestion {
|
|
|
118
126
|
prompt: string;
|
|
119
127
|
options: string[];
|
|
120
128
|
multi: boolean;
|
|
129
|
+
allowCustom: boolean;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
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 [];
|
|
133
|
+
if (!Array.isArray(input.questions)) return [];
|
|
134
|
+
return input.questions.map((item) => {
|
|
135
|
+
const question = (item ?? {}) as Record<string, unknown>;
|
|
136
|
+
const options = Array.isArray(question.options)
|
|
137
|
+
? question.options.filter((option): option is string =>
|
|
138
|
+
typeof option === "string"
|
|
139
|
+
)
|
|
140
|
+
: [];
|
|
141
|
+
return {
|
|
142
|
+
prompt: typeof question.question === "string"
|
|
143
|
+
? question.question
|
|
144
|
+
: "Choose an option",
|
|
145
|
+
options,
|
|
146
|
+
multi: question.multiSelect === true,
|
|
147
|
+
allowCustom: question.allowCustom === true || options.length === 0,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface DraftAnswer {
|
|
153
|
+
selections: string[];
|
|
154
|
+
text: string;
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
export function AskUserBlock({
|
|
151
158
|
call,
|
|
152
|
-
|
|
153
|
-
onAnswer,
|
|
159
|
+
onRespond,
|
|
154
160
|
}: {
|
|
155
161
|
call: CloudOsToolCall;
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
/** 缺省(宿主没接)时选项不可点 —— 形状对齐 ScheduleBlock 的 onOpen。 */
|
|
163
|
+
onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
|
|
158
164
|
}) {
|
|
159
165
|
const questions = questionsOf(call.input);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const [
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
166
|
+
// 不能用 `useState(() => questions.map(...))` 初始化:工具入参是**流式**到达的,
|
|
167
|
+
// 首帧 questions 往往还是空数组,那之后每次 setAnswers 都在空数组上 map,
|
|
168
|
+
// 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
|
|
169
|
+
const [answers, setAnswers] = useState<DraftAnswer[]>([]);
|
|
170
|
+
const [current, setCurrent] = useState(0);
|
|
171
|
+
const direction = useRef<"back" | "forward">("forward");
|
|
172
|
+
const answerAt = (index: number): DraftAnswer =>
|
|
173
|
+
answers[index] ?? { selections: [], text: "" };
|
|
174
|
+
const toggle = (questionIndex: number, option: string, multi: boolean) =>
|
|
175
|
+
setAnswers((current) => {
|
|
176
|
+
const next = questions.map((_, index) =>
|
|
177
|
+
current[index] ?? { selections: [], text: "" }
|
|
178
|
+
);
|
|
179
|
+
const answer = next[questionIndex]!;
|
|
180
|
+
next[questionIndex] = {
|
|
181
|
+
...answer,
|
|
182
|
+
selections: !multi
|
|
183
|
+
? [option]
|
|
184
|
+
: answer.selections.includes(option)
|
|
185
|
+
? answer.selections.filter((value) => value !== option)
|
|
186
|
+
: [...answer.selections, option],
|
|
187
|
+
};
|
|
188
|
+
return next;
|
|
189
|
+
});
|
|
190
|
+
const setText = (questionIndex: number, text: string) =>
|
|
191
|
+
setAnswers((current) => {
|
|
192
|
+
const next = questions.map((_, index) =>
|
|
193
|
+
current[index] ?? { selections: [], text: "" }
|
|
194
|
+
);
|
|
195
|
+
next[questionIndex] = { ...next[questionIndex]!, text };
|
|
196
|
+
return next;
|
|
197
|
+
});
|
|
198
|
+
// 在途只用于按钮转圈;权威状态永远是 part 的 state。
|
|
199
|
+
const [inFlight, setInFlight] = useState(false);
|
|
200
|
+
const outcome = askUserOutcome(call);
|
|
201
|
+
const settledAnswers = outcome.kind === "answered" ? outcome.answers : [];
|
|
202
|
+
// 服务端已经结算 = 这张卡永久不可操作,不管结算成什么。
|
|
203
|
+
const resolved = outcome.kind !== "pending" || inFlight;
|
|
204
|
+
const valid = questions.length > 0 && questions.every(
|
|
205
|
+
(_question, index) =>
|
|
206
|
+
answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
|
|
167
207
|
);
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
208
|
+
const currentIndex = Math.min(current, Math.max(questions.length - 1, 0));
|
|
209
|
+
const currentQuestion = questions[currentIndex];
|
|
210
|
+
const currentAnswer = answerAt(currentIndex);
|
|
211
|
+
const currentValid = currentAnswer.selections.length > 0 ||
|
|
212
|
+
Boolean(currentAnswer.text.trim());
|
|
213
|
+
const isLast = currentIndex === questions.length - 1;
|
|
214
|
+
const buttonLabel = outcome.kind === "answered"
|
|
215
|
+
? "Submitted"
|
|
216
|
+
: outcome.kind === "closed"
|
|
217
|
+
? "Closed"
|
|
218
|
+
: inFlight
|
|
219
|
+
? "Submitting…"
|
|
220
|
+
: "Submit";
|
|
221
|
+
const footnote = outcome.kind === "closed"
|
|
222
|
+
? outcome.reason === "user_replied_freeform"
|
|
223
|
+
? "Answered in the chat instead."
|
|
224
|
+
: "Closed without an answer."
|
|
225
|
+
: "";
|
|
226
|
+
|
|
227
|
+
const submit = () => {
|
|
228
|
+
if (!onRespond) return;
|
|
229
|
+
setInFlight(true);
|
|
230
|
+
// 失败就把按钮放回可点:权威状态由 part 决定,这里只管别把用户锁死。
|
|
231
|
+
void onRespond(call.toolCallId, {
|
|
232
|
+
answers: questions.map((_, index) => ({
|
|
233
|
+
selections: answerAt(index).selections,
|
|
234
|
+
text: answerAt(index).text.trim(),
|
|
235
|
+
})),
|
|
236
|
+
}).then((ok) => {
|
|
237
|
+
if (!ok) setInFlight(false);
|
|
238
|
+
}, () => setInFlight(false));
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const settledButton = (
|
|
242
|
+
<WorkshopButton
|
|
243
|
+
tone="primary"
|
|
244
|
+
disabled
|
|
245
|
+
className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
246
|
+
>
|
|
247
|
+
{buttonLabel}
|
|
248
|
+
</WorkshopButton>
|
|
172
249
|
);
|
|
173
250
|
|
|
174
251
|
return (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
})}
|
|
252
|
+
// 收成 w-fit:短问题不该撑成一条横贯全栏的长条,长问题到 560px 再换行。
|
|
253
|
+
<div
|
|
254
|
+
className="w-fit min-w-[15rem] max-w-[560px]"
|
|
255
|
+
data-part-type="ask_user"
|
|
256
|
+
data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
|
|
257
|
+
>
|
|
258
|
+
<div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-kumo-base px-3 py-2.5">
|
|
259
|
+
{outcome.kind === "pending" ? (
|
|
260
|
+
<>
|
|
261
|
+
{questions.length > 1 && (
|
|
262
|
+
<div className="flex flex-col gap-2">
|
|
263
|
+
<div className="flex items-center justify-between text-[11px] font-medium leading-4 text-kumo-inactive">
|
|
264
|
+
<span>Clarifying questions</span>
|
|
265
|
+
<span className="tabular-nums">
|
|
266
|
+
{currentIndex + 1}/{questions.length}
|
|
267
|
+
</span>
|
|
268
|
+
</div>
|
|
269
|
+
<div className="flex gap-1" aria-hidden="true">
|
|
270
|
+
{questions.map((question, index) => (
|
|
271
|
+
<span
|
|
272
|
+
key={`${question.prompt}:${index}`}
|
|
273
|
+
className={`h-1 flex-1 rounded-full ${
|
|
274
|
+
index < currentIndex
|
|
275
|
+
? "bg-kumo-brand"
|
|
276
|
+
: index === currentIndex
|
|
277
|
+
? "bg-kumo-brand/60"
|
|
278
|
+
: "bg-kumo-fill"
|
|
279
|
+
}`}
|
|
280
|
+
/>
|
|
281
|
+
))}
|
|
222
282
|
</div>
|
|
223
283
|
</div>
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
284
|
+
)}
|
|
285
|
+
{currentQuestion && (
|
|
286
|
+
<div className="overflow-hidden">
|
|
287
|
+
<div
|
|
288
|
+
key={currentIndex}
|
|
289
|
+
className="cos-ask-user-question flex flex-col gap-2.5"
|
|
290
|
+
data-direction={direction.current}
|
|
291
|
+
>
|
|
292
|
+
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
293
|
+
{currentQuestion.prompt}
|
|
294
|
+
</p>
|
|
295
|
+
{currentQuestion.options.length > 0 && (
|
|
296
|
+
<ul className="m-0 flex list-none flex-col gap-1.5 p-0">
|
|
297
|
+
{currentQuestion.options.map((option) => {
|
|
298
|
+
const selected = currentAnswer.selections.includes(
|
|
299
|
+
option,
|
|
300
|
+
);
|
|
301
|
+
return (
|
|
302
|
+
<li key={option} className="m-0 p-0">
|
|
303
|
+
<label
|
|
304
|
+
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 ${
|
|
305
|
+
resolved || !onRespond
|
|
306
|
+
? "cursor-default text-kumo-inactive"
|
|
307
|
+
: "cursor-pointer text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
308
|
+
} ${
|
|
309
|
+
selected
|
|
310
|
+
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
311
|
+
: "border-kumo-line bg-kumo-base"
|
|
312
|
+
}`}
|
|
313
|
+
>
|
|
314
|
+
<input
|
|
315
|
+
type={currentQuestion.multi
|
|
316
|
+
? "checkbox"
|
|
317
|
+
: "radio"}
|
|
318
|
+
name={`${call.toolCallId}-${currentIndex}`}
|
|
319
|
+
checked={selected}
|
|
320
|
+
disabled={resolved || !onRespond}
|
|
321
|
+
onChange={() =>
|
|
322
|
+
toggle(
|
|
323
|
+
currentIndex,
|
|
324
|
+
option,
|
|
325
|
+
currentQuestion.multi,
|
|
326
|
+
)}
|
|
327
|
+
className="h-4 w-4 shrink-0 accent-[var(--color-kumo-brand)]"
|
|
328
|
+
/>
|
|
329
|
+
<span>{option}</span>
|
|
330
|
+
</label>
|
|
331
|
+
</li>
|
|
332
|
+
);
|
|
333
|
+
})}
|
|
334
|
+
</ul>
|
|
335
|
+
)}
|
|
336
|
+
{currentQuestion.allowCustom && (
|
|
337
|
+
<label className="flex flex-col gap-1 text-[12px] leading-4 text-kumo-subtle">
|
|
338
|
+
<span>
|
|
339
|
+
{currentQuestion.options.length > 0 ? "Other" : "Answer"}
|
|
340
|
+
</span>
|
|
341
|
+
<input
|
|
342
|
+
type="text"
|
|
343
|
+
value={currentAnswer.text}
|
|
344
|
+
maxLength={2_000}
|
|
345
|
+
disabled={resolved || !onRespond}
|
|
346
|
+
aria-label={`自定义回答:${currentQuestion.prompt}`}
|
|
347
|
+
placeholder={currentQuestion.options.length > 0
|
|
348
|
+
? "Add a custom answer"
|
|
349
|
+
: "Type your answer"}
|
|
350
|
+
onChange={(event) =>
|
|
351
|
+
setText(currentIndex, event.currentTarget.value)}
|
|
352
|
+
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"
|
|
353
|
+
/>
|
|
354
|
+
</label>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
<div className="flex items-center gap-2">
|
|
360
|
+
{questions.length > 1 && (
|
|
361
|
+
<WorkshopButton
|
|
362
|
+
disabled={currentIndex === 0 || resolved}
|
|
363
|
+
onClick={() => {
|
|
364
|
+
direction.current = "back";
|
|
365
|
+
setCurrent((index) => Math.max(0, index - 1));
|
|
366
|
+
}}
|
|
367
|
+
className="!h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
368
|
+
>
|
|
369
|
+
<ArrowLeft size={13} weight="bold" />
|
|
370
|
+
Back
|
|
371
|
+
</WorkshopButton>
|
|
372
|
+
)}
|
|
373
|
+
<WorkshopButton
|
|
374
|
+
tone="primary"
|
|
375
|
+
disabled={
|
|
376
|
+
resolved || !onRespond ||
|
|
377
|
+
(questions.length > 1 && !isLast ? !currentValid : !valid)
|
|
378
|
+
}
|
|
379
|
+
onClick={() => {
|
|
380
|
+
if (questions.length > 1 && !isLast) {
|
|
381
|
+
direction.current = "forward";
|
|
382
|
+
setCurrent((index) =>
|
|
383
|
+
Math.min(questions.length - 1, index + 1)
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
submit();
|
|
388
|
+
}}
|
|
389
|
+
className="ml-auto !h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
390
|
+
>
|
|
391
|
+
{questions.length > 1 && !isLast ? (
|
|
392
|
+
<>
|
|
393
|
+
Next
|
|
394
|
+
<ArrowRight size={13} weight="bold" />
|
|
395
|
+
</>
|
|
396
|
+
) : buttonLabel}
|
|
397
|
+
</WorkshopButton>
|
|
398
|
+
</div>
|
|
399
|
+
</>
|
|
400
|
+
) : (
|
|
401
|
+
<>
|
|
402
|
+
{outcome.kind === "answered" &&
|
|
403
|
+
questions.map((question, questionIndex) => (
|
|
404
|
+
<div
|
|
405
|
+
key={`${question.prompt}:${questionIndex}`}
|
|
406
|
+
className="flex flex-col gap-1.5"
|
|
407
|
+
>
|
|
408
|
+
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
409
|
+
{question.prompt}
|
|
410
|
+
</p>
|
|
411
|
+
{settledAnswers[questionIndex] && (
|
|
412
|
+
<div className="flex flex-col gap-1 text-[12px] leading-4 text-kumo-inactive">
|
|
413
|
+
{settledAnswers[questionIndex]!.selections.length > 0 && (
|
|
414
|
+
<p className="m-0">
|
|
415
|
+
Answered: {settledAnswers[questionIndex]!.selections
|
|
416
|
+
.join(" / ")}
|
|
417
|
+
</p>
|
|
418
|
+
)}
|
|
419
|
+
{settledAnswers[questionIndex]!.text.trim() && (
|
|
420
|
+
<p className="m-0">
|
|
421
|
+
{settledAnswers[questionIndex]!.selections.length > 0
|
|
422
|
+
? `补充:${settledAnswers[questionIndex]!.text.trim()}`
|
|
423
|
+
: `Answered: ${settledAnswers[questionIndex]!.text.trim()}`}
|
|
424
|
+
</p>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
))}
|
|
430
|
+
<div className="flex items-center gap-3">
|
|
431
|
+
{footnote && (
|
|
432
|
+
<p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
|
|
433
|
+
{footnote}
|
|
434
|
+
</p>
|
|
435
|
+
)}
|
|
436
|
+
{settledButton}
|
|
437
|
+
</div>
|
|
438
|
+
</>
|
|
244
439
|
)}
|
|
245
440
|
</div>
|
|
246
441
|
</div>
|
|
@@ -257,23 +452,26 @@ export function SuggestionsBlock({
|
|
|
257
452
|
onSuggestion: (text: string) => void;
|
|
258
453
|
}) {
|
|
259
454
|
return (
|
|
260
|
-
<div className="max-w-[860px]
|
|
261
|
-
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
455
|
+
<div className="flex max-w-[860px] flex-col items-start gap-3">
|
|
456
|
+
{items.map((item) => (
|
|
457
|
+
<button
|
|
458
|
+
key={item}
|
|
459
|
+
type="button"
|
|
460
|
+
onClick={() => onSuggestion(item)}
|
|
461
|
+
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]"
|
|
462
|
+
>
|
|
463
|
+
<span className="grid size-5 flex-shrink-0 place-items-center" aria-hidden="true">
|
|
464
|
+
<span className="rotate-180 -scale-y-100">
|
|
465
|
+
<img
|
|
466
|
+
src={new URL("../assets/followup-arrow.svg", import.meta.url).href}
|
|
467
|
+
alt=""
|
|
468
|
+
className="h-[8.33333px] w-[11.6667px]"
|
|
469
|
+
/>
|
|
470
|
+
</span>
|
|
471
|
+
</span>
|
|
472
|
+
<span className="whitespace-nowrap">{item}</span>
|
|
473
|
+
</button>
|
|
474
|
+
))}
|
|
277
475
|
</div>
|
|
278
476
|
);
|
|
279
477
|
}
|
|
@@ -318,13 +516,73 @@ function scheduleCadence(input: Record<string, unknown>): string {
|
|
|
318
516
|
return timezone ? `${cadence} · ${timezone}` : cadence;
|
|
319
517
|
}
|
|
320
518
|
|
|
321
|
-
/** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id
|
|
519
|
+
/** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id 即 Inbox Schedule Definition 主键。 */
|
|
322
520
|
function scheduleIdOf(output: unknown): string {
|
|
323
521
|
if (typeof output !== "object" || output === null) return "";
|
|
324
522
|
const id = (output as Record<string, unknown>).id;
|
|
325
523
|
return typeof id === "string" ? id : "";
|
|
326
524
|
}
|
|
327
525
|
|
|
526
|
+
export function ScheduleConfirmation({
|
|
527
|
+
input,
|
|
528
|
+
disabled = false,
|
|
529
|
+
onCancel,
|
|
530
|
+
onConfirm,
|
|
531
|
+
}: {
|
|
532
|
+
input: Record<string, unknown>;
|
|
533
|
+
disabled?: boolean;
|
|
534
|
+
onCancel: () => void;
|
|
535
|
+
onConfirm: () => void;
|
|
536
|
+
}) {
|
|
537
|
+
const name = String(input.label ?? input.title ?? "Scheduled task");
|
|
538
|
+
const prompt = typeof input.prompt === "string" && input.prompt.trim()
|
|
539
|
+
? input.prompt.trim()
|
|
540
|
+
: "Run the scheduled task";
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<section
|
|
544
|
+
data-slot="schedule-card"
|
|
545
|
+
aria-label={`Confirm scheduled task: ${name}`}
|
|
546
|
+
className="themed-surface-inset flex w-full max-w-sm flex-col gap-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated 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)]"
|
|
547
|
+
>
|
|
548
|
+
<div className="flex items-center gap-2.5">
|
|
549
|
+
<span
|
|
550
|
+
className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-kumo-fill/70 text-kumo-subtle"
|
|
551
|
+
aria-hidden="true"
|
|
552
|
+
>
|
|
553
|
+
<Clock size={14} />
|
|
554
|
+
</span>
|
|
555
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
556
|
+
<span className="truncate text-[13.5px] leading-[18px] font-medium">
|
|
557
|
+
{name}
|
|
558
|
+
</span>
|
|
559
|
+
<span className="truncate font-mono text-[11px] leading-4 tracking-tight text-kumo-inactive">
|
|
560
|
+
{scheduleCadence(input)}
|
|
561
|
+
</span>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<div className="flex items-start gap-2 rounded-xl bg-kumo-fill/50 px-3 py-2">
|
|
566
|
+
<span className="shrink-0 font-mono text-[11px] leading-[18px] tracking-tight text-kumo-inactive">
|
|
567
|
+
task
|
|
568
|
+
</span>
|
|
569
|
+
<span className="min-w-0 flex-1 text-[13px] leading-[18px] text-kumo-subtle">
|
|
570
|
+
{prompt}
|
|
571
|
+
</span>
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<div className="flex items-center justify-end gap-2">
|
|
575
|
+
<WorkshopButton disabled={disabled} onClick={onCancel}>
|
|
576
|
+
Cancel
|
|
577
|
+
</WorkshopButton>
|
|
578
|
+
<WorkshopButton tone="primary" disabled={disabled} onClick={onConfirm}>
|
|
579
|
+
{disabled ? "Confirming…" : "Confirm schedule"}
|
|
580
|
+
</WorkshopButton>
|
|
581
|
+
</div>
|
|
582
|
+
</section>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
328
586
|
export function ScheduleBlock({
|
|
329
587
|
call,
|
|
330
588
|
onOpen,
|
|
@@ -333,6 +591,8 @@ export function ScheduleBlock({
|
|
|
333
591
|
/** 有 id 且宿主给了回调时,整张卡变成跳到「已安排」详情的按钮。 */
|
|
334
592
|
onOpen?: (scheduleId: string) => void;
|
|
335
593
|
}) {
|
|
594
|
+
// 待确认时由宿主把持久化审批渲染在输入框上方;这里不重复画第二张卡。
|
|
595
|
+
if (call.awaitingApproval) return null;
|
|
336
596
|
const state = call.failed ? "failed" : call.running ? "running" : "scheduled";
|
|
337
597
|
const scheduleId = state === "scheduled" ? scheduleIdOf(call.output) : "";
|
|
338
598
|
const clickable = Boolean(scheduleId && onOpen);
|
|
@@ -400,10 +660,109 @@ export function ScheduleBlock({
|
|
|
400
660
|
);
|
|
401
661
|
}
|
|
402
662
|
|
|
403
|
-
// ── 原位授权
|
|
663
|
+
// ── 原位授权 ──────────────────────────────────────────────
|
|
404
664
|
|
|
405
665
|
export type ApprovalDecision = "deny" | "allow_once" | "allow_level";
|
|
406
666
|
|
|
667
|
+
export function PermissionGrant({
|
|
668
|
+
capability,
|
|
669
|
+
requester,
|
|
670
|
+
reach,
|
|
671
|
+
disabled = false,
|
|
672
|
+
onGrant,
|
|
673
|
+
"aria-label": ariaLabel = `${capability} permission request`,
|
|
674
|
+
}: {
|
|
675
|
+
capability: string;
|
|
676
|
+
requester: string;
|
|
677
|
+
reach: readonly string[];
|
|
678
|
+
disabled?: boolean;
|
|
679
|
+
onGrant: (decision: ApprovalDecision) => void;
|
|
680
|
+
"aria-label"?: string;
|
|
681
|
+
}) {
|
|
682
|
+
const actionClassName =
|
|
683
|
+
"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";
|
|
684
|
+
|
|
685
|
+
return (
|
|
686
|
+
<section
|
|
687
|
+
aria-label={ariaLabel}
|
|
688
|
+
className="themed-surface-inset w-full rounded-xl border border-kumo-line bg-kumo-elevated p-4 text-kumo-default"
|
|
689
|
+
>
|
|
690
|
+
<div className="flex items-center gap-2">
|
|
691
|
+
<span
|
|
692
|
+
className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-kumo-fill text-kumo-subtle"
|
|
693
|
+
aria-hidden="true"
|
|
694
|
+
>
|
|
695
|
+
<Key size={14} weight="bold" />
|
|
696
|
+
</span>
|
|
697
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
698
|
+
<span className="truncate text-[14px] leading-[1.4] font-medium">
|
|
699
|
+
{capability}
|
|
700
|
+
</span>
|
|
701
|
+
<span className="truncate text-[12px] leading-4 text-kumo-inactive">
|
|
702
|
+
requested by {requester}
|
|
703
|
+
</span>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
<div className="mt-4 flex max-h-[200px] flex-col gap-2 overflow-y-auto">
|
|
708
|
+
<span className="text-[12px] leading-4 text-kumo-inactive">
|
|
709
|
+
this grants
|
|
710
|
+
</span>
|
|
711
|
+
{reach.map((item, index) => (
|
|
712
|
+
<span
|
|
713
|
+
key={`${index}:${item}`}
|
|
714
|
+
className="flex items-baseline gap-2 text-[14px] leading-[1.4] text-kumo-subtle"
|
|
715
|
+
>
|
|
716
|
+
<span aria-hidden="true">•</span>
|
|
717
|
+
<span className="min-w-0 break-words whitespace-pre-wrap">{item}</span>
|
|
718
|
+
</span>
|
|
719
|
+
))}
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
723
|
+
<Tooltip
|
|
724
|
+
content="Allow every action at this execution level, without future prompts."
|
|
725
|
+
asChild
|
|
726
|
+
>
|
|
727
|
+
<button
|
|
728
|
+
type="button"
|
|
729
|
+
disabled={disabled}
|
|
730
|
+
onClick={() => onGrant("allow_level")}
|
|
731
|
+
className={`${actionClassName} border-0 bg-[linear-gradient(90deg,#ffd077_0%,#ff9938_100%)] text-kumo-inverse enabled:hover:brightness-95`}
|
|
732
|
+
>
|
|
733
|
+
Allow level
|
|
734
|
+
</button>
|
|
735
|
+
</Tooltip>
|
|
736
|
+
<button
|
|
737
|
+
type="button"
|
|
738
|
+
disabled={disabled}
|
|
739
|
+
onClick={() => onGrant("allow_once")}
|
|
740
|
+
className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
|
|
741
|
+
>
|
|
742
|
+
Allow once
|
|
743
|
+
</button>
|
|
744
|
+
<button
|
|
745
|
+
type="button"
|
|
746
|
+
disabled={disabled}
|
|
747
|
+
onClick={() => onGrant("deny")}
|
|
748
|
+
className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
|
|
749
|
+
>
|
|
750
|
+
Deny
|
|
751
|
+
</button>
|
|
752
|
+
</div>
|
|
753
|
+
</section>
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function approvalReach(call: CloudOsToolCall): string[] {
|
|
758
|
+
const entries = Object.entries(call.input);
|
|
759
|
+
return entries.length > 0
|
|
760
|
+
? entries.map(
|
|
761
|
+
([name, value]) => `${humanizeToolName(name)}: ${safeStringify(value)}`,
|
|
762
|
+
)
|
|
763
|
+
: ["Run this tool without arguments"];
|
|
764
|
+
}
|
|
765
|
+
|
|
407
766
|
export function ApprovalBlock({
|
|
408
767
|
call,
|
|
409
768
|
approvalId,
|
|
@@ -415,70 +774,15 @@ export function ApprovalBlock({
|
|
|
415
774
|
disabled?: boolean;
|
|
416
775
|
onApprove: (approvalId: string, decision: ApprovalDecision) => void;
|
|
417
776
|
}) {
|
|
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
777
|
return (
|
|
425
778
|
<div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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>
|
|
779
|
+
<PermissionGrant
|
|
780
|
+
capability={humanizeToolName(call.toolName)}
|
|
781
|
+
requester="the agent"
|
|
782
|
+
reach={approvalReach(call)}
|
|
783
|
+
disabled={disabled}
|
|
784
|
+
onGrant={(decision) => onApprove(approvalId, decision)}
|
|
785
|
+
/>
|
|
482
786
|
</div>
|
|
483
787
|
);
|
|
484
788
|
}
|