@workerdeck/ui 2.13.0 → 2.14.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 (36) hide show
  1. package/build/{SessionPanel-CJwp5B3q.d.mts → SessionPanel-BVKqPs_3.d.mts} +9 -3
  2. package/build/index.d.mts +84 -6
  3. package/build/index.mjs +83 -37
  4. package/build/index.mjs.map +1 -1
  5. package/build/scoped.css +24 -3
  6. package/build/{use-path-links-CepFsMdW.mjs → use-path-links-BgWXmfAS.mjs} +849 -236
  7. package/build/use-path-links-BgWXmfAS.mjs.map +1 -0
  8. package/build/workspace.d.mts +19 -4
  9. package/build/workspace.mjs +7 -4
  10. package/build/workspace.mjs.map +1 -1
  11. package/package.json +16 -6
  12. package/src/components/agent/Composer.tsx +18 -13
  13. package/src/components/agent/SessionBrowser.tsx +24 -1
  14. package/src/components/agent/SessionItem.tsx +13 -3
  15. package/src/components/agent/SessionPanel.tsx +264 -211
  16. package/src/components/agent/SessionSteps.tsx +78 -27
  17. package/src/components/agent/SessionWorkspace.tsx +10 -1
  18. package/src/components/agent/ShellStrip.tsx +71 -0
  19. package/src/components/agent/ShellTerminal.tsx +170 -0
  20. package/src/components/agent/SubagentToggle.tsx +7 -1
  21. package/src/components/agent/TranscriptItemView.tsx +74 -1
  22. package/src/components/agent/scrubber-marks.ts +9 -1
  23. package/src/components/agent/shell-actions.tsx +25 -0
  24. package/src/components/agent/use-shell-frame.ts +111 -0
  25. package/src/components/terminal/TerminalTranscript.tsx +4 -0
  26. package/src/components/terminal/affordances.tsx +37 -0
  27. package/src/components/terminal/height.ts +14 -0
  28. package/src/components/terminal/items.tsx +72 -3
  29. package/src/components/terminal/press.tsx +5 -3
  30. package/src/components/terminal/scrubber.tsx +9 -1
  31. package/src/components/terminal/shell-row.ts +106 -0
  32. package/src/index.ts +12 -1
  33. package/src/styles/terminal.css +11 -0
  34. package/src/styles/theme.css +3 -0
  35. package/src/workspace.ts +1 -0
  36. package/build/use-path-links-CepFsMdW.mjs.map +0 -1
