@workerdeck/ui 0.23.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-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
  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 +15 -23
  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 +5 -126
  14. package/build/workspace.mjs +11 -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 +3 -9
  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 +315 -996
  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 +104 -219
  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 +96 -32
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-BobynUo4.d.mts +0 -669
  143. package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. package/build/status-BE-zg88x.mjs.map +0 -1
@@ -9,52 +9,25 @@ import { Tip } from '../ui/Tooltip.tsx'
9
9
  import { cn } from '../../lib/utils.ts'
10
10
  import { formatCost, formatCountdown, formatTokens } from '../../lib/format.ts'
11
11
  import { meterColorClass } from '../../lib/status.ts'
12
+ import { cssColor } from '../../lib/css.ts'
12
13
  import { STATUS_META } from './status.ts'
13
14
  import { useTranscriptVariant } from './transcript-variant.tsx'
14
15
 
15
16
  export interface StatusBarProps {
16
17
  state: TranscriptState
17
- /**
18
- * Plan windows to draw, when they should not be the session's own. The panel
19
- * hands over the gateway's per-profile state merged over this transcript's
20
- * reading (`mergeUsage`): a session's own `rate_limit` readings arrive only at
21
- * a turn's edges, so one idle since yesterday would otherwise render
22
- * yesterday's number as current. Absent = `state.rateLimits`, which is what a
23
- * bar composed by hand outside the panel gets.
24
- */
25
18
  rateLimits?: Record<string, RateLimitInfo>
26
- /** @deprecated Pass {@link StatusBarProps.connection}; kept so an embedder
27
- * still handing over a boolean keeps working. */
19
+ /** @deprecated Pass {@link StatusBarProps.connection}. */
28
20
  connected?: boolean
29
- /** How the client is doing at reaching the gateway. A dropped socket wins the
30
- * status slot: the session status held over a dead socket is a stale reading,
31
- * and presenting it as a live one is the thing worth avoiding. */
32
21
  connection?: ConnectionState
33
- /**
34
- * Where the gauges lead. Each one opens the panel that answers *its* question
35
- * — the two meters measure different things, so sending both to one "details"
36
- * list would be a detour every time. Omit a handler and that gauge stays a
37
- * read-only tooltip.
38
- */
39
22
  onOpenStatus?: () => void
40
23
  onOpenContext?: () => void
41
24
  onOpenUsage?: () => void
42
- /**
43
- * The session's own controls (model, permission mode), for
44
- * `controlsSurface: 'status'`. They sit at the end of the readings cluster —
45
- * status, context, usage, then what you can *change* — rather than out by the
46
- * actions menu, because they belong with the session facts they act on.
47
- */
48
25
  controls?: ReactNode
49
- /** Trailing slot — the session-actions menu, at the bar's trailing edge. */
50
26
  actions?: ReactNode
51
- /** Which edge the bar sits on, so its separating rule goes on the other side.
52
- * Placement is the panel's decision; this only styles it. */
53
27
  placement?: 'top' | 'bottom'
54
28
  className?: string
55
29
  }
56
30
 
