@workerdeck/ui 0.22.0 → 1.0.0

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 (145) hide show
  1. package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
  2. package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
  3. package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
  4. package/build/format-B8ZM9LPy.d.mts +16 -0
  5. package/build/format.d.mts +8 -75
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +63 -1138
  8. package/build/index.mjs +4 -300
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +80 -24
  11. package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
  12. package/build/status-C0HSLZas.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -124
  14. package/build/workspace.mjs +13 -143
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +4 -4
  17. package/src/components/agent/CodeEditor.tsx +31 -103
  18. package/src/components/agent/Composer.tsx +114 -333
  19. package/src/components/agent/ContextDialog.tsx +20 -41
  20. package/src/components/agent/ContextRing.tsx +6 -28
  21. package/src/components/agent/Conversation.tsx +21 -33
  22. package/src/components/agent/EditorTabs.tsx +39 -67
  23. package/src/components/agent/EngineIcon.tsx +29 -72
  24. package/src/components/agent/EntryIcon.tsx +16 -0
  25. package/src/components/agent/FileCard.tsx +10 -16
  26. package/src/components/agent/FileTree.tsx +52 -125
  27. package/src/components/agent/FileViewer.tsx +30 -60
  28. package/src/components/agent/HostFilesDialog.tsx +40 -69
  29. package/src/components/agent/Loader.tsx +3 -24
  30. package/src/components/agent/McpDialog.tsx +64 -137
  31. package/src/components/agent/Message.tsx +2 -21
  32. package/src/components/agent/ModelSelect.tsx +33 -62
  33. package/src/components/agent/PermissionModeSelect.tsx +21 -79
  34. package/src/components/agent/PermissionPrompt.tsx +59 -63
  35. package/src/components/agent/ProjectIcon.tsx +228 -93
  36. package/src/components/agent/PromptTokenText.tsx +10 -16
  37. package/src/components/agent/QuestionPrompt.tsx +43 -64
  38. package/src/components/agent/Reasoning.tsx +13 -16
  39. package/src/components/agent/Response.tsx +4 -6
  40. package/src/components/agent/Scrubber.tsx +31 -80
  41. package/src/components/agent/SessionBrowser.tsx +64 -243
  42. package/src/components/agent/SessionEmptyState.tsx +10 -40
  43. package/src/components/agent/SessionInfoDialog.tsx +46 -78
  44. package/src/components/agent/SessionItem.tsx +53 -320
  45. package/src/components/agent/SessionList.tsx +23 -39
  46. package/src/components/agent/SessionPanel.tsx +345 -923
  47. package/src/components/agent/SessionStatusIcon.tsx +14 -28
  48. package/src/components/agent/SessionSteps.tsx +34 -187
  49. package/src/components/agent/SessionWorkspace.tsx +108 -215
  50. package/src/components/agent/SkillsDialog.tsx +35 -80
  51. package/src/components/agent/StatusBar.tsx +51 -136
  52. package/src/components/agent/SubagentStrip.tsx +12 -77
  53. package/src/components/agent/ToolCallCard.tsx +94 -135
  54. package/src/components/agent/Transcript.tsx +103 -1288
  55. package/src/components/agent/TranscriptItemView.tsx +155 -0
  56. package/src/components/agent/TranscriptRows.tsx +418 -0
  57. package/src/components/agent/UsageDialog.tsx +7 -29
  58. package/src/components/agent/UsageMeters.tsx +18 -73
  59. package/src/components/agent/pulse.tsx +9 -32
  60. package/src/components/agent/scrubber-marks.ts +33 -103
  61. package/src/components/agent/status.ts +1 -5
  62. package/src/components/agent/tool-result-fetch.tsx +3 -25
  63. package/src/components/agent/tool-result-image.tsx +32 -111
  64. package/src/components/agent/transcript-rows.ts +41 -118
  65. package/src/components/agent/transcript-variant.tsx +3 -80
  66. package/src/components/agent/use-height-epoch.ts +10 -24
  67. package/src/components/agent/use-path-links.ts +45 -63
  68. package/src/components/agent/use-subagent-frame.ts +106 -0
  69. package/src/components/agent/use-transcript-jumps.ts +49 -107
  70. package/src/components/prompt-area/animated-placeholder.tsx +6 -5
  71. package/src/components/prompt-area/clipboard-helpers.ts +26 -21
  72. package/src/components/prompt-area/cursor-helpers.ts +79 -37
  73. package/src/components/prompt-area/dom-helpers.ts +97 -69
  74. package/src/components/prompt-area/file-strip.tsx +90 -80
  75. package/src/components/prompt-area/html-to-markdown.ts +67 -46
  76. package/src/components/prompt-area/image-strip.tsx +8 -18
  77. package/src/components/prompt-area/index.ts +2 -15
  78. package/src/components/prompt-area/prompt-area-engine.ts +100 -115
  79. package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
  80. package/src/components/prompt-area/prompt-area.tsx +31 -38
  81. package/src/components/prompt-area/remove-button.tsx +3 -9
  82. package/src/components/prompt-area/segment-helpers.ts +4 -12
  83. package/src/components/prompt-area/trigger-popover.tsx +15 -27
  84. package/src/components/prompt-area/trigger-presets.ts +0 -10
  85. package/src/components/prompt-area/types.ts +3 -8
  86. package/src/components/prompt-area/use-chip-editing.ts +305 -0
  87. package/src/components/prompt-area/use-markdown-mode.ts +7 -17
  88. package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
  89. package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
  90. package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
  91. package/src/components/prompt-area/use-prompt-area.ts +170 -896
  92. package/src/components/prompt-area/use-trigger-search.ts +21 -19
  93. package/src/components/terminal/PermissionPrompt.tsx +56 -59
  94. package/src/components/terminal/QuestionPrompt.tsx +28 -79
  95. package/src/components/terminal/StatusLine.tsx +13 -63
  96. package/src/components/terminal/TerminalTranscript.tsx +44 -160
  97. package/src/components/terminal/affordances.tsx +68 -98
  98. package/src/components/terminal/blocks.ts +20 -156
  99. package/src/components/terminal/diff.tsx +19 -67
  100. package/src/components/terminal/height.ts +163 -313
  101. package/src/components/terminal/image-box.ts +0 -44
  102. package/src/components/terminal/items.tsx +150 -305
  103. package/src/components/terminal/markdown.tsx +36 -95
  104. package/src/components/terminal/press.tsx +26 -53
  105. package/src/components/terminal/prompt.tsx +40 -170
  106. package/src/components/terminal/result-preview.ts +8 -56
  107. package/src/components/terminal/row.tsx +11 -98
  108. package/src/components/terminal/scrubber.tsx +108 -401
  109. package/src/components/terminal/surface.tsx +6 -54
  110. package/src/components/terminal/todos.ts +66 -0
  111. package/src/components/terminal/tool-run.ts +21 -192
  112. package/src/components/ui/AlertDialog.tsx +8 -23
  113. package/src/components/ui/Badge.tsx +18 -29
  114. package/src/components/ui/Button.tsx +7 -15
  115. package/src/components/ui/Card.tsx +2 -13
  116. package/src/components/ui/CodeBlock.tsx +10 -37
  117. package/src/components/ui/CopyButton.tsx +10 -23
  118. package/src/components/ui/Dialog.tsx +23 -36
  119. package/src/components/ui/Empty.tsx +6 -29
  120. package/src/components/ui/Input.tsx +14 -16
  121. package/src/components/ui/Menu.tsx +6 -18
  122. package/src/components/ui/PortalScope.tsx +1 -21
  123. package/src/components/ui/ProgressRing.tsx +8 -19
  124. package/src/components/ui/Select.tsx +18 -31
  125. package/src/components/ui/Sonner.tsx +1 -2
  126. package/src/components/ui/Spinner.tsx +1 -1
  127. package/src/components/ui/Splitter.tsx +30 -64
  128. package/src/components/ui/Textarea.tsx +2 -5
  129. package/src/components/ui/Tooltip.tsx +3 -13
  130. package/src/format.ts +0 -9
  131. package/src/index.ts +16 -113
  132. package/src/lib/clipboard.ts +8 -28
  133. package/src/lib/css.ts +8 -0
  134. package/src/lib/format.ts +101 -86
  135. package/src/lib/plan-request.ts +16 -0
  136. package/src/lib/status.ts +45 -95
  137. package/src/lib/tool-icon.ts +39 -47
  138. package/src/lib/utils.ts +1 -3
  139. package/src/styles/terminal.css +81 -49
  140. package/src/styles/theme.css +136 -19
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-13l25ubU.d.mts +0 -609
  143. package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. package/build/status-BE-zg88x.mjs.map +0 -1
