@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.
Files changed (55) hide show
  1. package/build/{SessionPanel-C1bU4yTM.d.mts → SessionPanel-BNNc8grK.d.mts} +9 -7
  2. package/build/index.d.mts +153 -45
  3. package/build/index.mjs +501 -226
  4. package/build/index.mjs.map +1 -1
  5. package/build/scoped.css +15 -6
  6. package/build/{use-path-links-BEWxt0ZM.mjs → use-path-links-C6Fh5zdx.mjs} +1926 -2086
  7. package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
  8. package/build/workspace.d.mts +1 -1
  9. package/build/workspace.mjs +1 -1
  10. package/package.json +4 -4
  11. package/src/components/agent/ApprovalPrompts.tsx +87 -0
  12. package/src/components/agent/Composer.tsx +25 -47
  13. package/src/components/agent/ContextDialog.tsx +75 -67
  14. package/src/components/agent/FrameStrip.tsx +74 -0
  15. package/src/components/agent/HostFilesDialog.tsx +3 -2
  16. package/src/components/agent/McpDialog.tsx +112 -78
  17. package/src/components/agent/PermissionPrompt.tsx +13 -22
  18. package/src/components/agent/QuestionPrompt.tsx +6 -41
  19. package/src/components/agent/Scrubber.tsx +6 -75
  20. package/src/components/agent/SessionBrowser.tsx +125 -214
  21. package/src/components/agent/SessionFilters.tsx +266 -0
  22. package/src/components/agent/SessionInfoDialog.tsx +47 -63
  23. package/src/components/agent/SessionItem.tsx +27 -7
  24. package/src/components/agent/SessionPanel.tsx +244 -488
  25. package/src/components/agent/SessionSteps.tsx +54 -11
  26. package/src/components/agent/ShellStrip.tsx +27 -43
  27. package/src/components/agent/SubagentStrip.tsx +13 -30
  28. package/src/components/agent/Transcript.tsx +13 -0
  29. package/src/components/agent/TranscriptItemView.tsx +6 -36
  30. package/src/components/agent/composer-commands.ts +80 -1
  31. package/src/components/agent/markdown-image.tsx +49 -1
  32. package/src/components/agent/scrubber-marks.ts +33 -11
  33. package/src/components/agent/scrubber-peek.tsx +122 -0
  34. package/src/components/agent/session-panel-providers.tsx +41 -0
  35. package/src/components/agent/shell-actions.tsx +36 -1
  36. package/src/components/agent/use-catch-up.ts +20 -0
  37. package/src/components/agent/use-frame.ts +88 -0
  38. package/src/components/agent/use-host-image.ts +81 -0
  39. package/src/components/agent/use-shell-frame.ts +12 -70
  40. package/src/components/agent/use-subagent-frame.ts +19 -75
  41. package/src/components/prompt-area/prompt-area-engine.ts +21 -20
  42. package/src/components/prompt-area/prompt-area-list-ops.ts +12 -15
  43. package/src/components/terminal/PermissionPrompt.tsx +19 -32
  44. package/src/components/terminal/QuestionPrompt.tsx +10 -46
  45. package/src/components/terminal/TerminalTranscript.tsx +3 -86
  46. package/src/components/terminal/items.tsx +5 -43
  47. package/src/components/terminal/scrubber.tsx +25 -272
  48. package/src/components/ui/Popover.tsx +29 -0
  49. package/src/components/ui/Select.tsx +25 -0
  50. package/src/index.ts +46 -8
  51. package/src/lib/file-link.ts +3 -16
  52. package/src/lib/permission-prompt.ts +57 -0
  53. package/src/lib/question-answers.ts +65 -0
  54. package/build/use-path-links-BEWxt0ZM.mjs.map +0 -1
  55. package/src/components/prompt-area/use-markdown-mode.ts +0 -103
