@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.
- package/README.md +60 -0
- package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-CnNYEX80.d.mts} +170 -21
- package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DMPhsNlW.mjs} +4759 -1483
- package/build/SessionPanel-DMPhsNlW.mjs.map +1 -0
- package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
- package/build/format.d.mts +39 -4
- package/build/format.mjs +2 -118
- package/build/index.d.mts +493 -45
- package/build/index.mjs +343 -17
- package/build/index.mjs.map +1 -1
- package/build/status-Ydzi7n6j.mjs +143 -0
- package/build/status-Ydzi7n6j.mjs.map +1 -0
- package/build/workspace.d.mts +9 -1
- package/build/workspace.mjs +108 -5
- package/build/workspace.mjs.map +1 -1
- package/package.json +16 -7
- package/src/components/agent/Composer.tsx +189 -89
- package/src/components/agent/Conversation.tsx +12 -12
- package/src/components/agent/FileCard.tsx +0 -26
- package/src/components/agent/FileTree.tsx +9 -8
- package/src/components/agent/Loader.tsx +22 -72
- package/src/components/agent/Message.tsx +11 -46
- package/src/components/agent/PermissionPrompt.tsx +0 -92
- package/src/components/agent/ProjectIcon.tsx +119 -0
- package/src/components/agent/QuestionPrompt.tsx +0 -122
- package/src/components/agent/Reasoning.tsx +5 -19
- package/src/components/agent/Response.tsx +1 -132
- package/src/components/agent/SessionBrowser.tsx +84 -3
- package/src/components/agent/SessionPanel.tsx +249 -28
- package/src/components/agent/SessionWorkspace.tsx +29 -0
- package/src/components/agent/StatusBar.tsx +20 -4
- package/src/components/agent/ToolCallCard.tsx +85 -112
- package/src/components/agent/Transcript.tsx +780 -203
- package/src/components/agent/UsageDialog.tsx +20 -106
- package/src/components/agent/UsageMeters.tsx +133 -0
- package/src/components/agent/pulse.tsx +3 -2
- package/src/components/agent/tool-result-fetch.tsx +36 -0
- package/src/components/agent/tool-result-image.tsx +209 -0
- package/src/components/agent/transcript-rows.ts +173 -0
- package/src/components/agent/transcript-variant.tsx +29 -51
- package/src/components/agent/use-height-epoch.ts +60 -0
- package/src/components/agent/use-path-links.ts +147 -0
- package/src/components/agent/use-transcript-jumps.ts +190 -0
- package/src/components/prompt-area/cursor-helpers.ts +65 -0
- package/src/components/prompt-area/use-prompt-area.ts +16 -10
- package/src/components/terminal/PermissionPrompt.tsx +119 -0
- package/src/components/terminal/QuestionPrompt.tsx +322 -0
- package/src/components/terminal/StatusLine.tsx +159 -0
- package/src/components/terminal/TerminalTranscript.tsx +225 -0
- package/src/components/terminal/affordances.tsx +118 -0
- package/src/components/terminal/blocks.ts +232 -0
- package/src/components/terminal/diff.tsx +130 -0
- package/src/components/terminal/height.ts +770 -0
- package/src/components/terminal/image-box.ts +53 -0
- package/src/components/terminal/items.tsx +486 -0
- package/src/components/terminal/markdown.tsx +191 -0
- package/src/components/terminal/press.tsx +120 -0
- package/src/components/terminal/prompt.tsx +343 -0
- package/src/components/terminal/result-preview.ts +86 -0
- package/src/components/terminal/row.tsx +132 -0
- package/src/components/terminal/scrubber.tsx +784 -0
- package/src/components/terminal/surface.tsx +80 -0
- package/src/components/terminal/tool-run.ts +224 -0
- package/src/index.ts +36 -1
- package/src/lib/status.ts +59 -3
- package/src/lib/tool-icon.ts +14 -0
- package/src/styles/terminal.css +1081 -0
- package/src/styles/theme.css +41 -0
- package/build/SessionPanel-DI1NO4l8.mjs.map +0 -1
- package/build/format.mjs.map +0 -1
- 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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
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
|
|
109
|
-
const shownResult =
|
|
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=
|
|
118
|
-
<
|
|
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
|
-
<
|
|
118
|
+
<PlainPayload code={item.logs.join('\n')} label='Logs'/>
|
|
126
119
|
) : null}
|
|
127
120
|
{item.result !== undefined ? (
|
|
128
121
|
<div>
|
|
129
|
-
<
|
|
122
|
+
<PlainPayload
|
|
130
123
|
code={shownResult || '(empty result)'}
|
|
131
124
|
language={resultLanguage(item)}
|
|
132
125
|
label={isError ? 'Error' : 'Result'}
|
|
133
|
-
|
|
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
|
-
{
|
|
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={() =>
|
|
141
|
-
|
|
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}
|
|
152
|
+
const image = imagePath && hostImage ? <HostImage path={imagePath} load={hostImage} /> : null
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
/**
|
|
263
|
-
|
|
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
|
-
/**
|
|
269
|
-
|
|
270
|
-
|
|
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=
|
|
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'}
|