@@ -3,41 +3,27 @@ import type { SessionRow } from '@workerdeck/protocol'
3
3
  import { Spinner } from '../ui/Spinner.tsx'
4
4
  import { cn } from '../../lib/utils.ts'
5
5
 
6
- /**
7
- * State as one glyph — a ringing bell when it wants a human, a spinner while it
8
- * works, a moon when it is only sleeping. Replaces the text badge: in a sidebar
9
- * the word costs more room than it earns, and the states that matter are the two
10
- * you can recognise without reading.
11
- *
12
- * **It reads `row.state`, not `info.status`, and that distinction is the whole
13
- * point of the row model.** `sessionState` already folds in the arm this glyph
14
- * cannot see for itself: a *background* sub-agent outlives its turn by design,
15
- * so the turn ends, `status` comes to rest at `idle`, and the agent keeps
16
- * working. Reading the raw status drew a **moon on a row filed under the
17
- * "Working" header** — the list contradicting itself on one line, which is
18
- * exactly what a derived view model exists to prevent. The value was in scope
19
- * and unread.
20
- *
21
- * The terminal statuses still come off `info.status`, because `ended` collapses
22
- * `failed` and `closed` into one bucket and those are worth telling apart here.
23
- *
24
- * It lives in its own file rather than inside the dashboard's browser because
25
- * the extension's cards draw it too, and a second copy there is how the two
26
- * lists last disagreed about what a parked session looks like.
27
- */
28
6
  export function SessionStatusIcon({ row, className }: { row: SessionRow; className?: string }) {
29
7
  const { info } = row
30
8
  const size = cn('size-4 shrink-0', className)
31
- if (row.state === 'attention') return <BellRing className={cn(size, 'animate-pulse text-warning')} />
32
- if (row.state === 'working') return <Spinner className={cn(size, 'text-info')} />
9
+ if (row.state === 'attention') {
10
+ return <BellRing className={cn(size, 'animate-pulse text-warning')} />
11
+ }
12
+ if (row.state === 'working') {
13
+ return <Spinner className={cn(size, 'text-info')} />
14
+ }
33
15
  switch (info.status) {
34
- case 'failed':
16
+ case 'failed': {
35
17
  return <CircleAlert className={cn(size, 'text-danger')} />
36
- case 'closed':
18
+ }
19
+ case 'closed': {
37
20
  return <CircleSlash className={cn(size, 'text-fg-4')} />
38
- case 'parked':
21
+ }
22
+ case 'parked': {
39
23
  return <PauseCircle className={cn(size, 'text-fg-3')} />
40
- default:
24
+ }
25
+ default: {
41
26
  return <Moon className={cn(size, 'text-fg-4')} />
27
+ }
42
28
  }
43
29
  }
