@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.
Files changed (75) hide show
  1. package/README.md +6 -5
  2. package/build/{SessionPanel-BVKqPs_3.d.mts → SessionPanel-BNNc8grK.d.mts} +18 -16
  3. package/build/index.d.mts +194 -49
  4. package/build/index.mjs +562 -260
  5. package/build/index.mjs.map +1 -1
  6. package/build/scoped.css +170 -21
  7. package/build/{use-path-links-BgWXmfAS.mjs → use-path-links-C6Fh5zdx.mjs} +3312 -2758
  8. package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
  9. package/build/workspace.d.mts +3 -3
  10. package/build/workspace.mjs +3 -3
  11. package/build/workspace.mjs.map +1 -1
  12. package/package.json +4 -4
  13. package/src/components/agent/ApprovalPrompts.tsx +87 -0
  14. package/src/components/agent/Composer.tsx +25 -47
  15. package/src/components/agent/ContextDialog.tsx +75 -67
  16. package/src/components/agent/FrameStrip.tsx +74 -0
  17. package/src/components/agent/HostFilesDialog.tsx +3 -2
  18. package/src/components/agent/McpDialog.tsx +112 -78
  19. package/src/components/agent/PermissionPrompt.tsx +22 -17
  20. package/src/components/agent/QuestionPrompt.tsx +6 -41
  21. package/src/components/agent/Response.tsx +8 -1
  22. package/src/components/agent/Scrubber.tsx +6 -75
  23. package/src/components/agent/SessionBrowser.tsx +131 -214
  24. package/src/components/agent/SessionFilters.tsx +266 -0
  25. package/src/components/agent/SessionInfoDialog.tsx +47 -63
  26. package/src/components/agent/SessionItem.tsx +29 -6
  27. package/src/components/agent/SessionPanel.tsx +249 -498
  28. package/src/components/agent/SessionSteps.tsx +98 -14
  29. package/src/components/agent/SessionWorkspace.tsx +3 -3
  30. package/src/components/agent/ShellStrip.tsx +43 -36
  31. package/src/components/agent/ShellTerminal.tsx +1 -1
  32. package/src/components/agent/SubagentStrip.tsx +13 -30
  33. package/src/components/agent/ToolCallCard.tsx +18 -41
  34. package/src/components/agent/Transcript.tsx +111 -93
  35. package/src/components/agent/TranscriptItemView.tsx +80 -67
  36. package/src/components/agent/TranscriptRows.tsx +4 -2
  37. package/src/components/agent/composer-commands.ts +80 -1
  38. package/src/components/agent/image-viewer.tsx +326 -0
  39. package/src/components/agent/markdown-image.tsx +139 -0
  40. package/src/components/agent/scrubber-marks.ts +33 -11
  41. package/src/components/agent/scrubber-peek.tsx +122 -0
  42. package/src/components/agent/session-panel-providers.tsx +41 -0
  43. package/src/components/agent/shell-actions.tsx +38 -1
  44. package/src/components/agent/tool-result-image.tsx +40 -0
  45. package/src/components/agent/transcript-variant.tsx +3 -21
  46. package/src/components/agent/use-catch-up.ts +20 -0
  47. package/src/components/agent/use-frame.ts +88 -0
  48. package/src/components/agent/use-host-image.ts +81 -0
  49. package/src/components/agent/use-shell-frame.ts +12 -70
  50. package/src/components/agent/use-subagent-frame.ts +19 -75
  51. package/src/components/prompt-area/prompt-area-engine.ts +21 -20
  52. package/src/components/prompt-area/prompt-area-list-ops.ts +12 -15
  53. package/src/components/prompt-area/use-prompt-area-keydown.ts +4 -2
  54. package/src/components/terminal/PermissionPrompt.tsx +31 -28
  55. package/src/components/terminal/QuestionPrompt.tsx +10 -46
  56. package/src/components/terminal/TerminalTranscript.tsx +23 -89
  57. package/src/components/terminal/affordances.tsx +100 -74
  58. package/src/components/terminal/height.ts +48 -32
  59. package/src/components/terminal/image-box.ts +31 -0
  60. package/src/components/terminal/items.tsx +70 -30
  61. package/src/components/terminal/markdown.tsx +4 -1
  62. package/src/components/terminal/scrubber.tsx +25 -272
  63. package/src/components/terminal/shell-row.ts +17 -3
  64. package/src/components/ui/Popover.tsx +29 -0
  65. package/src/components/ui/Select.tsx +25 -0
  66. package/src/index.ts +65 -12
  67. package/src/lib/file-link.ts +3 -16
  68. package/src/lib/permission-prompt.ts +57 -0
  69. package/src/lib/question-answers.ts +65 -0
  70. package/src/lib/shell-request.ts +115 -0
  71. package/src/lib/tool-icon.ts +13 -1
  72. package/src/styles/terminal.css +129 -22
  73. package/src/styles/theme.css +4 -0
  74. package/build/use-path-links-BgWXmfAS.mjs.map +0 -1
  75. package/src/components/prompt-area/use-markdown-mode.ts +0 -103
@@ -1,110 +1,15 @@
1
- import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react'
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 { formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
6
- import { parentOf } from './blocks.ts'
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
- const subagentParents = new Set<string>()
144
- for (const item of items) {
145
- const parent = parentOf(item)
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 lanes = new Map<Lane, { mark: Mark; y: number; h: number }[]>()
211
- for (const mark of marks) {
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(mark.rowIndex)
58
+ const rowH = sizeOfRow(rowIndex)
214
59
  const h = within ? MIN_MARK : Math.max(MIN_MARK, Math.round(rowH * scale))
215
- const y = Math.min(
216
- Math.max(0, railH - h),
217
- Math.round((offsetOfRow(mark.rowIndex) + (within ? (within.ordinal / within.count) * rowH : 0)) * scale),
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(peek.cluster, peek.mark, props)}
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 { ShellInfo } from '@workerdeck/protocol'
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 { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from './components/ui/Select.tsx'
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 { TerminalTranscript, TerminalItemView, type TerminalTranscriptProps } from './components/terminal/TerminalTranscript.tsx'
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 { CopyAction, WithActions, useAffordances, type TerminalAffordances } from './components/terminal/affordances.tsx'
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 { SessionInfoDialog, type SessionInfoDialogProps } from './components/agent/SessionInfoDialog.tsx'
120
- export { McpDialog, type McpDialogProps } from './components/agent/McpDialog.tsx'
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 { type ShellStepOptions, type Step, type StepKind, StepRow, sessionSteps } from './components/agent/SessionSteps.tsx'
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,
@@ -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 = target.startsWith('/') ? normalize(target) : cwd ? normalize(`${cwd}/${target}`) : undefined
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
- }