@workerdeck/ui 0.15.0 → 0.17.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 (71) hide show
  1. package/README.md +60 -0
  2. package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-CnNYEX80.d.mts} +170 -21
  3. package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DMPhsNlW.mjs} +4759 -1483
  4. package/build/SessionPanel-DMPhsNlW.mjs.map +1 -0
  5. package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
  6. package/build/format.d.mts +39 -4
  7. package/build/format.mjs +2 -118
  8. package/build/index.d.mts +493 -45
  9. package/build/index.mjs +343 -17
  10. package/build/index.mjs.map +1 -1
  11. package/build/status-Ydzi7n6j.mjs +143 -0
  12. package/build/status-Ydzi7n6j.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -1
  14. package/build/workspace.mjs +108 -5
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +16 -7
  17. package/src/components/agent/Composer.tsx +189 -89
  18. package/src/components/agent/Conversation.tsx +12 -12
  19. package/src/components/agent/FileCard.tsx +0 -26
  20. package/src/components/agent/FileTree.tsx +9 -8
  21. package/src/components/agent/Loader.tsx +22 -72
  22. package/src/components/agent/Message.tsx +11 -46
  23. package/src/components/agent/PermissionPrompt.tsx +0 -92
  24. package/src/components/agent/ProjectIcon.tsx +119 -0
  25. package/src/components/agent/QuestionPrompt.tsx +0 -122
  26. package/src/components/agent/Reasoning.tsx +5 -19
  27. package/src/components/agent/Response.tsx +1 -132
  28. package/src/components/agent/SessionBrowser.tsx +84 -3
  29. package/src/components/agent/SessionPanel.tsx +249 -28
  30. package/src/components/agent/SessionWorkspace.tsx +29 -0
  31. package/src/components/agent/StatusBar.tsx +20 -4
  32. package/src/components/agent/ToolCallCard.tsx +85 -112
  33. package/src/components/agent/Transcript.tsx +780 -203
  34. package/src/components/agent/UsageDialog.tsx +20 -106
  35. package/src/components/agent/UsageMeters.tsx +133 -0
  36. package/src/components/agent/pulse.tsx +3 -2
  37. package/src/components/agent/tool-result-fetch.tsx +36 -0
  38. package/src/components/agent/tool-result-image.tsx +209 -0
  39. package/src/components/agent/transcript-rows.ts +173 -0
  40. package/src/components/agent/transcript-variant.tsx +29 -51
  41. package/src/components/agent/use-height-epoch.ts +60 -0
  42. package/src/components/agent/use-path-links.ts +147 -0
  43. package/src/components/agent/use-transcript-jumps.ts +190 -0
  44. package/src/components/prompt-area/cursor-helpers.ts +65 -0
  45. package/src/components/prompt-area/use-prompt-area.ts +16 -10
  46. package/src/components/terminal/PermissionPrompt.tsx +119 -0
  47. package/src/components/terminal/QuestionPrompt.tsx +322 -0
  48. package/src/components/terminal/StatusLine.tsx +159 -0
  49. package/src/components/terminal/TerminalTranscript.tsx +225 -0
  50. package/src/components/terminal/affordances.tsx +118 -0
  51. package/src/components/terminal/blocks.ts +232 -0
  52. package/src/components/terminal/diff.tsx +130 -0
  53. package/src/components/terminal/height.ts +770 -0
  54. package/src/components/terminal/image-box.ts +53 -0
  55. package/src/components/terminal/items.tsx +486 -0
  56. package/src/components/terminal/markdown.tsx +191 -0
  57. package/src/components/terminal/press.tsx +120 -0
  58. package/src/components/terminal/prompt.tsx +343 -0
  59. package/src/components/terminal/result-preview.ts +86 -0
  60. package/src/components/terminal/row.tsx +132 -0
  61. package/src/components/terminal/scrubber.tsx +784 -0
  62. package/src/components/terminal/surface.tsx +80 -0
  63. package/src/components/terminal/tool-run.ts +224 -0
  64. package/src/index.ts +36 -1
  65. package/src/lib/status.ts +59 -3
  66. package/src/lib/tool-icon.ts +14 -0
  67. package/src/styles/terminal.css +1081 -0
  68. package/src/styles/theme.css +41 -0
  69. package/build/SessionPanel-DI1NO4l8.mjs.map +0 -1
  70. package/build/format.mjs.map +0 -1
  71. package/src/components/agent/line-prompt.tsx +0 -249
@@ -6,10 +6,15 @@ import { CodeBlock } from '../ui/CodeBlock.tsx'
6
6
  import { Spinner } from '../ui/Spinner.tsx'