@@ -0,0 +1,266 @@
1
+ import { useMemo } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { Filter, X } from 'lucide-react'
4
+ import { STATE_LABELS, STATE_ORDER, adaptersOf, displayCustomized, facetFilterCount, projectsOf } from '@workerdeck/protocol'
5
+ import type { GroupBy, SessionRow, SessionState, SortBy, StepDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
6
+ import { Button } from '../ui/Button.tsx'
7
+ import { Popover, PopoverContent, PopoverTrigger } from '../ui/Popover.tsx'
8
+ import { OptionSelect, Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
9
+ import { cn } from '../../lib/utils.ts'
10
+
11
+ export type SessionFiltersProps = {
12
+ config: ViewConfig
13
+ onConfigChange: (config: ViewConfig) => void
14
+ rows: readonly SessionRow[]
15
+ scope?: WorkspaceScope
16
+ gateways?: readonly { id: string; name: string }[]
17
+ className?: string
18
+ }
19
+
20
+ export const STEP_DISPLAY_OPTIONS: readonly { value: StepDisplay; label: string }[] = [
21
+ { value: 'all', label: 'All' },
22
+ { value: 'active', label: 'Active' },
23
+ { value: 'none', label: 'Hide' },
24
+ ]
25
+
26
+ export function SessionFilters({ config, onConfigChange, rows, scope, gateways: gatewaysProp, className }: SessionFiltersProps) {
27
+ const adapters = useMemo(() => adaptersOf(rows), [rows])
28
+ const projects = useMemo(() => projectsOf(rows), [rows])
29
+ const gateways = useMemo(() => {
30
+ if (gatewaysProp) {
31
+ return gatewaysProp
32
+ }
33
+ const seen = new Map<string, string>()
34
+ for (const row of rows) {
35
+ seen.set(row.hostId, row.hostName)
36
+ }
37
+ return [...seen].map(([id, name]) => ({ id, name }))
38
+ }, [rows, gatewaysProp])
39
+
40
+ const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
41
+
42
+ return (
43
+ <div data-slot="session-filters" className={cn('flex flex-col gap-3', className)}>
44
+ <Section title="Filter">
45
+ {scope ? (
46
+ <FilterRow label="Scope">
47
+ <OptionSelect
48
+ className="w-full min-w-0"
49
+ label="Scope"
50
+ value={config.scoped ? 'scoped' : 'all'}
51
+ options={[
52
+ { value: 'scoped', label: scope.label },
53
+ { value: 'all', label: 'All folders' },
54
+ ]}
55
+ onChange={(value) => set({ scoped: value === 'scoped' })}
56
+ />
57
+ </FilterRow>
58
+ ) : null}
59
+ <FilterRow label="State">
60
+ <FacetSelect
61
+ label="State"
62
+ value={config.states}
63
+ options={STATE_ORDER.map((s) => ({ value: s, label: STATE_LABELS[s] }))}
64
+ onChange={(states) => set({ states: states as SessionState[] })}
65
+ />
66
+ </FilterRow>
67
+ {adapters.length > 1 ? (
68
+ <FilterRow label="Engine">
69
+ <FacetSelect
70
+ label="Engine"
71
+ value={config.adapters}
72
+ options={adapters.map((a) => ({ value: a, label: a }))}
73
+ onChange={(next) => set({ adapters: next })}
74
+ />
75
+ </FilterRow>
76
+ ) : null}
77
+ {gateways.length > 1 ? (
78
+ <FilterRow label="Gateway">
79
+ <FacetSelect
80
+ label="Gateway"
81
+ value={config.gateways}
82
+ options={gateways.map((g) => ({ value: g.id, label: g.name }))}
83
+ onChange={(next) => set({ gateways: next })}
84
+ />
85
+ </FilterRow>
86
+ ) : null}
87
+ {projects.length > 1 ? (
88
+ <FilterRow label="Project">
89
+ <FacetSelect
90
+ label="Project"
91
+ value={config.projects ?? []}
92
+ options={projects.map((p) => ({ value: p.key, label: p.label }))}
93
+ onChange={(next) => set({ projects: next })}
94
+ />
95
+ </FilterRow>
96
+ ) : null}
97
+ </Section>
98
+
99
+ <Section title="Layout">
100
+ <FilterRow label="Group">
101
+ <OptionSelect
102
+ className="w-full min-w-0"
103
+ label="Group"
104
+ value={config.groupBy}
105
+ options={[
106
+ { value: 'none', label: 'No grouping' },
107
+ { value: 'state', label: 'By state' },
108
+ { value: 'adapter', label: 'By engine' },
109
+ ...(projects.length > 1 ? [{ value: 'project' as const, label: 'By project' }] : []),
110
+ ...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'By gateway' }] : []),
111
+ ]}
112
+ onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
113
+ />
114
+ </FilterRow>
115
+ <FilterRow label="Sort">
116
+ <OptionSelect
117
+ className="w-full min-w-0"
118
+ label="Sort"
119
+ value={config.sortBy}
120
+ options={[
121
+ { value: 'recent', label: 'Recent' },
122
+ { value: 'name', label: 'Name' },
123
+ { value: 'state', label: 'State' },
124
+ ...(projects.length > 1 ? [{ value: 'project' as const, label: 'Project' }] : []),
125
+ ...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'Gateway' }] : []),
126
+ ]}
127
+ onChange={(sortBy) => set({ sortBy: sortBy as SortBy })}
128
+ />
129
+ </FilterRow>
130
+ </Section>
131
+
132
+ <Section title="Show under each session">
133
+ <FilterRow label="Agents">
134
+ <StepDisplayControl label="Agents" value={config.subagents} onChange={(subagents) => set({ subagents })} />
135
+ </FilterRow>
136
+ <FilterRow label="Shells">
137
+ <StepDisplayControl label="Shells" value={config.shells ?? 'active'} onChange={(shells) => set({ shells })} />
138
+ </FilterRow>
139
+ <FilterRow label="Tasks">
140
+ <StepDisplayControl label="Tasks" value={config.tasks ?? 'active'} onChange={(tasks) => set({ tasks })} />
141
+ </FilterRow>
142
+ </Section>
143
+ </div>
144
+ )
145
+ }
146
+
147
+ export type SessionFiltersButtonProps = SessionFiltersProps & {
148
+ open?: boolean
149
+ onOpenChange?: (open: boolean) => void
150
+ }
151
+
152
+ export function SessionFiltersButton({ open, onOpenChange, className, ...props }: SessionFiltersButtonProps) {
153
+ const count = facetFilterCount(props.config)
154
+ const engaged = count > 0 || displayCustomized(props.config)
155
+ return (
156
+ <Popover open={open} onOpenChange={onOpenChange}>
157
+ <PopoverTrigger
158
+ render={
159
+ <Button
160
+ variant="ghost"
161
+ size="icon-sm"
162
+ aria-label={count > 0 ? `Filters (${count} active)` : 'Filters'}
163
+ title={count > 0 ? `${count} filter${count === 1 ? '' : 's'} active` : 'Filters'}
164
+ className={className}
165
+ >
166
+ <Filter className={cn('size-3.5', engaged && 'fill-current text-fg-1')} />
167
+ </Button>
168
+ }
169
+ />
170
+ <PopoverContent className="w-80">
171
+ <SessionFilters {...props} />
172
+ </PopoverContent>
173
+ </Popover>
174
+ )
175
+ }
176
+
177
+ export function StepDisplayControl({
178
+ label,
179
+ value,
180
+ onChange,
181
+ }: {
182
+ label: string
183
+ value: StepDisplay
184
+ onChange: (value: StepDisplay) => void
185
+ }) {
186
+ return (
187
+ <div role="radiogroup" aria-label={label} className="flex w-full rounded-md border border-border p-px">
188
+ {STEP_DISPLAY_OPTIONS.map((option) => (
189
+ <button
190
+ key={option.value}
191
+ type="button"
192
+ role="radio"
193
+ aria-checked={option.value === value}
194
+ onClick={() => onChange(option.value)}
195
+ className={cn(
196
+ 'min-w-0 flex-1 truncate rounded-[4px] px-1.5 py-0.5 text-label outline-none transition-colors',
197
+ 'focus-visible:ring-2 focus-visible:ring-ring',
198
+ option.value === value ? 'bg-row-selected text-fg-1' : 'text-fg-3 hover:bg-row-hover hover:text-fg-1',
199
+ )}
200
+ >
201
+ {option.label}
202
+ </button>
203
+ ))}
204
+ </div>
205
+ )
206
+ }
207
+
208
+ function Section({ title, children }: { title: string; children: ReactNode }) {
209
+ return (
210
+ <div className="flex flex-col gap-1.5">
211
+ <div className="text-label font-medium uppercase tracking-wide text-fg-4">{title}</div>
212
+ {children}
213
+ </div>
214
+ )
215
+ }
216
+
217
+ function FilterRow({ label, children }: { label: string; children: ReactNode }) {
218
+ return (
219
+ <div className="flex items-center gap-2">
220
+ <span aria-hidden className="w-16 shrink-0 truncate text-label text-fg-3">
221
+ {label}
222
+ </span>
223
+ <div className="min-w-0 flex-1">{children}</div>
224
+ </div>
225
+ )
226
+ }
227
+
228
+ function FacetSelect({
229
+ label,
230
+ value,
231
+ options,
232
+ onChange,
233
+ }: {
234
+ label: string
235
+ value: string[]
236
+ options: { value: string; label: string }[]
237
+ onChange: (value: string[]) => void
238
+ }) {
239
+ return (
240
+ <div className="flex items-center gap-1">
241
+ <Select multiple value={value} onValueChange={(v) => onChange(v as string[])}>
242
+ <SelectTrigger aria-label={label} className="min-w-0 flex-1">
243
+ <SelectValue>
244
+ {value.length === 0
245
+ ? 'All'
246
+ : value.length === 1
247
+ ? (options.find((o) => o.value === value[0])?.label ?? 'All')
248
+ : `${value.length} selected`}
249
+ </SelectValue>
250
+ </SelectTrigger>
251
+ <SelectContent>
252
+ {options.map((option) => (
253
+ <SelectItem key={option.value} value={option.value}>
254
+ <SelectItemText>{option.label}</SelectItemText>
255
+ </SelectItem>
256
+ ))}
257
+ </SelectContent>
258
+ </Select>
259
+ {value.length > 0 ? (
260
+ <Button variant="ghost" size="icon-sm" aria-label={`Clear ${label}`} onClick={() => onChange([])}>
261
+ <X className="size-3 text-fg-4" />
262
+ </Button>
263
+ ) : null}
264
+ </div>
265
+ )
266
+ }
@@ -1,7 +1,5 @@
1
- import { useEffect, useState } from 'react'
2
1
  import type { WorkerDeckClient } from '@workerdeck/client'