57
- /** Ticking clock for reset countdowns — rate_limit events are sparse, so tick locally. */
58
31
  function useNow(intervalMs = 30_000): number {
59
32
  const [now, setNow] = useState(() => Date.now())
60
33
  useEffect(() => {
@@ -64,44 +37,28 @@ function useNow(intervalMs = 30_000): number {
64
37
  return now
65
38
  }
66
39
 
67
- /** The shared ramp, aliased for the two call sites below — the thresholds live
68
- * in `lib/status.ts` beside the severity they mirror. */
69
- const utilizationColor = (pct: number) => meterColorClass(pct)
70
-
71
- /** The CLI reports category colors as its own theme token names ('inactive',
72
- * 'promptBorder', ...), not CSS colors — only pass through what CSS can render. */
73
- const cssColor = (color: string): string | undefined =>
74
- typeof CSS !== 'undefined' && CSS.supports('color', color) ? color : undefined
75
-
76
40
  function ContextMeter({ usage }: { usage: ContextUsage }) {
77
41
  return (
78
42
  <Tip
79
43
  content={
80
- <div className='flex min-w-44 flex-col gap-1 py-0.5'>
44
+ <div className="flex min-w-44 flex-col gap-1 py-0.5">
81
45
  {usage.categories.map((c) => (
82
- <div key={c.name} className='flex items-center gap-2'>
83
- <span
84
- className='size-2 shrink-0 rounded-full bg-fg-4'
85
- style={cssColor(c.color) ? { backgroundColor: c.color } : undefined}
86
- />
87
- <span className='flex-1'>{c.name}</span>
88
- <span className='font-mono text-fg-3'>{formatTokens(c.tokens)}</span>
46
+ <div key={c.name} className="flex items-center gap-2">
47
+ <span className="size-2 shrink-0 rounded-full bg-fg-4" style={cssColor(c.color) ? { backgroundColor: c.color } : undefined} />
48
+ <span className="flex-1">{c.name}</span>
49
+ <span className="font-mono text-fg-3">{formatTokens(c.tokens)}</span>
89
50
  </div>
90
51
  ))}
91
- <div className='mt-0.5 flex items-center justify-between gap-2 border-t border-border pt-1'>
52
+ <div className="mt-0.5 flex items-center justify-between gap-2 border-t border-border pt-1">
92
53
  <span>Total</span>
93
- <span className='font-mono text-fg-3'>
94
- {formatTokens(usage.totalTokens)} / {formatTokens(usage.maxTokens)} (
95
- {usage.percentage.toFixed(0)}%)
54
+ <span className="font-mono text-fg-3">
55
+ {formatTokens(usage.totalTokens)} / {formatTokens(usage.maxTokens)} ({usage.percentage.toFixed(0)}%)
96
56
  </span>
97
57
  </div>
98
58
  </div>
99
- }>
100
- <span
101
- className={cn(
102
- 'inline-flex cursor-default items-center gap-1 font-mono text-label',
103
- utilizationColor(usage.percentage),
104
- )}>
59
+ }
60
+ >
61
+ <span className={cn('inline-flex cursor-default items-center gap-1 font-mono text-label', meterColorClass(usage.percentage))}>
105
62
  Ctx {formatTokens(usage.totalTokens)}
106
63
  </span>
107
64
  </Tip>
@@ -109,69 +66,53 @@ function ContextMeter({ usage }: { usage: ContextUsage }) {
109
66
  }
110
67
 
111
68
  function RateLimitMeter({ label, info, now }: { label: string; info: RateLimitInfo; now: number }) {
112
- // The CLI omits utilization on some updates — show the window without a made-up 0%.
113
69
  const pct = info.utilization
114
70
  const resetsAtMs = info.resetsAt !== undefined ? info.resetsAt * 1000 : undefined
115
71
  return (
116
72
  <Tip
117
73
  content={
118
- <div className='flex min-w-36 flex-col gap-1 py-0.5'>
119
- <div className='flex items-center justify-between gap-2'>
74
+ <div className="flex min-w-36 flex-col gap-1 py-0.5">
75
+ <div className="flex items-center justify-between gap-2">
120
76
  <span>{label} usage</span>
121
- <span className='font-mono text-fg-3'>
122
- {pct !== undefined ? `${pct.toFixed(1)}%` : '—'}
123
- </span>
77
+ <span className="font-mono text-fg-3">{pct !== undefined ? `${pct.toFixed(1)}%` : '—'}</span>
124
78
  </div>
125
79
  {resetsAtMs !== undefined ? (
126
- <div className='flex items-center justify-between gap-2'>
80
+ <div className="flex items-center justify-between gap-2">
127
81
  <span>Resets in</span>
128
- <span className='font-mono text-fg-3'>{formatCountdown(resetsAtMs, now)}</span>
82
+ <span className="font-mono text-fg-3">{formatCountdown(resetsAtMs, now)}</span>
129
83
  </div>
130
84
  ) : null}
131
- {info.isUsingOverage ? <div className='text-warning'>Using overage</div> : null}
132
- {info.status === 'rejected' ? <div className='text-danger'>Limit reached</div> : null}
85
+ {info.isUsingOverage ? <div className="text-warning">Using overage</div> : null}
86
+ {info.status === 'rejected' ? <div className="text-danger">Limit reached</div> : null}
133
87
  </div>
134
- }>
135
- {/* Inline, not `inline-flex`: a flex container contributes its FIRST
136
- item's baseline, and the first item here is the ring — so a flex
137
- version hands the row a 13px circle's baseline instead of the number's,
138
- and every reading beside it sits on a different line. Inline text has
139
- only one baseline to give. `align-middle` centres the ring on the
140
- x-height, which is where it looked right anyway. */}
88
+ }
89
+ >
141
90
  <span
142
91
  className={cn(
143
92
  'cursor-default font-mono text-label whitespace-nowrap',
144
- info.status === 'rejected' ? 'text-danger' : utilizationColor(pct ?? 0),
145
- )}>
146
- <ProgressRing value={pct ?? 0} className='mr-1 inline-block align-middle' />
93
+ info.status === 'rejected' ? 'text-danger' : meterColorClass(pct ?? 0),
94
+ )}
95
+ >
96
+ <ProgressRing value={pct ?? 0} className="mr-1 inline-block align-middle" />
147
97
  {label}
148
98
  {pct !== undefined ? ` ${pct.toFixed(0)}%` : ''}
149
- {resetsAtMs !== undefined ? (
150
- <span className='text-fg-4'> · {formatCountdown(resetsAtMs, now)}</span>
151
- ) : null}
99
+ {resetsAtMs !== undefined ? <span className="text-fg-4"> · {formatCountdown(resetsAtMs, now)}</span> : null}
152
100
  </span>
153
101
  </Tip>
154
102
  )
155
103
  }
156
104
 
157
- /** A gauge that leads somewhere. `undefined` handler leaves it inert, so an
158
- * embedder that mounts no panels doesn't get buttons that do nothing. */
159
105
  function Slot({ onClick, hint, children }: { onClick?: () => void; hint: string; children: ReactNode }) {
160
- if (!onClick) return <>{children}</>
106
+ if (!onClick) {
107
+ return <>{children}</>
108
+ }
161
109
  return (
162
110
  <button
163
- type='button'
111
+ type="button"
164
112
  onClick={onClick}
165
113
  aria-label={hint}
166
- // `leading-4` matches the label metrics inside: a button's own line box is
167
- // the page's 24px one, and the extra strut was padding the bar by 2.5px
168
- // that nothing was using.
169
- // No horizontal padding: the bar's own 6px is the inset, and a slot that
170
- // added its own would start the first reading 10px in. The hover surface
171
- // hugs the text instead, which is what a status line's items do anyway.
172
- // `leading-4` matches the label metrics inside — a button's own line box
173
- // is the page's 24px one, and the strut padded the bar with nothing.
174
- className='rounded-md py-0.5 leading-4 transition-colors outline-none hover:bg-surface-hover focus-visible:bg-surface-hover'>
114
+ className="rounded-md py-0.5 leading-4 transition-colors outline-none hover:bg-surface-hover focus-visible:bg-surface-hover"
115
+ >
175
116
  {children}
176
117
  </button>
177
118
  )
@@ -199,73 +140,47 @@ export function StatusBar({
199
140
  const terminal = useTranscriptVariant() === 'terminal'
200
141
  return (
201
142
  <div
202
- data-slot='status-bar'
143
+ data-slot="status-bar"
203
144
  className={cn(
204
- // Baselines, not boxes. `items-center` centres each child's *box*, and
205
- // this bar's children are boxes of different heights for reasons that
206
- // have nothing to do with their text — a badge's pill padding, a ring
207
- // beside a number, a select's border and chevron. Centring those lands
208
- // their text on four slightly different lines. A flex item's baseline is
209
- // its first line's baseline, so aligning on it puts every reading on one
210
- // line by construction, whatever is drawn around it.
211
- // One explicit height, shared with the docked composer above it (see
212
- // `Composer.tsx`) — the two strips along the foot of the panel read as
213
- // one piece of chrome, and a pixel of drift between them shows.
214
145
  'flex h-[var(--wd-status-bar-height)] items-baseline gap-2 border-border bg-surface p-1.5',
215
- // The rule goes between the bar and the content, so which edge it sits
216
- // on follows the placement — except under the terminal theme at the
217
- // foot, where the composer directly above already closes itself with a
218
- // rule of its own. Two adjacent 1px rules is a 2px rule with a seam in
219
- // it, and it is off the line grid besides.
220
146
  placement === 'bottom' ? (terminal ? undefined : 'border-t') : 'border-b',
221
147
  className,
222
- )}>
223
- {/* One slot, two meanings: connection trouble wins it, because a session
224
- status shown over a dead socket is a stale reading presented as a live
225
- one. Tapping it opens the session's own facts — where it runs, on what,
226
- with which credentials — which is the question a status prompts. */}
227
- <Slot onClick={onOpenStatus} hint='Session info'>
148
+ )}
149
+ >
150
+ <Slot onClick={onOpenStatus} hint="Session info">
228
151
  {link === 'live' ? (
229
- // `items-baseline` so the badge answers the row with its label's
230
- // baseline rather than its pill's box; the dot and the spinner keep
231
- // their own centring.
232
- <Badge variant={meta.variant} dot={!meta.busy} className='items-baseline'>
233
- {meta.busy ? <Spinner className='size-3 self-center text-current' /> : null}
152
+ <Badge variant={meta.variant} dot={!meta.busy} className="items-baseline">
153
+ {meta.busy ? <Spinner className="size-3 self-center text-current" /> : null}
234
154
  {meta.label}
235
155
  </Badge>
236
156
  ) : (
237
- <Badge
238
- variant={link === 'offline' ? 'danger' : 'warning'}
239
- dot={false}
240
- className='items-baseline'>
157
+ <Badge variant={link === 'offline' ? 'danger' : 'warning'} dot={false} className="items-baseline">
241
158
  {link === 'offline' ? (
242
- <WifiOff className='size-3 self-center text-current' />
159
+ <WifiOff className="size-3 self-center text-current" />
243
160
  ) : (
244
- <RefreshCw className='size-3 animate-spin self-center text-current' />
161
+ <RefreshCw className="size-3 animate-spin self-center text-current" />
245
162
  )}
246
163
  {link === 'offline' ? 'Offline' : 'Reconnecting…'}
247
164
  </Badge>
248
165
  )}
249
166
  </Slot>
250
- {/* Never a 0% meter for an engine that doesn't measure: an absent reading
251
- and a full window are not the same claim. */}
252
167
  {state.capabilities.contextUsage && state.contextUsage ? (
253
- <Slot onClick={onOpenContext} hint='Context breakdown'>
168
+ <Slot onClick={onOpenContext} hint="Context breakdown">
254
169
  <ContextMeter usage={state.contextUsage} />
255
170
  </Slot>
256
171
  ) : null}
257
172
  {session || weekly ? (
258
- <Slot onClick={onOpenUsage} hint='Plan usage'>
259
- <span className='inline-flex items-baseline gap-2'>
260
- {session ? <RateLimitMeter label='Session' info={session} now={now} /> : null}
261
- {weekly ? <RateLimitMeter label='Weekly' info={weekly} now={now} /> : null}
173
+ <Slot onClick={onOpenUsage} hint="Plan usage">
174
+ <span className="inline-flex items-baseline gap-2">
175
+ {session ? <RateLimitMeter label="Session" info={session} now={now} /> : null}
176
+ {weekly ? <RateLimitMeter label="Weekly" info={weekly} now={now} /> : null}
262
177
  </span>
263
178
  </Slot>
264
179
  ) : null}
265
- {controls ? <span className='self-center'>{controls}</span> : null}
266
- <span className='flex-1' />
267
- <span className='font-mono text-label text-fg-3'>{formatCost(state.totalCostUsd)}</span>
268
- {actions ? <span className='self-center'>{actions}</span> : null}
180
+ {controls ? <span className="self-center">{controls}</span> : null}
181
+ <span className="flex-1" />
182
+ <span className="font-mono text-label text-fg-3">{formatCost(state.totalCostUsd)}</span>
183
+ {actions ? <span className="self-center">{actions}</span> : null}
269
184
  </div>
270
185
  )
271
186
  }
@@ -10,25 +10,6 @@ import type { ToolCallItem } from '../terminal/blocks.ts'
10
10
  import { usePulse } from './pulse.tsx'
11
11
  import { useTicker } from '../terminal/items.tsx'
12
12
 
13
- /**
14
- * The one line above a sub-agent takeover: who this is, how it is doing, and the
15
- * way back.
16
- *
17
- * **It claims exactly what the `TaskRow` it was opened from claims** — the same
18
- * `taskBusy` / `taskFailed` / tool count over the same items — and deliberately
19
- * *not* `SubagentInfo.status`. Protocol documents those two as divergent on
20
- * purpose (`index.ts:1476-1487`), but that divergence is transcript-versus-*list*
21
- * and this surface **is** the transcript. The disagreement that must not exist
22
- * here is between a header and the rows directly beneath it.
23
- *
24
- * The rollup is still allowed one job — naming an agent whose `Task` call is not
25
- * in the transcript — because a label is not content, and `SessionInfo.subagents`
26
- * keeps only eight settled records (`SUBAGENT_HISTORY`), so it can never be the
27
- * source of what a frame *shows*.
28
- *
29
- * Both variants, because the panel has two and a strip that only existed in one
30
- * would make the takeover a terminal-theme feature rather than a session feature.
31
- */
32
13
  export function SubagentStrip({
33
14
  task,
34
15
  items,
@@ -38,12 +19,8 @@ export function SubagentStrip({
38
19
  fontSize,
39
20
  lineHeight,
40
21
  }: {
41
- /** The spawning call. Absent when the transcript does not have it — the strip
42
- * still draws, because the way back must exist even when the content cannot. */
43
22
  task: ToolCallItem | undefined
44
- /** The frame's items, for the busy reading and the tool count. */
45
23
  items: readonly TranscriptItem[]
46
- /** Fallback name when there is no task item. */
47
24
  label: string
48
25
  onBack: () => void
49
26
  terminal: boolean
@@ -54,55 +31,22 @@ export function SubagentStrip({
54
31
  const failed = task ? taskFailed(task) : false
55
32
  const pulse = usePulse(busy)
56
33
  const tools = items.reduce((n, item) => n + (item.kind === 'tool_call' ? 1 : 0), 0)
57
- // Ticks only while it works, off the same once-a-second clock the working
58
- // line uses. A settled agent has no end timestamp to measure against, and
59
- // "N tools" is the settled reading anyway — a frozen clock would read as a
60
- // stall. Absent `ts` (an item from before the reducer stamped it) simply
61
- // draws no elapsed rather than counting from the epoch.
62
34
  const startedAt = busy ? task?.ts : undefined
63
35
  const now = useTicker(startedAt !== undefined)
64
36
  const elapsed = startedAt === undefined ? undefined : formatDuration(now - startedAt)
65
37
 
66
38
  const name = task ? taskIdentity(task) : label
67
- /**
68
- * Running or finished, in the theme's own words rather than new ones:
69
- * `taskSummary` already says `working…` and `done` for exactly this state one
70
- * row over, and a header that said "running"/"completed" about the same agent
71
- * would be a second vocabulary for one fact. The trailing ellipsis is the
72
- * theme's in-flight signal; the pulse beside it is the beat.
73
- *
74
- * Unknown when the transcript has no `Task` call to read — better silent than
75
- * confidently wrong about an agent we cannot see.
76
- */
77
39
  const status = !task ? undefined : failed ? 'failed' : busy ? `${pulse} working…` : 'done'
78
- const detail = [tools > 0 ? `${tools} tool${tools === 1 ? '' : 's'}` : undefined, elapsed]
79
- .filter(Boolean)
80
- .join(' · ')
40
+ const detail = [tools > 0 ? `${tools} tool${tools === 1 ? '' : 's'}` : undefined, elapsed].filter(Boolean).join(' · ')
81
41
 
82
42
  if (terminal) {
83
43
  return (
84
- <TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className='shrink-0'>
85
- {/* A row on the grid, not a chrome bar: the takeover is a mode of the
86
- transcript, and a toolbar in some other metric above it would read as
87
- a different application's. The whole line is the target — there is
88
- exactly one thing to do here. */}
89
- <button
90
- type='button'
91
- onClick={onBack}
92
- aria-label='Back to the session'
93
- className='block w-full cursor-pointer text-left'>
94
- {/* The arrow is in the gutter unconditionally. It used to give way to
95
- the pulse while the agent worked, which put the way *out* of the
96
- frame on a timer — the one control here should not come and go.
97
- The beat moved into the status instead. `indent` because this is a
98
- frame around the rows rather than one of them, and a marker flush
99
- against the panel edge reads as a clipped row. */}
100
- <Row glyph='←' glyphTone='dim' indent={1} tone={failed ? 'red' : 'green'}>
44
+ <TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className="shrink-0">
45
+ <button type="button" onClick={onBack} aria-label="Back to the session" className="block w-full cursor-pointer text-left">
46
+ <Row glyph="←" glyphTone="dim" indent={1} tone={failed ? 'red' : 'green'}>
101
47
  {name}
102
- {status ? (
103
- <Ink tone={failed ? 'red' : busy ? 'mark' : 'dim'}> · {status}</Ink>
104
- ) : null}
105
- {detail ? <Ink tone='faint'> · {detail}</Ink> : null}
48
+ {status ? <Ink tone={failed ? 'red' : busy ? 'mark' : 'dim'}> · {status}</Ink> : null}
49
+ {detail ? <Ink tone="faint"> · {detail}</Ink> : null}
106
50
  </Row>
107
51
  </button>
108
52
  </TerminalSurface>
@@ -110,25 +54,16 @@ export function SubagentStrip({
110
54
  }
111
55
 
112
56
  return (
113
- <div className='flex shrink-0 items-center gap-2 border-b border-border px-3 py-1.5'>
114
- <Button variant='ghost' size='sm' onClick={onBack} className='h-6 gap-1 px-1.5'>
115
- <ArrowLeft className='size-3.5' />
57
+ <div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1.5">
58
+ <Button variant="ghost" size="sm" onClick={onBack} className="h-6 gap-1 px-1.5">
59
+ <ArrowLeft className="size-3.5" />
116
60
  Back
117
61
  </Button>
118
- <span
119
- className={cn('min-w-0 flex-1 truncate text-body-sm', failed ? 'text-danger' : 'text-fg-2')}>
120
- {name}
121
- </span>
62
+ <span className={cn('min-w-0 flex-1 truncate text-body-sm', failed ? 'text-danger' : 'text-fg-2')}>{name}</span>
122
63
  {status ? (
123
- <span
124
- className={cn(
125
- 'shrink-0 text-label',
126
- failed ? 'text-danger' : busy ? 'text-accent' : 'text-fg-3',
127
- )}>
128
- {status}
129
- </span>
64
+ <span className={cn('shrink-0 text-label', failed ? 'text-danger' : busy ? 'text-accent' : 'text-fg-3')}>{status}</span>
130
65
  ) : null}
131
- {detail ? <span className='shrink-0 text-label text-fg-4'>{detail}</span> : null}
66
+ {detail ? <span className="shrink-0 text-label text-fg-4">{detail}</span> : null}
132
67
  </div>
133
68
  )
134
69
  }