@springbrand/message-panel 0.1.3-alpha.33 → 0.1.3-alpha.36

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.
@@ -10,14 +10,19 @@ import {
10
10
  UsersThree,
11
11
  WarningCircle,
12
12
  } from "@phosphor-icons/react";
13
- import { useId, useState } from "react";
13
+ import { useState } from "react";
14
+ import {
15
+ Collapsible,
16
+ CollapsibleContent,
17
+ CollapsibleTrigger,
18
+ } from "../primitives/collapsible";
14
19
  import { Tooltip } from "../primitives/tooltip";
15
20
  import { WorkshopButton } from "../primitives/workshop-controls";
16
21
  import { MarkdownMessage } from "./markdown-message";
22
+ import { collapsePanel } from "./surfaces";
17
23
  import {
18
- SECONDARY_DISCLOSURE_ICON,
19
- SECONDARY_DISCLOSURE_TITLE_WITH_ICON,
20
- SECONDARY_DISCLOSURE_TITLE_WITHOUT_ICON,
24
+ DISCLOSURE_ICON,
25
+ DISCLOSURE_TITLE_WITH_ICON,
21
26
  WorkIcon,
22
27
  } from "./tool-rows";
23
28
  import type {
@@ -31,13 +36,12 @@ import {
31
36
  safeStringify,
32
37
  type CloudOsToolCall,
33
38
  } from "./tool-presentation";
34
- import { useLifecycleDisclosure } from "./use-lifecycle-disclosure";
35
39
 
36
40
  /**
37
41
  * UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
38
42
  * 后续建议、定时任务、并行/子 Agent、原位授权。视觉语言全部沿用 cloud-os:
39
- * 折叠工作行用 `px-1.5 py-1` 的行式布局,卡片用
40
- * `rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
43
+ * 折叠工作行用 `py-1` 的行式布局,卡片用
44
+ * `themed-surface-inset rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
41
45
  * 授权卡沿用 renderActionCard 的 blocking callout(brand/40 边 + brand/10 底)。
42
46
  */
43
47
 
@@ -46,53 +50,58 @@ import { useLifecycleDisclosure } from "./use-lifecycle-disclosure";
46
50
  export function PlanBlock({
47
51
  steps,
48
52
  running,
49
- showCollapsedSummary = true,
53
+ defaultOpen = true,
50
54
  }: {
51
55
  steps: PlanStep[];
52
56
  running: boolean;
53
- showCollapsedSummary?: boolean;
57
+ defaultOpen?: boolean;
54
58
  }) {
55
59
  const done = steps.filter((step) => step.status === "done").length;
56
60
  const current = steps.find((step) => step.status === "in_progress");
57
- const [open, toggle] = useLifecycleDisclosure(running);
61
+ const [open, setOpen] = useState(defaultOpen);
58
62
 
59
63
  return (
60
- <div className="group">
61
- <button
62
- type="button"
63
- onClick={toggle}
64
- className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} w-full`}
65
- aria-expanded={open}
64
+ <Collapsible open={open} onOpenChange={setOpen} className="group">
65
+ <CollapsibleTrigger
66
+ className={`${DISCLOSURE_TITLE_WITH_ICON} w-full`}
66
67
  >
67
- <span className={SECONDARY_DISCLOSURE_ICON}>
68
- <WorkIcon Icon={ListChecks} />
68
+ <span
69
+ className={`${DISCLOSURE_ICON} rounded-full`}
70
+ role="progressbar"
71
+ aria-label="Plan progress"
72
+ aria-valuemin={0}
73
+ aria-valuemax={steps.length}
74
+ aria-valuenow={done}
75
+ style={{
76
+ background: `conic-gradient(var(--color-kumo-brand) ${(done / steps.length) * 100}%, var(--color-kumo-fill) 0)`,
77
+ }}
78
+ >
79
+ <span className="size-2.5 rounded-full bg-kumo-base" />
69
80
  </span>
70
- <span className="min-w-0 flex-1">
71
- <span className="flex min-w-0 items-center gap-2">
72
- <span className={`min-w-0 truncate ${running ? "cos-thinking-shimmer" : ""}`}>
73
- Plan · {done}/{steps.length}
74
- </span>
75
- <CaretRight
76
- size={16}
77
- className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
78
- />
81
+ <span className="flex min-w-0 flex-1 items-center gap-2">
82
+ <span className={`shrink-0 ${running ? "cos-thinking-shimmer" : ""}`}>
83
+ Plan · {done}/{steps.length}
79
84
  </span>
80
- {!open && showCollapsedSummary && current && (
81
- <span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
85
+ {!open && current && (
86
+ <span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-[rgba(0,0,0,0.3)]">
82
87
  {current.text}
83
88
  </span>
84
89
  )}
90
+ <CaretRight
91
+ size={16}
92
+ className={`ml-auto flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
93
+ />
85
94
  </span>
86
- </button>
87
- {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">
95
+ </CollapsibleTrigger>
96
+ <CollapsibleContent className={collapsePanel}>
97
+ <div className="-mx-2.5 mt-1 flex flex-col gap-2 rounded-xl border border-[rgba(0,0,0,0.07)] px-4 py-3">
89
98
  {steps.map((step, index) => (
90
99
  <div
91
100
  key={`${step.text}-${index}`}
92
101
  className="flex items-center gap-2 text-[14px] leading-[1.4] font-normal tracking-normal"
93
102
  >
94
103
  <span
95
- className="flex size-4 flex-shrink-0 items-center justify-center"
104
+ className="flex size-4 shrink-0 items-center justify-center"
96
105
  aria-hidden="true"
97
106
  >
98
107
  {step.status === "done" ? (
@@ -120,8 +129,8 @@ export function PlanBlock({
120
129
  </div>
121
130
  ))}
122
131
  </div>
123
- )}
124
- </div>
132
+ </CollapsibleContent>
133
+ </Collapsible>
125
134
  );
126
135
  }
127
136
 
@@ -172,7 +181,6 @@ export function AskUserBlock({
172
181
  // 首帧 questions 往往还是空数组,那之后每次 setAnswers 都在空数组上 map,
173
182
  // 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
174
183
  const [answers, setAnswers] = useState<DraftAnswer[]>([]);
175
- const contentId = useId();
176
184
  const answerAt = (index: number): DraftAnswer =>
177
185
  answers[index] ?? { selections: [], text: "" };
178
186
  const toggle = (questionIndex: number, option: string, multi: boolean) =>
@@ -202,9 +210,6 @@ export function AskUserBlock({
202
210
  // 在途只用于按钮转圈;权威状态永远是 part 的 state。
203
211
  const [inFlight, setInFlight] = useState(false);
204
212
  const outcome = askUserOutcome(call);
205
- const [open, toggleDisclosure] = useLifecycleDisclosure(
206
- outcome.kind === "pending",
207
- );
208
213
  const displayAnswerAt = (index: number): DraftAnswer => {
209
214
  if (outcome.kind !== "answered") return answerAt(index);
210
215
  const answer = outcome.answers[index];
@@ -219,26 +224,12 @@ export function AskUserBlock({
219
224
  (_question, index) =>
220
225
  answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
221
226
  );
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
227
  const buttonLabel = inFlight ? "Submitting…" : "Submit";
237
228
  const footnote = outcome.kind === "closed"
238
- ? outcome.reason === "user_replied_freeform"
239
- ? "Answered in the chat instead."
240
- : "Closed without an answer."
241
- : "";
229
+ ? outcome.reason === "user_replied_freeform"
230
+ ? "Answered in the chat instead."
231
+ : "Closed without an answer."
232
+ : "";
242
233
 
243
234
  const submit = () => {
244
235
  if (!onRespond) return;
@@ -254,129 +245,103 @@ export function AskUserBlock({
254
245
  }, () => setInFlight(false));
255
246
  };
256
247
 
257
- const closedButton = (
258
- <WorkshopButton
259
- tone="primary"
260
- disabled
261
- className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
262
- >
263
- Closed
264
- </WorkshopButton>
265
- );
266
-
267
248
  return (
268
249
  <div
269
250
  className="w-full"
270
251
  data-part-type="ask_user"
271
252
  data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
272
253
  >
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}
315
- 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
- })}
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"
364
- >
365
- {buttonLabel}
366
- </button>
254
+ <div className="flex flex-col gap-4 py-2.5">
255
+ {outcome.kind !== "closed" && questions.map((question, questionIndex) => {
256
+ const answer = displayAnswerAt(questionIndex);
257
+ const options = outcome.kind === "answered"
258
+ ? [...new Set([...question.options, ...answer.selections])]
259
+ : question.options;
260
+ return (
261
+ <div
262
+ key={`${question.prompt}:${questionIndex}`}
263
+ className="flex flex-col items-start gap-2"
264
+ >
265
+ <p className="m-0 text-[14px] leading-[1.4] font-normal text-kumo-default">
266
+ {question.prompt}
267
+ </p>
268
+ {options.length > 0 && (
269
+ <div className="flex flex-wrap items-center gap-2">
270
+ {options.map((option) => {
271
+ const selected = answer.selections.includes(option);
272
+ return (
273
+ <button
274
+ key={option}
275
+ type="button"
276
+ aria-pressed={selected}
277
+ disabled={resolved || !onRespond}
278
+ onClick={() =>
279
+ toggle(questionIndex, option, question.multi)}
280
+ style={selected
281
+ ? {
282
+ background:
283
+ "linear-gradient(#fff,#fff) padding-box, linear-gradient(90deg,#ff9938,#ffe46c) border-box",
284
+ }
285
+ : undefined}
286
+ 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 ${
287
+ selected
288
+ ? "border-solid border-transparent"
289
+ : resolved || !onRespond
290
+ ? "border-[rgba(0,0,0,0.07)] bg-kumo-base opacity-60"
291
+ : "cursor-pointer border-[rgba(0,0,0,0.07)] bg-kumo-base hover:border-[#ff9938]"
292
+ }`}
293
+ >
294
+ {option}
295
+ </button>
296
+ );
297
+ })}
298
+ </div>
299
+ )}
300
+ {question.allowCustom && (
301
+ <label className="flex w-full flex-col gap-2 text-[14px] leading-[1.4] text-kumo-default">
302
+ <span>{question.options.length > 0 ? "Other" : "Answer"}</span>
303
+ <input
304
+ type="text"
305
+ value={answer.text}
306
+ maxLength={2_000}
307
+ disabled={resolved || !onRespond}
308
+ aria-label={`自定义回答:${question.prompt}`}
309
+ placeholder={question.options.length > 0
310
+ ? "Add a custom answer"
311
+ : "Type your answer"}
312
+ onChange={(event) =>
313
+ setText(questionIndex, event.currentTarget.value)}
314
+ className="rounded-lg border border-[rgba(0,0,0,0.07)] bg-kumo-base px-4 py-2 text-[14px] leading-[1.4] text-kumo-subtle outline-none placeholder:text-kumo-inactive focus:border-[#ff9938] focus:ring-2 focus:ring-[#ff9938]/20 disabled:cursor-default disabled:opacity-60"
315
+ />
316
+ </label>
367
317
  )}
368
- </>
369
- ) : (
370
- <div className="flex items-center gap-3">
371
- {footnote && (
372
- <p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
373
- {footnote}
318
+ {outcome.kind === "answered" &&
319
+ !question.allowCustom && answer.text.trim() && (
320
+ <p className="m-0 text-[14px] leading-[1.4] text-kumo-subtle">
321
+ {answer.text.trim()}
374
322
  </p>
375
323
  )}
376
- {closedButton}
377
324
  </div>
378
- )}
379
- </div>
325
+ );
326
+ })}
327
+ {outcome.kind === "pending" && (
328
+ <button
329
+ type="button"
330
+ disabled={resolved || !onRespond || !valid}
331
+ onClick={submit}
332
+ 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"
333
+ >
334
+ {buttonLabel}
335
+ </button>
336
+ )}
337
+ {outcome.kind === "answered" && (
338
+ <p className="m-0 text-[12px] leading-4 text-kumo-inactive">Submitted</p>
339
+ )}
340
+ {outcome.kind === "closed" && (
341
+ <p className="m-0 text-[12px] leading-4 text-kumo-inactive">
342
+ Closed{footnote ? ` · ${footnote}` : ""}
343
+ </p>
344
+ )}
380
345
  </div>
381
346
  </div>
382
347
  );
@@ -483,7 +448,7 @@ export function ScheduleConfirmation({
483
448
  <section
484
449
  data-slot="schedule-card"
485
450
  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)]"
451
+ 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
452
  >
488
453
  <div className="flex items-center gap-2.5">
489
454
  <span
@@ -547,7 +512,7 @@ export function ScheduleBlock({
547
512
  "aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
548
513
  }
549
514
  : {})}
550
- className={`w-full rounded-2xl border border-kumo-line bg-operation-base px-4 py-3 text-left${
515
+ className={`w-full rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3 text-left${
551
516
  clickable
552
517
  ? " 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
518
  : ""
@@ -625,7 +590,7 @@ export function PermissionGrant({
625
590
  return (
626
591
  <section
627
592
  aria-label={ariaLabel}
628
- className="w-full rounded-xl border border-kumo-line bg-operation-base p-4 text-kumo-default"
593
+ className="themed-surface-inset w-full rounded-xl border border-kumo-line bg-kumo-elevated p-4 text-kumo-default"
629
594
  >
630
595
  <div className="flex items-center gap-2">
631
596
  <span
@@ -749,104 +714,72 @@ function StatusDot({ status }: { status: ParallelTool["status"] }) {
749
714
  export function ParallelBlock({
750
715
  label,
751
716
  tools,
752
- running = tools.some((tool) => tool.status === "running"),
753
717
  }: {
754
718
  label: string;
755
719
  tools: ParallelTool[];
756
- running?: boolean;
757
720
  }) {
758
721
  const done = tools.filter((tool) => tool.status === "done").length;
759
- const [open, toggle] = useLifecycleDisclosure(running);
760
722
  return (
761
723
  <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}>
724
+ <div className="flex items-start gap-2 px-1.5 py-1 text-[14px] leading-[1.4] text-kumo-inactive">
725
+ <span className={DISCLOSURE_ICON}>
769
726
  <WorkIcon Icon={ListChecks} />
770
727
  </span>
771
728
  <span className="min-w-0 truncate">
772
729
  {label} · {done}/{tools.length}
773
730
  </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
- )}
731
+ </div>
732
+ <div className="mt-1 space-y-1">
733
+ {tools.map((tool, index) => (
734
+ <div
735
+ key={`${tool.label}-${index}`}
736
+ className="flex items-center gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle"
737
+ >
738
+ <StatusDot status={tool.status} />
739
+ <span className="min-w-0 flex-1 truncate">{tool.label}</span>
740
+ {tool.meta && (
741
+ <span className="flex-shrink-0 font-mono text-[11px] leading-4 text-kumo-inactive">
742
+ {tool.meta}
743
+ </span>
744
+ )}
745
+ </div>
746
+ ))}
747
+ </div>
797
748
  </div>
798
749
  );
799
750
  }
800
751
 
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);
752
+ export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
809
753
  return (
810
754
  <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}>
755
+ <div className="flex items-start gap-2 px-1.5 py-1 text-[14px] leading-[1.4] text-kumo-inactive">
756
+ <span className={DISCLOSURE_ICON}>
818
757
  <WorkIcon Icon={UsersThree} />
819
758
  </span>
820
759
  <span className="min-w-0 truncate">
821
760
  {agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
822
761
  </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
- )}
762
+ </div>
763
+ <div className="mt-1 space-y-1">
764
+ {agents.map((agent, index) => (
765
+ <div
766
+ key={`${agent.name}-${index}`}
767
+ className="flex items-start gap-2 px-1.5 text-[13px] leading-[19px] tracking-[-0.25px]"
768
+ >
769
+ <span className="mt-1.5 flex">
770
+ <StatusDot status={agent.status} />
771
+ </span>
772
+ <span className="min-w-0 flex-1">
773
+ <span className="block truncate font-medium text-kumo-default">
774
+ {agent.name}
845
775
  </span>
846
- </div>
847
- ))}
848
- </div>
849
- )}
776
+ {agent.summary && (
777
+ <span className="block text-kumo-subtle">{agent.summary}</span>
778
+ )}
779
+ </span>
780
+ </div>
781
+ ))}
782
+ </div>
850
783
  </div>
851
784
  );
852
785
  }
@@ -862,27 +795,24 @@ export function ErrorBlock({
862
795
  body?: string;
863
796
  onRetry?: () => void;
864
797
  }) {
865
- const [expanded, toggle] = useLifecycleDisclosure(false);
798
+ const [expanded, setExpanded] = useState(false);
866
799
  return (
867
800
  <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">
801
+ <div className="flex w-full items-start gap-2">
869
802
  <button
870
803
  type="button"
871
- onClick={toggle}
872
- className={`${SECONDARY_DISCLOSURE_TITLE_WITH_ICON} flex-1`}
804
+ onClick={() => setExpanded((value) => !value)}
805
+ className={`${DISCLOSURE_TITLE_WITH_ICON} flex-1`}
873
806
  aria-expanded={expanded}
874
807
  >
875
808
  <span
876
- className={`${SECONDARY_DISCLOSURE_ICON} text-kumo-danger`}
809
+ className={`${DISCLOSURE_ICON} text-kumo-danger`}
877
810
  aria-hidden="true"
878
811
  >
879
812
  <WarningCircle size={15} weight="fill" />
880
813
  </span>
881
814
  <span className="flex min-w-0 flex-1 items-center gap-1">
882
- <span className="min-w-0 truncate">
883
- <span>{title}: </span>
884
- <span>{body}</span>
885
- </span>
815
+ <span className="min-w-0 truncate text-kumo-danger">{title}</span>
886
816
  {body && (
887
817
  <CaretRight
888
818
  size={16}