@@ -0,0 +1,111 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import type { SessionInfo, ShellInfo } from '@workerdeck/protocol'
3
+ import type { ShellItem, TranscriptItem } from '@workerdeck/react'
4
+
5
+ export type ShellFrameOptions = {
6
+ sessionId: string | undefined
7
+ items: readonly TranscriptItem[]
8
+ session: SessionInfo | undefined
9
+ shells?: readonly ShellInfo[]
10
+ reveal?: { toolUseId: string; nonce: number }
11
+ openShell?: { shellId: string; nonce: number }
12
+ onShellChange?: (shellId: string | undefined) => void
13
+ }
14
+
15
+ export type ShellFrame = {
16
+ shellId: string | undefined
17
+ enterShell: (shellId: string) => void
18
+ leaveShell: () => void
19
+ returnReveal: { toolUseId: string; nonce: number } | undefined
20
+ shell: ShellInfo | undefined
21
+ label: string
22
+ }
23
+
24
+ // The shell frame machine, the sub-agent frame's twin: entry keys on the nonce alone, Escape
25
+ // leaves, and the report is deduped through a ref so an echo of our own report is inert.
26
+ export function useShellFrame(options: ShellFrameOptions): ShellFrame {
27
+ const { sessionId, items, session, shells, reveal, openShell, onShellChange } = options
28
+
29
+ const [shellId, setShellId] = useState<string | undefined>(undefined)
30
+ const [returnReveal, setReturnReveal] = useState<{ toolUseId: string; nonce: number } | undefined>(undefined)
31
+ useEffect(() => {
32
+ setShellId(undefined)
33
+ setReturnReveal(undefined)
34
+ }, [sessionId])
35
+
36
+ const rowIdRef = useRef<string | undefined>(undefined)
37
+ const row = useMemo(
38
+ () => (shellId === undefined ? undefined : items.find((item): item is ShellItem => item.kind === 'shell' && item.shell.id === shellId)),
39
+ [items, shellId],
40
+ )
41
+ rowIdRef.current = row?.id
42
+
43
+ // Leaving reveals the `$` row the frame came from, so the reader lands where they left.
44
+ const leaveShell = useCallback(() => {
45
+ setShellId((current) => {
46
+ const rowId = rowIdRef.current
47
+ if (current !== undefined && rowId !== undefined) {
48
+ setReturnReveal({ toolUseId: rowId, nonce: Date.now() })
49
+ }
50
+ return undefined
51
+ })
52
+ }, [])
53
+
54
+ const openShellNonce = openShell?.nonce
55
+ const openShellId = openShell?.shellId
56
+ useEffect(() => {
57
+ if (openShellId === undefined) {
58
+ leaveShell()
59
+ } else {
60
+ setShellId(openShellId)
61
+ }
62
+ // Keyed on the nonce alone: an unchanged nonce is the host echoing our own report, and
63
+ // acting on it starts the URL -> panel -> URL cycle.
64
+ // eslint-disable-next-line react-hooks/exhaustive-deps
65
+ }, [openShellNonce])
66
+
67
+ const revealNonce = reveal?.nonce
68
+ useEffect(() => {
69
+ if (revealNonce === undefined) {
70
+ return
71
+ }
72
+ setShellId(undefined)
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, [revealNonce])
75
+
76
+ useEffect(() => {
77
+ if (shellId === undefined) {
78
+ return
79
+ }
80
+ const onKey = (event: KeyboardEvent) => {
81
+ if (event.key !== 'Escape' || event.defaultPrevented) {
82
+ return
83
+ }
84
+ leaveShell()
85
+ }
86
+ window.addEventListener('keydown', onKey)
87
+ return () => window.removeEventListener('keydown', onKey)
88
+ }, [shellId, leaveShell])
89
+
90
+ const onShellChangeRef = useRef(onShellChange)
91
+ onShellChangeRef.current = onShellChange
92
+ const reportedShellId = useRef<string | undefined>(undefined)
93
+ useEffect(() => {
94
+ if (reportedShellId.current === shellId) {
95
+ return
96
+ }
97
+ reportedShellId.current = shellId
98
+ onShellChangeRef.current?.(shellId)
99
+ }, [shellId])
100
+
101
+ const shell = useMemo(() => {
102
+ if (shellId === undefined) {
103
+ return undefined
104
+ }
105
+ return row?.shell ?? shells?.find((record) => record.id === shellId) ?? session?.shells?.find((record) => record.id === shellId)
106
+ }, [row, shells, session, shellId])
107
+
108
+ const label = shell ? shell.label || shell.command.split('\n')[0] || 'shell' : 'shell'
109
+
110
+ return { shellId, enterShell: setShellId, leaveShell, returnReveal, shell, label }
111
+ }
@@ -10,6 +10,7 @@ import {
10
10
  NoticeRow,
11
11
  PeerSendRow,
12
12
  RunRow,
13
+ ShellRow,
13
14
  ThinkingRow,
14
15
  ToolRow,
15
16
  TurnResultRow,
@@ -60,6 +61,9 @@ export function TerminalItemView({ item, fileUrl }: { item: TranscriptItem; file
60
61
  case 'file_delivered': {
61
62
  return <FileRow item={item} href={fileUrl?.(item.path)} />
62
63
  }
64
+ case 'shell': {
65
+ return <ShellRow item={item} />
66
+ }
63
67
  default: {
64
68
  return null
65
69
  }
@@ -1,5 +1,6 @@
1
1
  import { createContext, useContext, useState, type ReactNode } from 'react'
2
2
  import { copyText } from '../../lib/clipboard.ts'
3
+ import { SHELL_KILL_GLYPH } from './shell-row.ts'
3
4
  import { cn } from '../../lib/utils.ts'
4
5
 
5
6
  export type TerminalAffordances = {
@@ -97,6 +98,42 @@ export function OpenSubagentAction({ onOpen, label = 'Open sub-agent' }: { onOpe
97
98
  )
98
99
  }
99
100
 
101
+ export function OpenShellAction({ onOpen, label = 'Open terminal' }: { onOpen: () => void; label?: string }) {
102
+ return (
103
+ <button
104
+ type="button"
105
+ className="term-action"
106
+ title={label}
107
+ aria-label={label}
108
+ data-tone="magenta"
109
+ onClick={(event) => {
110
+ event.stopPropagation()
111
+ onOpen()
112
+ }}
113
+ >
114
+ ⤢
115
+ </button>
116
+ )
117
+ }
118
+
119
+ export function KillShellAction({ onKill, label = 'Kill this shell' }: { onKill: () => void; label?: string }) {
120
+ return (
121
+ <button
122
+ type="button"
123
+ className="term-action"
124
+ title={label}
125
+ aria-label={label}
126
+ data-tone="red"
127
+ onClick={(event) => {
128
+ event.stopPropagation()
129
+ onKill()
130
+ }}
131
+ >
132
+ {SHELL_KILL_GLYPH}
133
+ </button>
134
+ )
135
+ }
136
+
100
137
  export function CopyAction({ text, label = 'Copy' }: { text: string; label?: string }) {
101
138
  const [copied, setCopied] = useState(false)
102
139
  return (
@@ -4,6 +4,7 @@ import { compactionText, formatBytes, formatCost, formatDuration, toolInputPrevi
4
4
  import { taskChildItems, type TerminalBlock, type ToolCallItem } from './blocks.ts'
5
5
  import { IMAGE_BOX_LINES } from './image-box.ts'
6
6
  import { collapsedResult } from './result-preview.ts'
7
+ import { shellBodyLines, shellFooterText, shellHeaderText } from './shell-row.ts'
7
8
  import { todoLine, todoPreview } from './todos.ts'
8
9
  import { isPeerSend, peerName, planRun, runSummary, taskSummary } from './tool-run.ts'
9
10
 
@@ -634,6 +635,19 @@ export function itemHeight(item: TranscriptItem, m: CellMetrics): ComputedHeight
634
635
  const text = `${item.path} · ${formatBytes(item.bytes)}` + (item.description ? ` · ${item.description}` : '')
635
636
  return rowH(text, m, { extraPx })
636
637
  }
638
+ case 'shell': {
639
+ // Collapsed, as the row first draws: expanding is a state change the epoch's cache is invalidated by anyway.
640
+ const lines = shellBodyLines(item, false)
641
+ let acc = rowH(shellHeaderText(item), m, { extraPx })
642
+ for (const line of lines) {
643
+ acc = add(acc, rowH(line || ' ', m, { extraPx }))
644
+ }
645
+ const footer = shellFooterText(item, false, lines.length)
646
+ if (footer) {
647
+ acc = add(acc, rowH(footer, m, { extraPx }))
648
+ }
649
+ return acc
650
+ }
637
651
  default: {
638
652
  return { px: 0, exact: false }
639
653
  }
@@ -1,15 +1,15 @@
1
1
  import { useEffect, useState, type ReactNode } from 'react'
2
2
  import type { MessageOrigin } from '@workerdeck/protocol'
3
- import type { TranscriptItem } from '@workerdeck/react'
3
+ import type { ShellItem, TranscriptItem } from '@workerdeck/react'
4
4
  import { compactionText, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
5
5
  import { isMutatingTool } from '../../lib/tool-icon.ts'
6
6
  import { usePulse } from '../agent/pulse.tsx'
7
7
  import { PromptTokenText } from '../agent/PromptTokenText.tsx'
8
- import { BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
8
+ import { BookmarkAction, CopyAction, KillShellAction, OpenShellAction, WithActions } from './affordances.tsx'
9
9
  import { TerminalDiff } from './diff.tsx'
10
10
  import { TerminalMarkdown } from './markdown.tsx'
11
11
  import { usePeerNames } from './peer-names.tsx'
12
- import { Pressable, useRevealOnOpen } from './press.tsx'
12
+ import { Pressable, useRevealOnOpen, type PressModifiers } from './press.tsx'
13
13
  import { IMAGE_BOX_LINES, IMAGE_UNAVAILABLE, imagePlaceholder } from './image-box.ts'
14
14
  import { collapsedResult } from './result-preview.ts'
15
15
  import { useToolResultFetcher } from '../agent/tool-result-fetch.tsx'
@@ -17,6 +17,8 @@ import { useToolTitle } from '../agent/tool-titles.tsx'
17
17
  import { useToolResultImageSrc } from '../agent/tool-result-image.tsx'
18
18
  import { isPeerSend, peerName, peerOneLine, peerSendTarget, peerSendText, planRun, runFailed, runSummary } from './tool-run.ts'
19
19
  import { todoLine, todoPreview, type TodoPreview, type TodoStatus } from './todos.ts'
20
+ import { useShellActions } from '../agent/shell-actions.tsx'
21
+ import { SHELL_GLYPH, shellBodyLines, shellFailed, shellFooterText, shellLabel, shellStatusText } from './shell-row.ts'
20
22
  import { type ToolCallItem } from './blocks.ts'
21
23
  import { Band, Blank, Ink, Row, type Tone } from './row.tsx'
22
24
 
@@ -386,6 +388,73 @@ export function NoticeRow({ item }: { item: Extract<TranscriptItem, { kind: 'not
386
388
  )
387
389
  }
388
390
 
391
+ export function ShellRow({ item }: { item: ShellItem }) {
392
+ const [open, setOpen] = useState(false)
393
+ const [busy, setBusy] = useState(false)
394
+ const actions = useShellActions()
395
+ const reveal = useRevealOnOpen(open)
396
+ const running = item.shell.status === 'running'
397
+ const failed = shellFailed(item)
398
+ const tone: Tone = failed ? 'red' : running ? 'magenta' : 'dim'
399
+ const lines = shellBodyLines(item, open)
400
+ const footer = shellFooterText(item, open, lines.length)
401
+
402
+ const shellId = item.shell.id
403
+ const verify = actions.verify
404
+ useEffect(() => {
405
+ if (running) {
406
+ void verify(shellId)
407
+ }
408
+ }, [running, shellId, verify])
409
+
410
+ const press = (modifiers: PressModifiers) => {
411
+ if ((modifiers.meta || modifiers.ctrl) && actions.open) {
412
+ actions.open(shellId)
413
+ return
414
+ }
415
+ const next = !open
416
+ setOpen(next)
417
+ if (!next || !item.truncated || item.expanded !== undefined || item.missing) {
418
+ return
419
+ }
420
+ setBusy(true)
421
+ void actions.loadOutput(shellId).finally(() => setBusy(false))
422
+ }
423
+
424
+ return (
425
+ <div ref={reveal} className={open ? 'term-open' : undefined}>
426
+ <WithActions
427
+ actions={
428
+ <>
429
+ {actions.open ? <OpenShellAction onOpen={() => actions.open?.(shellId)} /> : null}
430
+ {running ? <KillShellAction onKill={() => void actions.kill(shellId)} /> : null}
431
+ <BookmarkAction id={item.id} />
432
+ <CopyAction text={item.shell.command} label="Copy command" />
433
+ </>
434
+ }
435
+ >
436
+ <Pressable onPress={press} expanded={open}>
437
+ <Row glyph={SHELL_GLYPH} glyphTone={tone} tone="fg">
438
+ <Ink bold tone="bright">
439
+ {shellLabel(item)}
440
+ </Ink>
441
+ <Ink tone={failed ? 'red' : 'faint'}>
442
+ {' · '}
443
+ {shellStatusText(item)}
444
+ </Ink>
445
+ </Row>
446
+ </Pressable>
447
+ {lines.map((line, index) => (
448
+ <Row key={index} tone={failed ? 'red' : 'dim'}>
449
+ {line || ' '}
450
+ </Row>
451
+ ))}
452
+ {footer || busy ? <Row tone="faint">{busy ? '… fetching the full output' : footer}</Row> : null}
453
+ </WithActions>
454
+ </div>
455
+ )
456
+ }
457
+
389
458
  export function FileRow({ item, href }: { item: Extract<TranscriptItem, { kind: 'file_delivered' }>; href?: string }) {
390
459
  return (
391
460
  <Row glyph="⤓" glyphTone="blue" tone="dim">
@@ -3,13 +3,15 @@ import { cn } from '../../lib/utils.ts'
3
3
 
4
4
  const DRAG_SLOP = 4
5
5
 
6
+ export type PressModifiers = { meta: boolean; ctrl: boolean; alt: boolean }
7
+
6
8
  export function Pressable({
7
9
  onPress,
8
10
  expanded,
9
11
  className,
10
12
  children,
11
13
  }: {
12
- onPress: () => void
14
+ onPress: (modifiers: PressModifiers) => void
13
15
  expanded?: boolean
14
16
  className?: string
15
17
  children: ReactNode
@@ -34,14 +36,14 @@ export function Pressable({
34
36
  if (selection && !selection.isCollapsed && selection.containsNode(event.currentTarget, true)) {
35
37
  return
36
38
  }
37
- onPress()
39
+ onPress({ meta: event.metaKey, ctrl: event.ctrlKey, alt: event.altKey })
38
40
  }}
39
41
  onKeyDown={(event) => {
40
42
  if (event.key !== 'Enter' && event.key !== ' ') {
41
43
  return
42
44
  }
43
45
  event.preventDefault()
44
- onPress()
46
+ onPress({ meta: event.metaKey, ctrl: event.ctrlKey, alt: event.altKey })
45
47
  }}
46
48
  >
47
49
  {children}
@@ -7,7 +7,7 @@ import { parentOf } from './blocks.ts'
7
7
  import { TerminalSurface } from './surface.tsx'
8
8
 
9
9
  type Lane = 'l' | 'r' | 'f'
10
- type MarkKind = 'user' | 'subagent' | 'turn' | 'turnFailed' | 'toolFailed' | 'error' | 'approval' | 'recap' | 'bookmark'
10
+ type MarkKind = 'user' | 'subagent' | 'shell' | 'turn' | 'turnFailed' | 'toolFailed' | 'error' | 'approval' | 'recap' | 'bookmark'
11
11
 
12
12
  type Mark = {
13
13
  kind: MarkKind
@@ -31,6 +31,7 @@ function nearestMember(cluster: Cluster, y: number): Mark | undefined {
31
31
  const LANE: Record<MarkKind, Lane> = {
32
32
  user: 'l',
33
33
  subagent: 'l',
34
+ shell: 'l',
34
35
  turn: 'r',
35
36
  turnFailed: 'r',
36
37
  toolFailed: 'r',
@@ -49,12 +50,14 @@ const LOUDNESS: Record<MarkKind, number> = {
49
50
  turn: 2,
50
51
  bookmark: 1,
51
52
  subagent: 1,
53
+ shell: 1,
52
54
  recap: 0,
53
55
  }
54
56
 
55
57
  const KIND_NAME: Record<MarkKind, string> = {
56
58
  user: 'you',
57
59
  subagent: 'sub-agent',
60
+ shell: 'shell',
58
61
  turn: 'response · turn end',
59
62
  turnFailed: 'turn failed',
60
63
  toolFailed: 'tool failed',
@@ -84,6 +87,9 @@ function excerpt(item: TranscriptItem): string {
84
87
  case 'notice': {
85
88
  return item.text
86
89
  }
90
+ case 'shell': {
91
+ return item.shell.command
92
+ }
87
93
  case 'tool_call': {
88
94
  return `${item.name}(${toolInputPreview(item.input)})`
89
95
  }
@@ -174,6 +180,8 @@ export function buildClusters(props: TerminalScrubberProps, railH: number): Clus
174
180
  closeSegment()
175
181
  } else if (item.kind === 'notice' && item.level === 'error') {
176
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) })
177
185
  } else if (
178
186
  item.kind === 'tool_call' &&
179
187
  (item.status === 'failed' || item.result?.isError === true) &&
@@ -0,0 +1,106 @@
1
+ import type { ShellInfo } from '@workerdeck/protocol'
2
+ import type { ShellItem } from '@workerdeck/react'
3
+
4
+ export const SHELL_GLYPH = '$'
5
+
6
+ export const SHELL_KILL_GLYPH = '✕'
7
+
8
+ export const SHELL_EXPAND_CHARS = 100_000
9
+
10
+ export const SHELL_MISSING = 'output expired or not tracked by this gateway'
11
+
12
+ export function shellInfoLabel(shell: ShellInfo): string {
13
+ return shell.label || shell.command.split('\n')[0] || ''
14
+ }
15
+
16
+ export function shellTitle(shell: ShellInfo): string {
17
+ return `#${shell.ordinal} ${shellInfoLabel(shell)}`
18
+ }
19
+
20
+ export function shellLabel(item: ShellItem): string {
21
+ return shellInfoLabel(item.shell)
22
+ }
23
+
24
+ export function shellStatusText(item: ShellItem): string {
25
+ return shellInfoStatusText(item.shell)
26
+ }
27
+
28
+ export function shellInfoFailed(shell: ShellInfo): boolean {
29
+ return shell.status === 'exited' && shell.exitCode !== 0
30
+ }
31
+
32
+ export function shellInfoStatusText(shell: ShellInfo): string {
33
+ if (shell.status === 'running') {
34
+ return 'running'
35
+ }
36
+ if (typeof shell.exitCode === 'number') {
37
+ return shell.exitCode === 0 ? 'exit 0' : `exit ${shell.exitCode}`
38
+ }
39
+ switch (shell.endReason) {
40
+ case 'killed': {
41
+ return 'killed'
42
+ }
43
+ case 'timeout': {
44
+ return 'timed out'
45
+ }
46
+ case 'server_stopped': {
47
+ return 'killed: the gateway stopped'
48
+ }
49
+ case 'server_restarted': {
50
+ return 'ended: the gateway restarted, the process may still be running'
51
+ }
52
+ case 'spawn_failed': {
53
+ return 'failed to start'
54
+ }
55
+ default: {
56
+ return 'ended'
57
+ }
58
+ }
59
+ }
60
+
61
+ export function shellFailed(item: ShellItem): boolean {
62
+ return shellInfoFailed(item.shell)
63
+ }
64
+
65
+ export function shellHeaderText(item: ShellItem): string {
66
+ return `${shellLabel(item)} · ${shellStatusText(item)}`
67
+ }
68
+
69
+ export function shellBodyLines(item: ShellItem, open: boolean): string[] {
70
+ const source = open && item.expanded !== undefined ? item.expanded : item.text
71
+ if (source === '') {
72
+ return []
73
+ }
74
+ const lines = source.split('\n')
75
+ if (!open) {
76
+ return lines
77
+ }
78
+ const kept: string[] = []
79
+ let chars = 0
80
+ for (const line of lines) {
81
+ if (kept.length > 0 && chars + line.length > SHELL_EXPAND_CHARS) {
82
+ break
83
+ }
84
+ kept.push(line)
85
+ chars += line.length + 1
86
+ }
87
+ return kept
88
+ }
89
+
90
+ // The footer is the row's one affordance line: what expanding will fetch, what it clipped, or why it cannot.
91
+ export function shellFooterText(item: ShellItem, open: boolean, shown: number): string | undefined {
92
+ if (item.missing) {
93
+ return SHELL_MISSING
94
+ }
95
+ if (!open) {
96
+ return item.truncated ? '… more output - expand to fetch it' : undefined
97
+ }
98
+ const total = item.expanded === undefined ? undefined : item.expanded.split('\n').length
99
+ if (total !== undefined && total > shown) {
100
+ return `… +${(total - shown).toLocaleString()} lines not shown`
101
+ }
102
+ if (item.expanded === undefined && item.truncated) {
103
+ return '… fetching the full output'
104
+ }
105
+ return undefined
106
+ }
package/src/index.ts CHANGED
@@ -125,7 +125,7 @@ export { SessionBrowser, rowShapeClass, type SessionBrowserProps } from './compo
125
125
  export { SessionItem, type SelectModifiers, type SessionItemProps } from './components/agent/SessionItem.tsx'
126
126
  export { SessionStatusIcon } from './components/agent/SessionStatusIcon.tsx'
127
127
  export { EngineIcon, engineMark, vendorMarkClass, vendorTextClass } from './components/agent/EngineIcon.tsx'
128
- export { type Step, StepRow, sessionSteps } from './components/agent/SessionSteps.tsx'
128
+ export { type ShellStepOptions, type Step, type StepKind, StepRow, sessionSteps } from './components/agent/SessionSteps.tsx'
129
129
  export {
130
130
  SUBAGENT_DISPLAY_META,
131
131
  SUBAGENT_DISPLAY_ORDER,
@@ -134,6 +134,17 @@ export {
134
134
  type SubagentToggleProps,
135
135
  } from './components/agent/SubagentToggle.tsx'
136
136
  export { SubagentStrip } from './components/agent/SubagentStrip.tsx'
137
+ export { ShellStrip, type ShellStripProps } from './components/agent/ShellStrip.tsx'
138
+ export { useShellFrame, type ShellFrame, type ShellFrameOptions } from './components/agent/use-shell-frame.ts'
139
+ export { ShellActionsProvider, useShellActions, type ShellActions } from './components/agent/shell-actions.tsx'
140
+ export {
141
+ SHELL_GLYPH,
142
+ SHELL_KILL_GLYPH,
143
+ shellInfoFailed,
144
+ shellInfoLabel,
145
+ shellInfoStatusText,
146
+ shellTitle,
147
+ } from './components/terminal/shell-row.ts'
137
148
  export { ProjectIcon } from './components/agent/ProjectIcon.tsx'
138
149
  export { SessionEmptyState, type SessionEmptyStateProps } from './components/agent/SessionEmptyState.tsx'
139
150
  export { PromptTokenText, SessionNamesProvider, SkillNamesProvider } from './components/agent/PromptTokenText.tsx'
@@ -470,6 +470,10 @@
470
470
  padding: 0;
471
471
  cursor: pointer;
472
472
  }
473
+ /* The shell drill-in, in the same magenta as the row's `$`. */
474
+ [data-terminal] .term-action[data-tone='magenta'] {
475
+ color: var(--term-magenta);
476
+ }
473
477
  [data-terminal] .term-action:hover {
474
478
  color: var(--term-fg);
475
479
  }
@@ -1152,6 +1156,13 @@ button.term-link {
1152
1156
  .term-scrubber .term-scrub-mark[data-kind='subagent'] {
1153
1157
  background: linear-gradient(to bottom, var(--term-green) 0 2px, color-mix(in srgb, var(--term-green) 25%, transparent) 2px);
1154
1158
  }
1159
+ /* A shell run, in the input lane because a `$` command is something you did:
1160
+ the composer's own magenta, at the bookmark's loudness so a long log never
1161
+ shouts over a failed turn. Drawn as an extent like the marks it shares the
1162
+ lane with. */
1163
+ .term-scrubber .term-scrub-mark[data-kind='shell'] {
1164
+ background: linear-gradient(to bottom, var(--wd-shell-accent) 0 2px, color-mix(in srgb, var(--wd-shell-accent) 25%, transparent) 2px);
1165
+ }
1155
1166
  .term-scrubber .term-scrub-mark[data-kind='turn'] {
1156
1167
  background: linear-gradient(to bottom, var(--term-fg) 0 2px, color-mix(in srgb, var(--term-fg) 25%, transparent) 2px);
1157
1168
  }
@@ -827,6 +827,9 @@
827
827
  .wd-scrubber .wd-scrub-mark[data-kind='subagent'] {
828
828
  background: var(--success);
829
829
  }
830
+ .wd-scrubber .wd-scrub-mark[data-kind='shell'] {
831
+ background: var(--wd-shell-accent);
832
+ }
830
833
  .wd-scrubber .wd-scrub-mark[data-kind='turn'] {
831
834
  background: var(--fg-3);
832
835
  }
package/src/workspace.ts CHANGED
@@ -3,3 +3,4 @@ export { FileTree, type FileTreeProps } from './components/agent/FileTree.tsx'
3
3
  export { EditorTabs, type EditorTabsProps } from './components/agent/EditorTabs.tsx'
4
4
  export { FileViewer, type FileViewerProps } from './components/agent/FileViewer.tsx'
5
5
  export { CodeEditor, type CodeEditorProps } from './components/agent/CodeEditor.tsx'
6
+ export { ShellTerminal, type ShellTerminalProps } from './components/agent/ShellTerminal.tsx'