@workerdeck/ui 2.15.0 → 3.0.1
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-C1bU4yTM.d.mts} +11 -11
- package/build/index.d.mts +46 -9
- package/build/index.mjs +70 -43
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +156 -16
- package/build/{use-path-links-BgWXmfAS.mjs → use-path-links-BEWxt0ZM.mjs} +1566 -852
- package/build/use-path-links-BEWxt0ZM.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/PermissionPrompt.tsx +16 -2
- package/src/components/agent/Response.tsx +8 -1
- package/src/components/agent/SessionBrowser.tsx +6 -0
- package/src/components/agent/SessionItem.tsx +3 -0
- package/src/components/agent/SessionPanel.tsx +28 -33
- package/src/components/agent/SessionSteps.tsx +46 -5
- package/src/components/agent/SessionWorkspace.tsx +3 -3
- package/src/components/agent/ShellStrip.tsx +27 -4
- package/src/components/agent/ShellTerminal.tsx +1 -1
- package/src/components/agent/ToolCallCard.tsx +18 -41
- package/src/components/agent/Transcript.tsx +98 -93
- package/src/components/agent/TranscriptItemView.tsx +102 -59
- package/src/components/agent/TranscriptRows.tsx +4 -2
- package/src/components/agent/image-viewer.tsx +326 -0
- package/src/components/agent/markdown-image.tsx +91 -0
- package/src/components/agent/shell-actions.tsx +2 -0
- package/src/components/agent/tool-result-image.tsx +40 -0
- package/src/components/agent/transcript-variant.tsx +3 -21
- package/src/components/prompt-area/use-prompt-area-keydown.ts +4 -2
- package/src/components/terminal/PermissionPrompt.tsx +20 -4
- package/src/components/terminal/TerminalTranscript.tsx +38 -21
- 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 +92 -14
- package/src/components/terminal/markdown.tsx +4 -1
- package/src/components/terminal/shell-row.ts +17 -3
- package/src/index.ts +19 -4
- 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
|
@@ -1,24 +1,42 @@
|
|
|
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 {
|
|
8
|
+
import {
|
|
9
|
+
ActionPlacementProvider,
|
|
10
|
+
AgentWriteAction,
|
|
11
|
+
BookmarkAction,
|
|
12
|
+
CopyAction,
|
|
13
|
+
KillShellAction,
|
|
14
|
+
OpenShellAction,
|
|
15
|
+
WithActions,
|
|
16
|
+
} from './affordances.tsx'
|
|
9
17
|
import { TerminalDiff } from './diff.tsx'
|
|
10
18
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
11
19
|
import { usePeerNames } from './peer-names.tsx'
|
|
12
20
|
import { Pressable, useRevealOnOpen, type PressModifiers } from './press.tsx'
|
|
13
|
-
import { IMAGE_BOX_LINES, IMAGE_UNAVAILABLE, imagePlaceholder } from './image-box.ts'
|
|
21
|
+
import { IMAGE_BOX_LINES, IMAGE_UNAVAILABLE, baseName, hostImagePathOf, imagePlaceholder, resultImageName } from './image-box.ts'
|
|
14
22
|
import { collapsedResult } from './result-preview.ts'
|
|
15
23
|
import { useToolResultFetcher } from '../agent/tool-result-fetch.tsx'
|
|
16
24
|
import { useToolTitle } from '../agent/tool-titles.tsx'
|
|
17
|
-
import { useToolResultImageSrc } from '../agent/tool-result-image.tsx'
|
|
25
|
+
import { useHostImageSrc, useToolResultImageSrc, type ToolResultImageState } from '../agent/tool-result-image.tsx'
|
|
26
|
+
import { ViewableImage, useOpenImage } from '../agent/image-viewer.tsx'
|
|
18
27
|
import { isPeerSend, peerName, peerOneLine, peerSendTarget, peerSendText, planRun, runFailed, runSummary } from './tool-run.ts'
|
|
19
28
|
import { todoLine, todoPreview, type TodoPreview, type TodoStatus } from './todos.ts'
|
|
20
29
|
import { useShellActions } from '../agent/shell-actions.tsx'
|
|
21
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
SHELL_GLYPH,
|
|
32
|
+
shellAgentWriteLabel,
|
|
33
|
+
shellBodyLines,
|
|
34
|
+
shellFailed,
|
|
35
|
+
shellFooterText,
|
|
36
|
+
shellGrantable,
|
|
37
|
+
shellLabel,
|
|
38
|
+
shellStatusText,
|
|
39
|
+
} from './shell-row.ts'
|
|
22
40
|
import { type ToolCallItem } from './blocks.ts'
|
|
23
41
|
import { Band, Blank, Ink, Row, type Tone } from './row.tsx'
|
|
24
42
|
|
|
@@ -101,13 +119,24 @@ export function PeerSendRow({ item }: { item: ToolCallItem }) {
|
|
|
101
119
|
)
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
export function UserRow({
|
|
122
|
+
export function UserRow({
|
|
123
|
+
item,
|
|
124
|
+
attachmentUrl,
|
|
125
|
+
}: {
|
|
126
|
+
item: Extract<TranscriptItem, { kind: 'user' }>
|
|
127
|
+
attachmentUrl?: (attachmentId: string) => string
|
|
128
|
+
}) {
|
|
105
129
|
return (
|
|
106
130
|
<WithActions actions={<BookmarkAction id={item.id} />}>
|
|
107
131
|
<div className="term-user" data-peer={item.origin ? '' : undefined}>
|
|
108
132
|
{item.attachments?.length ? (
|
|
109
133
|
<Row glyph={PROMPT_GLYPH} glyphTone="dim" tone="dim">
|
|
110
|
-
{item.attachments.map((attachment) =>
|
|
134
|
+
{item.attachments.map((attachment, index) => (
|
|
135
|
+
<Fragment key={attachment.id}>
|
|
136
|
+
{index > 0 ? ', ' : null}
|
|
137
|
+
<AttachmentName attachment={attachment} href={attachmentUrl?.(attachment.id)} />
|
|
138
|
+
</Fragment>
|
|
139
|
+
))}
|
|
111
140
|
</Row>
|
|
112
141
|
) : null}
|
|
113
142
|
{item.text
|
|
@@ -195,6 +224,7 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
195
224
|
const copyable = typeof command === 'string' ? command : text
|
|
196
225
|
const todos = todoPreview(item.name, item.input)
|
|
197
226
|
const title = useToolTitle(item.name)
|
|
227
|
+
const hostPath = hostImagePathOf(item)
|
|
198
228
|
|
|
199
229
|
return (
|
|
200
230
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
@@ -216,8 +246,9 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
216
246
|
{item.backend && item.backend !== 'server' ? <Ink tone="faint"> · {item.backend}</Ink> : null}
|
|
217
247
|
</Row>
|
|
218
248
|
</Pressable>
|
|
249
|
+
{hostPath !== undefined ? <TerminalHostImage path={hostPath} /> : null}
|
|
219
250
|
{item.result?.images?.map((image) => (
|
|
220
|
-
<TerminalImage key={image.partIndex} toolUseId={item.id} image={image} />
|
|
251
|
+
<TerminalImage key={image.partIndex} toolUseId={item.id} name={resultImageName(item.name, image)} image={image} />
|
|
221
252
|
))}
|
|
222
253
|
{item.patch && !open ? (
|
|
223
254
|
<TerminalDiff patch={item.patch} />
|
|
@@ -298,10 +329,39 @@ function TerminalTodos({ preview }: { preview: TodoPreview }) {
|
|
|
298
329
|
)
|
|
299
330
|
}
|
|
300
331
|
|
|
332
|
+
function AttachmentName({ attachment, href }: { attachment: { name: string; mediaType: string }; href?: string }) {
|
|
333
|
+
const open = useOpenImage()
|
|
334
|
+
if (!open || !href || !attachment.mediaType.startsWith('image/')) {
|
|
335
|
+
return <>{attachment.name}</>
|
|
336
|
+
}
|
|
337
|
+
return (
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
className="term-press term-link"
|
|
341
|
+
onClick={(event) => {
|
|
342
|
+
event.stopPropagation()
|
|
343
|
+
open({ src: href, name: attachment.name })
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
{attachment.name}
|
|
347
|
+
</button>
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
301
351
|
type ToolResultImage = NonNullable<NonNullable<ToolCallItem['result']>['images']>[number]
|
|
302
352
|
|
|
303
|
-
function TerminalImage({ toolUseId, image }: { toolUseId: string; image: ToolResultImage }) {
|
|
304
|
-
const
|
|
353
|
+
function TerminalImage({ toolUseId, name, image }: { toolUseId: string; name: string; image: ToolResultImage }) {
|
|
354
|
+
const state = useToolResultImageSrc({ toolUseId, ...image })
|
|
355
|
+
return <TerminalImageBox state={state} name={name} placeholder={imagePlaceholder(image)} />
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function TerminalHostImage({ path }: { path: string }) {
|
|
359
|
+
const state = useHostImageSrc(path)
|
|
360
|
+
return <TerminalImageBox state={state} name={baseName(path)} placeholder={baseName(path)} />
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function TerminalImageBox({ state, name, placeholder }: { state: ToolResultImageState; name: string; placeholder: string }) {
|
|
364
|
+
const { src, failed } = state
|
|
305
365
|
return (
|
|
306
366
|
<Row indent={1} columns={3}>
|
|
307
367
|
<div
|
|
@@ -310,9 +370,11 @@ function TerminalImage({ toolUseId, image }: { toolUseId: string; image: ToolRes
|
|
|
310
370
|
style={{ height: `calc(var(--term-line) * ${IMAGE_BOX_LINES})` }}
|
|
311
371
|
>
|
|
312
372
|
{src ? (
|
|
313
|
-
<
|
|
373
|
+
<ViewableImage image={{ src, name }}>
|
|
374
|
+
<img src={src} alt={name} />
|
|
375
|
+
</ViewableImage>
|
|
314
376
|
) : (
|
|
315
|
-
<Ink tone="faint">{failed ? IMAGE_UNAVAILABLE :
|
|
377
|
+
<Ink tone="faint">{failed ? IMAGE_UNAVAILABLE : placeholder}</Ink>
|
|
316
378
|
)}
|
|
317
379
|
</div>
|
|
318
380
|
</Row>
|
|
@@ -338,8 +400,17 @@ export function ToolRunRow({ items }: { items: ToolCallItem[] }) {
|
|
|
338
400
|
</Pressable>
|
|
339
401
|
{open ? (
|
|
340
402
|
<div>
|
|
341
|
-
{items.map((item) => (
|
|
342
|
-
<
|
|
403
|
+
{items.map((item, index) => (
|
|
404
|
+
<Fragment key={item.id}>
|
|
405
|
+
{index > 0 ? <Blank /> : null}
|
|
406
|
+
{index < items.length - 1 ? (
|
|
407
|
+
<ActionPlacementProvider value="below">
|
|
408
|
+
<ToolRow item={item} />
|
|
409
|
+
</ActionPlacementProvider>
|
|
410
|
+
) : (
|
|
411
|
+
<ToolRow item={item} />
|
|
412
|
+
)}
|
|
413
|
+
</Fragment>
|
|
343
414
|
))}
|
|
344
415
|
</div>
|
|
345
416
|
) : null}
|
|
@@ -427,6 +498,13 @@ export function ShellRow({ item }: { item: ShellItem }) {
|
|
|
427
498
|
actions={
|
|
428
499
|
<>
|
|
429
500
|
{actions.open ? <OpenShellAction onOpen={() => actions.open?.(shellId)} /> : null}
|
|
501
|
+
{actions.agentWrite && shellGrantable(item.shell) ? (
|
|
502
|
+
<AgentWriteAction
|
|
503
|
+
granted={item.shell.agentWrite === true}
|
|
504
|
+
label={shellAgentWriteLabel(item.shell)}
|
|
505
|
+
onToggle={() => void actions.agentWrite?.(shellId, item.shell.agentWrite !== true)}
|
|
506
|
+
/>
|
|
507
|
+
) : null}
|
|
430
508
|
{running ? <KillShellAction onKill={() => void actions.kill(shellId)} /> : null}
|
|
431
509
|
<BookmarkAction id={item.id} />
|
|
432
510
|
<CopyAction text={item.shell.command} label="Copy command" />
|
|
@@ -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}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { SHELL_INTERACTIVE_NOTE, type ShellInfo } from '@workerdeck/protocol'
|
|
2
2
|
import type { ShellItem } from '@workerdeck/react'
|
|
3
3
|
|
|
4
4
|
export const SHELL_GLYPH = '$'
|
|
@@ -9,6 +9,17 @@ export const SHELL_EXPAND_CHARS = 100_000
|
|
|
9
9
|
|
|
10
10
|
export const SHELL_MISSING = 'output expired or not tracked by this gateway'
|
|
11
11
|
|
|
12
|
+
export const SHELL_AGENT_WRITE_NOTE = 'agent may type'
|
|
13
|
+
|
|
14
|
+
// Only a running shell the user started takes a grant: the agent already drives its own.
|
|
15
|
+
export function shellGrantable(shell: ShellInfo): boolean {
|
|
16
|
+
return shell.status === 'running' && shell.owner === 'user'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function shellAgentWriteLabel(shell: ShellInfo): string {
|
|
20
|
+
return shell.agentWrite === true ? 'Revoke the agent’s access to this shell' : 'Let the agent type into this shell'
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export function shellInfoLabel(shell: ShellInfo): string {
|
|
13
24
|
return shell.label || shell.command.split('\n')[0] || ''
|
|
14
25
|
}
|
|
@@ -31,7 +42,7 @@ export function shellInfoFailed(shell: ShellInfo): boolean {
|
|
|
31
42
|
|
|
32
43
|
export function shellInfoStatusText(shell: ShellInfo): string {
|
|
33
44
|
if (shell.status === 'running') {
|
|
34
|
-
return 'running'
|
|
45
|
+
return shell.agentWrite === true ? `running · ${SHELL_AGENT_WRITE_NOTE}` : 'running'
|
|
35
46
|
}
|
|
36
47
|
if (typeof shell.exitCode === 'number') {
|
|
37
48
|
return shell.exitCode === 0 ? 'exit 0' : `exit ${shell.exitCode}`
|
|
@@ -68,7 +79,7 @@ export function shellHeaderText(item: ShellItem): string {
|
|
|
68
79
|
|
|
69
80
|
export function shellBodyLines(item: ShellItem, open: boolean): string[] {
|
|
70
81
|
const source = open && item.expanded !== undefined ? item.expanded : item.text
|
|
71
|
-
if (source === '') {
|
|
82
|
+
if (source === '' || item.shell.interactive) {
|
|
72
83
|
return []
|
|
73
84
|
}
|
|
74
85
|
const lines = source.split('\n')
|
|
@@ -89,6 +100,9 @@ export function shellBodyLines(item: ShellItem, open: boolean): string[] {
|
|
|
89
100
|
|
|
90
101
|
// The footer is the row's one affordance line: what expanding will fetch, what it clipped, or why it cannot.
|
|
91
102
|
export function shellFooterText(item: ShellItem, open: boolean, shown: number): string | undefined {
|
|
103
|
+
if (item.shell.interactive) {
|
|
104
|
+
return SHELL_INTERACTIVE_NOTE
|
|
105
|
+
}
|
|
92
106
|
if (item.missing) {
|
|
93
107
|
return SHELL_MISSING
|
|
94
108
|
}
|
package/src/index.ts
CHANGED
|
@@ -60,7 +60,14 @@ export { TerminalQuestionPrompt, type TerminalQuestionPromptProps } from './comp
|
|
|
60
60
|
export { TerminalDiff, previewPatch } from './components/terminal/diff.tsx'
|
|
61
61
|
export { TerminalMarkdown, type TerminalMarkdownProps } from './components/terminal/markdown.tsx'
|
|
62
62
|
export { Band, Blank, Ink, Row, type RowProps, type Tone } from './components/terminal/row.tsx'
|
|
63
|
-
export {
|
|
63
|
+
export {
|
|
64
|
+
AgentWriteIcon,
|
|
65
|
+
CopyAction,
|
|
66
|
+
WithActions,
|
|
67
|
+
useAffordances,
|
|
68
|
+
type ActionPlacement,
|
|
69
|
+
type TerminalAffordances,
|
|
70
|
+
} from './components/terminal/affordances.tsx'
|
|
64
71
|
export { FileLinkProvider, useFileLinks, type FileLinkHandle, type FileLinkOpener } from './components/terminal/file-link.tsx'
|
|
65
72
|
export { parseFileLink, type FileLink } from './lib/file-link.ts'
|
|
66
73
|
export { matchPath, type PathHit, type PathMatch } from './lib/path-match.ts'
|
|
@@ -68,11 +75,8 @@ export { usePathLinks, resolveAgainstCwd } from './components/agent/use-path-lin
|
|
|
68
75
|
export { Conversation, ConversationContent, ConversationScrollButton, type ConversationProps } from './components/agent/Conversation.tsx'
|
|
69
76
|
export { Message, MessageContent, type MessageProps } from './components/agent/Message.tsx'
|
|
70
77
|
export {
|
|
71
|
-
TranscriptDensityProvider,
|
|
72
78
|
TranscriptVariantProvider,
|
|
73
|
-
useTranscriptDensity,
|
|
74
79
|
useTranscriptVariant,
|
|
75
|
-
type TranscriptDensity,
|
|
76
80
|
type TranscriptFont,
|
|
77
81
|
type TranscriptVariant,
|
|
78
82
|
} from './components/agent/transcript-variant.tsx'
|
|
@@ -81,6 +85,14 @@ export { Reasoning, type ReasoningProps } from './components/agent/Reasoning.tsx
|
|
|
81
85
|
export { Loader } from './components/agent/Loader.tsx'
|
|
82
86
|
export { ToolCallCard, type ToolCallCardProps, type ToolCallItem } from './components/agent/ToolCallCard.tsx'
|
|
83
87
|
export { ToolTitleProvider, useToolTitle } from './components/agent/tool-titles.tsx'
|
|
88
|
+
export {
|
|
89
|
+
ImageViewer,
|
|
90
|
+
ImageViewerProvider,
|
|
91
|
+
ViewableImage,
|
|
92
|
+
useOpenImage,
|
|
93
|
+
type OpenImage,
|
|
94
|
+
type ViewedImage,
|
|
95
|
+
} from './components/agent/image-viewer.tsx'
|
|
84
96
|
export { FileCard, type FileCardProps, type FileDeliveredItem } from './components/agent/FileCard.tsx'
|
|
85
97
|
export { PermissionPrompt, type PermissionPromptProps } from './components/agent/PermissionPrompt.tsx'
|
|
86
98
|
export {
|
|
@@ -138,8 +150,11 @@ export { ShellStrip, type ShellStripProps } from './components/agent/ShellStrip.
|
|
|
138
150
|
export { useShellFrame, type ShellFrame, type ShellFrameOptions } from './components/agent/use-shell-frame.ts'
|
|
139
151
|
export { ShellActionsProvider, useShellActions, type ShellActions } from './components/agent/shell-actions.tsx'
|
|
140
152
|
export {
|
|
153
|
+
SHELL_AGENT_WRITE_NOTE,
|
|
141
154
|
SHELL_GLYPH,
|
|
142
155
|
SHELL_KILL_GLYPH,
|
|
156
|
+
shellAgentWriteLabel,
|
|
157
|
+
shellGrantable,
|
|
143
158
|
shellInfoFailed,
|
|
144
159
|
shellInfoLabel,
|
|
145
160
|
shellInfoStatusText,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The agent's shell write tools on a permission card. Allow-lists key on the tool name, and the danger is in the
|
|
2
|
+
// payload, so the card shows the command, the typed text and the keys verbatim rather than a JSON summary.
|
|
3
|
+
export type ShellRequestPayload =
|
|
4
|
+
| { kind: 'run'; command: string }
|
|
5
|
+
| { kind: 'write'; shellId: string; data?: string; keys?: string[] }
|
|
6
|
+
| { kind: 'kill'; shellId: string }
|
|
7
|
+
| { kind: 'grant'; shellId: string; reason: string }
|
|
8
|
+
|
|
9
|
+
type RequestLike = { toolName?: unknown; input?: unknown }
|
|
10
|
+
|
|
11
|
+
const CONTROL_NAMES: Record<string, string> = {
|
|
12
|
+
'\r': 'enter',
|
|
13
|
+
'\n': 'newline',
|
|
14
|
+
'\t': 'tab',
|
|
15
|
+
'\x1b': 'escape',
|
|
16
|
+
'\x7f': 'backspace',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ShellWriteTool = 'shell_run' | 'shell_write' | 'shell_kill' | 'shell_request_write'
|
|
20
|
+
|
|
21
|
+
const SHELL_WRITE_TOOLS: readonly string[] = ['shell_run', 'shell_write', 'shell_kill', 'shell_request_write']
|
|
22
|
+
|
|
23
|
+
export function shellToolOf(toolName: unknown): ShellWriteTool | undefined {
|
|
24
|
+
if (typeof toolName !== 'string') {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
const bare = toolName.startsWith('mcp__') ? toolName.slice(toolName.indexOf('__', 5) + 2) : toolName
|
|
28
|
+
return SHELL_WRITE_TOOLS.includes(bare) ? (bare as ShellWriteTool) : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function shellRequestPayload(request: RequestLike | null | undefined): ShellRequestPayload | undefined {
|
|
32
|
+
const tool = shellToolOf(request?.toolName)
|
|
33
|
+
if (!tool) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
const input = request?.input !== null && typeof request?.input === 'object' ? (request.input as Record<string, unknown>) : {}
|
|
37
|
+
const shellId = typeof input.shellId === 'string' ? input.shellId : ''
|
|
38
|
+
switch (tool) {
|
|
39
|
+
case 'shell_run': {
|
|
40
|
+
return { kind: 'run', command: typeof input.command === 'string' ? input.command : '' }
|
|
41
|
+
}
|
|
42
|
+
case 'shell_write': {
|
|
43
|
+
const keys = Array.isArray(input.keys) ? input.keys.filter((key): key is string => typeof key === 'string') : undefined
|
|
44
|
+
return {
|
|
45
|
+
kind: 'write',
|
|
46
|
+
shellId,
|
|
47
|
+
...(typeof input.data === 'string' ? { data: input.data } : {}),
|
|
48
|
+
...(keys && keys.length > 0 ? { keys } : {}),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
case 'shell_kill': {
|
|
52
|
+
return { kind: 'kill', shellId }
|
|
53
|
+
}
|
|
54
|
+
case 'shell_request_write': {
|
|
55
|
+
return { kind: 'grant', shellId, reason: typeof input.reason === 'string' ? input.reason : '' }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shellRequestTitle(payload: ShellRequestPayload): string {
|
|
61
|
+
switch (payload.kind) {
|
|
62
|
+
case 'run': {
|
|
63
|
+
return 'Agent wants to start a shell'
|
|
64
|
+
}
|
|
65
|
+
case 'write': {
|
|
66
|
+
return `Agent wants to type into shell ${payload.shellId}`
|
|
67
|
+
}
|
|
68
|
+
case 'kill': {
|
|
69
|
+
return `Agent wants to kill shell ${payload.shellId}`
|
|
70
|
+
}
|
|
71
|
+
case 'grant': {
|
|
72
|
+
return `Agent asks to type into your shell ${payload.shellId}`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The lines drawn verbatim under the title. Typed text keeps its shape; a control character is named in brackets
|
|
78
|
+
// so a hidden Enter or Escape is visible on the card.
|
|
79
|
+
export function shellRequestLines(payload: ShellRequestPayload): string[] {
|
|
80
|
+
switch (payload.kind) {
|
|
81
|
+
case 'run': {
|
|
82
|
+
return payload.command.split('\n').map((line, index) => (index === 0 ? `$ ${line}` : ` ${line}`))
|
|
83
|
+
}
|
|
84
|
+
case 'write': {
|
|
85
|
+
const lines: string[] = []
|
|
86
|
+
if (payload.data !== undefined) {
|
|
87
|
+
lines.push(`types: ${visibleControls(payload.data)}`)
|
|
88
|
+
}
|
|
89
|
+
if (payload.keys) {
|
|
90
|
+
lines.push(`presses: ${payload.keys.map((key) => `[${key}]`).join(' ')}`)
|
|
91
|
+
}
|
|
92
|
+
return lines
|
|
93
|
+
}
|
|
94
|
+
case 'kill': {
|
|
95
|
+
return [`kill ${payload.shellId}`]
|
|
96
|
+
}
|
|
97
|
+
case 'grant': {
|
|
98
|
+
return [`why: ${visibleControls(payload.reason)}`, 'until the shell ends or you revoke it']
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function visibleControls(text: string): string {
|
|
104
|
+
let out = ''
|
|
105
|
+
for (const char of text) {
|
|
106
|
+
const code = char.codePointAt(0)!
|
|
107
|
+
if (code >= 0x20 && code !== 0x7f) {
|
|
108
|
+
out += char
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
const named = CONTROL_NAMES[char]
|
|
112
|
+
out += named ? `[${named}]` : `[ctrl-${String.fromCharCode(code + 64).toLowerCase()}]`
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
}
|
package/src/lib/tool-icon.ts
CHANGED
|
@@ -22,7 +22,19 @@ export function toolIcon(toolName: string): LucideIcon {
|
|
|
22
22
|
switch (toolName) {
|
|
23
23
|
case 'Bash':
|
|
24
24
|
case 'BashOutput':
|
|
25
|
-
case 'KillShell':
|
|
25
|
+
case 'KillShell':
|
|
26
|
+
case 'shell_list':
|
|
27
|
+
case 'shell_read':
|
|
28
|
+
case 'shell_run':
|
|
29
|
+
case 'shell_write':
|
|
30
|
+
case 'shell_kill':
|
|
31
|
+
case 'shell_request_write':
|
|
32
|
+
case 'mcp__workerdeck__shell_list':
|
|
33
|
+
case 'mcp__workerdeck__shell_read':
|
|
34
|
+
case 'mcp__workerdeck__shell_run':
|
|
35
|
+
case 'mcp__workerdeck__shell_write':
|
|
36
|
+
case 'mcp__workerdeck__shell_kill':
|
|
37
|
+
case 'mcp__workerdeck__shell_request_write': {
|
|
26
38
|
return Terminal
|
|
27
39
|
}
|
|
28
40
|
case 'Read': {
|
package/src/styles/terminal.css
CHANGED
|
@@ -436,46 +436,144 @@
|
|
|
436
436
|
.term-hoverable {
|
|
437
437
|
position: relative;
|
|
438
438
|
}
|
|
439
|
-
/*
|
|
440
|
-
|
|
441
|
-
|
|
439
|
+
/* `below` sits in the blank line (cards: the row gap) under the block; `inline`
|
|
440
|
+
pins to the block's first line at its right edge, for blocks with no blank
|
|
441
|
+
line beneath them. Absolute either way, so no row moves when they appear. */
|
|
442
|
+
:is([data-terminal], [data-cards]) .term-actions {
|
|
442
443
|
position: absolute;
|
|
443
|
-
|
|
444
|
-
right: 0;
|
|
445
|
-
height: var(--term-line);
|
|
444
|
+
z-index: 1;
|
|
446
445
|
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
opacity: 0;
|
|
448
|
+
transition: opacity var(--motion-fast, 120ms);
|
|
449
|
+
}
|
|
450
|
+
[data-terminal] .term-actions {
|
|
451
|
+
height: var(--term-line);
|
|
447
452
|
gap: 1ch;
|
|
453
|
+
}
|
|
454
|
+
[data-cards] .term-actions {
|
|
455
|
+
height: 20px;
|
|
456
|
+
gap: 6px;
|
|
457
|
+
}
|
|
458
|
+
:is([data-terminal], [data-cards]) [data-placement='below'] > .term-actions {
|
|
459
|
+
top: 100%;
|
|
460
|
+
}
|
|
461
|
+
[data-terminal] [data-placement='below'] > .term-actions {
|
|
462
|
+
left: var(--term-cell);
|
|
463
|
+
}
|
|
464
|
+
[data-cards] [data-placement='below'] > .term-actions {
|
|
465
|
+
left: 0;
|
|
466
|
+
}
|
|
467
|
+
:is([data-terminal], [data-cards]) [data-placement='inline'] > .term-actions {
|
|
468
|
+
top: 0;
|
|
469
|
+
right: 0;
|
|
448
470
|
padding-inline: 1ch;
|
|
449
471
|
background: var(--bg, transparent);
|
|
450
|
-
opacity: 0;
|
|
451
|
-
transition: opacity var(--motion-fast, 120ms);
|
|
452
472
|
}
|
|
453
|
-
|
|
454
|
-
|
|
473
|
+
/* Only the innermost `below` block under the pointer shows its bar, so an
|
|
474
|
+
expanded chain does not light up its own and its parent's at once. */
|
|
475
|
+
:is([data-terminal], [data-cards])
|
|
476
|
+
[data-placement='below']:is(:hover, :focus-within):not(:has([data-placement='below']:is(:hover, :focus-within)))
|
|
477
|
+
> .term-actions,
|
|
478
|
+
:is([data-terminal], [data-cards]) [data-placement='inline']:is(:hover, :focus-within) > .term-actions {
|
|
455
479
|
opacity: 1;
|
|
456
480
|
}
|
|
481
|
+
/* A virtualized row paints under the next one, whose top padding is the gap
|
|
482
|
+
the bar sits in; lift the hovered row so the bar stays clickable. */
|
|
483
|
+
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:not(.absolute) {
|
|
484
|
+
position: relative;
|
|
485
|
+
}
|
|
486
|
+
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:is(:hover, :focus-within) {
|
|
487
|
+
z-index: 1;
|
|
488
|
+
}
|
|
457
489
|
@media (hover: none) {
|
|
458
|
-
|
|
459
|
-
all, and a phone has the room. */
|
|
460
|
-
[data-terminal] .term-actions {
|
|
490
|
+
:is([data-terminal], [data-cards]) .term-actions {
|
|
461
491
|
opacity: 1;
|
|
462
492
|
}
|
|
463
493
|
}
|
|
494
|
+
[data-cards] .term-action {
|
|
495
|
+
font: inherit;
|
|
496
|
+
font-size: 12px;
|
|
497
|
+
font-weight: 500;
|
|
498
|
+
line-height: 1;
|
|
499
|
+
height: 18px;
|
|
500
|
+
min-width: 22px;
|
|
501
|
+
display: inline-flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
justify-content: center;
|
|
504
|
+
gap: 4px;
|
|
505
|
+
padding-inline: 5px;
|
|
506
|
+
color: var(--fg-3);
|
|
507
|
+
background: var(--surface);
|
|
508
|
+
border: 1px solid var(--border);
|
|
509
|
+
border-radius: 4px;
|
|
510
|
+
white-space: nowrap;
|
|
511
|
+
cursor: pointer;
|
|
512
|
+
}
|
|
513
|
+
[data-cards] .term-action-icon {
|
|
514
|
+
width: 14px;
|
|
515
|
+
height: 14px;
|
|
516
|
+
stroke-width: 2.25;
|
|
517
|
+
flex: none;
|
|
518
|
+
}
|
|
519
|
+
[data-cards] .term-action-word {
|
|
520
|
+
padding-right: 2px;
|
|
521
|
+
}
|
|
522
|
+
[data-cards] .term-action:hover,
|
|
523
|
+
[data-cards] .term-action:focus-visible {
|
|
524
|
+
color: var(--fg-1);
|
|
525
|
+
background: var(--row-hover);
|
|
526
|
+
}
|
|
527
|
+
[data-cards] .term-action[data-tone='magenta'] {
|
|
528
|
+
color: var(--wd-shell-accent);
|
|
529
|
+
}
|
|
530
|
+
[data-cards] .term-action[data-tone='yellow'] {
|
|
531
|
+
color: var(--warning);
|
|
532
|
+
}
|
|
533
|
+
[data-cards] .term-action[data-tone='danger']:hover {
|
|
534
|
+
color: var(--danger);
|
|
535
|
+
}
|
|
464
536
|
[data-terminal] .term-action {
|
|
465
537
|
font: inherit;
|
|
466
|
-
|
|
538
|
+
font-weight: 500;
|
|
539
|
+
line-height: 1;
|
|
540
|
+
height: calc(var(--term-line) - 2px);
|
|
541
|
+
min-width: 3ch;
|
|
542
|
+
display: inline-flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
justify-content: center;
|
|
545
|
+
gap: 0.6ch;
|
|
546
|
+
padding-inline: 0.6ch;
|
|
467
547
|
color: var(--term-dim);
|
|
468
|
-
background:
|
|
469
|
-
border:
|
|
470
|
-
|
|
548
|
+
background: var(--term-band-bg);
|
|
549
|
+
border: 1px solid color-mix(in srgb, var(--term-faint) 55%, transparent);
|
|
550
|
+
border-radius: 3px;
|
|
551
|
+
white-space: pre;
|
|
471
552
|
cursor: pointer;
|
|
472
553
|
}
|
|
473
|
-
|
|
554
|
+
[data-terminal] .term-action-icon {
|
|
555
|
+
width: calc(var(--term-font-size) + 1px);
|
|
556
|
+
height: calc(var(--term-font-size) + 1px);
|
|
557
|
+
stroke-width: 2.25;
|
|
558
|
+
flex: none;
|
|
559
|
+
}
|
|
560
|
+
[data-terminal] .term-action-word {
|
|
561
|
+
padding-right: 0.2ch;
|
|
562
|
+
}
|
|
563
|
+
[data-terminal] .term-action:hover,
|
|
564
|
+
[data-terminal] .term-action:focus-visible {
|
|
565
|
+
color: var(--term-bright);
|
|
566
|
+
border-color: var(--term-dim);
|
|
567
|
+
background: var(--term-row-hover);
|
|
568
|
+
}
|
|
474
569
|
[data-terminal] .term-action[data-tone='magenta'] {
|
|
475
570
|
color: var(--term-magenta);
|
|
476
571
|
}
|
|
477
|
-
[data-terminal] .term-action
|
|
478
|
-
color: var(--term-
|
|
572
|
+
[data-terminal] .term-action[data-tone='yellow'] {
|
|
573
|
+
color: var(--term-yellow);
|
|
574
|
+
}
|
|
575
|
+
[data-terminal] .term-action[data-tone='danger']:hover {
|
|
576
|
+
color: var(--term-red);
|
|
479
577
|
}
|
|
480
578
|
|
|
481
579
|
/* What you typed, on a band of its own - the CLI's own treatment. */
|
|
@@ -714,7 +812,7 @@ button.term-link {
|
|
|
714
812
|
[data-terminal] [data-sticky-lane] > [data-sticky-headlane] {
|
|
715
813
|
position: absolute;
|
|
716
814
|
inset: 0;
|
|
717
|
-
z-index:
|
|
815
|
+
z-index: 2;
|
|
718
816
|
pointer-events: none;
|
|
719
817
|
}
|
|
720
818
|
|
|
@@ -1276,7 +1374,16 @@ button.term-link {
|
|
|
1276
1374
|
display: block;
|
|
1277
1375
|
overflow: hidden;
|
|
1278
1376
|
}
|
|
1279
|
-
.term-image >
|
|
1377
|
+
.term-image > [data-slot='viewable-image'] {
|
|
1378
|
+
display: block;
|
|
1379
|
+
height: 100%;
|
|
1380
|
+
max-width: 100%;
|
|
1381
|
+
padding: 0;
|
|
1382
|
+
border: 0;
|
|
1383
|
+
background: none;
|
|
1384
|
+
}
|
|
1385
|
+
.term-image img {
|
|
1386
|
+
display: block;
|
|
1280
1387
|
max-width: 100%;
|
|
1281
1388
|
height: 100%;
|
|
1282
1389
|
object-fit: contain;
|
package/src/styles/theme.css
CHANGED
|
@@ -821,6 +821,10 @@
|
|
|
821
821
|
left: 0;
|
|
822
822
|
width: 12px;
|
|
823
823
|
}
|
|
824
|
+
/* xterm.css paints its viewport #000 whatever the theme says; the drill-in shows the panel through. */
|
|
825
|
+
.wd-shell-term .xterm .xterm-viewport {
|
|
826
|
+
background-color: transparent;
|
|
827
|
+
}
|
|
824
828
|
.wd-scrubber .wd-scrub-mark[data-kind='user'] {
|
|
825
829
|
background: var(--accent);
|
|
826
830
|
}
|