7
7
  import { cn } from '../../lib/utils.ts'
8
8
  import { toolInputPreview } from '../../lib/format.ts'
9
- import { isMutatingTool, toolIcon } from '../../lib/tool-icon.ts'
10
- import { LINE_INDENT, LinePayload } from './line-prompt.tsx'
11
- import { usePulse } from './pulse.tsx'
12
- import { LineGlyph, useLines } from './transcript-variant.tsx'
9
+ import { toolIcon } from '../../lib/tool-icon.ts'
10
+ import { useToolResultFetcher } from './tool-result-fetch.tsx'
11
+ import { useToolResultImageSrc } from './tool-result-image.tsx'
12
+ // From the terminal folder, which is where the box's one spelling lives: the
13
+ // height calculator is the reason it is a constant at all, and a second copy
14
+ // here would be a second thing to keep in step for no gain. Cards has no
15
+ // calculator — this frame is about not reflowing a virtualized list when the
16
+ // bytes land, which is a claim both themes make.
17
+ import { IMAGE_UNAVAILABLE, imagePlaceholder } from '../terminal/image-box.ts'
13
18
 
14
19
  export type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
15
20
 
@@ -51,15 +56,6 @@ const STATE_BADGE = {
51
56
  failed: { label: 'Error', variant: 'danger', busy: false },
52
57
  } as const
53
58
 
