@stigmer/react 0.0.55 → 0.0.57

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.
Files changed (81) hide show
  1. package/execution/ApprovalCard.d.ts.map +1 -1
  2. package/execution/ApprovalCard.js +1 -1
  3. package/execution/ApprovalCard.js.map +1 -1
  4. package/execution/ArtifactsWidget.d.ts +1 -1
  5. package/execution/ArtifactsWidget.js +1 -1
  6. package/execution/ExecutionProgress.d.ts.map +1 -1
  7. package/execution/ExecutionProgress.js +2 -59
  8. package/execution/ExecutionProgress.js.map +1 -1
  9. package/execution/MessageThread.d.ts.map +1 -1
  10. package/execution/MessageThread.js +31 -6
  11. package/execution/MessageThread.js.map +1 -1
  12. package/execution/SubAgentSection.d.ts +25 -4
  13. package/execution/SubAgentSection.d.ts.map +1 -1
  14. package/execution/SubAgentSection.js +70 -11
  15. package/execution/SubAgentSection.js.map +1 -1
  16. package/execution/TodoList.d.ts +42 -0
  17. package/execution/TodoList.d.ts.map +1 -0
  18. package/execution/TodoList.js +108 -0
  19. package/execution/TodoList.js.map +1 -0
  20. package/execution/ToolCallItem.js +1 -1
  21. package/execution/ToolCallItem.js.map +1 -1
  22. package/execution/UsageWidget.d.ts +57 -0
  23. package/execution/UsageWidget.d.ts.map +1 -0
  24. package/execution/UsageWidget.js +72 -0
  25. package/execution/UsageWidget.js.map +1 -0
  26. package/execution/index.d.ts +4 -4
  27. package/execution/index.d.ts.map +1 -1
  28. package/execution/index.js +2 -2
  29. package/execution/index.js.map +1 -1
  30. package/execution/useExecutionArtifacts.d.ts +1 -1
  31. package/execution/useExecutionArtifacts.js +1 -1
  32. package/index.d.ts +4 -4
  33. package/index.d.ts.map +1 -1
  34. package/index.js +2 -2
  35. package/index.js.map +1 -1
  36. package/package.json +4 -4
  37. package/session/index.d.ts +2 -0
  38. package/session/index.d.ts.map +1 -1
  39. package/session/index.js +1 -0
  40. package/session/index.js.map +1 -1
  41. package/session/useSessionConversation.d.ts.map +1 -1
  42. package/session/useSessionConversation.js +42 -5
  43. package/session/useSessionConversation.js.map +1 -1
  44. package/session/useSessionUsage.d.ts +65 -0
  45. package/session/useSessionUsage.d.ts.map +1 -0
  46. package/session/useSessionUsage.js +107 -0
  47. package/session/useSessionUsage.js.map +1 -0
  48. package/src/execution/ApprovalCard.tsx +7 -13
  49. package/src/execution/ArtifactsWidget.tsx +1 -1
  50. package/src/execution/ExecutionProgress.tsx +2 -134
  51. package/src/execution/MessageThread.tsx +39 -6
  52. package/src/execution/SubAgentSection.tsx +323 -16
  53. package/src/execution/TodoList.tsx +202 -0
  54. package/src/execution/ToolCallItem.tsx +1 -1
  55. package/src/execution/{ExecutionCostSummary.tsx → UsageWidget.tsx} +43 -50
  56. package/src/execution/index.ts +10 -4
  57. package/src/execution/useExecutionArtifacts.ts +1 -1
  58. package/src/index.ts +12 -5
  59. package/src/session/index.ts +6 -0
  60. package/src/session/useSessionConversation.ts +56 -7
  61. package/src/session/useSessionUsage.ts +159 -0
  62. package/styles.css +1 -1
  63. package/execution/ExecutionCostSummary.d.ts +0 -47
  64. package/execution/ExecutionCostSummary.d.ts.map +0 -1
  65. package/execution/ExecutionCostSummary.js +0 -77
  66. package/execution/ExecutionCostSummary.js.map +0 -1
  67. package/execution/__tests__/ExecutionCostSummary.test.d.ts +0 -2
  68. package/execution/__tests__/ExecutionCostSummary.test.d.ts.map +0 -1
  69. package/execution/__tests__/ExecutionCostSummary.test.js +0 -255
  70. package/execution/__tests__/ExecutionCostSummary.test.js.map +0 -1
  71. package/execution/__tests__/useExecutionUsage.test.d.ts +0 -2
  72. package/execution/__tests__/useExecutionUsage.test.d.ts.map +0 -1
  73. package/execution/__tests__/useExecutionUsage.test.js +0 -303
  74. package/execution/__tests__/useExecutionUsage.test.js.map +0 -1
  75. package/execution/useExecutionUsage.d.ts +0 -45
  76. package/execution/useExecutionUsage.d.ts.map +0 -1
  77. package/execution/useExecutionUsage.js +0 -157
  78. package/execution/useExecutionUsage.js.map +0 -1
  79. package/src/execution/__tests__/ExecutionCostSummary.test.tsx +0 -416
  80. package/src/execution/__tests__/useExecutionUsage.test.tsx +0 -408
  81. package/src/execution/useExecutionUsage.ts +0 -213
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { useMemo, useState } from "react";
3
4
  import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
