@springbrand/message-panel 0.1.3-alpha.23 → 0.1.3-alpha.25

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.
@@ -4,6 +4,7 @@ import {
4
4
  Bell,
5
5
  CaretRight,
6
6
  Check,
7
+ CheckCircle,
7
8
  CircleNotch,
8
9
  Clock,
9
10
  Key,
@@ -32,7 +33,7 @@ import {
32
33
  * UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
33
34
  * 后续建议、定时任务、并行/子 Agent、原位授权。视觉语言全部沿用 cloud-os:
34
35
  * 折叠工作行用 `px-1.5 py-1` 的行式布局,卡片用
35
- * `themed-surface-inset rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
36
+ * `rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
36
37
  * 授权卡沿用 renderActionCard 的 blocking callout(brand/40 边 + brand/10 底)。
37
38
  */
38
39
 
@@ -41,13 +42,17 @@ import {
41
42
  export function PlanBlock({
42
43
  steps,
43
44
  running,
45
+ defaultOpen = true,
46
+ showCollapsedSummary = true,
44
47
  }: {
45
48
  steps: PlanStep[];
46
49
  running: boolean;
50
+ defaultOpen?: boolean;
51
+ showCollapsedSummary?: boolean;
47
52
  }) {
48
53
  const done = steps.filter((step) => step.status === "done").length;
49
54
  const current = steps.find((step) => step.status === "in_progress");
50
- const [open, setOpen] = useState(true);
55
+ const [open, setOpen] = useState(defaultOpen);
51
56
 
52
57
  return (
53
58
  <div className="group -ml-0.5">
@@ -71,7 +76,7 @@ export function PlanBlock({
71
76
  className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
72
77
  />
73
78
  </span>
74
- {!open && current && (
79
+ {!open && showCollapsedSummary && current && (
75
80
  <span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
76
81
  {current.text}
77
82
  </span>
@@ -79,35 +84,34 @@ export function PlanBlock({
79
84
  </span>
80
85
  </button>
81
86
  {open && (
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">
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">
83
88
  {steps.map((step, index) => (
84
89
  <div
85
90
  key={`${step.text}-${index}`}
86
- className="flex items-start gap-2.5 text-[13px] leading-[19px] tracking-[-0.25px]"
91
+ className="flex items-center gap-2 text-[14px] leading-[1.4] font-normal tracking-normal"
87
92
  >
88
93
  <span
89
- className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center"
94
+ className="flex size-4 flex-shrink-0 items-center justify-center"
90
95
  aria-hidden="true"
91
96
  >
92
97
  {step.status === "done" ? (
93
- <Check size={12} weight="bold" className="text-kumo-success" />
98
+ <CheckCircle size={16} weight="fill" className="text-[#00bf75]" />
94
99
  ) : step.status === "in_progress" ? (
95
100
  <CircleNotch
96
- size={12}
97
- weight="bold"
98
- className="animate-spin text-kumo-brand motion-reduce:animate-none"
101
+ size={16}
102
+ className="animate-spin text-[#ff9938] motion-reduce:animate-none"
99
103
  />
100
104
  ) : (
101
- <span className="h-1.5 w-1.5 rounded-full bg-kumo-fill" />
105
+ <span className="h-1.5 w-1.5 rounded-full bg-[color:var(--text-color-kumo-default)] opacity-20" />
102
106
  )}
103
107
  </span>
104
108
  <span
105
109
  className={
106
110
  step.status === "done"
107
- ? "min-w-0 flex-1 text-kumo-inactive line-through"
111
+ ? "min-w-0 flex-1 text-kumo-default/30 line-through"
108
112
  : step.status === "in_progress"
109
- ? "min-w-0 flex-1 font-medium text-kumo-default"
110
- : "min-w-0 flex-1 text-kumo-subtle"
113
+ ? "min-w-0 flex-1 text-kumo-default"
114
+ : "min-w-0 flex-1 text-kumo-default/70"
111
115
  }
112
116
  >
113
117
  {step.text}
@@ -255,7 +259,7 @@ export function AskUserBlock({
255
259
  data-part-type="ask_user"
256
260
  data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
257
261
  >
258
- <div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-kumo-base px-3 py-2.5">
262
+ <div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-operation-base px-3 py-2.5">
259
263
  {outcome.kind === "pending" ? (
260
264
  <>
261
265
  {questions.length > 1 && (
@@ -308,7 +312,7 @@ export function AskUserBlock({
308
312
  } ${
309
313
  selected
310
314
  ? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
311
- : "border-kumo-line bg-kumo-base"
315
+ : "border-kumo-line bg-operation-base"
312
316
  }`}
313
317
  >
314
318
  <input
@@ -543,7 +547,7 @@ export function ScheduleConfirmation({
543
547
  <section
544
548
  data-slot="schedule-card"
545
549
  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)]"
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)]"
547
551
  >
548
552
  <div className="flex items-center gap-2.5">
549
553
  <span
@@ -607,7 +611,7 @@ export function ScheduleBlock({
607
611
  "aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
608
612
  }
609
613
  : {})}
610
- className={`w-full rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3 text-left${
614
+ className={`w-full rounded-2xl border border-kumo-line bg-operation-base px-4 py-3 text-left${
611
615
  clickable
612
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"
613
617
  : ""
@@ -685,7 +689,7 @@ export function PermissionGrant({
685
689
  return (
686
690
  <section
687
691
  aria-label={ariaLabel}
688
- className="themed-surface-inset w-full rounded-xl border border-kumo-line bg-kumo-elevated p-4 text-kumo-default"
692
+ className="w-full rounded-xl border border-kumo-line bg-operation-base p-4 text-kumo-default"
689
693
  >
690
694
  <div className="flex items-center gap-2">
691
695
  <span
@@ -814,67 +818,93 @@ export function ParallelBlock({
814
818
  tools: ParallelTool[];
815
819
  }) {
816
820
  const done = tools.filter((tool) => tool.status === "done").length;
821
+ const [open, setOpen] = useState(false);
817
822
  return (
818
823
  <div className="max-w-[860px]">
819
- <div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
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
+ >
820
830
  <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
821
831
  <WorkIcon Icon={ListChecks} />
822
832
  </span>
823
833
  <span className="min-w-0 truncate">
824
834
  {label} · {done}/{tools.length}
825
835
  </span>
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>
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
+ )}
843
860
  </div>
844
861
  );
845
862
  }
846
863
 
847
864
  export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
865
+ const [open, setOpen] = useState(false);
848
866
  return (
849
867
  <div className="max-w-[860px]">
850
- <div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
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
+ >
851
874
  <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
852
875
  <WorkIcon Icon={UsersThree} />
853
876
  </span>
854
877
  <span className="min-w-0 truncate">
855
878
  {agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
856
879
  </span>
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}
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} />
870
895
  </span>
871
- {agent.summary && (
872
- <span className="block text-kumo-subtle">{agent.summary}</span>
873
- )}
874
- </span>
875
- </div>
876
- ))}
877
- </div>
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
+ )}
878
908
  </div>
879
909
  );
880
910
  }
@@ -1,6 +1,10 @@
1
1
  import { Atom, CaretDown, CaretRight, Copy } from "@phosphor-icons/react";
2
- import { memo, useId, useState } from "react";
3
- import { MarkdownMessage } from "./markdown-message";
2
+ import { motion, useReducedMotion } from "motion/react";
3
+ import { memo, useId, useState, type ReactNode } from "react";
4
+ import {
5
+ MarkdownMessage,
6
+ type CloudOsUrlResolver,
7
+ } from "./markdown-message";
4
8
  import {
5
9
  getToolCallSummary,
6
10
  getToolIcon,
@@ -10,6 +14,11 @@ import {
10
14
  type ToolCallGroup,
11
15
  } from "./tool-presentation";
12
16
 
17
+ const thinkingSpark = new URL(
18
+ "../assets/thinking-spark.svg",
19
+ import.meta.url,
20
+ ).href;
21
+
13
22
  /**
14
23
  * 工作行(work rows): WorkIcon / ToolCallDetails / NestedToolCallRow /
15
24
  * ToolGroupRow / ThinkingTraceRow。渲染 UIMessage 归一化后的 CloudOsToolCall。
@@ -34,26 +43,74 @@ function displayOutput(value: unknown, fallback: string): string {
34
43
  return displayValue(record);
35
44
  }
36
45
 
37
- function OutputPre({ value }: { value: string }) {
46
+ interface ToolDetailItem {
47
+ key: string;
48
+ label: string;
49
+ output: string;
50
+ error: string;
51
+ }
52
+
53
+ function detailItems(calls: readonly CloudOsToolCall[]): ToolDetailItem[] {
54
+ return calls.map((call) => {
55
+ const summary = getToolCallSummary(call);
56
+ return {
57
+ key: call.toolCallId,
58
+ label: `${summary.verb}${summary.target ? ` ${summary.target}` : ""}`,
59
+ output: displayOutput(call.output, call.outputText),
60
+ error: call.errorText,
61
+ };
62
+ });
63
+ }
64
+
65
+ function OutputCard({ items }: { items: readonly ToolDetailItem[] }) {
66
+ const copyValue = items
67
+ .map((item) => {
68
+ const detail = [item.error, item.output]
69
+ .filter(Boolean)
70
+ .join("\n\n") || "No additional data";
71
+ return items.length === 1 ? detail : `${item.label}\n${detail}`;
72
+ })
73
+ .join("\n\n");
38
74
  return (
39
75
  <div
40
- className="rounded-xl border border-kumo-line/70 bg-kumo-elevated/45 px-4 py-3 text-kumo-subtle"
76
+ className="rounded-xl border border-[rgba(0,0,0,0.07)] bg-[rgba(0,0,0,0.03)] px-4 py-3 text-cos-output"
41
77
  data-output-card=""
42
78
  >
43
79
  <div className="flex items-center justify-between gap-3 text-[14px] font-medium leading-5">
44
80
  <span>Output</span>
45
81
  <button
46
82
  type="button"
47
- onClick={() => navigator.clipboard.writeText(value)}
48
- className="flex h-4 w-4 items-center justify-center text-kumo-subtle transition-colors hover:text-kumo-default focus-visible:outline-none focus-visible:text-kumo-default"
83
+ onClick={() => navigator.clipboard.writeText(copyValue)}
84
+ className="flex h-4 w-4 items-center justify-center text-cos-output transition-colors hover:text-kumo-default focus-visible:outline-none focus-visible:text-kumo-default"
49
85
  aria-label="Copy output"
50
86
  >
51
87
  <Copy size={16} />
52
88
  </button>
53
89
  </div>
54
- <pre className="mt-2 max-h-56 overflow-auto border-t border-kumo-line/70 pt-3 font-sans text-[14px] font-normal leading-5 whitespace-pre-wrap">
55
- {value}
56
- </pre>
90
+ <div className="mt-3 max-h-56 space-y-3 overflow-auto border-t border-kumo-line/70 pt-3 text-[14px] font-normal leading-[1.4]">
91
+ {items.map((item) => (
92
+ <div key={item.key} className="space-y-1.5">
93
+ {items.length > 1 && (
94
+ <p className="m-0 text-[12px] leading-4 text-cos-output">
95
+ {item.label}
96
+ </p>
97
+ )}
98
+ {item.error && (
99
+ <p className="m-0 whitespace-pre-wrap text-kumo-danger">
100
+ {item.error}
101
+ </p>
102
+ )}
103
+ {item.output && (
104
+ <p className="m-0 whitespace-pre-wrap text-cos-output">
105
+ {item.output}
106
+ </p>
107
+ )}
108
+ {!item.error && !item.output && (
109
+ <p className="m-0 text-cos-output">No additional data</p>
110
+ )}
111
+ </div>
112
+ ))}
113
+ </div>
57
114
  </div>
58
115
  );
59
116
  }
@@ -63,22 +120,15 @@ export const ToolCallDetails = memo(function ToolCallDetails({
63
120
  }: {
64
121
  toolCall: CloudOsToolCall;
65
122
  }) {
66
- const output = displayOutput(tc.output, tc.outputText);
67
- return (
68
- <div className="space-y-2">
69
- {tc.errorText && (
70
- <pre className="rounded-xl border border-kumo-danger/20 bg-kumo-danger-tint/40 p-3 font-mono text-[12px] leading-[18px] text-kumo-danger whitespace-pre-wrap">
71
- {tc.errorText}
72
- </pre>
73
- )}
74
- {output && <OutputPre value={output} />}
75
- {!output && !tc.errorText && (
76
- <p className="m-0 text-[12px] leading-4 text-kumo-inactive">
77
- No additional data
78
- </p>
79
- )}
80
- </div>
81
- );
123
+ return <OutputCard items={detailItems([tc])} />;
124
+ });
125
+
126
+ export const ToolGroupDetails = memo(function ToolGroupDetails({
127
+ calls,
128
+ }: {
129
+ calls: readonly CloudOsToolCall[];
130
+ }) {
131
+ return <OutputCard items={detailItems(calls)} />;
82
132
  });
83
133
 
84
134
  export const NestedToolCallRow = memo(function NestedToolCallRow({
@@ -184,6 +234,172 @@ export const ThinkingTraceRow = memo(function ThinkingTraceRow({
184
234
  );
185
235
  });
186
236
 
237
+ export function formatWorkDuration(milliseconds: number): string {
238
+ const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
239
+ const hours = Math.floor(totalSeconds / 3600);
240
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
241
+ const seconds = totalSeconds % 60;
242
+ if (hours > 0) {
243
+ return `${hours}h ${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
244
+ }
245
+ if (minutes > 0) return `${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
246
+ return `${seconds}s`;
247
+ }
248
+
249
+ interface ActivitySummary {
250
+ leading: string;
251
+ emphasis?: string;
252
+ trailing?: string;
253
+ }
254
+
255
+ function plural(count: number, singular: string, plural = `${singular}s`): string {
256
+ return `${count} ${count === 1 ? singular : plural}`;
257
+ }
258
+
259
+ function activitySummary(groups: readonly ToolCallGroup[]): ActivitySummary | null {
260
+ const calls = groups.flatMap((group) => group.calls);
261
+ const count = (kind: CloudOsToolCall["kind"]) =>
262
+ calls.filter((call) => call.kind === kind).length;
263
+ const webSearches = count("web-search");
264
+ const webPages = count("web-fetch");
265
+ if (webSearches > 0 && webPages > 0) {
266
+ return {
267
+ leading: "searched the web and retrieved ",
268
+ emphasis: `${webPages} web`,
269
+ trailing: webPages === 1 ? " page" : " pages",
270
+ };
271
+ }
272
+ if (webSearches > 0) return { leading: "searched the web" };
273
+ if (webPages > 0) {
274
+ return {
275
+ leading: "retrieved ",
276
+ emphasis: `${webPages} web`,
277
+ trailing: webPages === 1 ? " page" : " pages",
278
+ };
279
+ }
280
+
281
+ const firstKnown = calls.find((call) =>
282
+ ["read", "write", "edit", "bash", "skill", "javascript"].includes(
283
+ call.kind,
284
+ ),
285
+ );
286
+ if (!firstKnown) return null;
287
+ const total = count(firstKnown.kind);
288
+ switch (firstKnown.kind) {
289
+ case "read":
290
+ return { leading: `read ${plural(total, "file")}` };
291
+ case "write":
292
+ return { leading: `wrote ${plural(total, "file")}` };
293
+ case "edit":
294
+ return { leading: `made ${plural(total, "edit")}` };
295
+ case "bash":
296
+ return { leading: `ran ${plural(total, "command")}` };
297
+ case "skill":
298
+ return { leading: `read ${plural(total, "instruction set")}` };
299
+ case "javascript":
300
+ return { leading: total === 1 ? "ran code" : `ran code ${total} times` };
301
+ default:
302
+ return null;
303
+ }
304
+ }
305
+
306
+ export const WorkTraceDisclosure = memo(function WorkTraceDisclosure({
307
+ durationMs,
308
+ groups,
309
+ completed = false,
310
+ open,
311
+ onToggle,
312
+ children,
313
+ }: {
314
+ durationMs?: number;
315
+ groups: readonly ToolCallGroup[];
316
+ completed?: boolean;
317
+ open: boolean;
318
+ onToggle: () => void;
319
+ children: ReactNode;
320
+ }) {
321
+ const contentId = useId();
322
+ const reduceMotion = useReducedMotion();
323
+ const activity = activitySummary(groups);
324
+ const duration = durationMs === undefined ? "" : ` for ${formatWorkDuration(durationMs)}`;
325
+ const verb = completed ? "Worked" : "Thought";
326
+ const label = `${verb}${duration}${activity ? `, ${activity.leading}${activity.emphasis ?? ""}${activity.trailing ?? ""}` : ""}`;
327
+
328
+ return (
329
+ <div data-agent-work="" data-state={open ? "open" : "closed"}>
330
+ <div className="w-full border-b border-kumo-line pb-2">
331
+ <button
332
+ type="button"
333
+ onClick={onToggle}
334
+ aria-expanded={open}
335
+ aria-controls={contentId}
336
+ aria-label={label}
337
+ className="flex min-w-0 items-center gap-2 text-left text-[14px] leading-[1.4] text-cos-work-description transition-colors duration-150 ease-out hover:text-cos-work-description-subtle focus-visible:text-cos-work-description-subtle focus-visible:outline-none"
338
+ >
339
+ <span className="grid size-4 shrink-0 place-items-center" aria-hidden="true">
340
+ <motion.img
341
+ src={thinkingSpark}
342
+ alt=""
343
+ className="size-[12.24px]"
344
+ initial={{ rotate: 0 }}
345
+ animate={
346
+ reduceMotion ? { rotate: 0 } : { rotate: [0, 360, -360, -360] }
347
+ }
348
+ transition={
349
+ reduceMotion
350
+ ? { duration: 0 }
351
+ : {
352
+ rotate: {
353
+ duration: 1.68,
354
+ times: [0, 0.8368, 0.8373, 1],
355
+ ease: [
356
+ [0.5, 0, 0.5, 1],
357
+ [0.5, 0, 0.5, 1],
358
+ "linear",
359
+ ],
360
+ repeat: Infinity,
361
+ },
362
+ }
363
+ }
364
+ />
365
+ </span>
366
+ <span className="min-w-0 truncate">
367
+ <span>{`${verb}${duration}${activity ? `, ${activity.leading}` : ""}`}</span>
368
+ {activity?.emphasis && (
369
+ <span className="text-kumo-default">{activity.emphasis}</span>
370
+ )}
371
+ {activity?.trailing}
372
+ </span>
373
+ <CaretRight
374
+ size={16}
375
+ className={`shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
376
+ aria-hidden="true"
377
+ />
378
+ </button>
379
+ </div>
380
+ {open && (
381
+ <div id={contentId} className="mt-4 space-y-3" data-agent-work-content="">
382
+ {children}
383
+ </div>
384
+ )}
385
+ </div>
386
+ );
387
+ });
388
+
389
+ export const WorkDescriptionRow = memo(function WorkDescriptionRow({
390
+ text,
391
+ resolveUrl,
392
+ }: {
393
+ text: string;
394
+ resolveUrl?: CloudOsUrlResolver;
395
+ }) {
396
+ return (
397
+ <div className="cos-markdown text-[14px] leading-[1.4] text-cos-work-description">
398
+ <MarkdownMessage message={text} resolveUrl={resolveUrl} />
399
+ </div>
400
+ );
401
+ });
402
+
187
403
  export const ToolGroupRow = memo(function ToolGroupRow({
188
404
  group,
189
405
  open,
@@ -195,19 +411,20 @@ export const ToolGroupRow = memo(function ToolGroupRow({
195
411
  expandedKeys: ReadonlySet<string>;
196
412
  onToggle: (key: string) => void;
197
413
  }) {
414
+ void expandedKeys;
198
415
  return (
199
- <div className="group -ml-0.5">
416
+ <div className="group/tool-call">
200
417
  <button
201
418
  type="button"
202
419
  onClick={() => onToggle(group.key)}
203
- 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]"
420
+ className="flex min-w-0 items-start gap-2 text-left text-[14px] leading-[1.4] text-cos-tool-group transition-colors duration-150 ease-out hover:text-kumo-subtle focus-visible:text-kumo-subtle focus-visible:outline-none"
204
421
  aria-expanded={open}
205
422
  >
206
- <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
423
+ <span className="mt-0.5 flex size-4 shrink-0 items-center justify-center">
207
424
  <WorkIcon Icon={group.Icon} />
208
425
  </span>
209
426
  <span className="min-w-0 flex-1">
210
- <span className="flex min-w-0 items-center gap-2 text-[14px] leading-5 tracking-[-0.25px]">
427
+ <span className="flex min-w-0 items-center gap-2">
211
428
  <span
212
429
  className={`min-w-0 truncate ${group.hasRunning ? "cos-thinking-shimmer" : ""}`}
213
430
  >
@@ -219,38 +436,22 @@ export const ToolGroupRow = memo(function ToolGroupRow({
219
436
  </span>
220
437
  )}
221
438
  <CaretRight
222
- size={13}
223
- weight="bold"
224
- className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
439
+ size={16}
440
+ className={`shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
225
441
  />
226
442
  </span>
227
443
  {group.detailLines.length > 1 && (
228
- <span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
444
+ <span className="mt-2 block truncate text-[12px] leading-4 text-[rgba(0,0,0,0.3)]">
229
445
  {group.detailLines.join(" · ")}
230
446
  </span>
231
447
  )}
232
448
  </span>
233
449
  </button>
234
- {open &&
235
- (group.calls.length === 1 ? (
236
- <div className="ml-8 mt-1 space-y-3">
237
- <ToolCallDetails toolCall={group.calls[0]} />
238
- </div>
239
- ) : (
240
- <div className="ml-8 mt-1 space-y-1">
241
- {group.calls.map((toolCall) => {
242
- const key = `call-${toolCall.toolCallId}`;
243
- return (
244
- <NestedToolCallRow
245
- key={toolCall.toolCallId}
246
- toolCall={toolCall}
247
- open={expandedKeys.has(key)}
248
- onToggle={onToggle}
249
- />
250
- );
251
- })}
252
- </div>
253
- ))}
450
+ {open && (
451
+ <div className="mt-3 pl-6">
452
+ <ToolGroupDetails calls={group.calls} />
453
+ </div>
454
+ )}
254
455
  </div>
255
456
  );
256
457
  });