54
- /** The gutter dot's colour in `lines`: the state, said without a badge. */
55
- const STATE_GLYPH = {
56
- running: 'text-info',
57
- pending: 'text-info',
58
- deferred: 'text-accent',
59
- settled: 'text-fg-4',
60
- failed: 'text-danger',
61
- } as const
62
-
63
59
  /**
64
60
  * The language to highlight a payload as.
65
61
  *
@@ -91,54 +87,59 @@ function resultLanguage(item: ToolCallItem): string | undefined {
91
87
  type Status = keyof typeof STATE_BADGE
92
88
 
93
89
  export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps) {
94
- const lines = useLines()
95
90
  const [open, setOpen] = useState(false)
96
91
  const [fullResult, setFullResult] = useState(false)
92
+ const [fetching, setFetching] = useState(false)
93
+ const fetchResult = useToolResultFetcher()
97
94
  const imagePath = imagePathOf(item)
98
95
  const status: Status = item.status ?? (item.result === undefined ? 'running' : 'settled')
99
96
  const badge = STATE_BADGE[status]
100
97
  const isError = status === 'failed' || item.result?.isError === true
101
- // Ticks only while this row is actually running, and only in the variant with a
102
- // gutter to pulse in — an idle transcript of a hundred settled tools starts no
103
- // timers at all.
104
- const pulse = usePulse(lines && badge.busy)
105
98
  const Icon = toolIcon(item.name)
106
99
 
107
100
  const resultText = item.result?.text ?? ''
108
- const truncated = !fullResult && resultText.length > RESULT_PREVIEW_CHARS
109
- const shownResult = truncated ? resultText.slice(0, RESULT_PREVIEW_CHARS) : resultText
101
+ const clipped = !fullResult && resultText.length > RESULT_PREVIEW_CHARS
102
+ const shownResult = clipped ? resultText.slice(0, RESULT_PREVIEW_CHARS) : resultText
103
+ // The replay sent a head (see protocol's `ToolResultBlock.truncated`): what is
104
+ // on screen is not merely clipped, it is all this client was given. The press
105
+ // therefore fetches rather than only lifting the clip, and the count has to be
106
+ // the real one — `resultText.length` here is the head's.
107
+ const headOnly = item.result?.truncated === true
108
+ const totalChars = item.result?.totalChars ?? resultText.length
110
109
 
111
- // In `lines` the payloads are dim label + highlighted band, not framed cards:
112
- // a tool call is already one row, and boxing what it expands into puts back
113
- // the chrome the variant exists to remove.
114
- const blockVariant = lines ? 'plain' : 'panel'
115
- const Payload = lines ? HighlightedPayload : PlainPayload
116
110
  const details = open ? (
117
- <div className={cn('flex flex-col', lines ? 'gap-1 py-1' : 'gap-2 border-t border-border p-2.5')}>
118
- <Payload
111
+ <div className='flex flex-col gap-2 border-t border-border p-2.5'>
112
+ <PlainPayload
119
113
  code={JSON.stringify(item.input, null, 2)}
120
114
  language='json'
121
115
  label='Parameters'
122
- variant={blockVariant}
123
116
  />
124
117
  {item.logs?.length ? (
125
- <Payload code={item.logs.join('\n')} label='Logs' variant={blockVariant} />
118
+ <PlainPayload code={item.logs.join('\n')} label='Logs'/>
126
119
  ) : null}
127
120
  {item.result !== undefined ? (
128
121
  <div>
129
- <Payload
122
+ <PlainPayload
130
123
  code={shownResult || '(empty result)'}
131
124
  language={resultLanguage(item)}
132
125
  label={isError ? 'Error' : 'Result'}
133
- variant={blockVariant}
134
- className={cn(isError && (lines ? '[&_pre]:text-danger' : 'border-danger/40 [&_pre]:text-danger'))}
126
+ className={cn(isError && 'border-danger/40 [&_pre]:text-danger')}
135
127
  />
136
- {truncated ? (
128
+ {fetching ? (
129
+ <p className='mt-1 text-label text-fg-3'>
130
+ Fetching {totalChars.toLocaleString()} chars…
131
+ </p>
132
+ ) : clipped || headOnly ? (
137
133
  <button
138
134
  type='button'
139
135
  className='mt-1 text-label text-fg-3 underline-offset-2 hover:underline'
140
- onClick={() => setFullResult(true)}>
141
- Show all {resultText.length.toLocaleString()} chars
136
+ onClick={() => {
137
+ setFullResult(true)
138
+ if (!headOnly) return
139
+ setFetching(true)
140
+ void fetchResult(item.id).finally(() => setFetching(false))
141
+ }}>
142
+ Show all {totalChars.toLocaleString()} chars
142
143
  </button>
143
144
  ) : null}
144
145
  </div>
@@ -148,63 +149,19 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
148
149
 
149
150
  // The picture is the point of the call — shown without expanding, the way the
150
151
  // tool's own output would be if the engine had sent bytes.
151
- const image = imagePath && hostImage ? <HostImage path={imagePath} load={hostImage} lines={lines} /> : null
152
+ const image = imagePath && hostImage ? <HostImage path={imagePath} load={hostImage} /> : null
152
153
 
153
- if (lines) {
154
- return (
155
- <div data-slot='tool-call' data-state={status} className={cn('w-full', className)}>
156
- <button
157
- type='button'
158
- onClick={() => setOpen((v) => !v)}
159
- className='flex w-full items-baseline gap-2 text-left outline-none'>
160
- <LineGlyph
161
- className={
162
- // A settled write is green: skimming a run, "what did it change"
163
- // is the question you come back to, and it is the one you might
164
- // need to undo. Every other state keeps its own colour — a failed
165
- // write is a failure first.
166
- status === 'settled' && !isError && isMutatingTool(item.name)
167
- ? 'text-success'
168
- : STATE_GLYPH[status]
169
- }>
170
- {/* Running: the mark's own pulse, so a working tool row and the
171
- transcript's working line beat together. Settled: a plain dot,
172
- which reads as "done" precisely by not moving. */}
173
- {badge.busy ? pulse : '●'}
174
- </LineGlyph>
175
- <span className='min-w-0 flex-1 truncate text-body-sm leading-5 text-fg-3'>
176
- <span className='font-medium text-fg-1'>{item.name}</span>
177
- <span className='text-fg-4'>({toolInputPreview(item.input)})</span>
178
- </span>
179
- {item.backend && item.backend !== 'server' ? (
180
- <span className='shrink-0 text-label text-fg-4'>{item.backend}</span>
181
- ) : null}
182
- {/* No Spinner here: the gutter glyph animates now, and two spinners on
183
- one row is one too many. `cards` keeps its own — it has no gutter. */}
184
- {status === 'deferred' ? <Clock className='size-3 shrink-0 self-center text-fg-4' /> : null}
185
- {isError && !badge.busy ? (
186
- <span className='shrink-0 text-label text-danger'>error</span>
187
- ) : null}
188
- </button>
189
- {/* Collapsed, the first line of the output is the whole story most of
190
- the time — a summary line costs one row and saves an expand. */}
191
- {!open && resultText ? (
192
- <div className='flex items-baseline gap-2'>
193
- <LineGlyph className='text-fg-4'>⎿</LineGlyph>
194
- <span
195
- className={cn(
196
- 'min-w-0 flex-1 truncate text-label leading-5',
197
- isError ? 'text-danger' : 'text-fg-4',
198
- )}>
199
- {resultSummary(resultText)}
200
- </span>
201
- </div>
202
- ) : null}
203
- {image}
204
- {details ? <div className={LINE_INDENT}>{details}</div> : null}
205
- </div>
206
- )
207
- }
154
+ // Beside the host-path picture above, never instead of it: that one is a file
155
+ // the engine wrote, read back through `/produced` or `/fs`; these are image
156
+ // parts of the result itself, addressed by `(seq, toolUseId, part)`. Different
157
+ // store, different route, and a call can plausibly have both.
158
+ const resultImages = item.result?.images?.length ? (
159
+ <div className='flex flex-col gap-2 border-t border-border p-2.5'>
160
+ {item.result.images.map((ref) => (
161
+ <ResultImage key={ref.partIndex} toolUseId={item.id} image={ref} />
162
+ ))}
163
+ </div>
164
+ ) : null
208
165
 
