@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.
- package/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -23
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +5 -126
- package/build/workspace.mjs +11 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- 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
|
-
|
|
29
|
-
if (!IMAGE_TOOLS.has(item.name))
|
|
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',
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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')
|
|
79
|
-
|
|
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')
|
|
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=
|
|
112
|
-
<PlainPayload
|
|
113
|
-
|
|
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
|
-
|
|
130
|
+
className={cn(isError && 'border-danger/40 [&_pre]:text-danger')}
|
|
127
131
|
/>
|
|
128
132
|
{fetching ? (
|
|
129
|
-
<p className=
|
|
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=
|
|
135
|
-
className=
|
|
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)
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
179
|
-
<
|
|
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=
|
|
182
|
+
<Badge variant="neutral" className="shrink-0">
|
|
182
183
|
{item.backend}
|
|
183
184
|
</Badge>
|
|
184
185
|
) : null}
|
|
185
|
-
<span className=
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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=
|
|
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=
|
|
212
|
+
<img src={src} alt={imagePlaceholder(image)} className="h-full max-w-full object-contain" />
|
|
239
213
|
) : (
|
|
240
|
-
<span className=
|
|
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)
|
|
271
|
-
|
|
272
|
-
|
|
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)
|
|
236
|
+
if (!src) {
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
280
239
|
return (
|
|
281
|
-
<div className=
|
|
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=
|
|
244
|
+
className="max-h-96 w-auto max-w-full rounded-md border border-border"
|
|
286
245
|
/>
|
|
287
246
|
</div>
|
|
288
247
|
)
|