@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
|
@@ -1,110 +1,15 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { useStickToBottomContext } from 'use-stick-to-bottom'
|
|
3
3
|
import type { PermissionRequest } from '@workerdeck/protocol'
|
|
4
4
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { approvalCluster, buildMarks, clusterMarks, MIN_MARK, nearestMember, type Cluster, type Mark } from '../agent/scrubber-marks.ts'
|
|
6
|
+
import { peekContent, TERMINAL_PEEK } from '../agent/scrubber-peek.tsx'
|
|
7
7
|
import { TerminalSurface } from './surface.tsx'
|
|
8
8
|
|
|
9
|
-
type Lane = 'l' | 'r' | 'f'
|
|
10
|
-
type MarkKind = 'user' | 'subagent' | 'shell' | 'turn' | 'turnFailed' | 'toolFailed' | 'error' | 'approval' | 'recap' | 'bookmark'
|
|
11
|
-
|
|
12
|
-
type Mark = {
|
|
13
|
-
kind: MarkKind
|
|
14
|
-
itemIndex: number
|
|
15
|
-
rowIndex: number
|
|
16
|
-
turnIndex?: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type Cluster = { lane: Lane; kind: MarkKind; y: number; h: number; marks: { mark: Mark; y: number }[] }
|
|
20
|
-
|
|
21
|
-
function nearestMember(cluster: Cluster, y: number): Mark | undefined {
|
|
22
|
-
let best: { mark: Mark; y: number } | undefined
|
|
23
|
-
for (const member of cluster.marks) {
|
|
24
|
-
if (!best || Math.abs(member.y - y) < Math.abs(best.y - y)) {
|
|
25
|
-
best = member
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return best?.mark
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const LANE: Record<MarkKind, Lane> = {
|
|
32
|
-
user: 'l',
|
|
33
|
-
subagent: 'l',
|
|
34
|
-
shell: 'l',
|
|
35
|
-
turn: 'r',
|
|
36
|
-
turnFailed: 'r',
|
|
37
|
-
toolFailed: 'r',
|
|
38
|
-
error: 'r',
|
|
39
|
-
approval: 'f',
|
|
40
|
-
recap: 'f',
|
|
41
|
-
bookmark: 'f',
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const LOUDNESS: Record<MarkKind, number> = {
|
|
45
|
-
approval: 7,
|
|
46
|
-
error: 6,
|
|
47
|
-
turnFailed: 5,
|
|
48
|
-
toolFailed: 4,
|
|
49
|
-
user: 3,
|
|
50
|
-
turn: 2,
|
|
51
|
-
bookmark: 1,
|
|
52
|
-
subagent: 1,
|
|
53
|
-
shell: 1,
|
|
54
|
-
recap: 0,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const KIND_NAME: Record<MarkKind, string> = {
|
|
58
|
-
user: 'you',
|
|
59
|
-
subagent: 'sub-agent',
|
|
60
|
-
shell: 'shell',
|
|
61
|
-
turn: 'response · turn end',
|
|
62
|
-
turnFailed: 'turn failed',
|
|
63
|
-
toolFailed: 'tool failed',
|
|
64
|
-
error: 'error',
|
|
65
|
-
approval: 'pending approval',
|
|
66
|
-
recap: 'catch-up boundary',
|
|
67
|
-
bookmark: 'bookmark',
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const MIN_MARK = 2
|
|
71
|
-
|
|
72
9
|
export function railScale(railH: number, totalSize: number, viewportH: number): number {
|
|
73
10
|
return totalSize > 0 ? railH / Math.max(totalSize, viewportH) : 0
|
|
74
11
|
}
|
|
75
12
|
|
|
76
|
-
type Segment = { response?: number; turn?: number; failed?: boolean }
|
|
77
|
-
|
|
78
|
-
function doneLine(turn: Extract<TranscriptItem, { kind: 'turn_result' }>): string {
|
|
79
|
-
return `${turn.isError ? turn.subtype : 'done'} · ${formatDuration(turn.durationMs)} · ${formatCost(turn.totalCostUsd)}`
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function excerpt(item: TranscriptItem): string {
|
|
83
|
-
switch (item.kind) {
|
|
84
|
-
case 'user':
|
|
85
|
-
case 'assistant_text':
|
|
86
|
-
case 'thinking':
|
|
87
|
-
case 'notice': {
|
|
88
|
-
return item.text
|
|
89
|
-
}
|
|
90
|
-
case 'shell': {
|
|
91
|
-
return item.shell.command
|
|
92
|
-
}
|
|
93
|
-
case 'tool_call': {
|
|
94
|
-
return `${item.name}(${toolInputPreview(item.input)})`
|
|
95
|
-
}
|
|
96
|
-
case 'turn_result': {
|
|
97
|
-
return doneLine(item)
|
|
98
|
-
}
|
|
99
|
-
case 'file_delivered': {
|
|
100
|
-
return item.path
|
|
101
|
-
}
|
|
102
|
-
default: {
|
|
103
|
-
return ''
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
13
|
export interface TerminalScrubberProps {
|
|
109
14
|
items: readonly TranscriptItem[]
|
|
110
15
|
pendingApprovals: readonly PermissionRequest[]
|
|
@@ -139,189 +44,28 @@ export function buildClusters(props: TerminalScrubberProps, railH: number): Clus
|
|
|
139
44
|
totalSize,
|
|
140
45
|
viewportH,
|
|
141
46
|
} = props
|
|
142
|
-
const marks: Mark[] =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (parent !== undefined) {
|
|
147
|
-
subagentParents.add(parent)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const rowOutcome = new Map<number, number>()
|
|
151
|
-
items.forEach((item, index) => {
|
|
152
|
-
if (item.kind !== 'tool_call' || parentOf(item) !== frameParentId) {
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
rowOutcome.set(rowIndexFor(index), index)
|
|
156
|
-
})
|
|
157
|
-
let segment: Segment = {}
|
|
158
|
-
const closeSegment = () => {
|
|
159
|
-
const anchor = segment.response ?? segment.turn
|
|
160
|
-
if (anchor !== undefined) {
|
|
161
|
-
marks.push({
|
|
162
|
-
kind: segment.failed ? 'turnFailed' : 'turn',
|
|
163
|
-
itemIndex: anchor,
|
|
164
|
-
rowIndex: rowIndexFor(anchor),
|
|
165
|
-
turnIndex: segment.turn,
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
segment = {}
|
|
169
|
-
}
|
|
170
|
-
items.forEach((item, index) => {
|
|
171
|
-
if (item.kind === 'tool_call' && subagentParents.has(item.id)) {
|
|
172
|
-
marks.push({ kind: 'subagent', itemIndex: index, rowIndex: rowIndexFor(index) })
|
|
173
|
-
}
|
|
174
|
-
if (item.kind === 'user' && parentOf(item) === frameParentId) {
|
|
175
|
-
closeSegment()
|
|
176
|
-
marks.push({ kind: 'user', itemIndex: index, rowIndex: rowIndexFor(index) })
|
|
177
|
-
} else if (item.kind === 'turn_result') {
|
|
178
|
-
segment.turn = index
|
|
179
|
-
segment.failed = item.isError
|
|
180
|
-
closeSegment()
|
|
181
|
-
} else if (item.kind === 'notice' && item.level === 'error') {
|
|
182
|
-
marks.push({ kind: 'error', itemIndex: index, rowIndex: rowIndexFor(index) })
|
|
183
|
-
} else if (item.kind === 'shell') {
|
|
184
|
-
marks.push({ kind: 'shell', itemIndex: index, rowIndex: rowIndexFor(index) })
|
|
185
|
-
} else if (
|
|
186
|
-
item.kind === 'tool_call' &&
|
|
187
|
-
(item.status === 'failed' || item.result?.isError === true) &&
|
|
188
|
-
rowOutcome.get(rowIndexFor(index)) === index
|
|
189
|
-
) {
|
|
190
|
-
marks.push({ kind: 'toolFailed', itemIndex: index, rowIndex: rowIndexFor(index) })
|
|
191
|
-
} else if (item.kind === 'assistant_text' && parentOf(item) === frameParentId) {
|
|
192
|
-
if (frameParentId !== undefined) {
|
|
193
|
-
marks.push({ kind: 'turn', itemIndex: index, rowIndex: rowIndexFor(index) })
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
segment.response = index
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
closeSegment()
|
|
200
|
-
for (const index of bookmarks) {
|
|
201
|
-
if (index >= 0 && index < items.length) {
|
|
202
|
-
marks.push({ kind: 'bookmark', itemIndex: index, rowIndex: rowIndexFor(index) })
|
|
203
|
-
}
|
|
204
|
-
}
|
|
47
|
+
const marks: Mark[] = buildMarks(items, { frameParentId, bookmarks, rowIndexFor }).map((mark) => ({
|
|
48
|
+
...mark,
|
|
49
|
+
rowIndex: rowIndexFor(mark.itemIndex),
|
|
50
|
+
}))
|
|
205
51
|
if (recapRow) {
|
|
206
52
|
marks.push({ kind: 'recap', itemIndex: -1, rowIndex: recapRow.rowIndex })
|
|
207
53
|
}
|
|
208
|
-
|
|
209
54
|
const scale = railScale(railH, totalSize, viewportH)
|
|
210
|
-
const
|
|
211
|
-
|
|
55
|
+
const clusters = clusterMarks(marks, (mark) => {
|
|
56
|
+
const rowIndex = mark.rowIndex ?? rowIndexFor(mark.itemIndex)
|
|
212
57
|
const within = mark.itemIndex >= 0 ? positionInRow?.(mark.itemIndex) : undefined
|
|
213
|
-
const rowH = sizeOfRow(
|
|
58
|
+
const rowH = sizeOfRow(rowIndex)
|
|
214
59
|
const h = within ? MIN_MARK : Math.max(MIN_MARK, Math.round(rowH * scale))
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)
|
|
219
|
-
const lane = LANE[mark.kind]
|
|
220
|
-
const list = lanes.get(lane) ?? []
|
|
221
|
-
list.push({ mark, y, h })
|
|
222
|
-
lanes.set(lane, list)
|
|
223
|
-
}
|
|
224
|
-
const clusters: Cluster[] = []
|
|
225
|
-
for (const [lane, list] of lanes) {
|
|
226
|
-
list.sort((a, b) => a.y - b.y)
|
|
227
|
-
let current: Cluster | null = null
|
|
228
|
-
for (const { mark, y, h } of list) {
|
|
229
|
-
if (current && y <= current.y + current.h + 1) {
|
|
230
|
-
current.h = Math.max(current.h, y + h - current.y)
|
|
231
|
-
if (LOUDNESS[mark.kind] > LOUDNESS[current.kind]) {
|
|
232
|
-
current.kind = mark.kind
|
|
233
|
-
}
|
|
234
|
-
current.marks.push({ mark, y })
|
|
235
|
-
} else {
|
|
236
|
-
current = { lane, kind: mark.kind, y, h, marks: [{ mark, y }] }
|
|
237
|
-
clusters.push(current)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
60
|
+
const offset = offsetOfRow(rowIndex) + (within ? (within.ordinal / within.count) * rowH : 0)
|
|
61
|
+
return { y: Math.min(Math.max(0, railH - h), Math.round(offset * scale)), h }
|
|
62
|
+
})
|
|
241
63
|
if (pendingApprovals.length > 0) {
|
|
242
|
-
clusters.push(
|
|
243
|
-
lane: LANE.approval,
|
|
244
|
-
kind: 'approval',
|
|
245
|
-
y: Math.max(0, railH - MIN_MARK),
|
|
246
|
-
h: MIN_MARK,
|
|
247
|
-
marks: [],
|
|
248
|
-
})
|
|
64
|
+
clusters.push(approvalCluster(railH))
|
|
249
65
|
}
|
|
250
66
|
return clusters
|
|
251
67
|
}
|
|
252
68
|
|
|
253
|
-
function peekContent(cluster: Cluster, first: Mark | undefined, { items, pendingApprovals, recapRow }: TerminalScrubberProps): ReactNode {
|
|
254
|
-
const more = cluster.marks.length > 1 ? ` · ${cluster.marks.length} marks` : ''
|
|
255
|
-
let body: ReactNode = null
|
|
256
|
-
if (cluster.kind === 'approval') {
|
|
257
|
-
const request = pendingApprovals[0]
|
|
258
|
-
body = request ? (
|
|
259
|
-
<>
|
|
260
|
-
<div data-tone="bright">{request.title ?? 'Permission required'}</div>
|
|
261
|
-
<div className="term-scrub-ex" data-tone="fg">
|
|
262
|
-
{`${request.displayName ?? request.toolName}(${toolInputPreview(request.input)})`}
|
|
263
|
-
</div>
|
|
264
|
-
</>
|
|
265
|
-
) : null
|
|
266
|
-
} else if (cluster.kind === 'recap' && !first) {
|
|
267
|
-
body = <div data-tone="faint">※ {recapRow?.label}</div>
|
|
268
|
-
} else if (first) {
|
|
269
|
-
const item = items[first.itemIndex]
|
|
270
|
-
if (first.kind === 'recap') {
|
|
271
|
-
body = <div data-tone="faint">※ {recapRow?.label}</div>
|
|
272
|
-
} else if (first.kind === 'turn' || first.kind === 'turnFailed') {
|
|
273
|
-
const turn = first.turnIndex === undefined ? undefined : items[first.turnIndex]
|
|
274
|
-
body = (
|
|
275
|
-
<>
|
|
276
|
-
{item?.kind === 'assistant_text' ? (
|
|
277
|
-
<div className="term-scrub-ex" data-tone="fg">
|
|
278
|
-
<span data-tone="dim">● </span>
|
|
279
|
-
{item.text}
|
|
280
|
-
</div>
|
|
281
|
-
) : null}
|
|
282
|
-
{turn?.kind === 'turn_result' ? (
|
|
283
|
-
<>
|
|
284
|
-
<div data-tone={turn.isError ? 'red' : 'faint'}>{doneLine(turn)}</div>
|
|
285
|
-
{turn.errors?.map((message, index) => (
|
|
286
|
-
<div key={index} data-tone="red">
|
|
287
|
-
{message}
|
|
288
|
-
</div>
|
|
289
|
-
))}
|
|
290
|
-
</>
|
|
291
|
-
) : null}
|
|
292
|
-
</>
|
|
293
|
-
)
|
|
294
|
-
} else if (item) {
|
|
295
|
-
const failure =
|
|
296
|
-
first.kind === 'toolFailed' && item.kind === 'tool_call'
|
|
297
|
-
? item.result?.text.split('\n').find((line) => line.trim() !== '')
|
|
298
|
-
: undefined
|
|
299
|
-
body = (
|
|
300
|
-
<>
|
|
301
|
-
<div className="term-scrub-ex" data-tone={first.kind === 'error' || first.kind === 'toolFailed' ? 'red' : 'fg'}>
|
|
302
|
-
{first.kind === 'user' ? <span data-tone="dim">{'❯ '}</span> : null}
|
|
303
|
-
{excerpt(item)}
|
|
304
|
-
</div>
|
|
305
|
-
{failure ? (
|
|
306
|
-
<div className="term-scrub-ex" data-tone="red">
|
|
307
|
-
{failure}
|
|
308
|
-
</div>
|
|
309
|
-
) : null}
|
|
310
|
-
</>
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return (
|
|
315
|
-
<>
|
|
316
|
-
<div data-tone="faint">
|
|
317
|
-
{KIND_NAME[first?.kind ?? cluster.kind]}
|
|
318
|
-
{more}
|
|
319
|
-
</div>
|
|
320
|
-
{body}
|
|
321
|
-
</>
|
|
322
|
-
)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
69
|
export function TerminalScrubber(props: TerminalScrubberProps) {
|
|
326
70
|
const { scrollOffset, viewportH, totalSize, onJumpToRow, interactive, fontSize, lineHeight } = props
|
|
327
71
|
const stick = useStickToBottomContext()
|
|
@@ -420,7 +164,7 @@ export function TerminalScrubber(props: TerminalScrubberProps) {
|
|
|
420
164
|
}
|
|
421
165
|
const mark = nearestMember(cluster, railY(clientY))
|
|
422
166
|
if (mark) {
|
|
423
|
-
onJumpToRow(mark.rowIndex)
|
|
167
|
+
onJumpToRow(mark.rowIndex ?? props.rowIndexFor(mark.itemIndex))
|
|
424
168
|
}
|
|
425
169
|
}
|
|
426
170
|
|
|
@@ -515,7 +259,16 @@ export function TerminalScrubber(props: TerminalScrubberProps) {
|
|
|
515
259
|
))}
|
|
516
260
|
{peek ? (
|
|
517
261
|
<div ref={peekRef} className="term-scrub-peek" style={{ top: peek.y }}>
|
|
518
|
-
{peekContent(
|
|
262
|
+
{peekContent(
|
|
263
|
+
{
|
|
264
|
+
cluster: peek.cluster,
|
|
265
|
+
first: peek.mark,
|
|
266
|
+
items: props.items,
|
|
267
|
+
pendingApprovals: props.pendingApprovals,
|
|
268
|
+
recapLabel: props.recapRow?.label,
|
|
269
|
+
},
|
|
270
|
+
TERMINAL_PEEK,
|
|
271
|
+
)}
|
|
519
272
|
</div>
|
|
520
273
|
) : null}
|
|
521
274
|
</div>
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'react'
|
|
2
|
+
import { Popover as PopoverPrimitive } from '@base-ui/react/popover'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
import { PortalScope } from './PortalScope.tsx'
|
|
5
|
+
|
|
6
|
+
export const Popover = PopoverPrimitive.Root
|
|
7
|
+
export const PopoverTrigger = PopoverPrimitive.Trigger
|
|
8
|
+
|
|
9
|
+
export const PopoverContent: FunctionComponent<
|
|
10
|
+
PopoverPrimitive.Popup.Props & Pick<PopoverPrimitive.Positioner.Props, 'align' | 'side' | 'sideOffset'>
|
|
11
|
+
> = ({ className, align = 'end', side = 'bottom', sideOffset = 6, ...props }) => (
|
|
12
|
+
<PopoverPrimitive.Portal>
|
|
13
|
+
<PortalScope>
|
|
14
|
+
<PopoverPrimitive.Positioner align={align} side={side} sideOffset={sideOffset} className="isolate z-80 outline-none">
|
|
15
|
+
<PopoverPrimitive.Popup
|
|
16
|
+
data-slot="popover-content"
|
|
17
|
+
className={cn(
|
|
18
|
+
'rounded-md border border-border bg-surface p-2 text-fg-1 shadow-(--shadow-lg) outline-none',
|
|
19
|
+
'transition-[opacity,transform] duration-(--motion-base)',
|
|
20
|
+
'data-starting-style:scale-95 data-starting-style:opacity-0',
|
|
21
|
+
'data-ending-style:scale-95 data-ending-style:opacity-0',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
</PopoverPrimitive.Positioner>
|
|
27
|
+
</PortalScope>
|
|
28
|
+
</PopoverPrimitive.Portal>
|
|
29
|
+
)
|
|
@@ -68,3 +68,28 @@ export const SelectItem: FunctionComponent<SelectPrimitive.Item.Props> = ({ clas
|
|
|
68
68
|
</SelectPrimitive.ItemIndicator>
|
|
69
69
|
</SelectPrimitive.Item>
|
|
70
70
|
)
|
|
71
|
+
|
|
72
|
+
export interface OptionSelectProps<T extends string> {
|
|
73
|
+
label: string
|
|
74
|
+
value: T
|
|
75
|
+
options: readonly { value: T; label: string }[]
|
|
76
|
+
onChange: (value: T) => void
|
|
77
|
+
className?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function OptionSelect<T extends string>({ label, value, options, onChange, className }: OptionSelectProps<T>) {
|
|
81
|
+
return (
|
|
82
|
+
<Select value={value} onValueChange={(next) => onChange(next as T)}>
|
|
83
|
+
<SelectTrigger aria-label={label} className={className}>
|
|
84
|
+
<SelectValue>{options.find((o) => o.value === value)?.label ?? label}</SelectValue>
|
|
85
|
+
</SelectTrigger>
|
|
86
|
+
<SelectContent>
|
|
87
|
+
{options.map((option) => (
|
|
88
|
+
<SelectItem key={option.value} value={option.value}>
|
|
89
|
+
<SelectItemText>{option.label}</SelectItemText>
|
|
90
|
+
</SelectItem>
|
|
91
|
+
))}
|
|
92
|
+
</SelectContent>
|
|
93
|
+
</Select>
|
|
94
|
+
)
|
|
95
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,16 @@ export { Badge, badgeVariants, type BadgeProps } from './components/ui/Badge.tsx
|
|
|
3
3
|
export { Card, CardContent, CardHeader, CardTitle } from './components/ui/Card.tsx'
|
|
4
4
|
export { Input } from './components/ui/Input.tsx'
|
|
5
5
|
export { Textarea } from './components/ui/Textarea.tsx'
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
OptionSelect,
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectItemText,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
type OptionSelectProps,
|
|
15
|
+
} from './components/ui/Select.tsx'
|
|
7
16
|
export {
|
|
8
17
|
AlertDialog,
|
|
9
18
|
AlertDialogClose,
|
|
@@ -13,6 +22,7 @@ export {
|
|
|
13
22
|
AlertDialogTrigger,
|
|
14
23
|
} from './components/ui/AlertDialog.tsx'
|
|
15
24
|
export { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from './components/ui/Menu.tsx'
|
|
25
|
+
export { Popover, PopoverContent, PopoverTrigger } from './components/ui/Popover.tsx'
|
|
16
26
|
export { Dialog, DialogBody, DialogClose, DialogContent, DialogHeader, DialogRow, DialogTrigger } from './components/ui/Dialog.tsx'
|
|
17
27
|
export { Tip, TooltipContent, TooltipProvider } from './components/ui/Tooltip.tsx'
|
|
18
28
|
export { Toaster, toast } from './components/ui/Sonner.tsx'
|
|
@@ -51,16 +61,23 @@ export {
|
|
|
51
61
|
type SessionVitals,
|
|
52
62
|
type TerminalMetrics,
|
|
53
63
|
} from './components/agent/SessionPanel.tsx'
|
|
54
|
-
export { Transcript, type TranscriptProps } from './components/agent/Transcript.tsx'
|
|
64
|
+
export { TerminalTranscript, Transcript, type TerminalTranscriptProps, type TranscriptProps } from './components/agent/Transcript.tsx'
|
|
55
65
|
export { TerminalSurface, type TerminalSurfaceProps } from './components/terminal/surface.tsx'
|
|
56
|
-
export {
|
|
66
|
+
export { TerminalItemView } from './components/terminal/TerminalTranscript.tsx'
|
|
57
67
|
export { TerminalStatusLine, type TerminalStatusLineProps } from './components/terminal/StatusLine.tsx'
|
|
58
68
|
export { TerminalPermissionPrompt, type TerminalPermissionPromptProps } from './components/terminal/PermissionPrompt.tsx'
|
|
59
69
|
export { TerminalQuestionPrompt, type TerminalQuestionPromptProps } from './components/terminal/QuestionPrompt.tsx'
|
|
60
70
|
export { TerminalDiff, previewPatch } from './components/terminal/diff.tsx'
|
|
61
71
|
export { TerminalMarkdown, type TerminalMarkdownProps } from './components/terminal/markdown.tsx'
|
|
62
72
|
export { Band, Blank, Ink, Row, type RowProps, type Tone } from './components/terminal/row.tsx'
|
|
63
|
-
export {
|
|
73
|
+
export {
|
|
74
|
+
AgentWriteIcon,
|
|
75
|
+
CopyAction,
|
|
76
|
+
WithActions,
|
|
77
|
+
useAffordances,
|
|
78
|
+
type ActionPlacement,
|
|
79
|
+
type TerminalAffordances,
|
|
80
|
+
} from './components/terminal/affordances.tsx'
|
|
64
81
|
export { FileLinkProvider, useFileLinks, type FileLinkHandle, type FileLinkOpener } from './components/terminal/file-link.tsx'
|
|
65
82
|
export { parseFileLink, type FileLink } from './lib/file-link.ts'
|
|
66
83
|
export { matchPath, type PathHit, type PathMatch } from './lib/path-match.ts'
|
|
@@ -68,11 +85,8 @@ export { usePathLinks, resolveAgainstCwd } from './components/agent/use-path-lin
|
|
|
68
85
|
export { Conversation, ConversationContent, ConversationScrollButton, type ConversationProps } from './components/agent/Conversation.tsx'
|
|
69
86
|
export { Message, MessageContent, type MessageProps } from './components/agent/Message.tsx'
|
|
70
87
|
export {
|
|
71
|
-
TranscriptDensityProvider,
|
|
72
88
|
TranscriptVariantProvider,
|
|
73
|
-
useTranscriptDensity,
|
|
74
89
|
useTranscriptVariant,
|
|
75
|
-
type TranscriptDensity,
|
|
76
90
|
type TranscriptFont,
|
|
77
91
|
type TranscriptVariant,
|
|
78
92
|
} from './components/agent/transcript-variant.tsx'
|
|
@@ -81,6 +95,14 @@ export { Reasoning, type ReasoningProps } from './components/agent/Reasoning.tsx
|
|
|
81
95
|
export { Loader } from './components/agent/Loader.tsx'
|
|
82
96
|
export { ToolCallCard, type ToolCallCardProps, type ToolCallItem } from './components/agent/ToolCallCard.tsx'
|
|
83
97
|
export { ToolTitleProvider, useToolTitle } from './components/agent/tool-titles.tsx'
|
|
98
|
+
export {
|
|
99
|
+
ImageViewer,
|
|
100
|
+
ImageViewerProvider,
|
|
101
|
+
ViewableImage,
|
|
102
|
+
useOpenImage,
|
|
103
|
+
type OpenImage,
|
|
104
|
+
type ViewedImage,
|
|
105
|
+
} from './components/agent/image-viewer.tsx'
|
|
84
106
|
export { FileCard, type FileCardProps, type FileDeliveredItem } from './components/agent/FileCard.tsx'
|
|
85
107
|
export { PermissionPrompt, type PermissionPromptProps } from './components/agent/PermissionPrompt.tsx'
|
|
86
108
|
export {
|
|
@@ -111,21 +133,49 @@ export {
|
|
|
111
133
|
type PermissionModeSelectProps,
|
|
112
134
|
} from './components/agent/PermissionModeSelect.tsx'
|
|
113
135
|
export { StatusBar, type StatusBarProps } from './components/agent/StatusBar.tsx'
|
|
114
|
-
export { ContextDialog, type ContextDialogProps } from './components/agent/ContextDialog.tsx'
|
|
136
|
+
export { ContextDialog, ContextPanel, type ContextDialogProps, type ContextPanelProps } from './components/agent/ContextDialog.tsx'
|
|
115
137
|
export { UsageDialog, type UsageDialogProps } from './components/agent/UsageDialog.tsx'
|
|
116
138
|
export { TaskList, type TaskListProps } from './components/agent/TaskList.tsx'
|
|
117
139
|
export { TasksDialog, type TasksDialogProps } from './components/agent/TasksDialog.tsx'
|
|
118
140
|
export { UsageMeters, useMinuteClock } from './components/agent/UsageMeters.tsx'
|
|
119
|
-
export {
|
|
120
|
-
|
|
141
|
+
export {
|
|
142
|
+
SessionInfoDialog,
|
|
143
|
+
SessionInfoPanel,
|
|
144
|
+
type SessionInfoDialogProps,
|
|
145
|
+
type SessionInfoPanelProps,
|
|
146
|
+
} from './components/agent/SessionInfoDialog.tsx'
|
|
147
|
+
export {
|
|
148
|
+
McpDialog,
|
|
149
|
+
McpPanel,
|
|
150
|
+
McpPanelActions,
|
|
151
|
+
useMcpPanel,
|
|
152
|
+
type McpDialogProps,
|
|
153
|
+
type McpPanelModel,
|
|
154
|
+
type McpPanelProps,
|
|
155
|
+
} from './components/agent/McpDialog.tsx'
|
|
121
156
|
export { SkillsDialog, type SkillsDialogProps } from './components/agent/SkillsDialog.tsx'
|
|
122
157
|
export { HostFilesDialog, type HostFilesDialogProps } from './components/agent/HostFilesDialog.tsx'
|
|
123
158
|
export { SessionList, SessionListItem, type SessionListItemProps, type SessionListProps } from './components/agent/SessionList.tsx'
|
|
124
|
-
export { SessionBrowser, rowShapeClass, type SessionBrowserProps } from './components/agent/SessionBrowser.tsx'
|
|
159
|
+
export { SessionBrowser, SessionSearch, rowShapeClass, type SessionBrowserProps } from './components/agent/SessionBrowser.tsx'
|
|
160
|
+
export {
|
|
161
|
+
STEP_DISPLAY_OPTIONS,
|
|
162
|
+
SessionFilters,
|
|
163
|
+
SessionFiltersButton,
|
|
164
|
+
StepDisplayControl,
|
|
165
|
+
type SessionFiltersButtonProps,
|
|
166
|
+
type SessionFiltersProps,
|
|
167
|
+
} from './components/agent/SessionFilters.tsx'
|
|
125
168
|
export { SessionItem, type SelectModifiers, type SessionItemProps } from './components/agent/SessionItem.tsx'
|
|
126
169
|
export { SessionStatusIcon } from './components/agent/SessionStatusIcon.tsx'
|
|
127
170
|
export { EngineIcon, engineMark, vendorMarkClass, vendorTextClass } from './components/agent/EngineIcon.tsx'
|
|
128
|
-
export {
|
|
171
|
+
export {
|
|
172
|
+
type ShellStepOptions,
|
|
173
|
+
type Step,
|
|
174
|
+
type StepKind,
|
|
175
|
+
type TaskStepOptions,
|
|
176
|
+
StepRow,
|
|
177
|
+
sessionSteps,
|
|
178
|
+
} from './components/agent/SessionSteps.tsx'
|
|
129
179
|
export {
|
|
130
180
|
SUBAGENT_DISPLAY_META,
|
|
131
181
|
SUBAGENT_DISPLAY_ORDER,
|
|
@@ -138,8 +188,11 @@ export { ShellStrip, type ShellStripProps } from './components/agent/ShellStrip.
|
|
|
138
188
|
export { useShellFrame, type ShellFrame, type ShellFrameOptions } from './components/agent/use-shell-frame.ts'
|
|
139
189
|
export { ShellActionsProvider, useShellActions, type ShellActions } from './components/agent/shell-actions.tsx'
|
|
140
190
|
export {
|
|
191
|
+
SHELL_AGENT_WRITE_NOTE,
|
|
141
192
|
SHELL_GLYPH,
|
|
142
193
|
SHELL_KILL_GLYPH,
|
|
194
|
+
shellAgentWriteLabel,
|
|
195
|
+
shellGrantable,
|
|
143
196
|
shellInfoFailed,
|
|
144
197
|
shellInfoLabel,
|
|
145
198
|
shellInfoStatusText,
|
package/src/lib/file-link.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolvePosix } from '@workerdeck/protocol'
|
|
2
|
+
|
|
1
3
|
export type FileLink = {
|
|
2
4
|
path: string
|
|
3
5
|
line?: number
|
|
@@ -45,7 +47,7 @@ export function parseFileLink(href: string | undefined, cwd: string | undefined)
|
|
|
45
47
|
return undefined
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
const path =
|
|
50
|
+
const path = resolvePosix(target, cwd)
|
|
49
51
|
return path ? { path, ...(line === undefined ? {} : { line }) } : undefined
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -53,18 +55,3 @@ function readLine(value: string | undefined): number | undefined {
|
|
|
53
55
|
const parsed = value === undefined ? Number.NaN : Number(value)
|
|
54
56
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
function normalize(path: string): string {
|
|
58
|
-
const out: string[] = []
|
|
59
|
-
for (const part of path.split('/')) {
|
|
60
|
-
if (part === '' || part === '.') {
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
if (part === '..') {
|
|
64
|
-
out.pop()
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
out.push(part)
|
|
68
|
-
}
|
|
69
|
-
return `/${out.join('/')}`
|
|
70
|
-
}
|