5
+ import type { TodoItem } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
4
6
  import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
7
  import {
6
8
  MessageType,
@@ -10,9 +12,23 @@ import { cn } from "@stigmer/theme";
10
12
  import { formatDuration } from "./ToolCallDetail";
11
13
  import { MessageEntry } from "./MessageEntry";
12
14
  import { ToolCallGroup } from "./ToolCallGroup";
15
+ import {
16
+ TodoList,
17
+ TodoInProgressIcon,
18
+ findActiveTodo,
19
+ todoCompletionSummary,
20
+ } from "./TodoList";
13
21
 
14
22
  export interface SubAgentSectionProps {
15
23
  readonly subAgentExecution: SubAgentExecution;
24
+ /**
25
+ * Whether to render as a collapsible card with expand/collapse
26
+ * toggle. Defaults to `true`.
27
+ *
28
+ * Set to `false` when rendered inside a parent that already
29
+ * provides its own expand/collapse (e.g. {@link ToolCallItem}).
30
+ */
31
+ readonly collapsible?: boolean;
16
32
  readonly className?: string;
17
33
  }
18
34
 
@@ -20,28 +36,221 @@ export interface SubAgentSectionProps {
20
36
  * Renders a sub-agent execution as a nested mini-thread inside the
21
37
  * parent conversation.
22
38
  *
39
+ * When `collapsible` is `true` (default), the component renders as a
40
+ * bordered card with a clickable summary row. Cards start collapsed
41
+ * — the summary row shows status, subject, and duration at a glance.
42
+ * Users click to expand and see the nested messages and tool calls.
43
+ *
44
+ * Visually distinguished from {@link ToolCallGroup} via a left
45
+ * accent border and bot icon, signaling delegated sub-agent work.
46
+ *
47
+ * When `collapsible` is `false`, the component renders flat content
48
+ * without a toggle — suitable for embedding inside a parent that
49
+ * already provides expand/collapse (e.g. {@link ToolCallItem}).
50
+ *
23
51
  * Composes {@link MessageEntry} and {@link ToolCallGroup} to display
24
52
  * the sub-agent's internal messages and tool calls — the same
25
53
  * building blocks used by the top-level {@link MessageThread}.
26
54
  *
27
- * Visually distinguished from the parent thread via a left border
28
- * and subtle background.
29
- *
30
55
  * @example
31
56
  * ```tsx
57
+ * // Standalone collapsible card (default)
32
58
  * <SubAgentSection subAgentExecution={sub} />
59
+ *
60
+ * // Flat layout inside a parent expand/collapse
61
+ * <SubAgentSection subAgentExecution={sub} collapsible={false} />
33
62
  * ```
34
63
  */
35
64
  export function SubAgentSection({
36
65
  subAgentExecution: sub,
66
+ collapsible = true,
37
67
  className,
38
68
  }: SubAgentSectionProps) {
39
69
  const duration = formatDuration(sub.startedAt, sub.completedAt);
40
70
  const statusInfo = SUB_AGENT_STATUS_MAP[sub.status];
41
- const Icon = statusInfo.icon;
71
+ const StatusIcon = statusInfo.icon;
42
72
  const isFailed = sub.status === SubAgentStatus.SUB_AGENT_FAILED;
43
73
  const threadItems = buildSubAgentThreadItems(sub.messages);
44
74
 
75
+ const displayLabel = sub.subject || sub.name;
76
+
77
+ if (!collapsible) {
78
+ return (
79
+ <FlatContent
80
+ sub={sub}
81
+ statusInfo={statusInfo}
82
+ StatusIcon={StatusIcon}
83
+ duration={duration}
84
+ isFailed={isFailed}
85
+ threadItems={threadItems}
86
+ className={className}
87
+ />
88
+ );
89
+ }
90
+
91
+ return (
92
+ <CollapsibleCard
93
+ sub={sub}
94
+ statusInfo={statusInfo}
95
+ displayLabel={displayLabel}
96
+ duration={duration}
97
+ isFailed={isFailed}
98
+ threadItems={threadItems}
99
+ className={className}
100
+ />
101
+ );
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Collapsible card — progressive disclosure (default mode)
106
+ // ---------------------------------------------------------------------------
107
+
108
+ interface CollapsibleCardProps {
109
+ readonly sub: SubAgentExecution;
110
+ readonly statusInfo: SubAgentStatusInfo;
111
+ readonly displayLabel: string;
112
+ readonly duration: string | null;
113
+ readonly isFailed: boolean;
114
+ readonly threadItems: SubAgentThreadItem[];
115
+ readonly className?: string;
116
+ }
117
+
118
+ function CollapsibleCard({
119
+ sub,
120
+ statusInfo,
121
+ displayLabel,
122
+ duration,
123
+ isFailed,
124
+ threadItems,
125
+ className,
126
+ }: CollapsibleCardProps) {
127
+ const [expanded, setExpanded] = useState(false);
128
+
129
+ const handleToggle = () => {
130
+ setExpanded((v) => !v);
131
+ };
132
+
133
+ const hasTodos =
134
+ sub.todos != null && Object.keys(sub.todos).length > 0;
135
+ const isRunning = sub.status === SubAgentStatus.SUB_AGENT_IN_PROGRESS;
136
+ const isCompleted = sub.status === SubAgentStatus.SUB_AGENT_COMPLETED;
137
+
138
+ const activeTodo = useMemo(() => findActiveTodo(sub.todos), [sub.todos]);
139
+ const completionLabel = useMemo(
140
+ () => (isCompleted && hasTodos ? todoCompletionSummary(sub.todos) : null),
141
+ [sub.todos, isCompleted, hasTodos],
142
+ );
143
+
144
+ const collapsedPreview =
145
+ isRunning && activeTodo
146
+ ? activeTodo.content
147
+ : isCompleted && completionLabel
148
+ ? completionLabel
149
+ : null;
150
+
151
+ const ariaLabel = `Sub-agent: ${displayLabel}, ${statusInfo.label}`;
152
+
153
+ return (
154
+ <div
155
+ role="group"
156
+ aria-label={ariaLabel}
157
+ className={cn(
158
+ "rounded-md border border-border border-l-2 border-l-primary/30 bg-muted/30",
159
+ className,
160
+ )}
161
+ >
162
+ {/* Summary trigger */}
163
+ <button
164
+ type="button"
165
+ aria-expanded={expanded}
166
+ onClick={handleToggle}
167
+ className={cn(
168
+ "flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-xs text-muted-foreground transition-colors",
169
+ "hover:bg-muted/50",
170
+ "cursor-pointer",
171
+ )}
172
+ >
173
+ <span className="shrink-0 text-primary/70" aria-hidden="true">
174
+ <BotIcon />
175
+ </span>
176
+ <span className="min-w-0 flex-1 truncate">{displayLabel}</span>
177
+ <span
178
+ className={cn(
179
+ "shrink-0 rounded px-1 py-0.5 text-[10px] font-medium leading-none",
180
+ statusInfo.badgeClass,
181
+ )}
182
+ >
183
+ {statusInfo.label}
184
+ </span>
185
+ {duration && (
186
+ <span className="shrink-0 tabular-nums text-muted-foreground">
187
+ {duration}
188
+ </span>
189
+ )}
190
+ <ChevronIcon expanded={expanded} />
191
+ </button>
192
+
193
+ {/* Active todo preview — visible when collapsed */}
194
+ {collapsedPreview && !expanded && (
195
+ <div className="flex items-center gap-1.5 px-2.5 pb-1.5 text-xs text-muted-foreground">
196
+ <span className="ml-[20px] shrink-0" aria-hidden="true">
197
+ {isRunning && activeTodo ? (
198
+ <TodoInProgressIcon />
199
+ ) : (
200
+ <TodoCompletedSmallIcon />
201
+ )}
202
+ </span>
203
+ <span className="min-w-0 truncate">{collapsedPreview}</span>
204
+ </div>
205
+ )}
206
+
207
+ {/* Expanded content — CSS grid-rows animation */}
208
+ <div
209
+ className={cn(
210
+ "grid transition-[grid-template-rows] duration-150 ease-out",
211
+ expanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
212
+ )}
213
+ >
214
+ <div className="overflow-hidden">
215
+ {expanded && (
216
+ <div className="border-t border-border/50 px-2.5 pb-2 pt-1.5">
217
+ <SubAgentThreadContent
218
+ threadItems={threadItems}
219
+ todos={sub.todos}
220
+ isFailed={isFailed}
221
+ error={sub.error}
222
+ />
223
+ </div>
224
+ )}
225
+ </div>
226
+ </div>
227
+ </div>
228
+ );
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Flat content — no toggle (used inside ToolCallItem detail panel)
233
+ // ---------------------------------------------------------------------------
234
+
235
+ interface FlatContentProps {
236
+ readonly sub: SubAgentExecution;
237
+ readonly statusInfo: SubAgentStatusInfo;
238
+ readonly StatusIcon: () => React.JSX.Element;
239
+ readonly duration: string | null;
240
+ readonly isFailed: boolean;
241
+ readonly threadItems: SubAgentThreadItem[];
242
+ readonly className?: string;
243
+ }
244
+
245
+ function FlatContent({
246
+ sub,
247
+ statusInfo,
248
+ StatusIcon,
249
+ duration,
250
+ isFailed,
251
+ threadItems,
252
+ className,
253
+ }: FlatContentProps) {
45
254
  return (
46
255
  <div
47
256
  className={cn(
@@ -55,7 +264,7 @@ export function SubAgentSection({
55
264
  className={cn("shrink-0", statusInfo.colorClass)}
56
265
  aria-hidden="true"
57
266
  >
58
- <Icon />
267
+ <StatusIcon />
59
268
  </span>
60
269
  <span className="font-medium text-foreground">
61
270
  {sub.name}
@@ -72,7 +281,39 @@ export function SubAgentSection({
72
281
  )}
73
282
  </div>
74
283
 
75
- {/* Nested messages and tool groups */}
284
+ <SubAgentThreadContent
285
+ threadItems={threadItems}
286
+ todos={sub.todos}
287
+ isFailed={isFailed}
288
+ error={sub.error}
289
+ />
290
+ </div>
291
+ );
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Shared thread content renderer
296
+ // ---------------------------------------------------------------------------
297
+
298
+ interface SubAgentThreadContentProps {
299
+ readonly threadItems: SubAgentThreadItem[];
300
+ readonly todos?: { readonly [key: string]: TodoItem };
301
+ readonly isFailed: boolean;
302
+ readonly error: string;
303
+ }
304
+
305
+ function SubAgentThreadContent({
306
+ threadItems,
307
+ todos,
308
+ isFailed,
309
+ error,
310
+ }: SubAgentThreadContentProps) {
311
+ const hasTodos = todos != null && Object.keys(todos).length > 0;
312
+
313
+ return (
314
+ <>
315
+ {hasTodos && <TodoList todos={todos!} className="pb-1" />}
316
+
76
317
  {threadItems.length > 0 && (
77
318
  <div className="flex flex-col gap-1 pb-1">
78
319
  {threadItems.map((item) => {
@@ -93,13 +334,12 @@ export function SubAgentSection({
93
334
  </div>
94
335
  )}
95
336
 
96
- {/* Error footer */}
97
- {isFailed && sub.error && (
337
+ {isFailed && error && (
98
338
  <div className="rounded-md border border-destructive/20 bg-destructive/5 px-2 py-1.5 text-xs text-destructive">
99
- {sub.error}
339
+ {error}
100
340
  </div>
101
341
  )}
102
- </div>
342
+ </>
103
343
  );
104
344
  }
105
345
 
@@ -142,6 +382,7 @@ function buildSubAgentThreadItems(
142
382
  interface SubAgentStatusInfo {
143
383
  label: string;
144
384
  colorClass: string;
385
+ badgeClass: string;
145
386
  icon: () => React.JSX.Element;
146
387
  }
147
388
 
@@ -149,31 +390,37 @@ const SUB_AGENT_STATUS_MAP: Record<SubAgentStatus, SubAgentStatusInfo> = {
149
390
  [SubAgentStatus.SUB_AGENT_STATUS_UNSPECIFIED]: {
150
391
  label: "Unknown",
151
392
  colorClass: "text-muted-foreground",
393
+ badgeClass: "bg-muted text-muted-foreground",
152
394
  icon: DotIcon,
153
395
  },
154
396
  [SubAgentStatus.SUB_AGENT_PENDING]: {
155
397
  label: "Pending",
156
398
  colorClass: "text-muted-foreground",
399
+ badgeClass: "bg-muted text-muted-foreground",
157
400
  icon: DotIcon,
158
401
  },
159
402
  [SubAgentStatus.SUB_AGENT_IN_PROGRESS]: {
160
403
  label: "Running",
161
404
  colorClass: "text-foreground",
405
+ badgeClass: "bg-muted text-foreground",
162
406
  icon: SpinnerIcon,
163
407
  },
164
408
  [SubAgentStatus.SUB_AGENT_COMPLETED]: {
165
409
  label: "Completed",
166
410
  colorClass: "text-success",
411
+ badgeClass: "bg-success/15 text-success",
167
412
  icon: CheckCircleIcon,
168
413
  },
169
414
  [SubAgentStatus.SUB_AGENT_FAILED]: {
170
415
  label: "Failed",
171
416
  colorClass: "text-destructive",
417
+ badgeClass: "bg-destructive/15 text-destructive",
172
418
  icon: XCircleIcon,
173
419
  },
174
420
  [SubAgentStatus.SUB_AGENT_CANCELLED]: {
175
421
  label: "Cancelled",
176
422
  colorClass: "text-muted-foreground",
423
+ badgeClass: "bg-muted text-muted-foreground",
177
424
  icon: XCircleIcon,
178
425
  },
179
426
  };
@@ -185,8 +432,8 @@ const SUB_AGENT_STATUS_MAP: Record<SubAgentStatus, SubAgentStatusInfo> = {
185
432
  function SpinnerIcon() {
186
433
  return (
187
434
  <svg
188
- width="10"
189
- height="10"
435
+ width="12"
436
+ height="12"
190
437
  viewBox="0 0 12 12"
191
438
  fill="none"
192
439
  stroke="currentColor"
@@ -201,8 +448,8 @@ function SpinnerIcon() {
201
448
  function CheckCircleIcon() {
202
449
  return (
203
450
  <svg
204
- width="10"
205
- height="10"
451
+ width="12"
452
+ height="12"
206
453
  viewBox="0 0 12 12"
207
454
  fill="none"
208
455
  stroke="currentColor"
@@ -219,8 +466,8 @@ function CheckCircleIcon() {
219
466
  function XCircleIcon() {
220
467
  return (
221
468
  <svg
222
- width="10"
223
- height="10"
469
+ width="12"
470
+ height="12"
224
471
  viewBox="0 0 12 12"
225
472
  fill="none"
226
473
  stroke="currentColor"
@@ -241,3 +488,63 @@ function DotIcon() {
241
488
  </svg>
242
489
  );
243
490
  }
491
+
492
+ function TodoCompletedSmallIcon() {
493
+ return (
494
+ <svg
495
+ width="12"
496
+ height="12"
497
+ viewBox="0 0 12 12"
498
+ fill="none"
499
+ stroke="currentColor"
500
+ strokeWidth="2"
501
+ strokeLinecap="round"
502
+ strokeLinejoin="round"
503
+ >
504
+ <path d="M2.5 6L5 8.5L9.5 3.5" />
505
+ </svg>
506
+ );
507
+ }
508
+
509
+ function BotIcon() {
510
+ return (
511
+ <svg
512
+ width="12"
513
+ height="12"
514
+ viewBox="0 0 16 16"
515
+ fill="none"
516
+ stroke="currentColor"
517
+ strokeWidth="1.5"
518
+ strokeLinecap="round"
519
+ strokeLinejoin="round"
520
+ >
521
+ <rect x="2" y="6" width="12" height="8" rx="2" />
522
+ <path d="M5.5 10H5.51M10.5 10H10.51" strokeWidth="2" />
523
+ <path d="M8 2V6" />
524
+ <circle cx="8" cy="1.5" r="1" />
525
+ <path d="M0.5 9.5H2M14 9.5H15.5" />
526
+ </svg>
527
+ );
528
+ }
529
+
530
+ function ChevronIcon({ expanded }: { expanded: boolean }) {
531
+ return (
532
+ <svg
533
+ width="12"
534
+ height="12"
535
+ viewBox="0 0 12 12"
536
+ fill="none"
537
+ stroke="currentColor"
538
+ strokeWidth="1.5"
539
+ strokeLinecap="round"
540
+ strokeLinejoin="round"
541
+ className={cn(
542
+ "shrink-0 text-muted-foreground transition-transform duration-150",
543
+ expanded && "rotate-90",
544
+ )}
545
+ aria-hidden="true"
546
+ >
547
+ <path d="M4.5 2.5L7.5 6L4.5 9.5" />
548
+ </svg>
549
+ );
550
+ }
@@ -0,0 +1,202 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import type { TodoItem } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
5
+ import { TodoStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
+ import { cn } from "@stigmer/theme";
7
+
8
+ export interface TodoListProps {
9
+ /**
10
+ * Todo items keyed by ID. Accepts the proto map shape directly
11
+ * (`execution.status.todos` or `subAgentExecution.todos`).
12
+ */
13
+ readonly todos: { readonly [key: string]: TodoItem };
14
+ readonly className?: string;
15
+ }
16
+
17
+ const STATUS_SORT_ORDER: ReadonlyMap<TodoStatus, number> = new Map([
18
+ [TodoStatus.TODO_IN_PROGRESS, 0],
19
+ [TodoStatus.TODO_PENDING, 1],
20
+ [TodoStatus.TODO_COMPLETED, 2],
21
+ [TodoStatus.TODO_CANCELLED, 3],
22
+ ]);
23
+
24
+ function todoSortKey(item: TodoItem): number {
25
+ return STATUS_SORT_ORDER.get(item.status) ?? 4;
26
+ }
27
+
28
+ /**
29
+ * Renders a sorted checklist of {@link TodoItem} entries.
30
+ *
31
+ * Shared by {@link ExecutionProgress} (main agent sidebar widget)
32
+ * and {@link SubAgentSection} (sub-agent expanded content).
33
+ *
34
+ * Items are sorted by activity: in-progress first, then pending,
35
+ * completed, and cancelled. Each row shows a status icon and the
36
+ * task description.
37
+ *
38
+ * All visual properties flow through `--stgm-*` tokens.
39
+ */
40
+ export function TodoList({ todos, className }: TodoListProps) {
41
+ const sortedTodos = useMemo(() => {
42
+ const items = Object.values(todos);
43
+ if (items.length === 0) return [];
44
+ return items.slice().sort((a, b) => todoSortKey(a) - todoSortKey(b));
45
+ }, [todos]);
46
+
47
+ if (sortedTodos.length === 0) return null;
48
+
49
+ return (
50
+ <ul
51
+ role="list"
52
+ className={cn("flex flex-col gap-1", className)}
53
+ aria-label="Tasks"
54
+ >
55
+ {sortedTodos.map((item) => (
56
+ <TodoRow key={item.id} item={item} />
57
+ ))}
58
+ </ul>
59
+ );
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Exported for reuse in collapsed sub-agent preview
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Returns the first in-progress todo from a todos map, or `null`.
68
+ * Used by {@link SubAgentSection} to show an active task preview
69
+ * in the collapsed summary row.
70
+ */
71
+ export function findActiveTodo(
72
+ todos: { readonly [key: string]: TodoItem } | undefined,
73
+ ): TodoItem | null {
74
+ if (!todos) return null;
75
+ for (const item of Object.values(todos)) {
76
+ if (item.status === TodoStatus.TODO_IN_PROGRESS) return item;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Builds a compact completion summary string (e.g. "3/5 completed").
83
+ * Returns `null` when there are no todos.
84
+ */
85
+ export function todoCompletionSummary(
86
+ todos: { readonly [key: string]: TodoItem } | undefined,
87
+ ): string | null {
88
+ if (!todos) return null;
89
+ const items = Object.values(todos);
90
+ if (items.length === 0) return null;
91
+ const completed = items.filter(
92
+ (t) => t.status === TodoStatus.TODO_COMPLETED,
93
+ ).length;
94
+ return `${completed}/${items.length} completed`;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // TodoRow — single todo item with status icon
99
+ // ---------------------------------------------------------------------------
100
+
101
+ function TodoRow({ item }: { readonly item: TodoItem }) {
102
+ const Icon = TODO_ICONS[item.status] ?? TodoPendingIcon;
103
+ const colorClass = TODO_COLORS[item.status] ?? "text-muted-foreground";
104
+ const cancelled = item.status === TodoStatus.TODO_CANCELLED;
105
+
106
+ return (
107
+ <li className="flex items-start gap-1.5 text-xs">
108
+ <span className={cn("mt-0.5 shrink-0", colorClass)} aria-hidden="true">
109
+ <Icon />
110
+ </span>
111
+ <span
112
+ className={cn(
113
+ "min-w-0 break-words",
114
+ cancelled ? "text-muted-foreground line-through" : "text-foreground",
115
+ )}
116
+ >
117
+ {item.content}
118
+ </span>
119
+ </li>
120
+ );
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Status icon / color mapping
125
+ // ---------------------------------------------------------------------------
126
+
127
+ const TODO_ICONS: Partial<Record<TodoStatus, () => React.JSX.Element>> = {
128
+ [TodoStatus.TODO_PENDING]: TodoPendingIcon,
129
+ [TodoStatus.TODO_IN_PROGRESS]: TodoInProgressIcon,
130
+ [TodoStatus.TODO_COMPLETED]: TodoCompletedIcon,
131
+ [TodoStatus.TODO_CANCELLED]: TodoCancelledIcon,
132
+ };
133
+
134
+ const TODO_COLORS: Partial<Record<TodoStatus, string>> = {
135
+ [TodoStatus.TODO_PENDING]: "text-muted-foreground",
136
+ [TodoStatus.TODO_IN_PROGRESS]: "text-foreground",
137
+ [TodoStatus.TODO_COMPLETED]: "text-success",
138
+ [TodoStatus.TODO_CANCELLED]: "text-muted-foreground",
139
+ };
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Inline SVG icons — no external icon dependency in SDK
143
+ // ---------------------------------------------------------------------------
144
+
145
+ function TodoPendingIcon() {
146
+ return (
147
+ <svg
148
+ width="12"
149
+ height="12"
150
+ viewBox="0 0 12 12"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ strokeWidth="1.5"
154
+ >
155
+ <circle cx="6" cy="6" r="4.5" />
156
+ </svg>
157
+ );
158
+ }
159
+
160
+ /** Exported for reuse in SubAgentSection collapsed preview. */
161
+ export function TodoInProgressIcon() {
162
+ return (
163
+ <span className="relative flex h-3 w-3 items-center justify-center">
164
+ <span className="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-current opacity-75" />
165
+ <span className="relative inline-flex h-2 w-2 rounded-full bg-current" />
166
+ </span>
167
+ );
168
+ }
169
+
170
+ function TodoCompletedIcon() {
171
+ return (
172
+ <svg
173
+ width="12"
174
+ height="12"
175
+ viewBox="0 0 12 12"
176
+ fill="none"
177
+ stroke="currentColor"
178
+ strokeWidth="2"
179
+ strokeLinecap="round"
180
+ strokeLinejoin="round"
181
+ >
182
+ <path d="M2.5 6L5 8.5L9.5 3.5" />
183
+ </svg>
184
+ );
185
+ }
186
+
187
+ function TodoCancelledIcon() {
188
+ return (
189
+ <svg
190
+ width="12"
191
+ height="12"
192
+ viewBox="0 0 12 12"
193
+ fill="none"
194
+ stroke="currentColor"
195
+ strokeWidth="2"
196
+ strokeLinecap="round"
197
+ strokeLinejoin="round"
198
+ >
199
+ <path d="M3 3L9 9M9 3L3 9" />
200
+ </svg>
201
+ );
202
+ }
@@ -187,7 +187,7 @@ export function ToolCallItem({
187
187
  {expanded && (
188
188
  <div className="px-2.5 pb-2.5 pt-1">
189
189
  {isSubAgent ? (
190
- <SubAgentSection subAgentExecution={subAgentExecution} />
190
+ <SubAgentSection subAgentExecution={subAgentExecution} collapsible={false} />
191
191
  ) : (
192
192
  <ToolCallDetail toolCall={toolCall} />
193
193
  )}