3
- import type { SessionFileInfo } from '@workerdeck/protocol'
4
- import type { TranscriptState } from '@workerdeck/react'
2
+ import { useAsync, type TranscriptState } from '@workerdeck/react'
5
3
  import { Download } from 'lucide-react'
6
4
  import { CopyButton } from '../ui/CopyButton.tsx'
7
5
  import { Dialog, DialogBody, DialogContent, DialogHeader, DialogRow } from '../ui/Dialog.tsx'
@@ -17,46 +15,59 @@ export interface SessionInfoDialogProps {
17
15
  onOpenChange: (open: boolean) => void
18
16
  }
19
17
 
18
+ export interface SessionInfoPanelProps {
19
+ state: TranscriptState
20
+ client: WorkerDeckClient
21
+ sessionId: string | undefined
22
+ active?: boolean
23
+ }
24
+
20
25
  export function SessionInfoDialog({ state, client, sessionId, open, onOpenChange }: SessionInfoDialogProps) {
21
- const session = state.session
22
- const mode = state.permissionMode ? permissionModeMeta(state.permissionMode) : undefined
23
26
  return (
24
27
  <Dialog open={open} onOpenChange={onOpenChange}>
25
28
  <DialogContent>
26
- <DialogHeader title="Session info" description={session?.title} />
29
+ <DialogHeader title="Session info" description={state.session?.title} />
27
30
  <DialogBody>
28
- <div className="flex flex-col divide-y divide-border">
29
- <div className="pb-2">
30
- <DialogRow label="Engine">{state.engine ?? 'claude'}</DialogRow>
31
- {session?.profile ? <DialogRow label="Profile">{session.profile}</DialogRow> : null}
32
- {state.model ? (
33
- <DialogRow label="Model" mono>
34
- {state.model}
35
- </DialogRow>
36
- ) : null}
37
- {mode ? <DialogRow label="Permission mode">{mode.label}</DialogRow> : null}
38
- {session?.apiKeySource ? <DialogRow label="Credentials">{session.apiKeySource}</DialogRow> : null}
39
- </div>
40
- <div className="py-2">
41
- {state.cwd ? <CopyRow label="Working directory" value={state.cwd} /> : null}
42
- {state.sdkSessionId ? <CopyRow label="Engine session id" value={state.sdkSessionId} /> : null}
43
- {session ? <CopyRow label="Gateway session id" value={session.id} /> : null}
44
- </div>
45
- <div className="py-2">
46
- {session?.createdAt ? <DialogRow label="Started">{formatRelativeTime(session.createdAt)}</DialogRow> : null}
47
- {session?.numTurns !== undefined ? <DialogRow label="Turns">{session.numTurns}</DialogRow> : null}
48
- <DialogRow label="Cost" mono>
49
- {formatCost(state.costUsd ?? (state.totalCostUsd || undefined))}
50
- </DialogRow>
51
- </div>
52
- {state.capabilities.vfs ? <SessionFiles client={client} sessionId={sessionId} open={open} /> : null}
53
- </div>
31
+ <SessionInfoPanel state={state} client={client} sessionId={sessionId} active={open} />
54
32
  </DialogBody>
55
33
  </DialogContent>
56
34
  </Dialog>
57
35
  )
58
36
  }