@@ -1,96 +1,21 @@
1
- import {
2
- ArrowRight,
3
- Check,
4
- ChevronDown,
5
- ChevronRight,
6
- CircleAlert,
7
- Dot,
8
- PauseCircle,
9
- } from 'lucide-react'
1
+ import { ArrowRight, Check, ChevronDown, ChevronRight, CircleAlert, Dot, PauseCircle } from 'lucide-react'
10
2
  import { isAgentRecord, subagentLabel } from '@workerdeck/protocol'
11
3
  import type { SessionInfo, SubagentInfo } from '@workerdeck/protocol'
12
4
  import { Spinner } from '../ui/Spinner.tsx'
13
5
  import { cn } from '../../lib/utils.ts'
14
6
 
15
- /**
16
- * The work *under* a session row, and the one row shape every client renders it
17
- * through.
18
- *
19
- * This lived inside the VS Code webview first, which is exactly why the
20
- * dashboard had none of it. Nothing in here is extension-specific: a session's
21
- * sub-agents are a protocol fact, and a disclosure over them is a list
22
- * affordance. It sits in `packages/ui` so the sidebar, the dashboard and (in its
23
- * own idiom) the phone are annotating rows the same way rather than three ways.
24
- *
25
- * Today the only source is `SessionInfo.subagents`. The other — the CLI's own
26
- * **task checklist**, the to-do list it keeps for the current turn — is what the
27
- * design was drawn from, and it is not built: nothing on the wire carries it yet
28
- * (see `_docs/features/sub-agent-handling.md`, second thread, which opens with
29
- * "check a capture" rather than "design a surface"). When it arrives it is a
30
- * *source*, not a second row component: checklist when the session has one, its
31
- * sub-agents otherwise.
32
- *
33
- * That is also why `state` has a `pending` arm no sub-agent can produce. A
34
- * sub-agent record exists only once dispatched, so it is never queued; a to-do
35
- * is queued for most of its life, and dropping the state would mean widening the
36
- * union later — the shape is cheaper to state now than to retrofit.
37
- */
38
7
  export type Step = {
39
8
  key: string
40
9
  label: string
41
- /** What one of these is called, for the disclosure's count. */
42
10
  noun: string
43
- /**
44
- * Both kinds press. The kind decides **where the press goes**, which is the
45
- * whole of the distinction.
46
- *
47
- * An **agent** has an identity and work of its own, so it wears the sub-agent
48
- * colour and opens that agent's own frame. A **task** is something the model
49
- * described with no agent behind it (`isAgentRecord`), so there is no frame:
50
- * framing its id selects no items and draws an empty screen. What it has
51
- * instead is a *place* — the spawning call's row in the transcript — so it is
52
- * muted, never selected, and travels there. See `StepRow` for why an inert
53
- * row was the worse answer.
54
- *
55
- * Two values and not a boolean because the checklist source this shape was
56
- * drawn for (see above) produces the second kind natively — a to-do is a task
57
- * in exactly this sense, and it will slot in here rather than widening
58
- * anything.
59
- */
60
11
  kind: 'agent' | 'task'
61
12
  state: 'done' | 'running' | 'pending' | 'failed'
62
- /** A trailing reading — a sub-agent's tool count. Absent draws nothing. */
63
13
  detail?: string
64
14
  title: string
65
15
  onSelect: () => void
66
16
  }
