@springbrand/message-panel 0.1.3-alpha.32 → 0.1.3-alpha.34

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.
@@ -1,8 +1,9 @@
1
1
  import {
2
+ ArrowLeft,
3
+ ArrowRight,
2
4
  Bell,
3
5
  CaretRight,
4
6
  Check,
5
- CheckCircle,
6
7
  CircleNotch,
7
8
  Clock,
8
9
  Key,
@@ -10,16 +11,11 @@ import {
10
11
  UsersThree,
11
12
  WarningCircle,
12
13
  } from "@phosphor-icons/react";
13
- import { useId, useState } from "react";
14
+ import { useRef, useState } from "react";
14
15
  import { Tooltip } from "../primitives/tooltip";
15
16
  import { WorkshopButton } from "../primitives/workshop-controls";
16
17
  import { MarkdownMessage } from "./markdown-message";
17
- import {
18
- SECONDARY_DISCLOSURE_ICON,
19
- SECONDARY_DISCLOSURE_TITLE_WITH_ICON,
20
- SECONDARY_DISCLOSURE_TITLE_WITHOUT_ICON,
21
- WorkIcon,
22
- } from "./tool-rows";
18
+ import { WorkIcon } from "./tool-rows";
23
19
  import type {
24
20
  ParallelTool,
25
21
  PlanStep,
@@ -31,13 +27,12 @@ import {
31
27
  safeStringify,
32
28
  type CloudOsToolCall,
33
29
  } from "./tool-presentation";
34
- import { useLifecycleDisclosure } from "./use-lifecycle-disclosure";
35
30
 
36
31
  /**
37
32
  * UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
38
33
  * 后续建议、定时任务、并行/子 Agent、原位授权。视觉语言全部沿用 cloud-os:
39
34
  * 折叠工作行用 `px-1.5 py-1` 的行式布局,卡片用
40
- * `rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
35
+ * `themed-surface-inset rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
41
36
  * 授权卡沿用 renderActionCard 的 blocking callout(brand/40 边 + brand/10 底)。
42
37
  */
43
38
 
@@ -46,38 +41,37 @@ import { useLifecycleDisclosure } from "./use-lifecycle-disclosure";
46
41
  export function PlanBlock({
47
42
  steps,
48
43
  running,
49
- showCollapsedSummary = true,
50
44
  }: {
51
45
  steps: PlanStep[];
52
46
  running: boolean;
53
- showCollapsedSummary?: boolean;
54
47
  }) {
55
48
  const done = steps.filter((step) => step.status === "done").length;
56
49
  const current = steps.find((step) => step.status === "in_progress");
57
- const [open, toggle] = useLifecycleDisclosure(running);
50
+ const [open, setOpen] = useState(true);
58
51
 
59
52
  return (
60
- <div className="group">
53
+ <div className="group -ml-0.5">
61
54
  <button
62
55
  type="button"
63
- onClick={toggle}
64
- className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} w-full`}
56
+ onClick={() => setOpen((value) => !value)}
57
+ 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]"
65
58
  aria-expanded={open}
66
59
  >
67
- <span className={SECONDARY_DISCLOSURE_ICON}>
60
+ <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
68
61
  <WorkIcon Icon={ListChecks} />
69
62
  </span>
70
63
  <span className="min-w-0 flex-1">
71
- <span className="flex min-w-0 items-center gap-2">
64
+ <span className="flex min-w-0 items-center gap-2 text-[14px] leading-5 tracking-[-0.25px]">
72
65
  <span className={`min-w-0 truncate ${running ? "cos-thinking-shimmer" : ""}`}>
73
66
  Plan · {done}/{steps.length}
74
67
  </span>
75
68
  <CaretRight
76
- size={16}
69
+ size={13}
70
+ weight="bold"
77
71
  className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
78
72
  />
79
73
  </span>
80
- {!open && showCollapsedSummary && current && (
74
+ {!open && current && (
81
75
  <span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
82
76
  {current.text}
83
77
  </span>
@@ -85,34 +79,35 @@ export function PlanBlock({
85
79
  </span>
86
80
  </button>
87
81
  {open && (
88
- <div className="mt-1 flex flex-col gap-2 rounded-xl border border-kumo-line bg-[rgba(0,0,0,0.03)] px-4 py-3">
82
+ <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">
89
83
  {steps.map((step, index) => (
90
84
  <div
91
85
  key={`${step.text}-${index}`}
92
- className="flex items-center gap-2 text-[14px] leading-[1.4] font-normal tracking-normal"
86
+ className="flex items-start gap-2.5 text-[13px] leading-[19px] tracking-[-0.25px]"
93
87
  >
94
88
  <span
95
- className="flex size-4 flex-shrink-0 items-center justify-center"
89
+ className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center"
96
90
  aria-hidden="true"
97
91
  >
98
92
  {step.status === "done" ? (
99
- <CheckCircle size={16} weight="fill" className="text-[#00bf75]" />
93
+ <Check size={12} weight="bold" className="text-kumo-success" />
100
94
  ) : step.status === "in_progress" ? (
101
95
  <CircleNotch
102
- size={16}
103
- className="animate-spin text-[#ff9938] motion-reduce:animate-none"
96
+ size={12}
97
+ weight="bold"
98
+ className="animate-spin text-kumo-brand motion-reduce:animate-none"
104
99
  />
105
100
  ) : (
106
- <span className="h-1.5 w-1.5 rounded-full bg-[color:var(--text-color-kumo-default)] opacity-20" />
101
+ <span className="h-1.5 w-1.5 rounded-full bg-kumo-fill" />
107
102
  )}
108
103
  </span>
109
104
  <span
110
105
  className={
111
106
  step.status === "done"
112
- ? "min-w-0 flex-1 text-kumo-default/30 line-through"
107
+ ? "min-w-0 flex-1 text-kumo-inactive line-through"
113
108
  : step.status === "in_progress"
114
- ? "min-w-0 flex-1 text-kumo-default"
115
- : "min-w-0 flex-1 text-kumo-default/70"
109
+ ? "min-w-0 flex-1 font-medium text-kumo-default"
110
+ : "min-w-0 flex-1 text-kumo-subtle"
116
111
  }
117
112
  >
118
113
  {step.text}
@@ -172,7 +167,8 @@ export function AskUserBlock({
172
167
  // 首帧 questions 往往还是空数组,那之后每次 setAnswers 都在空数组上 map,
173
168
  // 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
174
169
  const [answers, setAnswers] = useState<DraftAnswer[]>([]);
175
- const contentId = useId();
170
+ const [current, setCurrent] = useState(0);
171
+ const direction = useRef<"back" | "forward">("forward");
176
172
  const answerAt = (index: number): DraftAnswer =>
177
173
  answers[index] ?? { selections: [], text: "" };
178
174
  const toggle = (questionIndex: number, option: string, multi: boolean) =>
@@ -202,43 +198,31 @@ export function AskUserBlock({
202
198
  // 在途只用于按钮转圈;权威状态永远是 part 的 state。
203
199
  const [inFlight, setInFlight] = useState(false);
204
200
  const outcome = askUserOutcome(call);
205
- const [open, toggleDisclosure] = useLifecycleDisclosure(
206
- outcome.kind === "pending",
207
- );
208
- const displayAnswerAt = (index: number): DraftAnswer => {
209
- if (outcome.kind !== "answered") return answerAt(index);
210
- const answer = outcome.answers[index];
211
- return {
212
- selections: answer?.selections ?? [],
213
- text: answer?.text ?? "",
214
- };
215
- };
201
+ const settledAnswers = outcome.kind === "answered" ? outcome.answers : [];
216
202
  // 服务端已经结算 = 这张卡永久不可操作,不管结算成什么。
217
203
  const resolved = outcome.kind !== "pending" || inFlight;
218
204
  const valid = questions.length > 0 && questions.every(
219
205
  (_question, index) =>
220
206
  answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
221
207
  );
222
- const titleBase = questions.length === 1
223
- ? "Clarifying question"
224
- : "Clarifying questions";
225
- const disclosureTitle = outcome.kind === "pending"
226
- ? questions.length === 1 && questions[0]
227
- ? `${titleBase} ${questions[0].prompt}`
228
- : questions.length > 1
229
- ? `${titleBase} · ${questions.length} questions`
230
- : titleBase
231
- : outcome.kind === "answered"
232
- ? questions.length > 1
233
- ? `${titleBase} · ${questions.length} answered`
234
- : `${titleBase} · Answered`
235
- : `${titleBase} · Closed`;
236
- const buttonLabel = inFlight ? "Submitting…" : "Submit";
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";
237
221
  const footnote = outcome.kind === "closed"
238
- ? outcome.reason === "user_replied_freeform"
239
- ? "Answered in the chat instead."
240
- : "Closed without an answer."
241
- : "";
222
+ ? outcome.reason === "user_replied_freeform"
223
+ ? "Answered in the chat instead."
224
+ : "Closed without an answer."
225
+ : "";
242
226
 
243
227
  const submit = () => {
244
228
  if (!onRespond) return;
@@ -254,129 +238,205 @@ export function AskUserBlock({
254
238
  }, () => setInFlight(false));
255
239
  };
256
240
 
257
- const closedButton = (
241
+ const settledButton = (
258
242
  <WorkshopButton
259
243
  tone="primary"
260
244
  disabled
261
245
  className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
262
246
  >
263
- Closed
247
+ {buttonLabel}
264
248
  </WorkshopButton>
265
249
  );
266
250
 
267
251
  return (
252
+ // 收成 w-fit:短问题不该撑成一条横贯全栏的长条,长问题到 560px 再换行。
268
253
  <div
269
- className="w-full"
254
+ className="w-fit min-w-[15rem] max-w-[560px]"
270
255
  data-part-type="ask_user"
271
256
  data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
272
257
  >
273
- <div className="flex flex-col gap-3 py-2.5">
274
- <button
275
- type="button"
276
- onClick={toggleDisclosure}
277
- aria-expanded={open}
278
- aria-controls={contentId}
279
- title={disclosureTitle}
280
- className={`${SECONDARY_DISCLOSURE_TITLE_WITHOUT_ICON} max-w-full`}
281
- >
282
- <span className="min-w-0 truncate">{disclosureTitle}</span>
283
- <CaretRight
284
- size={16}
285
- className={`shrink-0 transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
286
- />
287
- </button>
288
- <div
289
- id={contentId}
290
- data-ask-user-content=""
291
- className={open ? "flex flex-col gap-3" : "hidden"}
292
- >
293
- {outcome.kind !== "closed" ? (
294
- <>
295
- <div className="flex flex-col gap-4">
296
- {questions.map((question, questionIndex) => {
297
- const answer = displayAnswerAt(questionIndex);
298
- return (
299
- <div
300
- key={`${question.prompt}:${questionIndex}`}
301
- className="flex flex-col items-start gap-2"
302
- >
303
- <p className="m-0 text-[14px] leading-[1.4] font-normal text-black">
304
- {question.prompt}
305
- </p>
306
- {question.options.length > 0 && (
307
- <div className="flex flex-wrap items-center gap-2">
308
- {question.options.map((option) => {
309
- const selected = answer.selections.includes(option);
310
- return (
311
- <button
312
- key={option}
313
- type="button"
314
- aria-pressed={selected}
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
+ ))}
282
+ </div>
283
+ </div>
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}
315
320
  disabled={resolved || !onRespond}
316
- onClick={() =>
317
- toggle(questionIndex, option, question.multi)}
318
- className={`inline-flex w-fit items-center justify-center rounded-lg border px-4 py-2 text-left text-[14px] leading-[1.4] font-normal text-[rgba(0,0,0,0.7)] transition-[background,border-color] duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff9938]/30 ${
319
- selected
320
- ? `${resolved || !onRespond ? "cursor-default" : "cursor-pointer"} border-[#ff9938] bg-[#fff]`
321
- : resolved
322
- ? "cursor-default border-[rgba(0,0,0,0.07)] bg-[#fff]"
323
- : !onRespond
324
- ? "cursor-default border-[rgba(0,0,0,0.07)] bg-[#fff] opacity-60"
325
- : "cursor-pointer border-[rgba(0,0,0,0.07)] bg-[#fff] hover:border-transparent hover:bg-[linear-gradient(#fff,#fff)_padding-box,linear-gradient(90deg,#ff9938,#ffe46c)_border-box]"
326
- }`}
327
- >
328
- {option}
329
- </button>
330
- );
331
- })}
332
- </div>
333
- )}
334
- {question.allowCustom && (
335
- <label className="flex w-full flex-col gap-2">
336
- <span className="m-0 text-[14px] leading-[1.4] font-normal text-black">
337
- {question.options.length > 0 ? "Other" : "Answer"}
338
- </span>
339
- <input
340
- type="text"
341
- value={answer.text}
342
- maxLength={2_000}
343
- disabled={resolved || !onRespond}
344
- aria-label={`自定义回答:${question.prompt}`}
345
- placeholder={question.options.length > 0
346
- ? "Add a custom answer"
347
- : "Type your answer"}
348
- onChange={(event) =>
349
- setText(questionIndex, event.currentTarget.value)}
350
- className="rounded-lg border border-[rgba(0,0,0,0.07)] bg-[#fff] px-4 py-2 text-[14px] leading-[1.4] font-normal text-[rgba(0,0,0,0.7)] outline-none placeholder:text-kumo-inactive focus:border-[#ff9938] focus:ring-2 focus:ring-[#ff9938]/30 disabled:cursor-default"
351
- />
352
- </label>
353
- )}
354
- </div>
355
- );
356
- })}
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
357
  </div>
358
- {outcome.kind === "pending" && (
359
- <button
360
- type="button"
361
- disabled={resolved || !onRespond || !valid}
362
- onClick={submit}
363
- className="inline-flex h-10 min-w-[120px] self-start items-center justify-center rounded-lg bg-gradient-to-r from-[#ff9938] to-[#ffe46c] px-5 py-2.5 text-[14px] leading-5 font-medium text-black transition-opacity duration-150 ease-out hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff9938]/30 disabled:cursor-default disabled:opacity-50"
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]"
364
368
  >
365
- {buttonLabel}
366
- </button>
369
+ <ArrowLeft size={13} weight="bold" />
370
+ Back
371
+ </WorkshopButton>
367
372
  )}
368
- </>
369
- ) : (
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
+ ))}
370
430
  <div className="flex items-center gap-3">
371
431
  {footnote && (
372
432
  <p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
373
433
  {footnote}
374
434
  </p>
375
435
  )}
376
- {closedButton}
436
+ {settledButton}
377
437
  </div>
378
- )}
379
- </div>
438
+ </>
439
+ )}
380
440
  </div>
381
441
  </div>
382
442
  );
@@ -483,7 +543,7 @@ export function ScheduleConfirmation({
483
543
  <section
484
544
  data-slot="schedule-card"
485
545
  aria-label={`Confirm scheduled task: ${name}`}
486
- 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)]"
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)]"
487
547
  >
488
548
  <div className="flex items-center gap-2.5">
489
549
  <span
@@ -547,7 +607,7 @@ export function ScheduleBlock({
547
607
  "aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
548
608
  }
549
609
  : {})}
550
- className={`w-full rounded-2xl border border-kumo-line bg-operation-base px-4 py-3 text-left${
610
+ className={`w-full rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3 text-left${
551
611
  clickable
552
612
  ? " 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"
553
613
  : ""
@@ -625,7 +685,7 @@ export function PermissionGrant({
625
685
  return (
626
686
  <section
627
687
  aria-label={ariaLabel}
628
- className="w-full rounded-xl border border-kumo-line bg-operation-base p-4 text-kumo-default"
688
+ className="themed-surface-inset w-full rounded-xl border border-kumo-line bg-kumo-elevated p-4 text-kumo-default"
629
689
  >
630
690
  <div className="flex items-center gap-2">
631
691
  <span
@@ -749,104 +809,72 @@ function StatusDot({ status }: { status: ParallelTool["status"] }) {
749
809
  export function ParallelBlock({
750
810
  label,
751
811
  tools,
752
- running = tools.some((tool) => tool.status === "running"),
753
812
  }: {
754
813
  label: string;
755
814
  tools: ParallelTool[];
756
- running?: boolean;
757
815
  }) {
758
816
  const done = tools.filter((tool) => tool.status === "done").length;
759
- const [open, toggle] = useLifecycleDisclosure(running);
760
817
  return (
761
818
  <div className="max-w-[860px]">
762
- <button
763
- type="button"
764
- onClick={toggle}
765
- className={SECONDARY_DISCLOSURE_TITLE_WITH_ICON}
766
- aria-expanded={open}
767
- >
768
- <span className={SECONDARY_DISCLOSURE_ICON}>
819
+ <div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
820
+ <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
769
821
  <WorkIcon Icon={ListChecks} />
770
822
  </span>
771
823
  <span className="min-w-0 truncate">
772
824
  {label} · {done}/{tools.length}
773
825
  </span>
774
- <CaretRight
775
- size={16}
776
- className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
777
- />
778
- </button>
779
- {open && (
780
- <div className="mt-1 space-y-1">
781
- {tools.map((tool, index) => (
782
- <div
783
- key={`${tool.label}-${index}`}
784
- className="flex items-center gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle"
785
- >
786
- <StatusDot status={tool.status} />
787
- <span className="min-w-0 flex-1 truncate">{tool.label}</span>
788
- {tool.meta && (
789
- <span className="flex-shrink-0 font-mono text-[11px] leading-4 text-kumo-inactive">
790
- {tool.meta}
791
- </span>
792
- )}
793
- </div>
794
- ))}
795
- </div>
796
- )}
826
+ </div>
827
+ <div className="ml-8 mt-1 space-y-1">
828
+ {tools.map((tool, index) => (
829
+ <div
830
+ key={`${tool.label}-${index}`}
831
+ className="flex items-center gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle"
832
+ >
833
+ <StatusDot status={tool.status} />
834
+ <span className="min-w-0 flex-1 truncate">{tool.label}</span>
835
+ {tool.meta && (
836
+ <span className="flex-shrink-0 font-mono text-[11px] leading-4 text-kumo-inactive">
837
+ {tool.meta}
838
+ </span>
839
+ )}
840
+ </div>
841
+ ))}
842
+ </div>
797
843
  </div>
798
844
  );
799
845
  }
800
846
 
801
- export function SubAgentsBlock({
802
- agents,
803
- running = agents.some((agent) => agent.status === "running"),
804
- }: {
805
- agents: SubAgentView[];
806
- running?: boolean;
807
- }) {
808
- const [open, toggle] = useLifecycleDisclosure(running);
847
+ export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
809
848
  return (
810
849
  <div className="max-w-[860px]">
811
- <button
812
- type="button"
813
- onClick={toggle}
814
- className={SECONDARY_DISCLOSURE_TITLE_WITH_ICON}
815
- aria-expanded={open}
816
- >
817
- <span className={SECONDARY_DISCLOSURE_ICON}>
850
+ <div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
851
+ <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
818
852
  <WorkIcon Icon={UsersThree} />
819
853
  </span>
820
854
  <span className="min-w-0 truncate">
821
855
  {agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
822
856
  </span>
823
- <CaretRight
824
- size={16}
825
- className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
826
- />
827
- </button>
828
- {open && (
829
- <div className="mt-1 space-y-1">
830
- {agents.map((agent, index) => (
831
- <div
832
- key={`${agent.name}-${index}`}
833
- className="flex items-start gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px]"
834
- >
835
- <span className="mt-1.5 flex">
836
- <StatusDot status={agent.status} />
837
- </span>
838
- <span className="min-w-0 flex-1">
839
- <span className="block truncate font-medium text-kumo-default">
840
- {agent.name}
841
- </span>
842
- {agent.summary && (
843
- <span className="block text-kumo-subtle">{agent.summary}</span>
844
- )}
857
+ </div>
858
+ <div className="ml-8 mt-1 space-y-1">
859
+ {agents.map((agent, index) => (
860
+ <div
861
+ key={`${agent.name}-${index}`}
862
+ className="flex items-start gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px]"
863
+ >
864
+ <span className="mt-1.5 flex">
865
+ <StatusDot status={agent.status} />
866
+ </span>
867
+ <span className="min-w-0 flex-1">
868
+ <span className="block truncate font-medium text-kumo-default">
869
+ {agent.name}
845
870
  </span>
846
- </div>
847
- ))}
848
- </div>
849
- )}
871
+ {agent.summary && (
872
+ <span className="block text-kumo-subtle">{agent.summary}</span>
873
+ )}
874
+ </span>
875
+ </div>
876
+ ))}
877
+ </div>
850
878
  </div>
851
879
  );
852
880
  }
@@ -862,30 +890,31 @@ export function ErrorBlock({
862
890
  body?: string;
863
891
  onRetry?: () => void;
864
892
  }) {
865
- const [expanded, toggle] = useLifecycleDisclosure(false);
893
+ const [expanded, setExpanded] = useState(false);
866
894
  return (
867
895
  <div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
868
- <div className="flex w-full items-center gap-2">
896
+ <div className="flex w-full items-center gap-2 px-1.5 py-1">
869
897
  <button
870
898
  type="button"
871
- onClick={toggle}
872
- className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} flex-1`}
899
+ onClick={() => setExpanded((value) => !value)}
900
+ 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]"
873
901
  aria-expanded={expanded}
874
902
  >
875
903
  <span
876
- className={`${SECONDARY_DISCLOSURE_ICON} text-kumo-danger`}
904
+ className="flex h-5 w-5 flex-shrink-0 items-center justify-center text-kumo-danger"
877
905
  aria-hidden="true"
878
906
  >
879
- <WarningCircle size={15} weight="fill" />
907
+ <WarningCircle size={16} weight="fill" />
880
908
  </span>
881
909
  <span className="flex min-w-0 flex-1 items-center gap-1">
882
910
  <span className="min-w-0 truncate">
883
- <span>{title}: </span>
884
- <span>{body}</span>
911
+ <span className="font-medium text-kumo-danger">{title}: </span>
912
+ <span className="text-kumo-subtle">{body}</span>
885
913
  </span>
886
914
  {body && (
887
915
  <CaretRight
888
- size={16}
916
+ size={13}
917
+ weight="bold"
889
918
  className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${expanded ? "rotate-90" : ""}`}
890
919
  />
891
920
  )}
@@ -904,7 +933,7 @@ export function ErrorBlock({
904
933
  )}
905
934
  </div>
906
935
  {expanded && body && (
907
- <div className="mt-1">
936
+ <div className="ml-8 mt-1">
908
937
  <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">
909
938
  {body}
910
939
  </pre>