dsh-code 0.8.0 → 0.9.1

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/src/app.ts CHANGED
@@ -52,6 +52,7 @@ import {
52
52
  deepseekWaveTier,
53
53
  deepseekWaveWordHue,
54
54
  deepseekWaveWordVisible,
55
+ effortAboveHigh,
55
56
  isOfficialDeepSeekLabel,
56
57
  pulseFrame,
57
58
  WAVE_BASE_DARK,
@@ -66,7 +67,8 @@ import type { ProviderSettingsDirectory, ProviderTargetView } from './provider-s
66
67
  import type { QuestionSnapshot, QuestionStore } from './questions.ts'
67
68
  import type { SkillsView, SkillRow } from './skills.ts'
68
69
  import type { MentionCandidate } from './mentions.ts'
69
- import { EffortPanel, ModePanel, HistoryPanel, PermissionPanel, PluginPanel, ResumePanel, StatuslinePanel } from './kernel-panels.ts'
70
+ import type { SubagentFeedView, SubagentRow } from './subagents.ts'
71
+ import { AgentsPanel, EffortPanel, ModePanel, HistoryPanel, PermissionPanel, PluginPanel, ResumePanel, StatuslinePanel, SubagentPanel } from './kernel-panels.ts'
70
72
  import type { PresetRow } from './presets.ts'
71
73
  import type { PermissionRow } from './permissions.ts'
72
74
  import type { PluginRow } from './plugin-inventory.ts'
@@ -131,6 +133,8 @@ export interface AppProps {
131
133
  approval: ApprovalStore
132
134
  /** ask_user_question store fed by the single UI provider. */
133
135
  questions: QuestionStore
136
+ /** Live subagent activity feed (child sessions of the current root). */
137
+ subagents: SubagentFeedView
134
138
  /** Live slash-command descriptor list (completion candidates). */
135
139
  commands: CommandsView
136
140
  /** Live user-invocable skill catalog (completion candidates). */
@@ -167,6 +171,14 @@ export interface AppProps {
167
171
  loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
168
172
  /** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
169
173
  selectModel(row: ModelRow, effortId?: string): string
174
+ /** The /subagent override label, '' when delegated agents follow the current model. */
175
+ subagentModel: string
176
+ /** Apply one /subagent model pick; returns the override label. */
177
+ setSubagentModel(row: ModelRow, effortId?: string): string
178
+ /** Drop the /subagent override (delegated agents follow the current model). */
179
+ clearSubagentModel(): void
180
+ /** Delete one session subtree; resolves with the outcome line. */
181
+ deleteSession(id: string): Promise<string>
170
182
  /** Load provider/settings/credential facts for the optional /model provider stage. */
171
183
  loadModelProviders?(): Promise<ProviderSettingsDirectory>
172
184
  /** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */
@@ -193,6 +205,8 @@ export interface AppProps {
193
205
  createSession(mode?: string): void
194
206
  loadSessions(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>
195
207
  loadSessionTranscript(id: string, signal?: AbortSignal): Promise<string>
208
+ /** Load this session's subagent conversations (children by lineage). */
209
+ loadSubagents(): Promise<readonly SessionRow[]>
196
210
  switchSession(row: SessionRow): void
197
211
  cancelSessionSwitch(): boolean
198
212
  loadPlugins(): readonly PluginRow[]
@@ -691,6 +705,30 @@ function todoMark(status: TodoItem['status']): string {
691
705
  return status === 'completed' ? '✓' : status === 'in_progress' ? '●' : '○'
692
706
  }
693
707
 
708
+ /**
709
+ * One-row live subagent summary (the Codex agent status feed, compressed to
710
+ * the transcript's budget): running count, total, and the most recently
711
+ * active child's current activity. One line, never more — the full view is
712
+ * the /agents panel.
713
+ */
714
+ function AgentsLine({ rows }: { rows: readonly SubagentRow[] }): ReactElement | undefined {
715
+ if (rows.length === 0) return undefined
716
+ const running = rows.filter(row => row.state !== 'done').length
717
+ const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]!
718
+ const mark = newest.state === 'done' ? '✓' : newest.state === 'idle' ? '⏸' : '●'
719
+ return createElement(
720
+ Box,
721
+ { paddingX: 1 },
722
+ createElement(
723
+ Text,
724
+ { color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' },
725
+ `agents ${running} live`,
726
+ createElement(Text, { color: inkColor(getPalette().dim) }, ` · ${rows.length} total · /agents`),
727
+ createElement(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`),
728
+ ),
729
+ )
730
+ }
731
+
694
732
  /** One-row todo summary: task count cannot grow the live Ink tree. */
695
733
  function TodoPanel({ todos }: { todos: readonly TodoItem[] }): ReactElement | undefined {
696
734
  if (todos.length === 0) return undefined
@@ -780,9 +818,10 @@ function statusToneProps(tone: StatusTone): {
780
818
 
781
819
  /** Theme anchors for the one-shot composer wave, read from the active palette
782
820
  * so the wave stays coordinated in both themes. The flash tier runs the
783
- * brand blues; the deepseek tier swaps in the code sky-blue for a brighter,
784
- * richer mix. Codex's Wave bands carry no hue index (only hues[0] tints the
785
- * row), so the accent the prompt keeps is always hues[0]. */
821
+ * brand blues; the deepseek AND unknown tiers swap in the code sky-blue for
822
+ * a brighter, richer mix (the unknown tier reuses the pro palette). Codex's
823
+ * Wave bands carry no hue index (only hues[0] tints the row), so the accent
824
+ * the prompt keeps is always hues[0]. */
786
825
  function deepseekWaveHues(tier: DeepseekWaveTier): readonly [RgbTriple, RgbTriple, RgbTriple] {
787
826
  const palette = getPalette()
788
827
  return tier === 'flash'
@@ -877,72 +916,139 @@ function NoticeLine({ text, tone, columns }: {
877
916
  )
878
917
  }
879
918
 
880
- /** The y/n approval bar rendered while an approval ask is pending. */
881
- function ApprovalBar({ snapshot, locked }: { snapshot: ApprovalSnapshot; locked: boolean }): ReactElement | undefined {
919
+ /** One selectable approval decision (Codex approval-overlay wording). */
920
+ interface ApprovalOption {
921
+ readonly key: 'allow' | 'reject-note' | 'reject'
922
+ readonly label: string
923
+ readonly hotkey: string
924
+ }
925
+
926
+ /** The fixed decision list; answers stay in the binary answerer vocabulary. */
927
+ const APPROVAL_OPTIONS: readonly ApprovalOption[] = [
928
+ { key: 'allow', label: 'Yes, proceed', hotkey: 'y' },
929
+ { key: 'reject-note', label: 'No, and tell it what to do differently', hotkey: 'n' },
930
+ { key: 'reject', label: 'No, continue without running it', hotkey: 'd' },
931
+ ]
932
+
933
+ /**
934
+ * The approval dialog (Codex ApprovalOverlay contract): a bold question
935
+ * header, the bounded command body with an explicit overflow marker, a
936
+ * numbered option list with a `›` cursor, single-key shortcuts, and digits
937
+ * for direct selection. Askers queue FIFO — the count rides the header.
938
+ * The upstream answerer vocabulary stays binary (`allowed-once` /
939
+ * `rejected`): "tell it what to do differently" rejects and hands the
940
+ * composer back with a hint notice, exactly Codex's decline-then-type flow.
941
+ */
942
+ function ApprovalBar({ snapshot, locked, notify }: {
943
+ snapshot: ApprovalSnapshot
944
+ locked: boolean
945
+ notify(text: string, tone?: NoticeTone): void
946
+ }): ReactElement | undefined {
882
947
  const stdout = useStdout().stdout
883
948
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
884
- const [scroll, setScroll] = useState(0)
949
+ const [cursor, setCursor] = useState(0)
885
950
  const pending = snapshot.pending
886
- const active = !locked && snapshot.pending !== undefined && !snapshot.answered
887
- const content = useMemo<readonly StyledLine[]>(() => pending === undefined
951
+ const active = !locked && pending !== undefined && !snapshot.answered
952
+ const body = useMemo<readonly StyledLine[]>(() => pending === undefined || pending.command === ''
888
953
  ? []
889
- : [
890
- ...styledLines([lineSegment(pending.headline, 'warn')], viewport.contentColumns),
891
- ...(pending.command === '' ? [] : textLines(` ${pending.command}`, viewport.contentColumns, 'dim')),
892
- ], [pending, viewport.contentColumns])
893
- const visibleScroll = clampScroll(scroll, content.length, viewport.bodyRows)
954
+ : textLines(pending.command, viewport.contentColumns, 'dim'), [pending, viewport.contentColumns])
894
955
 
895
956
  useEffect(() => {
896
- setScroll(0)
957
+ setCursor(0)
897
958
  }, [pending])
898
959
 
899
- useEffect(() => {
900
- if (visibleScroll !== scroll) setScroll(visibleScroll)
901
- }, [visibleScroll, scroll])
960
+ const decide = (option: ApprovalOption): void => {
961
+ const ask = snapshot.pending
962
+ if (ask === undefined || snapshot.answered) return
963
+ if (option.key === 'allow') {
964
+ ask.answer('allowed-once')
965
+ return
966
+ }
967
+ ask.answer('rejected')
968
+ if (option.key === 'reject-note') {
969
+ notify('rejected — type below what it should do differently (it steers the next step)', 'warning')
970
+ }
971
+ }
902
972
 
903
973
  useInput((input, key) => {
904
- if (snapshot.pending === undefined) return
974
+ const ask = snapshot.pending
975
+ if (ask === undefined || snapshot.answered) return
905
976
  if (key.upArrow) {
906
- setScroll(current => moveScroll(current, -1, content.length, viewport.bodyRows))
977
+ setCursor(current => (current + APPROVAL_OPTIONS.length - 1) % APPROVAL_OPTIONS.length)
907
978
  return
908
979
  }
909
980
  if (key.downArrow) {
910
- setScroll(current => moveScroll(current, 1, content.length, viewport.bodyRows))
981
+ setCursor(current => (current + 1) % APPROVAL_OPTIONS.length)
911
982
  return
912
983
  }
913
- if (key.pageUp) {
914
- setScroll(current => moveScroll(current, -Math.max(1, viewport.bodyRows - 1), content.length, viewport.bodyRows))
984
+ if (key.return) {
985
+ decide(APPROVAL_OPTIONS[cursor]!)
915
986
  return
916
987
  }
917
- if (key.pageDown) {
918
- setScroll(current => moveScroll(current, Math.max(1, viewport.bodyRows - 1), content.length, viewport.bodyRows))
988
+ if (key.escape) {
989
+ decide(APPROVAL_OPTIONS[2]!)
919
990
  return
920
991
  }
921
- if (snapshot.answered) return
922
992
  if (input === 'y' || input === 'Y') {
923
- snapshot.pending.answer('allowed-once')
993
+ decide(APPROVAL_OPTIONS[0]!)
924
994
  return
925
995
  }
926
996
  if (input === 'n' || input === 'N') {
927
- snapshot.pending.answer('rejected')
997
+ decide(APPROVAL_OPTIONS[1]!)
998
+ return
999
+ }
1000
+ if (input === 'd' || input === 'D') {
1001
+ decide(APPROVAL_OPTIONS[2]!)
1002
+ return
1003
+ }
1004
+ if (/^[1-9]$/u.test(input)) {
1005
+ const index = Number(input) - 1
1006
+ if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index]!)
928
1007
  }
929
1008
  }, { isActive: active })
930
- if (snapshot.pending === undefined) return undefined
1009
+
1010
+ if (pending === undefined) return undefined
931
1011
  if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
1012
+ const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : ''
932
1013
  if (viewport.compact) {
933
- return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('approval · y allow · n reject', viewport.contentColumns))
1014
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`approval${queuedSuffix} · enter/y allow · esc/n reject`, viewport.contentColumns))
934
1015
  }
935
- const { answered } = snapshot
1016
+ // Body budget: title + options + footer consume fixed rows; the command
1017
+ // preview shrinks with an explicit overflow marker (Codex's "[… N lines]").
1018
+ const reservedRows = 3 + APPROVAL_OPTIONS.length
1019
+ const bodyBudget = Math.max(1, viewport.bodyRows - reservedRows)
1020
+ const visibleBody = body.slice(0, bodyBudget)
1021
+ const overflow = body.length - visibleBody.length
936
1022
  return createElement(
937
1023
  Box,
938
1024
  { flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().warn) },
939
- createElement(Text, { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' }, truncateColumns(`⏸ waiting for approval · lines ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)),
940
- createElement(PanelGap, { visible: viewport.gapRows > 0 }),
941
- createElement(StyledRows, { lines: content.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
942
- createElement(PanelGap, { visible: viewport.gapRows > 0 }),
943
- createElement(Text, { dimColor: true, wrap: 'truncate-end' }, dim(truncateColumns(answered
1025
+ createElement(
1026
+ Text,
1027
+ { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' },
1028
+ truncateColumns(`${pending.headline}${queuedSuffix}`, viewport.contentColumns),
1029
+ ),
1030
+ createElement(PanelGap, { visible: viewport.gapRows > 0 && body.length > 0 }),
1031
+ ...visibleBody.map((line, index) => createElement(StyledRows, { key: `body-${index}`, lines: [line] })),
1032
+ ...(overflow > 0
1033
+ ? [createElement(Text, { key: 'overflow', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(`… +${overflow} more lines · ctrl+o shows the full call in the transcript`, viewport.contentColumns))]
1034
+ : []),
1035
+ ...(body.length > 0 ? [createElement(PanelGap, { visible: viewport.gapRows > 0 })] : []),
1036
+ ...APPROVAL_OPTIONS.map((option, index) => {
1037
+ const selected = !snapshot.answered && index === cursor
1038
+ return createElement(
1039
+ Text,
1040
+ {
1041
+ key: option.key,
1042
+ color: selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().text),
1043
+ bold: selected || undefined,
1044
+ wrap: 'truncate-end',
1045
+ },
1046
+ truncateColumns(`${selected ? '›' : ' '} ${index + 1}. ${option.label} (${option.hotkey})`, viewport.contentColumns),
1047
+ )
1048
+ }),
1049
+ createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(snapshot.answered
944
1050
  ? 'submitted…'
945
- : '↑↓/pgup/pgdn scroll · y allow once · n reject', viewport.contentColumns))),
1051
+ : '↑↓ choose · enter confirm · y/n/d quick · esc reject', viewport.contentColumns)),
946
1052
  )
947
1053
  }
948
1054
 
@@ -1217,9 +1323,11 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
1217
1323
  }
1218
1324
 
1219
1325
  /** The /model panel: a scrolling list over the advisory model directory. */
1220
- function ModelPanel({ directory, error, onSelect, onProviders, onRetry, onClose }: {
1326
+ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry, onClose }: {
1221
1327
  directory: ModelDirectory | undefined
1222
1328
  error: string | undefined
1329
+ /** `provider/model` label of the applied model: the cursor lands on it once. */
1330
+ current?: string
1223
1331
  onSelect(row: ModelRow): void
1224
1332
  onProviders?(): void
1225
1333
  onRetry(): void
@@ -1229,14 +1337,27 @@ function ModelPanel({ directory, error, onSelect, onProviders, onRetry, onClose
1229
1337
  const stdout = useStdout().stdout
1230
1338
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
1231
1339
  const rows = directory?.rows ?? []
1340
+ const positioned = useRef(false)
1232
1341
 
1233
1342
  useEffect(() => {
1234
- if (rows.length === 0) {
1235
- if (cursor !== 0) setCursor(0)
1343
+ // Open ON the applied model (Codex resumes the previous pick): the first
1344
+ // non-empty directory positions the cursor once, never on later refreshes.
1345
+ if (positioned.current || rows.length === 0 || current === undefined) {
1346
+ if (rows.length === 0) {
1347
+ if (cursor !== 0) setCursor(0)
1348
+ return
1349
+ }
1350
+ if (cursor >= rows.length) setCursor(rows.length - 1)
1236
1351
  return
1237
1352
  }
1238
- if (cursor >= rows.length) setCursor(rows.length - 1)
1239
- }, [rows.length, cursor])
1353
+ const index = rows.findIndex(row => `${row.provider}/${row.model}` === current)
1354
+ if (index >= 0) {
1355
+ positioned.current = true
1356
+ setCursor(index)
1357
+ } else if (cursor >= rows.length) {
1358
+ setCursor(Math.max(0, rows.length - 1))
1359
+ }
1360
+ }, [rows, cursor, current])
1240
1361
 
1241
1362
  useInput((input, key) => {
1242
1363
  if (key.escape || input === 'q') {
@@ -1685,6 +1806,9 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
1685
1806
  createElement(Box, { key: 'local-statusline' }, row('/statusline', 'customize the status line items')),
1686
1807
  createElement(Box, { key: 'local-theme' }, row('/theme', 'switch the color theme')),
1687
1808
  createElement(Box, { key: 'local-history' }, row('/history', 'search and recall past prompts')),
1809
+ createElement(Box, { key: 'local-agents' }, row('/agents', 'inspect subagent sessions of this conversation')),
1810
+ createElement(Box, { key: 'local-subagent' }, row('/subagent', 'choose the model delegated subagents run on')),
1811
+ createElement(Box, { key: 'local-delete' }, row('/delete', 'delete a session and its subagent threads')),
1688
1812
  createElement(Box, { key: 'local-clear' }, row('/clear', 'clear the screen')),
1689
1813
  createElement(Box, { key: 'local-export' }, row('/export', 'export the transcript to markdown (/export [path])')),
1690
1814
  createElement(Box, { key: 'local-title' }, row('/title', 'rename this session (/title <text>)')),
@@ -2021,6 +2145,9 @@ export function completionCandidates(
2021
2145
  { label: '/statusline', description: 'customize the status line', origin: 'command' },
2022
2146
  { label: '/theme', description: 'switch the color theme', origin: 'command' },
2023
2147
  { label: '/history', description: 'search and recall past prompts', origin: 'command' },
2148
+ { label: '/agents', description: 'inspect subagent sessions of this conversation', origin: 'command' },
2149
+ { label: '/subagent', description: 'choose the model delegated subagents run on', origin: 'command' },
2150
+ { label: '/delete', description: 'delete a session and its subagent threads', origin: 'command' },
2024
2151
  { label: '/clear', description: 'clear the screen', origin: 'command' },
2025
2152
  { label: '/export', description: 'export the transcript to markdown', origin: 'command' },
2026
2153
  { label: '/title', description: 'rename this session', origin: 'command' },
@@ -2123,7 +2250,7 @@ function CompletionMenu({ active, mention, index, rows }: {
2123
2250
  * While a modal (approval / question / model panel) owns the keys, the
2124
2251
  * box passes every key through untouched.
2125
2252
  */
2126
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }: {
2253
+ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, openAgents, openSubagent, openDelete, deleteConfirm, confirmDelete, cancelDelete, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }: {
2127
2254
  active: boolean
2128
2255
  frozen: boolean
2129
2256
  busy: boolean
@@ -2143,6 +2270,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2143
2270
  openStatusline(): void
2144
2271
  openTheme(): void
2145
2272
  openHistory(): void
2273
+ /** Open the /agents panel (live subagent feed + transcript entry). */
2274
+ openAgents(): void
2275
+ /** Open the /subagent model panel. */
2276
+ openSubagent(): void
2277
+ /** Open the /resume picker in delete mode, optionally pre-armed on one id. */
2278
+ openDelete(id?: string): void
2279
+ /** The row id awaiting y/n in this box, when a deletion is pending. */
2280
+ deleteConfirm?: string
2281
+ /** Confirm the pending deletion (y in the box). */
2282
+ confirmDelete(): void
2283
+ /** Cancel the pending deletion (any other key in the box). */
2284
+ cancelDelete(): void
2146
2285
  createSession(mode?: string): void
2147
2286
  cancelSessionSwitch(): boolean
2148
2287
  notify(text: string, tone?: NoticeTone): void
@@ -2170,9 +2309,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2170
2309
  historyFill: { text: string; index: number } | undefined
2171
2310
  /** Marks the accepted entry consumed (called after the fill is applied). */
2172
2311
  historyConsumed(): void
2173
- /** DeepSeek easter-egg wave tier of the applied official DeepSeek model
2174
- * (null otherwise): drives the persistent prompt glyph/accent and the
2175
- * sparkle tier. */
2312
+ /** DeepSeek easter-egg wave tier of the applied route (null otherwise):
2313
+ * official DeepSeek models drive their flash/pro tiers, non-DeepSeek
2314
+ * models running an effort above high drive the "Into the Unknown"
2315
+ * variant. Drives the persistent prompt glyph/accent and the sparkle
2316
+ * tier. */
2176
2317
  waveTier: DeepseekWaveTier | null
2177
2318
  /** The ignition style running, if any: Wave / Aurora / Pulse. */
2178
2319
  waveStyle: DeepseekWaveStyle | null
@@ -2289,9 +2430,53 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2289
2430
  }))
2290
2431
  : candidates
2291
2432
 
2433
+ /** Accept the highlighted completion-menu candidate into the draft. */
2434
+ const acceptMenuCandidate = (): void => {
2435
+ if (mentionActive && mentionToken !== undefined) {
2436
+ const row = mentionRows[completionIndex % mentionRows.length]
2437
+ if (row !== undefined) {
2438
+ // Session rows carry the canonical @[label](dsh-session:…) token;
2439
+ // file rows insert `@path` (directories keep their trailing slash).
2440
+ const insertion = row.label.startsWith('@')
2441
+ ? row.label
2442
+ : `@${row.label}${row.kind === 'directory' ? '/' : ''}`
2443
+ setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor))
2444
+ setCursor(mentionToken.start + insertion.length)
2445
+ }
2446
+ } else if (pathActive) {
2447
+ const row = pathRows[completionIndex % Math.max(1, pathRows.length)]
2448
+ if (row !== undefined) {
2449
+ // Bare path completion replaces the typed token with the chosen
2450
+ // workspace path (directories keep their trailing slash).
2451
+ const insertion = row.kind === 'directory' ? `${row.label}/` : row.label
2452
+ setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor))
2453
+ setCursor(pathTokenStart + insertion.length)
2454
+ }
2455
+ } else {
2456
+ const candidate = candidates[completionIndex % candidates.length]
2457
+ if (candidate !== undefined) {
2458
+ setValue(`${candidate.label} `)
2459
+ setCursor(candidate.label.length + 1)
2460
+ }
2461
+ }
2462
+ setCompletionIndex(0)
2463
+ setDismissedMenuValue(undefined)
2464
+ }
2465
+
2292
2466
  useInput((input, key) => {
2293
2467
  // Modal ownership: approval/question/model dialogs consume all keys.
2294
2468
  if (!active) return
2469
+ // Deletion confirm owns the box: y proceeds, anything else cancels.
2470
+ // Typed in the INPUT BOX (codex delete-confirm): the keystroke is echoed
2471
+ // as the box's own prompt, not an invisible panel keypress.
2472
+ if (deleteConfirm !== undefined) {
2473
+ if (input === 'y' || input === 'Y') {
2474
+ confirmDelete()
2475
+ } else {
2476
+ cancelDelete()
2477
+ }
2478
+ return
2479
+ }
2295
2480
  // Shift+Tab cycles the permission preset (Claude-Code convention).
2296
2481
  if (key.tab && key.shift) {
2297
2482
  try {
@@ -2363,6 +2548,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2363
2548
  setDismissedMenuValue(undefined)
2364
2549
  return
2365
2550
  }
2551
+ // Enter on an open completion menu accepts the highlighted candidate
2552
+ // (Codex list parity: Tab and Enter are both accept keys — many users
2553
+ // never discover Tab) — UNLESS the draft already spells one candidate
2554
+ // exactly, in which case Enter submits it (typing a full "/effort" and
2555
+ // pressing return must run the command, not re-accept its own text).
2556
+ if (menuActive) {
2557
+ const exactSlash = !mentionActive && !pathActive && candidates.some(candidate => candidate.label === value)
2558
+ if (!exactSlash) {
2559
+ acceptMenuCandidate()
2560
+ return
2561
+ }
2562
+ }
2366
2563
  const text = value.trim()
2367
2564
  setValue('')
2368
2565
  setCursor(0)
@@ -2461,6 +2658,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2461
2658
  openHistory()
2462
2659
  return
2463
2660
  }
2661
+ if (text === '/agents') {
2662
+ openAgents()
2663
+ return
2664
+ }
2665
+ if (text === '/subagent') {
2666
+ openSubagent()
2667
+ return
2668
+ }
2669
+ if (text === '/delete' || text.startsWith('/delete ')) {
2670
+ openDelete(text.slice(7).trim())
2671
+ return
2672
+ }
2464
2673
  if (busy && !text.startsWith('/')) {
2465
2674
  // A running turn is steered, not blocked: the inbox delivers this
2466
2675
  // text at the next step boundary (Esc/Ctrl+C still cancels outright).
@@ -2506,35 +2715,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2506
2715
  return
2507
2716
  }
2508
2717
  if (key.tab && menuActive) {
2509
- if (mentionActive && mentionToken !== undefined) {
2510
- const row = mentionRows[completionIndex % mentionRows.length]
2511
- if (row !== undefined) {
2512
- // Session rows carry the canonical @[label](dsh-session:…) token;
2513
- // file rows insert `@path` (directories keep their trailing slash).
2514
- const insertion = row.label.startsWith('@')
2515
- ? row.label
2516
- : `@${row.label}${row.kind === 'directory' ? '/' : ''}`
2517
- setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor))
2518
- setCursor(mentionToken.start + insertion.length)
2519
- }
2520
- } else if (pathActive) {
2521
- const row = pathRows[completionIndex % Math.max(1, pathRows.length)]
2522
- if (row !== undefined) {
2523
- // Bare path completion replaces the typed token with the chosen
2524
- // workspace path (directories keep their trailing slash).
2525
- const insertion = row.kind === 'directory' ? `${row.label}/` : row.label
2526
- setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor))
2527
- setCursor(pathTokenStart + insertion.length)
2528
- }
2529
- } else {
2530
- const candidate = candidates[completionIndex % candidates.length]
2531
- if (candidate !== undefined) {
2532
- setValue(`${candidate.label} `)
2533
- setCursor(candidate.label.length + 1)
2534
- }
2535
- }
2536
- setCompletionIndex(0)
2537
- setDismissedMenuValue(undefined)
2718
+ acceptMenuCandidate()
2538
2719
  return
2539
2720
  }
2540
2721
  if (key.backspace || key.delete) {
@@ -2595,7 +2776,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2595
2776
  // starts the sweep whenever that pair changes (App picks a NEW random style
2596
2777
  // for every replay — including effort changes on the same route — so the
2597
2778
  // pair always differs when a new wave should run) and stops it when the
2598
- // model leaves the official DeepSeek route (tier becomes null).
2779
+ // route leaves every wave tier (tier becomes null).
2599
2780
  const [waveTick, setWaveTick] = useState<number | null>(null)
2600
2781
  const wavePrevious = useRef<{ tier: DeepseekWaveTier | null; style: DeepseekWaveStyle | null }>({ tier: null, style: null })
2601
2782
  useEffect(() => {
@@ -2632,6 +2813,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2632
2813
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0])
2633
2814
  const promptGlyph = waveTier === 'flash' ? '›' : waveTier === 'deepseek' ? '»' : '❯'
2634
2815
  if (frozen) {
2816
+ // A pending deletion turns the box into the confirm prompt: the y/n is
2817
+ // typed HERE, with a readable warn-styled hint instead of a dim footer.
2818
+ if (deleteConfirm !== undefined) {
2819
+ return createElement(
2820
+ Box,
2821
+ { width: Math.max(1, columns - 1), borderStyle: 'round', borderColor: inkColor(getPalette().warn), paddingX: 1 },
2822
+ createElement(
2823
+ Text,
2824
+ { wrap: 'truncate-end' },
2825
+ createElement(Text, { color: inkColor(getPalette().warn), bold: true }, '❯ '),
2826
+ createElement(Text, { color: inkColor(getPalette().warn), bold: true }, 'y delete · any other key cancels'),
2827
+ ),
2828
+ )
2829
+ }
2635
2830
  const frozen = value === ''
2636
2831
  ? 'type a message'
2637
2832
  : verboseLine(value, Math.max(1, columns - 6))
@@ -2713,11 +2908,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2713
2908
  while (cells.length < contentWidth) {
2714
2909
  cells.push({ char: ' ', backgroundColor: waveBg(cells.length) })
2715
2910
  }
2716
- // The brand wordmark rides the wave's middle: `deepseek` in the tier's
2717
- // cycled hues, placed in the row's mid-section and only over blank or
2718
- // placeholder cells real draft text is never covered.
2911
+ // The wordmark rides the wave's middle: `deepseek` on the official
2912
+ // tiers, `Into the Unknown` on the non-DeepSeek high-effort variant
2913
+ // in the tier's cycled hues, placed in the row's mid-section and only
2914
+ // over blank or placeholder cells — real draft text is never covered.
2719
2915
  if (deepseekWaveWordVisible(waveTick!, waveTier!, style)) {
2720
- const word = 'deepseek'
2916
+ const word = waveTier === 'unknown' ? 'Into the Unknown' : 'deepseek'
2721
2917
  const start = Math.max(2, Math.floor((contentWidth - word.length) / 2))
2722
2918
  let clear = true
2723
2919
  for (let at = 0; at < word.length; at += 1) {
@@ -2734,9 +2930,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
2734
2930
  }
2735
2931
  }
2736
2932
  }
2737
- // The tail sparkles belong to the Wave style's deepseek tier only
2738
- // (Codex paints spark_frame on Wave+Ultra).
2739
- if (waveTier === 'deepseek' && style === 'wave') {
2933
+ // The tail sparkles belong to the Wave style's pro tiers only (the
2934
+ // deepseek and unknown tiers share the Ultra parameters — Codex paints
2935
+ // spark_frame on Wave+Ultra).
2936
+ if ((waveTier === 'deepseek' || waveTier === 'unknown') && style === 'wave') {
2740
2937
  const spark = deepseekWaveSpark(waveTick!)
2741
2938
  if (spark !== null) {
2742
2939
  const last = cells[cells.length - 1]
@@ -2950,16 +3147,18 @@ export function App(props: AppProps): ReactElement {
2950
3147
  const [effortFor, setEffortFor] = useState<ModelRow | undefined>(undefined)
2951
3148
  /** Effective reasoning effort, shown in the /model picker and switch notice. */
2952
3149
  const [effortLabel, setEffortLabel] = useState<string | undefined>(props.effort)
2953
- /** DeepSeek easter egg: switching INTO an official DeepSeek route plays
2954
- * one of Codex's three ignition styles (Wave / Aurora / Pulse, picked at
2955
- * random without repeating) across the composer's padded band (33ms tick,
2956
- * per-style durations), then the band returns to static while the prompt
2957
- * marker keeps the tier accent. The trigger follows the applied model
2958
- * label (what the status bar actually shows), never the initial paint,
2959
- * and the tier is derived from the label and cached at the switch. The
2960
- * 33ms tick itself lives inside Input, so the sweep re-renders only the
2961
- * composer row, not the whole tree, at 30fps; App owns the rarely-changing
2962
- * tier/style and Input starts the sweep whenever that pair changes. */
3150
+ /** DeepSeek easter egg: switching INTO an official DeepSeek route — or
3151
+ * onto a NON-DeepSeek model running a reasoning effort strictly above
3152
+ * high plays one of Codex's three ignition styles (Wave / Aurora /
3153
+ * Pulse, picked at random without repeating) across the composer's
3154
+ * padded band (33ms tick, per-style durations), then the band returns
3155
+ * to static while the prompt marker keeps the tier accent. The trigger
3156
+ * follows the applied model label (what the status bar actually shows),
3157
+ * never the initial paint, and the tier is derived from the label and
3158
+ * cached at the switch. The 33ms tick itself lives inside Input, so the
3159
+ * sweep re-renders only the composer row, not the whole tree, at 30fps;
3160
+ * App owns the rarely-changing tier/style and Input starts the sweep
3161
+ * whenever that pair changes. */
2963
3162
  const [waveTier, setWaveTier] = useState<DeepseekWaveTier | null>(null)
2964
3163
  const [waveStyle, setWaveStyle] = useState<DeepseekWaveStyle | null>(null)
2965
3164
  const previousModel = useRef<string | undefined>(undefined)
@@ -2969,18 +3168,23 @@ export function App(props: AppProps): ReactElement {
2969
3168
  const previous = previousModel.current
2970
3169
  previousModel.current = modelLabel
2971
3170
  // The wave replays when the applied model changes OR its effort level
2972
- // changes on the same official DeepSeek route (Codex replays the
2973
- // ignition on effort changes too).
3171
+ // changes (Codex replays the ignition on effort changes too). Official
3172
+ // DeepSeek routes run their flash/pro tiers; a NON-DeepSeek model
3173
+ // running a reasoning effort STRICTLY above high runs the "Into the
3174
+ // Unknown" variant — the deepseek tier's exact motion with a different
3175
+ // wordmark. Any other non-DeepSeek route stays static.
2974
3176
  const effortChanged = previousEffort.current !== effortLabel
2975
3177
  previousEffort.current = effortLabel
2976
3178
  const modelChanged = previous !== undefined && previous !== modelLabel
2977
- if (!isOfficialDeepSeekLabel(modelLabel)) {
3179
+ const official = isOfficialDeepSeekLabel(modelLabel)
3180
+ const unknownTrigger = !official && effortAboveHigh(effortLabel)
3181
+ if (!official && !unknownTrigger) {
2978
3182
  setWaveTier(null)
2979
3183
  setWaveStyle(null)
2980
3184
  return
2981
3185
  }
2982
3186
  if (modelChanged || effortChanged) {
2983
- setWaveTier(deepseekWaveTier(modelLabel))
3187
+ setWaveTier(official ? deepseekWaveTier(modelLabel) : 'unknown')
2984
3188
  const nextStyle = deepseekWaveStyleRandom(previousStyle.current)
2985
3189
  previousStyle.current = nextStyle
2986
3190
  setWaveStyle(nextStyle)
@@ -3053,6 +3257,34 @@ export function App(props: AppProps): ReactElement {
3053
3257
  const [statuslineItems, setStatuslineItems] = useState<readonly StatusItemId[]>(() => parseStatuslineItems(props.statusline))
3054
3258
  const [themeOpen, setThemeOpen] = useState(false)
3055
3259
  const [historyOpen, setHistoryOpen] = useState(false)
3260
+ const [agentsOpen, setAgentsOpen] = useState(false)
3261
+ const [subagentOpen, setSubagentOpen] = useState(false)
3262
+ /** /delete state: delete-mode hint plus an optional pre-armed row id. */
3263
+ const [resumeDelete, setResumeDelete] = useState<{ mode: boolean; id?: string }>({ mode: false })
3264
+ /** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
3265
+ * composer takes the keys, the resume panel yields until it settles. */
3266
+ const [deleteConfirmId, setDeleteConfirmId] = useState<string | undefined>(undefined)
3267
+ /** Bumped after a deletion so the /resume listing reloads immediately. */
3268
+ const [deleteReloadToken, setDeleteReloadToken] = useState(0)
3269
+ const requestDelete = useCallback((row: SessionRow): void => {
3270
+ setDeleteConfirmId(row.id)
3271
+ }, [])
3272
+ const cancelDelete = useCallback((): void => {
3273
+ setDeleteConfirmId(undefined)
3274
+ }, [])
3275
+ const confirmDelete = useCallback((): void => {
3276
+ const id = deleteConfirmId
3277
+ if (id === undefined) return
3278
+ setDeleteConfirmId(undefined)
3279
+ void props.deleteSession(id).then(outcome => {
3280
+ notify(outcome)
3281
+ // Keep the picker open and reload: a successful deletion must vanish
3282
+ // from the list immediately, not look like a no-op.
3283
+ setDeleteReloadToken(token => token + 1)
3284
+ }, (reason: unknown) => {
3285
+ notify(`delete failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
3286
+ })
3287
+ }, [deleteConfirmId, props.deleteSession, notify])
3056
3288
  /** The /history panel's accepted entry: text plus its recall-space index. */
3057
3289
  const [historyFill, setHistoryFill] = useState<{ text: string; index: number } | undefined>(undefined)
3058
3290
  /** Submissions recorded in this process (Codex local history; persistent file stays in the runner). */
@@ -3089,10 +3321,16 @@ export function App(props: AppProps): ReactElement {
3089
3321
  const [refreshEpoch, setRefreshEpoch] = useState(0)
3090
3322
  const approvalSnapshot = useSyncExternalStore(props.approval.subscribe, props.approval.getSnapshot)
3091
3323
  const questionSnapshot = useSyncExternalStore(props.questions.subscribe, props.questions.getSnapshot)
3324
+ const agentRows = useSyncExternalStore(props.subagents.subscribe, props.subagents.getSnapshot)
3092
3325
  const approvalPending = approvalSnapshot.pending !== undefined
3093
3326
  const questionPending = questionSnapshot.pending !== undefined
3094
3327
  // While any modal owns the keys, the prompt box passes everything through.
3095
- const inputActive = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending
3328
+ // While a deletion waits for y/n, the composer takes the keys (the resume
3329
+ // panel yields): the confirm is typed IN the input box, not as an invisible
3330
+ // panel keypress.
3331
+ const inputActive = deleteConfirmId !== undefined
3332
+ ? !approvalPending && !questionPending
3333
+ : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending
3096
3334
 
3097
3335
  // Human questions outrank local inspectors. Close the lower modal instead
3098
3336
  // of leaving an approval/question visible but keyboard-locked behind it.
@@ -3110,6 +3348,9 @@ export function App(props: AppProps): ReactElement {
3110
3348
  setStatuslineOpen(false)
3111
3349
  setThemeOpen(false)
3112
3350
  setHistoryOpen(false)
3351
+ setAgentsOpen(false)
3352
+ setSubagentOpen(false)
3353
+ setDeleteConfirmId(undefined)
3113
3354
  setVerboseOpen(false)
3114
3355
  }, [approvalPending, questionPending])
3115
3356
 
@@ -3213,9 +3454,9 @@ export function App(props: AppProps): ReactElement {
3213
3454
  ? Math.max(1, Math.floor(streamRows / 3))
3214
3455
  : 1
3215
3456
  const answerRows = view.streaming === '' ? 0 : Math.max(1, streamRows - reasoningRows)
3216
- const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending
3457
+ const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending
3217
3458
  const inspectorVisible = verboseOpen && !approvalPending && !questionPending
3218
- const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || inspectorVisible || approvalPending || questionPending
3459
+ const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || inspectorVisible || approvalPending || questionPending
3219
3460
  const closeInspector = useCallback((): void => {
3220
3461
  setVerboseOpen(false)
3221
3462
  }, [])
@@ -3322,6 +3563,9 @@ export function App(props: AppProps): ReactElement {
3322
3563
  })
3323
3564
  } else if (effortFor !== undefined) {
3324
3565
  modelSurface = createElement(EffortPanel, {
3566
+ // Keyed per row: switching models remounts the stage so its cursor
3567
+ // initializes on the new model's effective effort.
3568
+ key: `${effortFor.provider}/${effortFor.model}`,
3325
3569
  row: effortFor,
3326
3570
  current: effortLabel,
3327
3571
  select: (effortId: string) => applyModel(effortFor, effortId),
@@ -3331,6 +3575,7 @@ export function App(props: AppProps): ReactElement {
3331
3575
  modelSurface = createElement(ModelPanel, {
3332
3576
  directory,
3333
3577
  error: modelError,
3578
+ current: modelLabel,
3334
3579
  onSelect: (row: ModelRow) => {
3335
3580
  // A model advertising several levels opens the effort stage first;
3336
3581
  // one advertised level is its only option, while no capability uses
@@ -3387,8 +3632,9 @@ export function App(props: AppProps): ReactElement {
3387
3632
  )
3388
3633
  : undefined,
3389
3634
  transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : undefined,
3635
+ transcriptVisible ? createElement(AgentsLine, { rows: agentRows }) : undefined,
3390
3636
  createElement(QuestionBar, { store: props.questions, snapshot: questionSnapshot, locked: false }),
3391
- createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending }),
3637
+ createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending, notify }),
3392
3638
  modelSurface,
3393
3639
  helpOpen && !approvalPending && !questionPending
3394
3640
  ? createElement(HelpPanel, {
@@ -3441,6 +3687,10 @@ export function App(props: AppProps): ReactElement {
3441
3687
  currentCwd: props.workspaceRoot,
3442
3688
  load: props.loadSessions,
3443
3689
  readTranscript: props.loadSessionTranscript,
3690
+ requestDelete,
3691
+ deleteConfirmId,
3692
+ reloadToken: deleteReloadToken,
3693
+ deleteMode: resumeDelete.mode,
3444
3694
  select: (row: SessionRow) => { props.switchSession(row); setResumeOpen(false) },
3445
3695
  close: () => setResumeOpen(false),
3446
3696
  })
@@ -3483,6 +3733,37 @@ export function App(props: AppProps): ReactElement {
3483
3733
  close: () => setHistoryOpen(false),
3484
3734
  })
3485
3735
  : undefined,
3736
+ agentsOpen && !approvalPending && !questionPending
3737
+ ? createElement(AgentsPanel, {
3738
+ live: agentRows,
3739
+ load: props.loadSubagents,
3740
+ readTranscript: props.loadSessionTranscript,
3741
+ close: () => setAgentsOpen(false),
3742
+ })
3743
+ : undefined,
3744
+ subagentOpen && !approvalPending && !questionPending
3745
+ ? createElement(SubagentPanel, {
3746
+ current: props.subagentModel,
3747
+ load: props.loadModels,
3748
+ pick: (row: ModelRow, effortId?: string) => {
3749
+ try {
3750
+ // The runner's label already carries the effort suffix
3751
+ // (`provider/model@effort`), so no second append here.
3752
+ const label = props.setSubagentModel(row, effortId)
3753
+ notify(`subagents → ${label}`)
3754
+ setSubagentOpen(false)
3755
+ } catch (reason: unknown) {
3756
+ notify(`subagent model change failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
3757
+ }
3758
+ },
3759
+ inherit: () => {
3760
+ props.clearSubagentModel()
3761
+ notify('subagents → inherit current model')
3762
+ setSubagentOpen(false)
3763
+ },
3764
+ close: () => setSubagentOpen(false),
3765
+ })
3766
+ : undefined,
3486
3767
  notice === undefined
3487
3768
  ? undefined
3488
3769
  : createElement(NoticeLine, {
@@ -3542,7 +3823,12 @@ export function App(props: AppProps): ReactElement {
3542
3823
  return
3543
3824
  }
3544
3825
  if (row.reasoning === undefined || row.reasoning.efforts.length === 0) {
3545
- notify('current model does not expose reasoning efforts', 'warning')
3826
+ // A model that advertises no levels still opens the stage: the
3827
+ // panel itself carries the empty state (the web effort pane's
3828
+ // "no levels" copy), instead of a bare notice that reads like
3829
+ // a failure.
3830
+ setEffortFor(row)
3831
+ setModelOpen(true)
3546
3832
  return
3547
3833
  }
3548
3834
  setEffortFor(row)
@@ -3556,11 +3842,22 @@ export function App(props: AppProps): ReactElement {
3556
3842
  },
3557
3843
  openMode: () => setModeOpen(true),
3558
3844
  openPermission: () => setPermissionOpen(true),
3559
- openResume: () => setResumeOpen(true),
3845
+ openResume: () => { setResumeDelete({ mode: false }); setResumeOpen(true) },
3560
3846
  openPlugin: (query = '') => { setPluginQuery(query); setPluginOpen(true) },
3561
3847
  openStatusline: () => setStatuslineOpen(true),
3562
3848
  openTheme: () => setThemeOpen(true),
3563
3849
  openHistory: () => setHistoryOpen(true),
3850
+ openAgents: () => setAgentsOpen(true),
3851
+ openSubagent: () => setSubagentOpen(true),
3852
+ openDelete: (id?: string) => {
3853
+ const armed = id === undefined || id === '' ? undefined : id
3854
+ setResumeDelete({ mode: true, ...armed === undefined ? {} : { id: armed } })
3855
+ setDeleteConfirmId(armed)
3856
+ setResumeOpen(true)
3857
+ },
3858
+ deleteConfirm: deleteConfirmId,
3859
+ confirmDelete,
3860
+ cancelDelete,
3564
3861
  createSession: props.createSession,
3565
3862
  cancelSessionSwitch: props.cancelSessionSwitch,
3566
3863
  notify,