67
17
 
68
- /**
69
- * The steps under one session, **agents first**.
70
- *
71
- * The two kinds do different things when pressed and are worth different
72
- * amounts of attention: an agent has work of its own to go and read, a task is a
73
- * marker in a transcript. Interleaved in dispatch order they read as one
74
- * undifferentiated list, and the rows you can actually open are scattered
75
- * through it. Grouped, the openable ones are a block at the top and the markers
76
- * are a tail you can skip.
77
- *
78
- * Stable **within** each group, deliberately: dispatch order is the only order
79
- * these records have that means anything (it is the order the work was started
80
- * in), so the sort partitions and never reorders inside a partition.
81
- *
82
- * `onSelect` is handed the **kind** as well as the id, because the two kinds go
83
- * to different places — see `SessionItem`, which is what routes them. Passing
84
- * only the id is what let a task be opened as if it were an agent, and a framed
85
- * task id matches no items, so the panel drew an **empty agent view**.
86
- */
87
- export function sessionSteps(
88
- info: SessionInfo,
89
- onSelect: (toolUseId: string, kind: Step['kind']) => void,
90
- ): Step[] {
91
- // The label is protocol's `subagentLabel`, not a spelling of its own: the
92
- // dashboard and the phone render the same rows from the same records, and two
93
- // spellings would be two different answers to "which agent is this".
18
+ export function sessionSteps(info: SessionInfo, onSelect: (toolUseId: string, kind: Step['kind']) => void): Step[] {
94
19
  const steps = (info.subagents ?? []).map((sub) => {
95
20
  const kind = isAgentRecord(sub) ? ('agent' as const) : ('task' as const)
96
21
  return {
@@ -109,30 +34,22 @@ export function sessionSteps(
109
34
 
110
35
  function stepState(status: SubagentInfo['status']): Step['state'] {
111
36
  switch (status) {
112
- case 'running':
37
+ case 'running': {
113
38
  return 'running'
114
- case 'failed':
39
+ }
40
+ case 'failed': {
115
41
  return 'failed'
116
- default:
42
+ }
43
+ default: {
117
44
  return 'done'
45
+ }
118
46
  }
119
47
  }
120
48
 
121
- /** How many of these are still going — the live half of the disclosure's count. */
122
49
  export function runningSteps(steps: readonly Step[]): number {
123
50
  return steps.filter((s) => s.state === 'running').length
124
51
  }
125
52
 
126
- /**
127
- * The disclosure, which is also the reading: `3 agents` — or `2 of 3 agents`
128
- * while some have settled, because "how many are still going" is the live
129
- * question and a bare total answers it wrong the moment one finishes.
130
- *
131
- * Sub-agents are an annotation on a working row rather than a state of their own
132
- * (see `runningSubagents` in protocol's `session-list.ts`), so this never
133
- * competes with the row's status glyph: that still says what the *session* is
134
- * doing.
135
- */
136
53
  export function StepToggle({
137
54
  expanded,
138
55
  running,
@@ -146,27 +63,16 @@ export function StepToggle({
146
63
  noun: string
147
64
  onToggle: () => void
148
65
  }) {
149
- // Two spellings of one count. The **words** are the honest reading and go in
150
- // the tooltip and to a screen reader; the line itself gets the digits, because
151
- // this sits on the second line of a 280px row next to the folder and the age,
152
- // and `1 of 6 agents` truncated the folder name away to say something the row
153
- // could say in three characters.
154
66
  const label = running > 0 && running < total ? `${running}/${total}` : String(total)
155
- const words =
156
- running > 0 && running < total
157
- ? `${running} of ${total} ${noun}s running`
158
- : `${total} ${noun}${total === 1 ? '' : 's'}`
67
+ const words = running > 0 && running < total ? `${running} of ${total} ${noun}s running` : `${total} ${noun}${total === 1 ? '' : 's'}`
159
68
  const Chevron = expanded ? ChevronDown : ChevronRight
160
69
  return (
161
70
  <button
162
- type='button'
71
+ type="button"
163
72
  aria-expanded={expanded}
164
73
  aria-label={`${expanded ? 'Hide' : 'Show'} ${words}`}
165
74
  title={`${expanded ? 'Hide' : 'Show'} ${words}`}
166
75
  onClick={(e) => {
167
- // The whole row is a button and this one does not mean "select" — the
168
- // same guard the row's overflow needs, and the reason a drag-select
169
- // inside the row does not toggle it.
170
76
  e.stopPropagation()
171
77
  onToggle()
172
78
  }}
@@ -174,83 +80,24 @@ export function StepToggle({
174
80
  'flex shrink-0 items-center gap-0.5 rounded-[4px] py-0.5 pr-1 pl-0.5 outline-none',
175
81
  'text-[0.75rem] leading-3 hover:bg-row-hover hover:text-fg-2',
176
82
  running > 0 ? 'text-info' : 'text-fg-4',
177
- )}>
178
- <Chevron className='size-3' />
179
- <span className='tabular-nums'>{label}</span>
83
+ )}
84
+ >
85
+ <Chevron className="size-3" />
86
+ <span className="tabular-nums">{label}</span>
180
87
  </button>
181
88
  )
182
89
  }
183
90
 
184
- /**
185
- * One step under its session — **pressable, all of them**, and what a press
186
- * means is what tells the two kinds apart.
187
- *
188
- * Pressing an **agent** hands the panel over to that agent's own work
189
- * (`SessionPanel.openSubagent`): it is not a session and never becomes one, but
190
- * it has a surface, so it can be the selected thing. Pressing a **task** selects
191
- * the *session* and travels to that task's marker inside it — a task is a
192
- * reference to a place in a transcript, not a thing with a screen, so it can be
193
- * followed but never held.
194
- *
195
- * That is a reversal, and a deliberate one. A task used to be inert markup on
196
- * the argument that "a disabled-looking button still announces itself as one" —
197
- * correct about the markup, wrong about the premise, because there *was* always
198
- * somewhere to go and the row simply swallowed the click on its way there. A row
199
- * that looks like a list item, sits in a list, and does nothing when pressed is
200
- * the worse lie. So every step answers the pointer and every step answers a
201
- * press; only what the press does differs.
202
- *
203
- * Divided from the card's header by **indentation and its own hit shape**, not
204
- * by a rule. The rules came first, on the argument that at 11px an indent is not
205
- * enough to say "list inside a row" — and they were right about the reading and
206
- * wrong about the cost: a stack of hairlines across every open card turned the
207
- * list into a ledger, and a rule cannot answer a pointer. A step that lights up
208
- * under the cursor and fills when it is the one on screen says *list* far more
209
- * plainly than a line between two of them, and it says it while doing the job
210
- * the rule could not.
211
- *
212
- * **Only an agent can wear the selection**, and `active` is guarded on that here
213
- * rather than trusted from the caller: a host that hands back a task's key is
214
- * describing where it navigated, not what it selected, and the row must not
215
- * paint itself blue for it.
216
- *
217
- * The hover is `--row-active` — **alpha, not a flat fill** — because this row
218
- * has to answer the pointer on three different grounds: a transparent card, a
219
- * blue one (its session is selected), and a grey one (a sibling agent is). A
220
- * flat value tuned for any of those is wrong on the other two; a tint darkens or
221
- * lifts whatever it lands on. It is the one place in the list where the alpha
222
- * token earns its keep.
223
- */
224
- export function StepRow({
225
- step,
226
- active = false,
227
- onSelect,
228
- }: {
229
- step: Step
230
- /** The panel is showing this step's own work. Ignored for tasks, which cannot
231
- * be the selected thing — see above. */
232
- active?: boolean
233
- onSelect: () => void
234
- }) {
91
+ export function StepRow({ step, active = false, onSelect }: { step: Step; active?: boolean; onSelect: () => void }) {
235
92
  const agent = step.kind === 'agent'
236
93
  const selected = active && agent
237
- // Body colour by *kind*, state carried by the icon — the rule the transcript's
238
- // own `TaskRow` already follows, where the body is green and the marker holds
239
- // the beat. Green means sub-agent across this product (it is the one hue
240
- // nothing else on a session surface claims), so a list that spent blue on
241
- // "running" here would be saying something different from the transcript
242
- // about the same agent. Failure still outranks it: an alarm is not a category.
243
94
  const body = step.state === 'failed' ? 'text-danger' : agent ? 'text-success' : 'text-fg-4'
244
95
  return (
245
96
  <button
246
- type='button'
97
+ type="button"
247
98
  title={step.title}
248
99
  aria-current={selected || undefined}
249
100
  onClick={(e) => {
250
- // The whole card is pressable underneath and means "open the session".
251
- // This row has its own answer — for an agent a different destination,
252
- // for a task the same one plus a place to land — so it must not also
253
- // fire the card's.
254
101
  e.stopPropagation()
255
102
  onSelect()
256
103
  }}
@@ -259,32 +106,32 @@ export function StepRow({
259
106
  'text-left text-micro outline-none',
260
107
  selected ? 'bg-row-selected' : 'hover:bg-row-active',
261
108
  body,
262
- )}>
109
+ )}
110
+ >
263
111
  <StepIcon state={step.state} kind={step.kind} />
264
- <span className='min-w-0 flex-1 truncate'>{step.label}</span>
265
- {/* The progress reading while it works, and what it cost when it is done.
266
- Zero draws nothing: `0 tools` beside a thinking agent reads as a stall,
267
- which is the same call `taskSummary` makes one surface over. */}
268
- {step.detail ? <span className='shrink-0 tabular-nums text-fg-4'>{step.detail}</span> : null}
269
- {agent ? <ArrowRight className='size-3.5 shrink-0 text-fg-4' /> : null}
112
+ <span className="min-w-0 flex-1 truncate">{step.label}</span>
113
+ {step.detail ? <span className="shrink-0 tabular-nums text-fg-4">{step.detail}</span> : null}
114
+ {agent ? <ArrowRight className="size-3.5 shrink-0 text-fg-4" /> : null}
270
115
  </button>
271
116
  )
272
117
  }
273
118
 
274
119
  function StepIcon({ state, kind }: { state: Step['state']; kind: Step['kind'] }) {
275
- // A task that is neither running nor failed gets a neutral dot rather than a
276
- // tick: `done` is a claim about work, and nothing here did any.
277
120
  if (kind === 'task' && state !== 'running' && state !== 'failed') {
278
- return <Dot className='size-[11px] shrink-0' />
121
+ return <Dot className="size-[11px] shrink-0" />
279
122
  }
280
123
  switch (state) {
281
- case 'running':
282
- return <Spinner className='size-[11px] shrink-0' />
283
- case 'failed':
284
- return <CircleAlert className='size-[11px] shrink-0' />
285
- case 'pending':
286
- return <PauseCircle className='size-[11px] shrink-0' />
287
- default:
288
- return <Check className='size-[11px] shrink-0' />
124
+ case 'running': {
125
+ return <Spinner className="size-[11px] shrink-0" />
126
+ }
127
+ case 'failed': {
128
+ return <CircleAlert className="size-[11px] shrink-0" />
129
+ }
130
+ case 'pending': {
131
+ return <PauseCircle className="size-[11px] shrink-0" />
132
+ }
133
+ default: {
134
+ return <Check className="size-[11px] shrink-0" />
135
+ }
289
136
  }
290
137
  }