@workerdeck/ui 0.7.0 → 0.11.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 +81 -3
- package/build/SessionPanel-CyhygZx_.d.mts +277 -0
- package/build/SessionPanel-_U8tjX29.mjs +8409 -0
- package/build/SessionPanel-_U8tjX29.mjs.map +1 -0
- package/build/format-DqR56Y8l.mjs +162 -0
- package/build/format-DqR56Y8l.mjs.map +1 -0
- package/build/format-ljc3lKpA.d.mts +59 -0
- package/build/format.d.mts +2 -0
- package/build/format.mjs +2 -0
- package/build/index.d.mts +615 -87
- package/build/index.mjs +6 -5081
- package/build/index.mjs.map +1 -1
- package/build/workspace.d.mts +199 -0
- package/build/workspace.mjs +849 -0
- package/build/workspace.mjs.map +1 -0
- package/package.json +22 -4
- package/src/components/agent/CodeEditor.tsx +300 -0
- package/src/components/agent/Composer.tsx +522 -87
- package/src/components/agent/ContextDialog.tsx +99 -0
- package/src/components/agent/Conversation.tsx +11 -3
- package/src/components/agent/EditorTabs.tsx +165 -0
- package/src/components/agent/FileCard.tsx +26 -0
- package/src/components/agent/FileTree.tsx +287 -0
- package/src/components/agent/FileViewer.tsx +148 -0
- package/src/components/agent/HostFilesDialog.tsx +218 -0
- package/src/components/agent/Loader.tsx +120 -14
- package/src/components/agent/McpDialog.tsx +363 -0
- package/src/components/agent/Message.tsx +51 -17
- package/src/components/agent/ModelSelect.tsx +34 -6
- package/src/components/agent/PermissionModeSelect.tsx +133 -22
- package/src/components/agent/PermissionPrompt.tsx +164 -6
- package/src/components/agent/PromptTokenText.tsx +39 -0
- package/src/components/agent/QuestionPrompt.tsx +122 -0
- package/src/components/agent/Reasoning.tsx +20 -5
- package/src/components/agent/Response.tsx +128 -0
- package/src/components/agent/SessionEmptyState.tsx +65 -0
- package/src/components/agent/SessionInfoDialog.tsx +163 -0
- package/src/components/agent/SessionPanel.tsx +756 -90
- package/src/components/agent/SessionWorkspace.tsx +282 -0
- package/src/components/agent/SkillsDialog.tsx +195 -0
- package/src/components/agent/StatusBar.tsx +85 -18
- package/src/components/agent/ToolCallCard.tsx +243 -30
- package/src/components/agent/Transcript.tsx +540 -27
- package/src/components/agent/UsageDialog.tsx +168 -0
- package/src/components/agent/line-prompt.tsx +249 -0
- package/src/components/agent/transcript-variant.tsx +61 -0
- package/src/components/prompt-area/prompt-area-engine.ts +53 -0
- package/src/components/prompt-area/types.ts +15 -0
- package/src/components/prompt-area/use-prompt-area.ts +20 -0
- package/src/components/ui/CodeBlock.tsx +40 -2
- package/src/components/ui/CopyButton.tsx +28 -3
- package/src/components/ui/Dialog.tsx +92 -0
- package/src/components/ui/Menu.tsx +55 -0
- package/src/components/ui/Splitter.tsx +133 -0
- package/src/components/ui/Tooltip.tsx +22 -5
- package/src/format.ts +10 -0
- package/src/index.ts +63 -2
- package/src/lib/clipboard.ts +56 -0
- package/src/lib/format.ts +114 -0
- package/src/lib/tool-icon.ts +96 -0
- package/src/workspace.ts +28 -0
|
@@ -1,18 +1,41 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
-
import { ChevronDown, Clock
|
|
3
|
+
import { ChevronDown, Clock } from 'lucide-react'
|
|
4
4
|
import { Badge } from '../ui/Badge.tsx'
|
|
5
5
|
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 { LineGlyph, useLines } from './transcript-variant.tsx'
|
|
9
12
|
|
|
10
13
|
export type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
11
14
|
|
|
12
15
|
const RESULT_PREVIEW_CHARS = 2000
|
|
13
16
|
|
|
17
|
+
/** Codex's built-in image tools name a host path rather than sending bytes —
|
|
18
|
+
* an event log carries references, never base64. Rendering the picture means
|
|
19
|
+
* reading that path back through the gateway's host-file route. */
|
|
20
|
+
const IMAGE_TOOLS = new Set(['CodexImageGeneration', 'CodexImageView'])
|
|
21
|
+
|
|
22
|
+
const imagePathOf = (item: ToolCallItem): string | undefined => {
|
|
23
|
+
if (!IMAGE_TOOLS.has(item.name)) return undefined
|
|
24
|
+
const input = item.input as { savedPath?: unknown; path?: unknown } | null
|
|
25
|
+
const path = input?.savedPath ?? input?.path
|
|
26
|
+
return typeof path === 'string' ? path : undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export interface ToolCallCardProps {
|
|
15
30
|
item: ToolCallItem
|
|
31
|
+
/**
|
|
32
|
+
* Reads a host file as a data URL, for tools whose output is a picture on the
|
|
33
|
+
* host. Resolves `undefined` when the gateway won't serve that path — a
|
|
34
|
+
* generated image saved outside the allowed roots (codex's default
|
|
35
|
+
* `$CODEX_HOME/generated_images/`) is one, and the card then names the path
|
|
36
|
+
* instead of showing it.
|
|
37
|
+
*/
|
|
38
|
+
hostImage?: (path: string) => Promise<string | undefined>
|
|
16
39
|
className?: string
|
|
17
40
|
}
|
|
18
41
|
|
|
@@ -27,17 +50,153 @@ const STATE_BADGE = {
|
|
|
27
50
|
failed: { label: 'Error', variant: 'danger', busy: false },
|
|
28
51
|
} as const
|
|
29
52
|
|
|
30
|
-
|
|
53
|
+
/** The gutter dot's colour in `lines`: the state, said without a badge. */
|
|
54
|
+
const STATE_GLYPH = {
|
|
55
|
+
running: 'text-info',
|
|
56
|
+
pending: 'text-info',
|
|
57
|
+
deferred: 'text-accent',
|
|
58
|
+
settled: 'text-fg-4',
|
|
59
|
+
failed: 'text-danger',
|
|
60
|
+
} as const
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The language to highlight a payload as.
|
|
64
|
+
*
|
|
65
|
+
* Parameters are always JSON. A result is whatever file the call was about — so
|
|
66
|
+
* the extension in `file_path`/`path` is the best evidence there is, and a call
|
|
67
|
+
* that names no file gets no guess (plain text renders fine and a wrong grammar
|
|
68
|
+
* is worse than none).
|
|
69
|
+
*/
|
|
70
|
+
const EXTENSION_LANGUAGE: Record<string, string> = {
|
|
71
|
+
ts: 'ts', tsx: 'tsx', js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js',
|
|
72
|
+
json: 'json', jsonc: 'json', md: 'md', mdx: 'md', css: 'css', scss: 'scss',
|
|
73
|
+
html: 'html', xml: 'xml', yml: 'yaml', yaml: 'yaml', toml: 'toml',
|
|
74
|
+
py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java', kt: 'kotlin',
|
|
75
|
+
swift: 'swift', c: 'c', h: 'c', cpp: 'cpp', hpp: 'cpp', cs: 'csharp',
|
|
76
|
+
php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'fish', sql: 'sql',
|
|
77
|
+
graphql: 'graphql', dockerfile: 'dockerfile', diff: 'diff', patch: 'diff',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resultLanguage(item: ToolCallItem): string | undefined {
|
|
81
|
+
if (item.name === 'Bash' || item.name === 'CodexCommand') return 'bash'
|
|
82
|
+
if (item.name === 'CodexFileChange') return 'diff'
|
|
83
|
+
const input = item.input as { file_path?: unknown; path?: unknown } | null
|
|
84
|
+
const path = input?.file_path ?? input?.path
|
|
85
|
+
if (typeof path !== 'string') return undefined
|
|
86
|
+
const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
|
|
87
|
+
return EXTENSION_LANGUAGE[extension]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type Status = keyof typeof STATE_BADGE
|
|
91
|
+
|
|
92
|
+
export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps) {
|
|
93
|
+
const lines = useLines()
|
|
31
94
|
const [open, setOpen] = useState(false)
|
|
32
95
|
const [fullResult, setFullResult] = useState(false)
|
|
33
|
-
const
|
|
96
|
+
const imagePath = imagePathOf(item)
|
|
97
|
+
const status: Status = item.status ?? (item.result === undefined ? 'running' : 'settled')
|
|
34
98
|
const badge = STATE_BADGE[status]
|
|
35
99
|
const isError = status === 'failed' || item.result?.isError === true
|
|
100
|
+
const Icon = toolIcon(item.name)
|
|
36
101
|
|
|
37
102
|
const resultText = item.result?.text ?? ''
|
|
38
103
|
const truncated = !fullResult && resultText.length > RESULT_PREVIEW_CHARS
|
|
39
104
|
const shownResult = truncated ? resultText.slice(0, RESULT_PREVIEW_CHARS) : resultText
|
|
40
105
|
|
|
106
|
+
// In `lines` the payloads are dim label + highlighted band, not framed cards:
|
|
107
|
+
// a tool call is already one row, and boxing what it expands into puts back
|
|
108
|
+
// the chrome the variant exists to remove.
|
|
109
|
+
const blockVariant = lines ? 'plain' : 'panel'
|
|
110
|
+
const Payload = lines ? HighlightedPayload : PlainPayload
|
|
111
|
+
const details = open ? (
|
|
112
|
+
<div className={cn('flex flex-col', lines ? 'gap-1 py-1' : 'gap-2 border-t border-border p-2.5')}>
|
|
113
|
+
<Payload
|
|
114
|
+
code={JSON.stringify(item.input, null, 2)}
|
|
115
|
+
language='json'
|
|
116
|
+
label='Parameters'
|
|
117
|
+
variant={blockVariant}
|
|
118
|
+
/>
|
|
119
|
+
{item.logs?.length ? (
|
|
120
|
+
<Payload code={item.logs.join('\n')} label='Logs' variant={blockVariant} />
|
|
121
|
+
) : null}
|
|
122
|
+
{item.result !== undefined ? (
|
|
123
|
+
<div>
|
|
124
|
+
<Payload
|
|
125
|
+
code={shownResult || '(empty result)'}
|
|
126
|
+
language={resultLanguage(item)}
|
|
127
|
+
label={isError ? 'Error' : 'Result'}
|
|
128
|
+
variant={blockVariant}
|
|
129
|
+
className={cn(isError && (lines ? '[&_pre]:text-danger' : 'border-danger/40 [&_pre]:text-danger'))}
|
|
130
|
+
/>
|
|
131
|
+
{truncated ? (
|
|
132
|
+
<button
|
|
133
|
+
type='button'
|
|
134
|
+
className='mt-1 text-label text-fg-3 underline-offset-2 hover:underline'
|
|
135
|
+
onClick={() => setFullResult(true)}>
|
|
136
|
+
Show all {resultText.length.toLocaleString()} chars
|
|
137
|
+
</button>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
) : null}
|
|
141
|
+
</div>
|
|
142
|
+
) : null
|
|
143
|
+
|
|
144
|
+
// The picture is the point of the call — shown without expanding, the way the
|
|
145
|
+
// tool's own output would be if the engine had sent bytes.
|
|
146
|
+
const image = imagePath && hostImage ? <HostImage path={imagePath} load={hostImage} lines={lines} /> : null
|
|
147
|
+
|
|
148
|
+
if (lines) {
|
|
149
|
+
return (
|
|
150
|
+
<div data-slot='tool-call' data-state={status} className={cn('w-full', className)}>
|
|
151
|
+
<button
|
|
152
|
+
type='button'
|
|
153
|
+
onClick={() => setOpen((v) => !v)}
|
|
154
|
+
className='flex w-full items-baseline gap-2 text-left outline-none'>
|
|
155
|
+
<LineGlyph
|
|
156
|
+
className={
|
|
157
|
+
// A settled write is green: skimming a run, "what did it change"
|
|
158
|
+
// is the question you come back to, and it is the one you might
|
|
159
|
+
// need to undo. Every other state keeps its own colour — a failed
|
|
160
|
+
// write is a failure first.
|
|
161
|
+
status === 'settled' && !isError && isMutatingTool(item.name)
|
|
162
|
+
? 'text-success'
|
|
163
|
+
: STATE_GLYPH[status]
|
|
164
|
+
}>
|
|
165
|
+
{badge.busy ? '◐' : '●'}
|
|
166
|
+
</LineGlyph>
|
|
167
|
+
<span className='min-w-0 flex-1 truncate text-body-sm leading-5 text-fg-3'>
|
|
168
|
+
<span className='font-medium text-fg-1'>{item.name}</span>
|
|
169
|
+
<span className='text-fg-4'>({toolInputPreview(item.input)})</span>
|
|
170
|
+
</span>
|
|
171
|
+
{item.backend && item.backend !== 'server' ? (
|
|
172
|
+
<span className='shrink-0 text-label text-fg-4'>{item.backend}</span>
|
|
173
|
+
) : null}
|
|
174
|
+
{badge.busy ? <Spinner className='size-3 shrink-0 self-center text-fg-4' /> : null}
|
|
175
|
+
{status === 'deferred' ? <Clock className='size-3 shrink-0 self-center text-fg-4' /> : null}
|
|
176
|
+
{isError && !badge.busy ? (
|
|
177
|
+
<span className='shrink-0 text-label text-danger'>error</span>
|
|
178
|
+
) : null}
|
|
179
|
+
</button>
|
|
180
|
+
{/* Collapsed, the first line of the output is the whole story most of
|
|
181
|
+
the time — a summary line costs one row and saves an expand. */}
|
|
182
|
+
{!open && resultText ? (
|
|
183
|
+
<div className='flex items-baseline gap-2'>
|
|
184
|
+
<LineGlyph className='text-fg-4'>⎿</LineGlyph>
|
|
185
|
+
<span
|
|
186
|
+
className={cn(
|
|
187
|
+
'min-w-0 flex-1 truncate text-label leading-5',
|
|
188
|
+
isError ? 'text-danger' : 'text-fg-4',
|
|
189
|
+
)}>
|
|
190
|
+
{resultSummary(resultText)}
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
) : null}
|
|
194
|
+
{image}
|
|
195
|
+
{details ? <div className={LINE_INDENT}>{details}</div> : null}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
41
200
|
return (
|
|
42
201
|
<div
|
|
43
202
|
data-slot='tool-call'
|
|
@@ -50,8 +209,13 @@ export function ToolCallCard({ item, className }: ToolCallCardProps) {
|
|
|
50
209
|
'flex w-full items-center gap-2 px-3 py-2 text-left transition-colors outline-none',
|
|
51
210
|
'hover:bg-surface-hover focus-visible:bg-surface-hover',
|
|
52
211
|
)}>
|
|
53
|
-
<
|
|
212
|
+
<Icon className='size-3.5 shrink-0 text-fg-3' />
|
|
54
213
|
<span className='shrink-0 font-mono text-body-sm font-medium text-fg-1'>{item.name}</span>
|
|
214
|
+
{item.backend && item.backend !== 'server' ? (
|
|
215
|
+
<Badge variant='neutral' className='shrink-0'>
|
|
216
|
+
{item.backend}
|
|
217
|
+
</Badge>
|
|
218
|
+
) : null}
|
|
55
219
|
<span className='min-w-0 flex-1 truncate font-mono text-label text-fg-4'>
|
|
56
220
|
{toolInputPreview(item.input)}
|
|
57
221
|
</span>
|
|
@@ -64,31 +228,80 @@ export function ToolCallCard({ item, className }: ToolCallCardProps) {
|
|
|
64
228
|
className={cn('size-3.5 shrink-0 text-fg-4 transition-transform', open && 'rotate-180')}
|
|
65
229
|
/>
|
|
66
230
|
</button>
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
231
|
+
{image}
|
|
232
|
+
{details}
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The first line worth showing of a tool result, for the collapsed summary. */
|
|
238
|
+
function resultSummary(text: string): string {
|
|
239
|
+
const first = text.split('\n').find((line) => line.trim() !== '') ?? ''
|
|
240
|
+
const rest = text.trimEnd().split('\n').length - 1
|
|
241
|
+
const trimmed = first.trim()
|
|
242
|
+
return rest > 0 ? `${trimmed} (+${rest} lines)` : trimmed
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type PayloadProps = {
|
|
246
|
+
code: string
|
|
247
|
+
label: string
|
|
248
|
+
language?: string
|
|
249
|
+
variant: 'panel' | 'plain'
|
|
250
|
+
className?: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The framed card, for the `cards` transcript. Unhighlighted by design: it is
|
|
254
|
+
* structured data in a panel, not a file. */
|
|
255
|
+
function PlainPayload({ code, label, variant, className }: PayloadProps) {
|
|
256
|
+
return <CodeBlock code={code} label={label} variant={variant} className={className} />
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The terminal payload — shared with the line-shaped prompts. */
|
|
260
|
+
function HighlightedPayload({ code, label, language, className }: PayloadProps) {
|
|
261
|
+
return <LinePayload code={code} label={label} language={language} className={className} />
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A picture that lives on the host, fetched through the gateway's host-file
|
|
266
|
+
* route and shown inline.
|
|
267
|
+
*
|
|
268
|
+
* Silent on failure by design: a path outside the server's allowed roots is the
|
|
269
|
+
* *expected* case for codex's default save location, and the card's result text
|
|
270
|
+
* already names where the file went. An error banner over that would be noise
|
|
271
|
+
* about a thing the operator can fix in one line of config.
|
|
272
|
+
*/
|
|
273
|
+
function HostImage({
|
|
274
|
+
path,
|
|
275
|
+
load,
|
|
276
|
+
lines,
|
|
277
|
+
}: {
|
|
278
|
+
path: string
|
|
279
|
+
load: (path: string) => Promise<string | undefined>
|
|
280
|
+
lines?: boolean
|
|
281
|
+
}) {
|
|
282
|
+
const [src, setSrc] = useState<string | undefined>()
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
let cancelled = false
|
|
285
|
+
setSrc(undefined)
|
|
286
|
+
load(path)
|
|
287
|
+
.then((url) => {
|
|
288
|
+
if (!cancelled) setSrc(url)
|
|
289
|
+
})
|
|
290
|
+
.catch(() => {
|
|
291
|
+
// Not readable from here — the path in the result is the answer.
|
|
292
|
+
})
|
|
293
|
+
return () => {
|
|
294
|
+
cancelled = true
|
|
295
|
+
}
|
|
296
|
+
}, [path, load])
|
|
297
|
+
if (!src) return null
|
|
298
|
+
return (
|
|
299
|
+
<div className={cn(lines ? cn('py-1', LINE_INDENT) : 'border-t border-border p-2.5')}>
|
|
300
|
+
<img
|
|
301
|
+
src={src}
|
|
302
|
+
alt={path.split('/').pop() ?? 'Generated image'}
|
|
303
|
+
className='max-h-96 w-auto max-w-full rounded-md border border-border'
|
|
304
|
+
/>
|
|
92
305
|
</div>
|
|
93
306
|
)
|
|
94
307
|
}
|