@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
@@ -9,24 +9,18 @@ import { toolInputPreview } from '../../lib/format.ts'
9
9
  import { toolIcon } from '../../lib/tool-icon.ts'
10
10
  import { useToolResultFetcher } from './tool-result-fetch.tsx'
11
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
12
  import { IMAGE_UNAVAILABLE, imagePlaceholder } from '../terminal/image-box.ts'
18
13
 
19
14
  export type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
20
15
 
21
16
  const RESULT_PREVIEW_CHARS = 2000
22
17
 
23
- /** Codex's built-in image tools name a host path rather than sending bytes —
24
- * an event log carries references, never base64. Rendering the picture means
25
- * reading that path back through the gateway's host-file route. */
26
18
  const IMAGE_TOOLS = new Set(['CodexImageGeneration', 'CodexImageView'])
27
19
 
28
- const imagePathOf = (item: ToolCallItem): string | undefined => {
29
- if (!IMAGE_TOOLS.has(item.name)) return undefined
20
+ function imagePathOf(item: ToolCallItem): string | undefined {
21
+ if (!IMAGE_TOOLS.has(item.name)) {
22
+ return undefined
23
+ }
30
24
  const input = item.input as { savedPath?: unknown; path?: unknown } | null
31
25
  const path = input?.savedPath ?? input?.path
32
26
  return typeof path === 'string' ? path : undefined
@@ -34,20 +28,10 @@ const imagePathOf = (item: ToolCallItem): string | undefined => {
34
28
 
35
29
  export interface ToolCallCardProps {
36
30
  item: ToolCallItem
37
- /**
38
- * Reads a host file as a data URL, for tools whose output is a picture on the
39
- * host. Resolves `undefined` when the gateway won't serve that path — a
40
- * generated image saved outside the allowed roots (codex's default
41
- * `$CODEX_HOME/generated_images/`) is one, and the card then names the path
42
- * instead of showing it.
43
- */
44
31
  hostImage?: (path: string) => Promise<string | undefined>
45
32
  className?: string
46
33
  }
47
34
 
48
- /** Badge per execution state. `pending`/`deferred` mean the work is happening
49
- * somewhere else (this tab's sandbox, a queue) — distinct from the model still
50
- * running the call itself. */
51
35
  const STATE_BADGE = {
52
36
  running: { label: 'Running', variant: 'info', busy: true },
53
37
  pending: { label: 'Executing', variant: 'info', busy: true },
@@ -56,30 +40,60 @@ const STATE_BADGE = {
56
40
  failed: { label: 'Error', variant: 'danger', busy: false },
57
41
  } as const
58
42
 
59
- /**
60
- * The language to highlight a payload as.
61
- *
62
- * Parameters are always JSON. A result is whatever file the call was about — so
63
- * the extension in `file_path`/`path` is the best evidence there is, and a call
64
- * that names no file gets no guess (plain text renders fine and a wrong grammar
65
- * is worse than none).
66
- */
67
43
  const EXTENSION_LANGUAGE: Record<string, string> = {
68
- ts: 'ts', tsx: 'tsx', js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js',
69
- json: 'json', jsonc: 'json', md: 'md', mdx: 'md', css: 'css', scss: 'scss',
70
- html: 'html', xml: 'xml', yml: 'yaml', yaml: 'yaml', toml: 'toml',
71
- py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java', kt: 'kotlin',
72
- swift: 'swift', c: 'c', h: 'c', cpp: 'cpp', hpp: 'cpp', cs: 'csharp',
73
- php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'fish', sql: 'sql',
74
- graphql: 'graphql', dockerfile: 'dockerfile', diff: 'diff', patch: 'diff',
44
+ ts: 'ts',
45
+ tsx: 'tsx',
46
+ js: 'js',
47
+ jsx: 'jsx',
48
+ mjs: 'js',
49
+ cjs: 'js',
50
+ json: 'json',
51
+ jsonc: 'json',
52
+ md: 'md',
53
+ mdx: 'md',
54
+ css: 'css',
55
+ scss: 'scss',
56
+ html: 'html',
57
+ xml: 'xml',
58
+ yml: 'yaml',
59
+ yaml: 'yaml',
60
+ toml: 'toml',
61
+ py: 'python',
62
+ rb: 'ruby',
63
+ go: 'go',
64
+ rs: 'rust',
65
+ java: 'java',
66
+ kt: 'kotlin',
67
+ swift: 'swift',
68
+ c: 'c',
69
+ h: 'c',
70
+ cpp: 'cpp',
71
+ hpp: 'cpp',
72
+ cs: 'csharp',
73
+ php: 'php',
74
+ sh: 'bash',
75
+ bash: 'bash',
76
+ zsh: 'bash',
77
+ fish: 'fish',
78
+ sql: 'sql',
79
+ graphql: 'graphql',
80
+ dockerfile: 'dockerfile',
81
+ diff: 'diff',
82
+ patch: 'diff',
75
83
  }
76
84
 
77
85
  function resultLanguage(item: ToolCallItem): string | undefined {
78
- if (item.name === 'Bash' || item.name === 'CodexCommand') return 'bash'
79
- if (item.name === 'CodexFileChange') return 'diff'
86
+ if (item.name === 'Bash' || item.name === 'CodexCommand') {
87
+ return 'bash'
88
+ }
89
+ if (item.name === 'CodexFileChange') {
90
+ return 'diff'
91
+ }
80
92
  const input = item.input as { file_path?: unknown; path?: unknown } | null
81
93
  const path = input?.file_path ?? input?.path
82
- if (typeof path !== 'string') return undefined
94
+ if (typeof path !== 'string') {
95
+ return undefined
96
+ }
83
97
  const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
84
98
  return EXTENSION_LANGUAGE[extension]
85
99
  }
@@ -100,45 +114,36 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
100
114
  const resultText = item.result?.text ?? ''
101
115
  const clipped = !fullResult && resultText.length > RESULT_PREVIEW_CHARS
102
116
  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
117
  const headOnly = item.result?.truncated === true
108
118
  const totalChars = item.result?.totalChars ?? resultText.length
109
119
 
110
120
  const details = open ? (
111
- <div className='flex flex-col gap-2 border-t border-border p-2.5'>
112
- <PlainPayload
113
- code={JSON.stringify(item.input, null, 2)}
114
- language='json'
115
- label='Parameters'
116
- />
117
- {item.logs?.length ? (
118
- <PlainPayload code={item.logs.join('\n')} label='Logs'/>
119
- ) : null}
121
+ <div className="flex flex-col gap-2 border-t border-border p-2.5">
122
+ <PlainPayload code={JSON.stringify(item.input, null, 2)} language="json" label="Parameters" />
123
+ {item.logs?.length ? <PlainPayload code={item.logs.join('\n')} label="Logs" /> : null}
120
124
  {item.result !== undefined ? (
121
125
  <div>
122
126
  <PlainPayload
123
127
  code={shownResult || '(empty result)'}
124
128
  language={resultLanguage(item)}
125
129
  label={isError ? 'Error' : 'Result'}
126
- className={cn(isError && 'border-danger/40 [&_pre]:text-danger')}
130
+ className={cn(isError && 'border-danger/40 [&_pre]:text-danger')}
127
131
  />
128
132
  {fetching ? (
129
- <p className='mt-1 text-label text-fg-3'>
130
- Fetching {totalChars.toLocaleString()} chars…
131
- </p>
133
+ <p className="mt-1 text-label text-fg-3">Fetching {totalChars.toLocaleString()} chars…</p>
132
134
  ) : clipped || headOnly ? (
133
135
  <button
134
- type='button'
135
- className='mt-1 text-label text-fg-3 underline-offset-2 hover:underline'
136
+ type="button"
137
+ className="mt-1 text-label text-fg-3 underline-offset-2 hover:underline"
136
138
  onClick={() => {
137
139
  setFullResult(true)
138
- if (!headOnly) return
140
+ if (!headOnly) {
141
+ return
142
+ }
139
143
  setFetching(true)
140
144
  void fetchResult(item.id).finally(() => setFetching(false))
141
- }}>
145
+ }}
146
+ >
142
147
  Show all {totalChars.toLocaleString()} chars
143
148
  </button>
144
149
  ) : null}
@@ -147,16 +152,10 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
147
152
  </div>
148
153
  ) : null
149
154
 
150
- // The picture is the point of the call — shown without expanding, the way the
151
- // tool's own output would be if the engine had sent bytes.
152
155
  const image = imagePath && hostImage ? <HostImage path={imagePath} load={hostImage} /> : null
153
156
 
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
157
  const resultImages = item.result?.images?.length ? (
159
- <div className='flex flex-col gap-2 border-t border-border p-2.5'>
158
+ <div className="flex flex-col gap-2 border-t border-border p-2.5">
160
159
  {item.result.images.map((ref) => (
161
160
  <ResultImage key={ref.partIndex} toolUseId={item.id} image={ref} />
162
161
  ))}
@@ -165,34 +164,32 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
165
164
 
166
165
  return (
167
166
  <div
168
- data-slot='tool-call'
167
+ data-slot="tool-call"
169
168
  data-state={status}
170
- className={cn('w-full overflow-hidden rounded-lg border border-border bg-surface', className)}>
169
+ className={cn('w-full overflow-hidden rounded-lg border border-border bg-surface', className)}
170
+ >
171
171
  <button
172
- type='button'
172
+ type="button"
173
173
  onClick={() => setOpen((v) => !v)}
174
174
  className={cn(
175
175
  'flex w-full items-center gap-2 px-3 py-2 text-left transition-colors outline-none',
176
176
  'hover:bg-surface-hover focus-visible:bg-surface-hover',
177
- )}>
178
- <Icon className='size-3.5 shrink-0 text-fg-3' />
179
- <span className='shrink-0 font-mono text-body-sm font-medium text-fg-1'>{item.name}</span>
177
+ )}
178
+ >
179
+ <Icon className="size-3.5 shrink-0 text-fg-3" />
180
+ <span className="shrink-0 font-mono text-body-sm font-medium text-fg-1">{item.name}</span>
180
181
  {item.backend && item.backend !== 'server' ? (
181
- <Badge variant='neutral' className='shrink-0'>
182
+ <Badge variant="neutral" className="shrink-0">
182
183
  {item.backend}
183
184
  </Badge>
184
185
  ) : null}
185
- <span className='min-w-0 flex-1 truncate font-mono text-label text-fg-4'>
186
- {toolInputPreview(item.input)}
187
- </span>
188
- <Badge variant={badge.variant} dot={!badge.busy} className='shrink-0 gap-1'>
189
- {badge.busy ? <Spinner className='size-3 text-current' /> : null}
190
- {status === 'deferred' ? <Clock className='size-3 text-current' /> : null}
186
+ <span className="min-w-0 flex-1 truncate font-mono text-label text-fg-4">{toolInputPreview(item.input)}</span>
187
+ <Badge variant={badge.variant} dot={!badge.busy} className="shrink-0 gap-1">
188
+ {badge.busy ? <Spinner className="size-3 text-current" /> : null}
189
+ {status === 'deferred' ? <Clock className="size-3 text-current" /> : null}
191
190
  {badge.label}
192
191
  </Badge>
193
- <ChevronDown
194
- className={cn('size-3.5 shrink-0 text-fg-4 transition-transform', open && 'rotate-180')}
195
- />
192
+ <ChevronDown className={cn('size-3.5 shrink-0 text-fg-4 transition-transform', open && 'rotate-180')} />
196
193
  </button>
197
194
  {image}
198
195
  {resultImages}
@@ -201,88 +198,50 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
201
198
  )
202
199
  }
203
200
 
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
- }: {
211
- code: string
212
- label: string
213
- language?: string
214
- className?: string
215
- }) {
216
- return <CodeBlock code={code} label={label} variant='panel' className={className} />
201
+ function PlainPayload({ code, label, className }: { code: string; label: string; language?: string; className?: string }) {
202
+ return <CodeBlock code={code} label={label} variant="panel" className={className} />
217
203
  }
218
204
 
219
- /** One image part of a tool result, as the reducer holds it. */
220
205
  type ToolResultImage = NonNullable<NonNullable<ToolCallItem['result']>['images']>[number]
221
206
 
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
207
  function ResultImage({ toolUseId, image }: { toolUseId: string; image: ToolResultImage }) {
234
208
  const { src, failed } = useToolResultImageSrc({ toolUseId, ...image })
235
209
  return (
236
- <div className='flex h-60 items-start overflow-hidden rounded-md border border-border bg-surface-hover'>
210
+ <div className="flex h-60 items-start overflow-hidden rounded-md border border-border bg-surface-hover">
237
211
  {src ? (
238
- <img src={src} alt={imagePlaceholder(image)} className='h-full max-w-full object-contain' />
212
+ <img src={src} alt={imagePlaceholder(image)} className="h-full max-w-full object-contain" />
239
213
  ) : (
240
- <span className='p-2 text-label text-fg-4'>
241
- {failed ? IMAGE_UNAVAILABLE : imagePlaceholder(image)}
242
- </span>
214
+ <span className="p-2 text-label text-fg-4">{failed ? IMAGE_UNAVAILABLE : imagePlaceholder(image)}</span>
243
215
  )}
244
216
  </div>
245
217
  )
246
218
  }
247
219
 
248
- /**
249
- * A picture that lives on the host, fetched through the gateway's host-file
250
- * route and shown inline.
251
- *
252
- * Silent on failure by design: a path outside the server's allowed roots is the
253
- * *expected* case for codex's default save location, and the card's result text
254
- * already names where the file went. An error banner over that would be noise
255
- * about a thing the operator can fix in one line of config.
256
- */
257
- function HostImage({
258
- path,
259
- load,
260
- }: {
261
- path: string
262
- load: (path: string) => Promise<string | undefined>
263
- }) {
220
+ function HostImage({ path, load }: { path: string; load: (path: string) => Promise<string | undefined> }) {
264
221
  const [src, setSrc] = useState<string | undefined>()
265
222
  useEffect(() => {
266
223
  let cancelled = false
267
224
  setSrc(undefined)
268
225
  load(path)
269
226
  .then((url) => {
270
- if (!cancelled) setSrc(url)
271
- })
272
- .catch(() => {
273
- // Not readable from here — the path in the result is the answer.
227
+ if (!cancelled) {
228
+ setSrc(url)
229
+ }
274
230
  })
231
+ .catch(() => {})
275
232
  return () => {
276
233
  cancelled = true
277
234
  }
278
235
  }, [path, load])
279
- if (!src) return null
236
+ if (!src) {
237
+ return null
238
+ }
280
239
  return (
281
- <div className='border-t border-border p-2.5'>
240
+ <div className="border-t border-border p-2.5">
282
241
  <img
283
242
  src={src}
284
243
  alt={path.split('/').pop() ?? 'Generated image'}
285
- className='max-h-96 w-auto max-w-full rounded-md border border-border'
244
+ className="max-h-96 w-auto max-w-full rounded-md border border-border"
286
245
  />
287
246
  </div>
288
247
  )