59
37
 
38
+ export function SessionInfoPanel({ state, client, sessionId, active = true }: SessionInfoPanelProps) {
39
+ const session = state.session
40
+ const mode = state.permissionMode ? permissionModeMeta(state.permissionMode) : undefined
41
+ return (
42
+ <div className="flex flex-col divide-y divide-border">
43
+ <div className="pb-2">
44
+ <DialogRow label="Engine">{state.engine ?? 'claude'}</DialogRow>
45
+ {session?.profile ? <DialogRow label="Profile">{session.profile}</DialogRow> : null}
46
+ {state.model ? (
47
+ <DialogRow label="Model" mono>
48
+ {state.model}
49
+ </DialogRow>
50
+ ) : null}
51
+ {mode ? <DialogRow label="Permission mode">{mode.label}</DialogRow> : null}
52
+ {session?.apiKeySource ? <DialogRow label="Credentials">{session.apiKeySource}</DialogRow> : null}
53
+ </div>
54
+ <div className="py-2">
55
+ {state.cwd ? <CopyRow label="Working directory" value={state.cwd} /> : null}
56
+ {state.sdkSessionId ? <CopyRow label="Engine session id" value={state.sdkSessionId} /> : null}
57
+ {session ? <CopyRow label="Gateway session id" value={session.id} /> : null}
58
+ </div>
59
+ <div className="py-2">
60
+ {session?.createdAt ? <DialogRow label="Started">{formatRelativeTime(session.createdAt)}</DialogRow> : null}
61
+ {session?.numTurns !== undefined ? <DialogRow label="Turns">{session.numTurns}</DialogRow> : null}
62
+ <DialogRow label="Cost" mono>
63
+ {formatCost(state.costUsd ?? (state.totalCostUsd || undefined))}
64
+ </DialogRow>
65
+ </div>
66
+ {state.capabilities.vfs ? <SessionFiles client={client} sessionId={sessionId} active={active} /> : null}
67
+ </div>
68
+ )
69
+ }
70
+
60
71
  function CopyRow({ label, value }: { label: string; value: string }) {
61
72
  return (
62
73
  <div className="flex items-start justify-between gap-2 py-1.5">
@@ -69,37 +80,10 @@ function CopyRow({ label, value }: { label: string; value: string }) {
69
80
  )
70
81
  }
71
82
 
72
- function SessionFiles({ client, sessionId, open }: { client: WorkerDeckClient; sessionId: string | undefined; open: boolean }) {
73
- const [files, setFiles] = useState<SessionFileInfo[] | undefined>()
74
- const [loading, setLoading] = useState(false)
75
-
76
- useEffect(() => {
77
- if (!open || !sessionId) {
78
- return
79
- }
80
- let cancelled = false
81
- setLoading(true)
82
- client
83
- .listSessionFiles(sessionId)
84
- .then((list) => {
85
- if (!cancelled) {
86
- setFiles(list)
87
- }
88
- })
89
- .catch(() => {
90
- if (!cancelled) {
91
- setFiles([])
92
- }
93
- })
94
- .finally(() => {
95
- if (!cancelled) {
96
- setLoading(false)
97
- }
98
- })
99
- return () => {
100
- cancelled = true
101
- }
102
- }, [client, sessionId, open])
83
+ function SessionFiles({ client, sessionId, active }: { client: WorkerDeckClient; sessionId: string | undefined; active: boolean }) {
84
+ const { data: files, loading } = useAsync(() => client.listSessionFiles(sessionId!), [client, sessionId], {
85
+ enabled: active && !!sessionId,
86
+ })
103
87
 
104
88
  return (
105
89
  <div className="pt-3">
@@ -1,7 +1,7 @@
1
1
  import { Fragment, useEffect, useRef, useState } from 'react'
2
2
  import type { ReactNode } from 'react'
3
3
  import { projectLabel, projectName, projectSubpath, sessionLabel } from '@workerdeck/protocol'
4
- import type { SessionRow, SubagentDisplay } from '@workerdeck/protocol'
4
+ import type { SessionRow, SessionTask, StepDisplay, SubagentDisplay } from '@workerdeck/protocol'
5
5
  import { ContextRing } from './ContextRing.tsx'
6
6
  import { EngineIcon, vendorMarkClass, vendorTextClass } from './EngineIcon.tsx'
7
7
  import { ProjectIcon } from './ProjectIcon.tsx'
@@ -21,8 +21,12 @@ export interface SessionItemProps {
21
21
  showProject?: boolean
22
22
  projectIcons?: Record<string, string>
23
23
  subagents?: SubagentDisplay
24
+ shells?: StepDisplay
25
+ tasks?: StepDisplay
24
26
  onSelect?: (modifiers: SelectModifiers) => void
25
27
  onSelectSubagent?: (toolUseId: string) => void
28
+ onSelectTask?: (task: SessionTask) => void
29
+ onStopTask?: (toolUseId: string) => void
26
30
  onSelectShell?: (shellId: string) => void
27
31
  onKillShell?: (shellId: string) => void
28
32
  onShellAgentWrite?: (shellId: string, enabled: boolean) => void
@@ -45,8 +49,12 @@ export function SessionItem({
45
49
  showProject = true,
46
50
  projectIcons,
47
51
  subagents = 'active',
52
+ shells = 'active',
53
+ tasks = 'active',
48
54
  onSelect,
49
55
  onSelectSubagent,
56
+ onSelectTask,
57
+ onStopTask,
50
58
  onSelectShell,
51
59
  onKillShell,
52
60
  onShellAgentWrite,
@@ -98,12 +106,24 @@ export function SessionItem({
98
106
  for (const extra of extras) {
99
107
  parts.push(<span key={extra}>{extra}</span>)
100
108
  }
101
- const steps = sessionSteps(info, (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)), subagents, {
102
- now: now ?? Date.now(),
103
- onSelect: (shellId) => (onSelectShell ? onSelectShell(shellId) : onSelect?.(NO_MODIFIERS)),
104
- onKill: onKillShell,
105
- onAgentWrite: onShellAgentWrite,
106
- })
109
+ const steps = sessionSteps(
110
+ info,
111
+ (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)),
112
+ subagents,
113
+ {
114
+ now: now ?? Date.now(),
115
+ show: shells,
116
+ onSelect: (shellId) => (onSelectShell ? onSelectShell(shellId) : onSelect?.(NO_MODIFIERS)),
117
+ onKill: onKillShell,
118
+ onAgentWrite: onShellAgentWrite,
119
+ },
120
+ {
121
+ show: tasks,
122
+ live: info.status === 'running' || info.status === 'starting' || info.status === 'awaiting_approval',
123
+ onSelect: (task) => (onSelectTask ? onSelectTask(task) : onSelect?.(NO_MODIFIERS)),
124
+ onStop: onStopTask,
125
+ },
126
+ )
107
127
  const holdsOpenStep = steps.some((s) => s.key === activeStepKey)
108
128
 
109
129
  return (