@workerdeck/ui 2.15.0 → 3.1.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 +6 -5
- package/build/{SessionPanel-BVKqPs_3.d.mts → SessionPanel-BNNc8grK.d.mts} +18 -16
- package/build/index.d.mts +194 -49
- package/build/index.mjs +562 -260
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +170 -21
- package/build/{use-path-links-BgWXmfAS.mjs → use-path-links-C6Fh5zdx.mjs} +3312 -2758
- package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
- package/build/workspace.d.mts +3 -3
- package/build/workspace.mjs +3 -3
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/ApprovalPrompts.tsx +87 -0
- package/src/components/agent/Composer.tsx +25 -47
- package/src/components/agent/ContextDialog.tsx +75 -67
- package/src/components/agent/FrameStrip.tsx +74 -0
- package/src/components/agent/HostFilesDialog.tsx +3 -2
- package/src/components/agent/McpDialog.tsx +112 -78
- package/src/components/agent/PermissionPrompt.tsx +22 -17
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Response.tsx +8 -1
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +131 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +29 -6
- package/src/components/agent/SessionPanel.tsx +249 -498
- package/src/components/agent/SessionSteps.tsx +98 -14
- package/src/components/agent/SessionWorkspace.tsx +3 -3
- package/src/components/agent/ShellStrip.tsx +43 -36
- package/src/components/agent/ShellTerminal.tsx +1 -1
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/ToolCallCard.tsx +18 -41
- package/src/components/agent/Transcript.tsx +111 -93
- package/src/components/agent/TranscriptItemView.tsx +80 -67
- package/src/components/agent/TranscriptRows.tsx +4 -2
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/image-viewer.tsx +326 -0
- package/src/components/agent/markdown-image.tsx +139 -0
- package/src/components/agent/scrubber-marks.ts +33 -11
- package/src/components/agent/scrubber-peek.tsx +122 -0
- package/src/components/agent/session-panel-providers.tsx +41 -0
- package/src/components/agent/shell-actions.tsx +38 -1
- package/src/components/agent/tool-result-image.tsx +40 -0
- package/src/components/agent/transcript-variant.tsx +3 -21
- package/src/components/agent/use-catch-up.ts +20 -0
- package/src/components/agent/use-frame.ts +88 -0
- package/src/components/agent/use-host-image.ts +81 -0
- package/src/components/agent/use-shell-frame.ts +12 -70
- package/src/components/agent/use-subagent-frame.ts +19 -75
- package/src/components/prompt-area/prompt-area-engine.ts +21 -20
- package/src/components/prompt-area/prompt-area-list-ops.ts +12 -15
- package/src/components/prompt-area/use-prompt-area-keydown.ts +4 -2
- package/src/components/terminal/PermissionPrompt.tsx +31 -28
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +23 -89
- package/src/components/terminal/affordances.tsx +100 -74
- package/src/components/terminal/height.ts +48 -32
- package/src/components/terminal/image-box.ts +31 -0
- package/src/components/terminal/items.tsx +70 -30
- package/src/components/terminal/markdown.tsx +4 -1
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/terminal/shell-row.ts +17 -3
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +65 -12
- package/src/lib/file-link.ts +3 -16
- package/src/lib/permission-prompt.ts +57 -0
- package/src/lib/question-answers.ts +65 -0
- package/src/lib/shell-request.ts +115 -0
- package/src/lib/tool-icon.ts +13 -1
- package/src/styles/terminal.css +129 -22
- package/src/styles/theme.css +4 -0
- package/build/use-path-links-BgWXmfAS.mjs.map +0 -1
- package/src/components/prompt-area/use-markdown-mode.ts +0 -103
|
@@ -2,7 +2,7 @@ import type { FilePatch, PatchHunk } from '@workerdeck/protocol'
|
|
|
2
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
3
|
import { compactionText, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
4
4
|
import { taskChildItems, type TerminalBlock, type ToolCallItem } from './blocks.ts'
|
|
5
|
-
import { IMAGE_BOX_LINES } from './image-box.ts'
|
|
5
|
+
import { IMAGE_BOX_LINES, hostImagePathOf } from './image-box.ts'
|
|
6
6
|
import { collapsedResult } from './result-preview.ts'
|
|
7
7
|
import { shellBodyLines, shellFooterText, shellHeaderText } from './shell-row.ts'
|
|
8
8
|
import { todoLine, todoPreview } from './todos.ts'
|
|
@@ -60,6 +60,10 @@ export function measureCh(surface: HTMLElement): number {
|
|
|
60
60
|
return width
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const MARKDOWN_IMAGE = /!\[[^\]]*\]\([^)]*\)/
|
|
64
|
+
|
|
65
|
+
const MARKDOWN_IMAGE_LINE = /^\s*!\[[^\]]*\]\([^)\s]+\)\s*$/
|
|
66
|
+
|
|
63
67
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' })
|
|
64
68
|
|
|
65
69
|
const WIDE_RANGES: [number, number][] = [
|
|
@@ -276,7 +280,8 @@ function stripInline(s: string): string {
|
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
type MdBlock =
|
|
279
|
-
| { t: 'p'; lines: string[] }
|
|
283
|
+
| { t: 'p'; lines: string[]; images: boolean }
|
|
284
|
+
| { t: 'image' }
|
|
280
285
|
| { t: 'h'; text: string }
|
|
281
286
|
| { t: 'fence'; code: string }
|
|
282
287
|
| { t: 'list'; items: { lines: string[]; gutter: number; indent: number }[] }
|
|
@@ -302,11 +307,37 @@ function hardLines(source: string[]): string[] {
|
|
|
302
307
|
return out
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
function isBlank(s: string | undefined): boolean {
|
|
311
|
+
return s !== undefined && s.trim() === ''
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function listMarker(s: string): { depth: number; ordered: boolean; text: string } | undefined {
|
|
315
|
+
const m = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(s)
|
|
316
|
+
if (!m) {
|
|
317
|
+
return undefined
|
|
318
|
+
}
|
|
319
|
+
return { depth: Math.floor(m[1]!.length / 2), ordered: /\d/.test(m[2]!), text: m[3]! }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function numberHunk(hunk: PatchHunk): { number: number; text: string }[] {
|
|
323
|
+
let newLine = hunk.newStart
|
|
324
|
+
let oldLine = hunk.oldStart
|
|
325
|
+
return hunk.lines.map((line) => {
|
|
326
|
+
const kind = line.startsWith('+') ? 'add' : line.startsWith('-') ? 'remove' : 'context'
|
|
327
|
+
const text = line.slice(1)
|
|
328
|
+
const number = kind === 'add' ? newLine++ : kind === 'remove' ? oldLine++ : newLine
|
|
329
|
+
if (kind === 'context') {
|
|
330
|
+
oldLine++
|
|
331
|
+
newLine++
|
|
332
|
+
}
|
|
333
|
+
return { number, text }
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
305
337
|
function parseBlocks(md: string): MdBlock[] {
|
|
306
338
|
const lines = md.split('\n')
|
|
307
339
|
const blocks: MdBlock[] = []
|
|
308
340
|
let i = 0
|
|
309
|
-
const isBlank = (s: string | undefined) => s !== undefined && s.trim() === ''
|
|
310
341
|
while (i < lines.length) {
|
|
311
342
|
const line = lines[i]!
|
|
312
343
|
if (line.trim() === '') {
|
|
@@ -359,15 +390,6 @@ function parseBlocks(md: string): MdBlock[] {
|
|
|
359
390
|
blocks.push({ t: 'quote', paras })
|
|
360
391
|
continue
|
|
361
392
|
}
|
|
362
|
-
const listMarker = (s: string) => {
|
|
363
|
-
const m = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(s)
|
|
364
|
-
if (!m) {
|
|
365
|
-
return undefined
|
|
366
|
-
}
|
|
367
|
-
const depth = Math.floor(m[1]!.length / 2)
|
|
368
|
-
const ordered = /\d/.test(m[2]!)
|
|
369
|
-
return { depth, ordered, text: m[3]! }
|
|
370
|
-
}
|
|
371
393
|
if (listMarker(line)) {
|
|
372
394
|
const items: { lines: string[]; gutter: number; indent: number }[] = []
|
|
373
395
|
const gutters: number[] = []
|
|
@@ -431,7 +453,11 @@ function parseBlocks(md: string): MdBlock[] {
|
|
|
431
453
|
para.push(lines[i]!)
|
|
432
454
|
i += 1
|
|
433
455
|
}
|
|
434
|
-
|
|
456
|
+
if (para.length === 1 && MARKDOWN_IMAGE_LINE.test(para[0]!)) {
|
|
457
|
+
blocks.push({ t: 'image' })
|
|
458
|
+
} else {
|
|
459
|
+
blocks.push({ t: 'p', lines: hardLines(para), images: para.some((row) => MARKDOWN_IMAGE.test(row)) })
|
|
460
|
+
}
|
|
435
461
|
}
|
|
436
462
|
return blocks
|
|
437
463
|
}
|
|
@@ -449,10 +475,14 @@ export function markdownHeight(md: string, m: CellMetrics, extraPx = 0): Acc {
|
|
|
449
475
|
case 'p': {
|
|
450
476
|
for (const line of block.lines) {
|
|
451
477
|
const r = textLines(line, cols)
|
|
452
|
-
acc = add(acc, { px: r.lines * m.line, exact: r.exact })
|
|
478
|
+
acc = add(acc, { px: r.lines * m.line, exact: r.exact && !block.images })
|
|
453
479
|
}
|
|
454
480
|
break
|
|
455
481
|
}
|
|
482
|
+
case 'image': {
|
|
483
|
+
acc = add(acc, { px: IMAGE_BOX_LINES * m.line, exact: true })
|
|
484
|
+
break
|
|
485
|
+
}
|
|
456
486
|
case 'h': {
|
|
457
487
|
const r = textLines(block.text, cols)
|
|
458
488
|
acc = add(acc, { px: r.lines * m.line, exact: r.exact })
|
|
@@ -512,21 +542,7 @@ export function markdownHeight(md: string, m: CellMetrics, extraPx = 0): Acc {
|
|
|
512
542
|
}
|
|
513
543
|
|
|
514
544
|
function diffHeight(patch: FilePatch, m: CellMetrics, extraPx = 0): Acc {
|
|
515
|
-
const
|
|
516
|
-
let newLine = hunk.newStart
|
|
517
|
-
let oldLine = hunk.oldStart
|
|
518
|
-
return hunk.lines.map((line) => {
|
|
519
|
-
const kind = line.startsWith('+') ? 'add' : line.startsWith('-') ? 'remove' : 'context'
|
|
520
|
-
const text = line.slice(1)
|
|
521
|
-
const number = kind === 'add' ? newLine++ : kind === 'remove' ? oldLine++ : newLine
|
|
522
|
-
if (kind === 'context') {
|
|
523
|
-
oldLine++
|
|
524
|
-
newLine++
|
|
525
|
-
}
|
|
526
|
-
return { number, text }
|
|
527
|
-
})
|
|
528
|
-
}
|
|
529
|
-
const hunks = patch.hunks.map(walk)
|
|
545
|
+
const hunks = patch.hunks.map(numberHunk)
|
|
530
546
|
const numbered = patch.hunks.some((hunk) => hunk.newStart > 0)
|
|
531
547
|
const width = numbered ? String(Math.max(...hunks.flat().map((row) => row.number), 1)).length : 0
|
|
532
548
|
const columns = numbered ? width + 3 : 2
|
|
@@ -551,9 +567,9 @@ function toolRowHeight(item: ToolCallItem, m: CellMetrics, extraPx: number): Acc
|
|
|
551
567
|
const backend = item.backend && item.backend !== 'server' ? ` · ${item.backend}` : ''
|
|
552
568
|
let acc = rowH(`${item.name}(${preview})${backend}`, m, { gutterCells: 2, extraPx })
|
|
553
569
|
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
556
|
-
acc = add(acc, { px:
|
|
570
|
+
const boxes = (item.result?.images?.length ?? 0) + (hostImagePathOf(item) === undefined ? 0 : 1)
|
|
571
|
+
if (boxes > 0) {
|
|
572
|
+
acc = add(acc, { px: boxes * IMAGE_BOX_LINES * m.line, exact: true })
|
|
557
573
|
}
|
|
558
574
|
|
|
559
575
|
if (item.patch) {
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
+
import type { TranscriptItem } from '@workerdeck/react'
|
|
1
2
|
import { formatBytes } from '../../lib/format.ts'
|
|
2
3
|
|
|
4
|
+
type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
5
|
+
|
|
3
6
|
export const IMAGE_BOX_LINES = 12
|
|
4
7
|
|
|
8
|
+
const HOST_IMAGE_TOOLS = new Set(['CodexImageGeneration', 'CodexImageView'])
|
|
9
|
+
|
|
10
|
+
const MEDIA_EXTENSIONS: Record<string, string> = {
|
|
11
|
+
'image/png': 'png',
|
|
12
|
+
'image/jpeg': 'jpg',
|
|
13
|
+
'image/gif': 'gif',
|
|
14
|
+
'image/webp': 'webp',
|
|
15
|
+
'image/svg+xml': 'svg',
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
export function imagePlaceholder(image: { bytes: number }): string {
|
|
6
19
|
return `image · ${formatBytes(image.bytes)}`
|
|
7
20
|
}
|
|
8
21
|
|
|
9
22
|
export const IMAGE_UNAVAILABLE = 'image unavailable'
|
|
23
|
+
|
|
24
|
+
export function hostImagePathOf(item: ToolCallItem): string | undefined {
|
|
25
|
+
if (!HOST_IMAGE_TOOLS.has(item.name)) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
const input = item.input as { savedPath?: unknown; path?: unknown } | null
|
|
29
|
+
const path = input?.savedPath ?? input?.path
|
|
30
|
+
return typeof path === 'string' ? path : undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function baseName(path: string): string {
|
|
34
|
+
return path.split(/[\\/]/).pop() || path
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resultImageName(toolName: string, image: { partIndex: number; mediaType: string }): string {
|
|
38
|
+
const extension = MEDIA_EXTENSIONS[image.mediaType] ?? image.mediaType.split('/').pop() ?? 'img'
|
|
39
|
+
return `${toolName.toLowerCase()}-${image.partIndex + 1}.${extension}`
|
|
40
|
+
}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { useEffect, useState, type ReactNode } from 'react'
|
|
1
|
+
import { Fragment, useEffect, useState, type ReactNode } from 'react'
|
|
2
2
|
import type { MessageOrigin } from '@workerdeck/protocol'
|
|
3
3
|
import type { ShellItem, TranscriptItem } from '@workerdeck/react'
|
|
4
4
|
import { compactionText, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
5
5
|
import { isMutatingTool } from '../../lib/tool-icon.ts'
|
|
6
6
|
import { usePulse } from '../agent/pulse.tsx'
|
|
7
7
|
import { PromptTokenText } from '../agent/PromptTokenText.tsx'
|
|
8
|
-
import { BookmarkAction, CopyAction,
|
|
8
|
+
import { ActionPlacementProvider, BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
9
9
|
import { TerminalDiff } from './diff.tsx'
|
|
10
10
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
11
11
|
import { usePeerNames } from './peer-names.tsx'
|
|
12
12
|
import { Pressable, useRevealOnOpen, type PressModifiers } from './press.tsx'
|
|
13
|
-
import { IMAGE_BOX_LINES, IMAGE_UNAVAILABLE, imagePlaceholder } from './image-box.ts'
|
|
13
|
+
import { IMAGE_BOX_LINES, IMAGE_UNAVAILABLE, baseName, hostImagePathOf, imagePlaceholder, resultImageName } from './image-box.ts'
|
|
14
14
|
import { collapsedResult } from './result-preview.ts'
|
|
15
15
|
import { useToolResultFetcher } from '../agent/tool-result-fetch.tsx'
|
|
16
16
|
import { useToolTitle } from '../agent/tool-titles.tsx'
|
|
17
|
-
import { useToolResultImageSrc } from '../agent/tool-result-image.tsx'
|
|
17
|
+
import { useHostImageSrc, useToolResultImageSrc, type ToolResultImageState } from '../agent/tool-result-image.tsx'
|
|
18
|
+
import { ViewableImage, useOpenImage } from '../agent/image-viewer.tsx'
|
|
18
19
|
import { isPeerSend, peerName, peerOneLine, peerSendTarget, peerSendText, planRun, runFailed, runSummary } from './tool-run.ts'
|
|
19
20
|
import { todoLine, todoPreview, type TodoPreview, type TodoStatus } from './todos.ts'
|
|
20
|
-
import { useShellActions } from '../agent/shell-actions.tsx'
|
|
21
|
+
import { ShellItemActions, useShellActions, useVerifyRunning } from '../agent/shell-actions.tsx'
|
|
21
22
|
import { SHELL_GLYPH, shellBodyLines, shellFailed, shellFooterText, shellLabel, shellStatusText } from './shell-row.ts'
|
|
22
23
|
import { type ToolCallItem } from './blocks.ts'
|
|
23
24
|
import { Band, Blank, Ink, Row, type Tone } from './row.tsx'
|
|
@@ -101,13 +102,24 @@ export function PeerSendRow({ item }: { item: ToolCallItem }) {
|
|
|
101
102
|
)
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
export function UserRow({
|
|
105
|
+
export function UserRow({
|
|
106
|
+
item,
|
|
107
|
+
attachmentUrl,
|
|
108
|
+
}: {
|
|
109
|
+
item: Extract<TranscriptItem, { kind: 'user' }>
|
|
110
|
+
attachmentUrl?: (attachmentId: string) => string
|
|
111
|
+
}) {
|
|
105
112
|
return (
|
|
106
113
|
<WithActions actions={<BookmarkAction id={item.id} />}>
|
|
107
114
|
<div className="term-user" data-peer={item.origin ? '' : undefined}>
|
|
108
115
|
{item.attachments?.length ? (
|
|
109
116
|
<Row glyph={PROMPT_GLYPH} glyphTone="dim" tone="dim">
|
|
110
|
-
{item.attachments.map((attachment) =>
|
|
117
|
+
{item.attachments.map((attachment, index) => (
|
|
118
|
+
<Fragment key={attachment.id}>
|
|
119
|
+
{index > 0 ? ', ' : null}
|
|
120
|
+
<AttachmentName attachment={attachment} href={attachmentUrl?.(attachment.id)} />
|
|
121
|
+
</Fragment>
|
|
122
|
+
))}
|
|
111
123
|
</Row>
|
|
112
124
|
) : null}
|
|
113
125
|
{item.text
|
|
@@ -195,6 +207,7 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
195
207
|
const copyable = typeof command === 'string' ? command : text
|
|
196
208
|
const todos = todoPreview(item.name, item.input)
|
|
197
209
|
const title = useToolTitle(item.name)
|
|
210
|
+
const hostPath = hostImagePathOf(item)
|
|
198
211
|
|
|
199
212
|
return (
|
|
200
213
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
@@ -216,8 +229,9 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
216
229
|
{item.backend && item.backend !== 'server' ? <Ink tone="faint"> · {item.backend}</Ink> : null}
|
|
217
230
|
</Row>
|
|
218
231
|
</Pressable>
|
|
232
|
+
{hostPath !== undefined ? <TerminalHostImage path={hostPath} /> : null}
|
|
219
233
|
{item.result?.images?.map((image) => (
|
|
220
|
-
<TerminalImage key={image.partIndex} toolUseId={item.id} image={image} />
|
|
234
|
+
<TerminalImage key={image.partIndex} toolUseId={item.id} name={resultImageName(item.name, image)} image={image} />
|
|
221
235
|
))}
|
|
222
236
|
{item.patch && !open ? (
|
|
223
237
|
<TerminalDiff patch={item.patch} />
|
|
@@ -298,10 +312,39 @@ function TerminalTodos({ preview }: { preview: TodoPreview }) {
|
|
|
298
312
|
)
|
|
299
313
|
}
|
|
300
314
|
|
|
315
|
+
function AttachmentName({ attachment, href }: { attachment: { name: string; mediaType: string }; href?: string }) {
|
|
316
|
+
const open = useOpenImage()
|
|
317
|
+
if (!open || !href || !attachment.mediaType.startsWith('image/')) {
|
|
318
|
+
return <>{attachment.name}</>
|
|
319
|
+
}
|
|
320
|
+
return (
|
|
321
|
+
<button
|
|
322
|
+
type="button"
|
|
323
|
+
className="term-press term-link"
|
|
324
|
+
onClick={(event) => {
|
|
325
|
+
event.stopPropagation()
|
|
326
|
+
open({ src: href, name: attachment.name })
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
{attachment.name}
|
|
330
|
+
</button>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
301
334
|
type ToolResultImage = NonNullable<NonNullable<ToolCallItem['result']>['images']>[number]
|
|
302
335
|
|
|
303
|
-
function TerminalImage({ toolUseId, image }: { toolUseId: string; image: ToolResultImage }) {
|
|
304
|
-
const
|
|
336
|
+
function TerminalImage({ toolUseId, name, image }: { toolUseId: string; name: string; image: ToolResultImage }) {
|
|
337
|
+
const state = useToolResultImageSrc({ toolUseId, ...image })
|
|
338
|
+
return <TerminalImageBox state={state} name={name} placeholder={imagePlaceholder(image)} />
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function TerminalHostImage({ path }: { path: string }) {
|
|
342
|
+
const state = useHostImageSrc(path)
|
|
343
|
+
return <TerminalImageBox state={state} name={baseName(path)} placeholder={baseName(path)} />
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function TerminalImageBox({ state, name, placeholder }: { state: ToolResultImageState; name: string; placeholder: string }) {
|
|
347
|
+
const { src, failed } = state
|
|
305
348
|
return (
|
|
306
349
|
<Row indent={1} columns={3}>
|
|
307
350
|
<div
|
|
@@ -310,9 +353,11 @@ function TerminalImage({ toolUseId, image }: { toolUseId: string; image: ToolRes
|
|
|
310
353
|
style={{ height: `calc(var(--term-line) * ${IMAGE_BOX_LINES})` }}
|
|
311
354
|
>
|
|
312
355
|
{src ? (
|
|
313
|
-
<
|
|
356
|
+
<ViewableImage image={{ src, name }}>
|
|
357
|
+
<img src={src} alt={name} />
|
|
358
|
+
</ViewableImage>
|
|
314
359
|
) : (
|
|
315
|
-
<Ink tone="faint">{failed ? IMAGE_UNAVAILABLE :
|
|
360
|
+
<Ink tone="faint">{failed ? IMAGE_UNAVAILABLE : placeholder}</Ink>
|
|
316
361
|
)}
|
|
317
362
|
</div>
|
|
318
363
|
</Row>
|
|
@@ -338,8 +383,17 @@ export function ToolRunRow({ items }: { items: ToolCallItem[] }) {
|
|
|
338
383
|
</Pressable>
|
|
339
384
|
{open ? (
|
|
340
385
|
<div>
|
|
341
|
-
{items.map((item) => (
|
|
342
|
-
<
|
|
386
|
+
{items.map((item, index) => (
|
|
387
|
+
<Fragment key={item.id}>
|
|
388
|
+
{index > 0 ? <Blank /> : null}
|
|
389
|
+
{index < items.length - 1 ? (
|
|
390
|
+
<ActionPlacementProvider value="below">
|
|
391
|
+
<ToolRow item={item} />
|
|
392
|
+
</ActionPlacementProvider>
|
|
393
|
+
) : (
|
|
394
|
+
<ToolRow item={item} />
|
|
395
|
+
)}
|
|
396
|
+
</Fragment>
|
|
343
397
|
))}
|
|
344
398
|
</div>
|
|
345
399
|
) : null}
|
|
@@ -400,12 +454,7 @@ export function ShellRow({ item }: { item: ShellItem }) {
|
|
|
400
454
|
const footer = shellFooterText(item, open, lines.length)
|
|
401
455
|
|
|
402
456
|
const shellId = item.shell.id
|
|
403
|
-
|
|
404
|
-
useEffect(() => {
|
|
405
|
-
if (running) {
|
|
406
|
-
void verify(shellId)
|
|
407
|
-
}
|
|
408
|
-
}, [running, shellId, verify])
|
|
457
|
+
useVerifyRunning(item)
|
|
409
458
|
|
|
410
459
|
const press = (modifiers: PressModifiers) => {
|
|
411
460
|
if ((modifiers.meta || modifiers.ctrl) && actions.open) {
|
|
@@ -423,16 +472,7 @@ export function ShellRow({ item }: { item: ShellItem }) {
|
|
|
423
472
|
|
|
424
473
|
return (
|
|
425
474
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
426
|
-
<WithActions
|
|
427
|
-
actions={
|
|
428
|
-
<>
|
|
429
|
-
{actions.open ? <OpenShellAction onOpen={() => actions.open?.(shellId)} /> : null}
|
|
430
|
-
{running ? <KillShellAction onKill={() => void actions.kill(shellId)} /> : null}
|
|
431
|
-
<BookmarkAction id={item.id} />
|
|
432
|
-
<CopyAction text={item.shell.command} label="Copy command" />
|
|
433
|
-
</>
|
|
434
|
-
}
|
|
435
|
-
>
|
|
475
|
+
<WithActions actions={<ShellItemActions item={item} />}>
|
|
436
476
|
<Pressable onPress={press} expanded={open}>
|
|
437
477
|
<Row glyph={SHELL_GLYPH} glyphTone={tone} tone="fg">
|
|
438
478
|
<Ink bold tone="bright">
|
|
@@ -5,6 +5,7 @@ import { parseFileLink } from '../../lib/file-link.ts'
|
|
|
5
5
|
import { CopyAction, WithActions } from './affordances.tsx'
|
|
6
6
|
import { useFileLinks } from './file-link.tsx'
|
|
7
7
|
import { Band } from './row.tsx'
|
|
8
|
+
import { MARKDOWN_REHYPE_PLUGINS, MarkdownImage } from '../agent/markdown-image.tsx'
|
|
8
9
|
|
|
9
10
|
function codeText(node: ReactNode): string {
|
|
10
11
|
if (node === null || node === undefined || typeof node === 'boolean') {
|
|
@@ -29,7 +30,7 @@ function fenceLanguage(node: ReactNode): string | undefined {
|
|
|
29
30
|
|
|
30
31
|
function CodeBand({ code, language }: { code: string; language?: string }) {
|
|
31
32
|
return (
|
|
32
|
-
<WithActions className="term-block" actions={<CopyAction text={code} label="Copy code" />}>
|
|
33
|
+
<WithActions className="term-block" placement="inline" actions={<CopyAction text={code} label="Copy code" />}>
|
|
33
34
|
<Band className="term-code" data-language={language}>
|
|
34
35
|
<pre className="term-pre">{code}</pre>
|
|
35
36
|
</Band>
|
|
@@ -116,6 +117,7 @@ const TERMINAL_COMPONENTS: Components = {
|
|
|
116
117
|
),
|
|
117
118
|
em: ({ children }) => <em className="term-em">{children}</em>,
|
|
118
119
|
a: ({ children, href }) => <Link href={href}>{children}</Link>,
|
|
120
|
+
img: ({ src, alt }) => <MarkdownImage src={src} alt={alt} terminal />,
|
|
119
121
|
|
|
120
122
|
table: ({ children }) => (
|
|
121
123
|
<div className="term-block term-table-wrap">
|
|
@@ -148,6 +150,7 @@ export const TerminalMarkdown = memo(
|
|
|
148
150
|
parseIncompleteMarkdown={streaming}
|
|
149
151
|
controls={false}
|
|
150
152
|
components={TERMINAL_COMPONENTS}
|
|
153
|
+
rehypePlugins={MARKDOWN_REHYPE_PLUGINS}
|
|
151
154
|
className={cn('term-md', className)}
|
|
152
155
|
>
|
|
153
156
|
{children}
|