209
166
  return (
210
167
  <div
@@ -238,36 +195,54 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
238
195
  />
239
196
  </button>
240
197
  {image}
198
+ {resultImages}
241
199
  {details}
242
200
  </div>
243
201
  )
244
202
  }
245
203
 
246
- /** The first line worth showing of a tool result, for the collapsed summary. */
247
- function resultSummary(text: string): string {
248
- const first = text.split('\n').find((line) => line.trim() !== '') ?? ''
249
- const rest = text.trimEnd().split('\n').length - 1
250
- const trimmed = first.trim()
251
- return rest > 0 ? `${trimmed} (+${rest} lines)` : trimmed
252
- }
253
-
254
- type PayloadProps = {
204
+ /** The framed card the `cards` transcript expands into. Unhighlighted by
205
+ * design: it is structured data in a panel, not a file. */
206
+ function PlainPayload({
207
+ code,
208
+ label,
209
+ className,
210
+ }: {
255
211
  code: string
256
212
  label: string
257
213
  language?: string
258
- variant: 'panel' | 'plain'
259
214
  className?: string
215
+ }) {
216
+ return <CodeBlock code={code} label={label} variant='panel' className={className} />
260
217
  }
261
218
 
262
- /** The framed card, for the `cards` transcript. Unhighlighted by design: it is
263
- * structured data in a panel, not a file. */
264
- function PlainPayload({ code, label, variant, className }: PayloadProps) {
265
- return <CodeBlock code={code} label={label} variant={variant} className={className} />
266
- }
219
+ /** One image part of a tool result, as the reducer holds it. */
220
+ type ToolResultImage = NonNullable<NonNullable<ToolCallItem['result']>['images']>[number]
267
221
 
268
- /** The terminal payload — shared with the line-shaped prompts. */
269
- function HighlightedPayload({ code, label, language, className }: PayloadProps) {
270
- return <LinePayload code={code} label={label} language={language} className={className} />
222
+ /**
223
+ * An image part of the result, fetched by reference.
224
+ *
225
+ * The frame is a **fixed height in all three states** — placeholder, picture,
226
+ * failure — which is the one rule this shares with the terminal theme and the
227
+ * only reason it is worth a component: the transcript is virtualized in both,
228
+ * and a box that appears when the bytes land shoves every row below it down
229
+ * while the reader is mid-sentence. Unlike `HostImage`, a failure here is *said*
230
+ * rather than swallowed: there is no host path in the result text to fall back
231
+ * on, so silence would be a blank frame with no account of itself.
232
+ */
233
+ function ResultImage({ toolUseId, image }: { toolUseId: string; image: ToolResultImage }) {
234
+ const { src, failed } = useToolResultImageSrc({ toolUseId, ...image })
235
+ return (
236
+ <div className='flex h-60 items-start overflow-hidden rounded-md border border-border bg-surface-hover'>
237
+ {src ? (
238
+ <img src={src} alt={imagePlaceholder(image)} className='h-full max-w-full object-contain' />
239
+ ) : (
240
+ <span className='p-2 text-label text-fg-4'>
241
+ {failed ? IMAGE_UNAVAILABLE : imagePlaceholder(image)}
242
+ </span>
243
+ )}
244
+ </div>
245
+ )
271
246
  }
272
247
 
273
248
  /**
@@ -282,11 +257,9 @@ function HighlightedPayload({ code, label, language, className }: PayloadProps)
282
257
  function HostImage({
283
258
  path,
284
259
  load,
285
- lines,
286
260
  }: {
287
261
  path: string
288
262
  load: (path: string) => Promise<string | undefined>
289
- lines?: boolean
290
263
  }) {
291
264
  const [src, setSrc] = useState<string | undefined>()
292
265
  useEffect(() => {
@@ -305,7 +278,7 @@ function HostImage({
305
278
  }, [path, load])
306
279
  if (!src) return null
307
280
  return (
308
- <div className={cn(lines ? cn('py-1', LINE_INDENT) : 'border-t border-border p-2.5')}>
281
+ <div className='border-t border-border p-2.5'>
309
282
  <img
310
283
  src={src}
311
284
  alt={path.split('/').pop() ?? 'Generated image'}