@workerdeck/ui 3.0.1 → 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/build/{SessionPanel-C1bU4yTM.d.mts → SessionPanel-BNNc8grK.d.mts} +9 -7
- package/build/index.d.mts +153 -45
- package/build/index.mjs +501 -226
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -6
- package/build/{use-path-links-BEWxt0ZM.mjs → use-path-links-C6Fh5zdx.mjs} +1926 -2086
- package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
- package/build/workspace.d.mts +1 -1
- package/build/workspace.mjs +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 +13 -22
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +125 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +27 -7
- package/src/components/agent/SessionPanel.tsx +244 -488
- package/src/components/agent/SessionSteps.tsx +54 -11
- package/src/components/agent/ShellStrip.tsx +27 -43
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/Transcript.tsx +13 -0
- package/src/components/agent/TranscriptItemView.tsx +6 -36
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/markdown-image.tsx +49 -1
- 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 +36 -1
- 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/terminal/PermissionPrompt.tsx +19 -32
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +3 -86
- package/src/components/terminal/items.tsx +5 -43
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +46 -8
- 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/build/use-path-links-BEWxt0ZM.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>
|
|
@@ -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,9 +61,9 @@ 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'
|
|
@@ -123,21 +133,49 @@ export {
|
|
|
123
133
|
type PermissionModeSelectProps,
|
|
124
134
|
} from './components/agent/PermissionModeSelect.tsx'
|
|
125
135
|
export { StatusBar, type StatusBarProps } from './components/agent/StatusBar.tsx'
|
|
126
|
-
export { ContextDialog, type ContextDialogProps } from './components/agent/ContextDialog.tsx'
|
|
136
|
+
export { ContextDialog, ContextPanel, type ContextDialogProps, type ContextPanelProps } from './components/agent/ContextDialog.tsx'
|
|
127
137
|
export { UsageDialog, type UsageDialogProps } from './components/agent/UsageDialog.tsx'
|
|
128
138
|
export { TaskList, type TaskListProps } from './components/agent/TaskList.tsx'
|
|
129
139
|
export { TasksDialog, type TasksDialogProps } from './components/agent/TasksDialog.tsx'
|
|
130
140
|
export { UsageMeters, useMinuteClock } from './components/agent/UsageMeters.tsx'
|
|
131
|
-
export {
|
|
132
|
-
|
|
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'
|
|
133
156
|
export { SkillsDialog, type SkillsDialogProps } from './components/agent/SkillsDialog.tsx'
|
|
134
157
|
export { HostFilesDialog, type HostFilesDialogProps } from './components/agent/HostFilesDialog.tsx'
|
|
135
158
|
export { SessionList, SessionListItem, type SessionListItemProps, type SessionListProps } from './components/agent/SessionList.tsx'
|
|
136
|
-
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'
|
|
137
168
|
export { SessionItem, type SelectModifiers, type SessionItemProps } from './components/agent/SessionItem.tsx'
|
|
138
169
|
export { SessionStatusIcon } from './components/agent/SessionStatusIcon.tsx'
|
|
139
170
|
export { EngineIcon, engineMark, vendorMarkClass, vendorTextClass } from './components/agent/EngineIcon.tsx'
|
|
140
|
-
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'
|
|
141
179
|
export {
|
|
142
180
|
SUBAGENT_DISPLAY_META,
|
|
143
181
|
SUBAGENT_DISPLAY_ORDER,
|
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
|
-
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PermissionRequest } from '@workerdeck/protocol'
|
|
2
|
+
import { planFromRequest } from './plan-request.ts'
|
|
3
|
+
import { shellRequestPayload, shellRequestTitle, type ShellRequestPayload } from './shell-request.ts'
|
|
4
|
+
|
|
5
|
+
export type PermissionChoiceKey = 'allow' | 'deny' | 'stop'
|
|
6
|
+
|
|
7
|
+
export type PermissionChoice = {
|
|
8
|
+
key: PermissionChoiceKey
|
|
9
|
+
button: string
|
|
10
|
+
option: string
|
|
11
|
+
danger?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PermissionPromptModel = {
|
|
15
|
+
plan?: string
|
|
16
|
+
shell?: ShellRequestPayload
|
|
17
|
+
heading: string
|
|
18
|
+
question: string
|
|
19
|
+
description?: string
|
|
20
|
+
decisionReason?: string
|
|
21
|
+
choices: [PermissionChoice, PermissionChoice, PermissionChoice]
|
|
22
|
+
denyPlaceholder: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type PermissionRequestLike = Pick<PermissionRequest, 'toolName' | 'input' | 'title' | 'displayName' | 'description' | 'decisionReason'>
|
|
26
|
+
|
|
27
|
+
export function permissionPromptModel(request: PermissionRequestLike): PermissionPromptModel {
|
|
28
|
+
const plan = planFromRequest(request)
|
|
29
|
+
const shell = plan ? undefined : shellRequestPayload(request)
|
|
30
|
+
const context = { description: request.description, decisionReason: request.decisionReason }
|
|
31
|
+
if (plan) {
|
|
32
|
+
return {
|
|
33
|
+
plan,
|
|
34
|
+
heading: 'Plan ready for review',
|
|
35
|
+
question: 'Ready to implement this plan?',
|
|
36
|
+
...context,
|
|
37
|
+
choices: [
|
|
38
|
+
{ key: 'allow', button: 'Approve plan', option: 'Approve plan' },
|
|
39
|
+
{ key: 'deny', button: 'Keep planning', option: 'Keep planning - tell it what to change' },
|
|
40
|
+
{ key: 'stop', button: 'Stop the turn', option: 'No, and stop the turn', danger: true },
|
|
41
|
+
],
|
|
42
|
+
denyPlaceholder: 'What should change? (optional) - the agent keeps planning and reads this',
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
shell,
|
|
47
|
+
heading: shell ? shellRequestTitle(shell) : (request.title ?? request.displayName ?? 'Permission needed'),
|
|
48
|
+
question: shell ? 'Do you want to let it?' : 'Do you want to proceed?',
|
|
49
|
+
...context,
|
|
50
|
+
choices: [
|
|
51
|
+
{ key: 'allow', button: 'Allow', option: 'Yes' },
|
|
52
|
+
{ key: 'deny', button: 'Deny', option: 'No, and tell the agent what to do differently' },
|
|
53
|
+
{ key: 'stop', button: 'Deny & stop', option: 'No, and stop the turn', danger: true },
|
|
54
|
+
],
|
|
55
|
+
denyPlaceholder: 'Reason (optional) - the agent reads this and can try something else',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { UserQuestion } from '@workerdeck/protocol'
|
|
3
|
+
|
|
4
|
+
export type Selection = { labels: string[]; other: string; otherActive: boolean }
|
|
5
|
+
|
|
6
|
+
export const EMPTY_SELECTION: Selection = { labels: [], other: '', otherActive: false }
|
|
7
|
+
|
|
8
|
+
export function answerFor(selection: Selection): string {
|
|
9
|
+
const parts = [...selection.labels]
|
|
10
|
+
if (selection.otherActive && selection.other.trim()) {
|
|
11
|
+
parts.push(selection.other.trim())
|
|
12
|
+
}
|
|
13
|
+
return parts.join(', ')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toggleLabel(selection: Selection, label: string, multiSelect: boolean): Selection {
|
|
17
|
+
if (!multiSelect) {
|
|
18
|
+
return { ...selection, labels: [label], otherActive: false }
|
|
19
|
+
}
|
|
20
|
+
const labels = selection.labels.includes(label) ? selection.labels.filter((l) => l !== label) : [...selection.labels, label]
|
|
21
|
+
return { ...selection, labels }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function toggleOther(selection: Selection, multiSelect: boolean): Selection {
|
|
25
|
+
const otherActive = !selection.otherActive
|
|
26
|
+
return { ...selection, otherActive, labels: otherActive && !multiSelect ? [] : selection.labels }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function answersFor(questions: readonly UserQuestion[], selections: readonly Selection[]): Record<string, string> {
|
|
30
|
+
const answers: Record<string, string> = {}
|
|
31
|
+
questions.forEach((question, index) => {
|
|
32
|
+
answers[question.question] = answerFor(selections[index] ?? EMPTY_SELECTION)
|
|
33
|
+
})
|
|
34
|
+
return answers
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface QuestionAnswers {
|
|
38
|
+
selections: Selection[]
|
|
39
|
+
selectionFor: (index: number) => Selection
|
|
40
|
+
answered: (index: number) => boolean
|
|
41
|
+
complete: boolean
|
|
42
|
+
update: (index: number, patch: Partial<Selection>) => void
|
|
43
|
+
toggle: (index: number, label: string) => void
|
|
44
|
+
toggleOther: (index: number) => void
|
|
45
|
+
answers: () => Record<string, string>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useQuestionAnswers(questions: readonly UserQuestion[]): QuestionAnswers {
|
|
49
|
+
const [selections, setSelections] = useState<Selection[]>(() => questions.map(() => EMPTY_SELECTION))
|
|
50
|
+
const selectionFor = (index: number) => selections[index] ?? EMPTY_SELECTION
|
|
51
|
+
const answered = (index: number) => answerFor(selectionFor(index)) !== ''
|
|
52
|
+
const edit = (index: number, change: (selection: Selection) => Selection) =>
|
|
53
|
+
setSelections((prev) => questions.map((_, i) => (i === index ? change(prev[i] ?? EMPTY_SELECTION) : (prev[i] ?? EMPTY_SELECTION))))
|
|
54
|
+
const multiSelect = (index: number) => questions[index]?.multiSelect === true
|
|
55
|
+
return {
|
|
56
|
+
selections,
|
|
57
|
+
selectionFor,
|
|
58
|
+
answered,
|
|
59
|
+
complete: questions.every((_, index) => answered(index)),
|
|
60
|
+
update: (index, patch) => edit(index, (selection) => ({ ...selection, ...patch })),
|
|
61
|
+
toggle: (index, label) => edit(index, (selection) => toggleLabel(selection, label, multiSelect(index))),
|
|
62
|
+
toggleOther: (index) => edit(index, (selection) => toggleOther(selection, multiSelect(index))),
|
|
63
|
+
answers: () => answersFor(questions, selections),
|
|
64
|
+
}
|
|
65
|
+
}
|