@namzu/cli 15.1.1 → 16.0.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 (62) hide show
  1. package/README.md +22 -6
  2. package/dist/config/load.d.ts.map +1 -1
  3. package/dist/config/load.js +8 -0
  4. package/dist/config/load.js.map +1 -1
  5. package/dist/config/schema.d.ts +8 -0
  6. package/dist/config/schema.d.ts.map +1 -1
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/context/capabilities.d.ts +7 -4
  9. package/dist/context/capabilities.d.ts.map +1 -1
  10. package/dist/context/capabilities.js +7 -4
  11. package/dist/context/capabilities.js.map +1 -1
  12. package/dist/context/sandbox.d.ts +5 -0
  13. package/dist/context/sandbox.d.ts.map +1 -1
  14. package/dist/context/sandbox.js +11 -4
  15. package/dist/context/sandbox.js.map +1 -1
  16. package/dist/doctor/checks/chain.d.ts.map +1 -1
  17. package/dist/doctor/checks/chain.js +1 -0
  18. package/dist/doctor/checks/chain.js.map +1 -1
  19. package/dist/integrations/subagents/activity.d.ts +70 -0
  20. package/dist/integrations/subagents/activity.d.ts.map +1 -0
  21. package/dist/integrations/subagents/activity.js +332 -0
  22. package/dist/integrations/subagents/activity.js.map +1 -0
  23. package/dist/integrations/subagents/runtime.d.ts +11 -1
  24. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  25. package/dist/integrations/subagents/runtime.js +27 -2
  26. package/dist/integrations/subagents/runtime.js.map +1 -1
  27. package/dist/tui/AgentExplorer.d.ts +36 -0
  28. package/dist/tui/AgentExplorer.d.ts.map +1 -0
  29. package/dist/tui/AgentExplorer.js +174 -0
  30. package/dist/tui/AgentExplorer.js.map +1 -0
  31. package/dist/tui/App.d.ts.map +1 -1
  32. package/dist/tui/App.js +269 -106
  33. package/dist/tui/App.js.map +1 -1
  34. package/dist/tui/Composer.d.ts +7 -1
  35. package/dist/tui/Composer.d.ts.map +1 -1
  36. package/dist/tui/Composer.js +26 -5
  37. package/dist/tui/Composer.js.map +1 -1
  38. package/dist/tui/LiveActivity.d.ts +9 -3
  39. package/dist/tui/LiveActivity.d.ts.map +1 -1
  40. package/dist/tui/LiveActivity.js +27 -10
  41. package/dist/tui/LiveActivity.js.map +1 -1
  42. package/dist/tui/PermissionOverlay.d.ts +8 -9
  43. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  44. package/dist/tui/PermissionOverlay.js +6 -10
  45. package/dist/tui/PermissionOverlay.js.map +1 -1
  46. package/dist/tui/StatusBar.d.ts +1 -47
  47. package/dist/tui/StatusBar.d.ts.map +1 -1
  48. package/dist/tui/StatusBar.js +9 -79
  49. package/dist/tui/StatusBar.js.map +1 -1
  50. package/dist/tui/agent.d.ts +3 -0
  51. package/dist/tui/agent.d.ts.map +1 -1
  52. package/dist/tui/agent.js +199 -176
  53. package/dist/tui/agent.js.map +1 -1
  54. package/dist/tui/permission-review.d.ts +18 -2
  55. package/dist/tui/permission-review.d.ts.map +1 -1
  56. package/dist/tui/permission-review.js +88 -2
  57. package/dist/tui/permission-review.js.map +1 -1
  58. package/dist/tui/slashCommands.d.ts +4 -0
  59. package/dist/tui/slashCommands.d.ts.map +1 -1
  60. package/dist/tui/slashCommands.js +14 -0
  61. package/dist/tui/slashCommands.js.map +1 -1
  62. package/package.json +4 -4
package/dist/tui/App.js CHANGED
@@ -28,19 +28,18 @@ import { checkUpdates } from '../integrations/updates.js';
28
28
  import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
29
29
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
30
30
  import { discoverUserCommands } from '../user-commands/store.js';
31
+ import { AgentPicker, AgentTranscript, agentPickerPageSize, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
31
32
  import { ChoicePicker } from './ChoicePicker.js';
32
33
  import { Composer, suggestionWindowSize, } from './Composer.js';
33
34
  import { CopyPicker } from './CopyPicker.js';
34
35
  import { EditPromptPicker } from './EditPromptPicker.js';
35
36
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
36
37
  import { PermissionOverlay } from './PermissionOverlay.js';
37
- import { PERMISSION_REVIEW_PAGE_ROWS, buildPermissionReview, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
38
38
  import { Picker } from './Picker.js';
39
39
  import { ResumePicker } from './ResumePicker.js';
40
40
  import { StatusBar } from './StatusBar.js';
41
41
  import { TextPrompt } from './TextPrompt.js';
42
42
  import { Transcript, willCollapse } from './Transcript.js';
43
- import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
44
43
  import { TrustPrompt } from './TrustPrompt.js';
45
44
  import { createAgentSession, probeAgentSession, } from './agent.js';
46
45
  import { keepRecentRows } from './compact-transcript.js';
@@ -48,18 +47,20 @@ import { approvalIsDeliberate } from './consent-timing.js';
48
47
  import { planTurnPublication } from './conversation-history.js';
49
48
  import { copyTargetsForResponse } from './copy-targets.js';
50
49
  import { editablePrompts } from './edit-prompts.js';
50
+ import { editDraftInExternalEditor } from './external-editor.js';
51
51
  import { liveWindow } from './live-window.js';
52
- import { describeRunInterruption } from './run-interruption.js';
53
52
  import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
54
53
  import { NAMZU_MARK, NAMZU_MARK_COLOR, NAMZU_WORDMARK, NAMZU_WORDMARK_GRADIENT, NAMZU_WORDMARK_MIN_WIDTH, } from './logo.js';
55
54
  import { expandFileMentions, listMentionableFiles } from './mentions.js';
56
55
  import { openInBrowser } from './open-browser.js';
56
+ import { PERMISSION_REVIEW_PAGE_ROWS, buildPermissionReview, buildPermissionSummary, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
57
+ import { describeRunInterruption } from './run-interruption.js';
58
+ import { moveSelection } from './selection-window.js';
57
59
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
58
60
  import { splitCompleteBlocks } from './stream-blocks.js';
59
- import { moveSelection } from './selection-window.js';
61
+ import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
60
62
  import { theme } from './theme.js';
61
63
  import { useSelectionIndex } from './use-selection-index.js';
62
- import { editDraftInExternalEditor } from './external-editor.js';
63
64
  import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
64
65
  import { listReviewBranches, listReviewCommits, reviewMergeBase, } from './workspace-review.js';
65
66
  /** Last non-empty assistant text in a durable conversation, newest first. */
@@ -293,6 +294,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
293
294
  const [permission, setPermission] = useState(null);
294
295
  const [permissionReviewOffset, setPermissionReviewOffsetState] = useState(0);
295
296
  const permissionReviewOffsetRef = useRef(0);
297
+ const [permissionDetailsOpen, setPermissionDetailsOpenState] = useState(false);
298
+ const permissionDetailsOpenRef = useRef(false);
299
+ const setPermissionDetailsOpen = useCallback((next) => {
300
+ permissionDetailsOpenRef.current = next;
301
+ setPermissionDetailsOpenState(next);
302
+ }, []);
296
303
  const setPermissionReviewOffset = useCallback((next) => {
297
304
  permissionReviewOffsetRef.current = next;
298
305
  setPermissionReviewOffsetState(next);
@@ -319,10 +326,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
319
326
  const [mentionCandidates, setMentionCandidates] = useState([]);
320
327
  const mentionLoadOwnerRef = useRef(null);
321
328
  const [usage, setUsage] = useState(null);
322
- // Context fill, straight from the kernel and held apart from `usage` —
323
- // they are different quantities and conflating them is what made the
324
- // gauge climb with turn count instead of with context.
325
- const [context, setContext] = useState(null);
326
329
  // Tools currently executing — rendered live (spinner + elapsed) below the
327
330
  // transcript, then committed as static lines on completion.
328
331
  const [activeTools, setActiveTools] = useState([]);
@@ -392,11 +395,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
392
395
  /** `/copy` owns this exact response snapshot until selection or cancellation. */
393
396
  const [copyPicker, setCopyPickerState] = useState(null);
394
397
  const copyPickerRef = useRef(null);
398
+ const copyPickerCommittedRef = useRef(null);
395
399
  const { selection: selectedCopy, selectionRef: selectedCopyRef, setSelection: setSelectedCopy, } = useSelectionIndex(0);
396
400
  const setCopyPicker = useCallback((next) => {
397
401
  copyPickerRef.current = next;
402
+ if (next === null)
403
+ copyPickerCommittedRef.current = null;
398
404
  setCopyPickerState(next);
399
405
  }, []);
406
+ useEffect(() => {
407
+ copyPickerCommittedRef.current = copyPicker;
408
+ }, [copyPicker]);
400
409
  /** Finite slash-command choice owned synchronously until apply or cancel. */
401
410
  const [choicePicker, setChoicePickerState] = useState(null);
402
411
  const choicePickerRef = useRef(null);
@@ -426,7 +435,37 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
426
435
  setTextPromptState(next);
427
436
  }, []);
428
437
  const [composerDraft, setComposerDraft] = useState(null);
438
+ const [composerHasDraft, setComposerHasDraft] = useState(false);
429
439
  const composerDraftTokenRef = useRef(0);
440
+ /** Bounded child-run projection published by the current AgentSession. */
441
+ const [subagents, setSubagentsState] = useState([]);
442
+ const subagentsRef = useRef([]);
443
+ const [agentSurface, setAgentSurfaceState] = useState(null);
444
+ const agentSurfaceRef = useRef(null);
445
+ const agentSurfaceCommittedRef = useRef(null);
446
+ const setAgentSurface = useCallback((next) => {
447
+ const wasVisible = agentSurfaceCommittedRef.current !== null;
448
+ agentSurfaceRef.current = next;
449
+ if (next === null)
450
+ agentSurfaceCommittedRef.current = null;
451
+ // Only opening the surface needs a paint fence. Once the operator has
452
+ // seen it, navigation may update the state object several times in one
453
+ // terminal input burst; those updates remain actionable immediately.
454
+ else if (wasVisible)
455
+ agentSurfaceCommittedRef.current = next;
456
+ setAgentSurfaceState(next);
457
+ }, []);
458
+ useEffect(() => {
459
+ agentSurfaceCommittedRef.current = agentSurface;
460
+ }, [agentSurface]);
461
+ const replaceSubagents = useCallback((next) => {
462
+ subagentsRef.current = next;
463
+ setSubagentsState(next);
464
+ const surface = agentSurfaceRef.current;
465
+ if (surface && !next.some((agent) => agent.viewId === surface.selectedId)) {
466
+ setAgentSurface(null);
467
+ }
468
+ }, [setAgentSurface]);
430
469
  /** One git-backed review choice may be resolving while its visible picker remains authoritative. */
431
470
  const reviewChoiceInFlightRef = useRef(null);
432
471
  const exitArmedRef = useRef(false);
@@ -466,6 +505,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
466
505
  * server's child process outlives the object that opened it.
467
506
  */
468
507
  const previousSessionRef = useRef(null);
508
+ useEffect(() => {
509
+ const source = session?.subagents;
510
+ setAgentSurface(null);
511
+ if (!source) {
512
+ replaceSubagents([]);
513
+ return;
514
+ }
515
+ const refresh = () => replaceSubagents(source.getSnapshot());
516
+ refresh();
517
+ return source.subscribe(refresh);
518
+ }, [replaceSubagents, session, setAgentSurface]);
519
+ const resetSubagentActivity = useCallback(() => {
520
+ session?.subagents?.reset();
521
+ setAgentSurface(null);
522
+ replaceSubagents([]);
523
+ }, [replaceSubagents, session, setAgentSurface]);
469
524
  // Source of truth for in-flight tools (the event loop runs across renders, so
470
525
  // a ref avoids stale state); `activeTools` mirrors it for rendering.
471
526
  const activeToolsRef = useRef([]);
@@ -1341,8 +1396,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1341
1396
  const mentionLoadOwner = {};
1342
1397
  mentionLoadOwnerRef.current = mentionLoadOwner;
1343
1398
  void listMentionableFiles(activeCtx.cwd, appLifetime.signal).then((files) => {
1344
- if (!appLifetime.signal.aborted &&
1345
- mentionLoadOwnerRef.current === mentionLoadOwner)
1399
+ if (!appLifetime.signal.aborted && mentionLoadOwnerRef.current === mentionLoadOwner)
1346
1400
  setMentionCandidates(files);
1347
1401
  });
1348
1402
  setCurrentProvider(primaryProvider(prefs).id);
@@ -1715,7 +1769,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1715
1769
  settled: settledRef.current,
1716
1770
  raw: rawOutput,
1717
1771
  });
1718
- settledRef.current = window.settled;
1772
+ // A child view is a redrawable overlay above the parent's native scrollback.
1773
+ // Freeze the parent's Static floor while it is open; otherwise a parent turn
1774
+ // settling in the background prints through the child screen. Returning
1775
+ // advances the floor once and emits those finalized rows exactly once.
1776
+ if (agentSurface === null)
1777
+ settledRef.current = window.settled;
1778
+ const renderedSettled = agentSurface === null ? window.settled : settledRef.current;
1719
1779
  // One merged vocabulary for the session: this host's own commands plus
1720
1780
  // whatever the kernel's registry reports. Built here so `/help`, the
1721
1781
  // autocomplete and the dispatcher all answer from the same list — three
@@ -1801,10 +1861,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1801
1861
  permissionResolveRef.current = null;
1802
1862
  permissionOpenedAtRef.current = null;
1803
1863
  setPermissionReviewOffset(0);
1864
+ setPermissionDetailsOpen(false);
1804
1865
  setPermission(null);
1805
1866
  if (resolve)
1806
1867
  resolve(decision);
1807
- }, [setPermissionReviewOffset]);
1868
+ }, [setPermissionDetailsOpen, setPermissionReviewOffset]);
1808
1869
  /**
1809
1870
  * Stop the running turn, and hand the screen back in a usable state.
1810
1871
  *
@@ -1918,6 +1979,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1918
1979
  goalActivation.clear();
1919
1980
  wakeGoalDriver();
1920
1981
  conversationGenRef.current += 1;
1982
+ resetSubagentActivity();
1921
1983
  activeTurnTokenRef.current = null;
1922
1984
  resetTranscript();
1923
1985
  setMessages(restored);
@@ -1946,6 +2008,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1946
2008
  interruptTurn,
1947
2009
  nextId,
1948
2010
  pushMessage,
2011
+ resetSubagentActivity,
1949
2012
  resetTranscript,
1950
2013
  wakeGoalDriver,
1951
2014
  ]);
@@ -1985,11 +2048,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1985
2048
  goalActivation.clear();
1986
2049
  wakeGoalDriver();
1987
2050
  conversationGenRef.current += 1;
2051
+ resetSubagentActivity();
1988
2052
  activeTurnTokenRef.current = null;
1989
2053
  modelHistoryRef.current = [];
1990
2054
  lastAssistantMessage.current = null;
1991
2055
  lastCompletedOutputRef.current = null;
1992
- setContext(null);
1993
2056
  if (sourceScope && targetSessionId)
1994
2057
  sourceScope.sessionId = targetSessionId;
1995
2058
  if (clearScreen) {
@@ -2017,7 +2080,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2017
2080
  conversationMutationRef.current = null;
2018
2081
  setConversationMutation(null);
2019
2082
  }
2020
- }, [discardQueued, goalActivation, interruptTurn, pushMessage, resetTranscript, wakeGoalDriver]);
2083
+ }, [
2084
+ discardQueued,
2085
+ goalActivation,
2086
+ interruptTurn,
2087
+ pushMessage,
2088
+ resetSubagentActivity,
2089
+ resetTranscript,
2090
+ wakeGoalDriver,
2091
+ ]);
2021
2092
  /**
2022
2093
  * `/rename` (and the older `/title` alias): edit or set the name this
2023
2094
  * conversation appears under.
@@ -2139,6 +2210,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2139
2210
  // The transcript on screen is already the fork's history, so nothing
2140
2211
  // is reloaded or reset. Only where the NEXT turn is written changes.
2141
2212
  scope.sessionId = forked.id;
2213
+ resetSubagentActivity();
2142
2214
  goalActivation.clear();
2143
2215
  wakeGoalDriver();
2144
2216
  pushMessage('system', `Forked into "${forked.title}" — ${forked.copied} message(s) copied. This screen continues in the copy; ${original} is unchanged and still in /resume.`);
@@ -2150,7 +2222,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2150
2222
  conversationMutationRef.current = null;
2151
2223
  setConversationMutation(null);
2152
2224
  }
2153
- }, [ensureSessions, goalActivation, hasUnsettledTurn, pushMessage, wakeGoalDriver]);
2225
+ }, [
2226
+ ensureSessions,
2227
+ goalActivation,
2228
+ hasUnsettledTurn,
2229
+ pushMessage,
2230
+ resetSubagentActivity,
2231
+ wakeGoalDriver,
2232
+ ]);
2154
2233
  /** Open the durable prompt picker after the composer's second empty Esc. */
2155
2234
  const openPromptEditor = useCallback(() => {
2156
2235
  if (conversationMutationRef.current || compactingRef.current)
@@ -2206,6 +2285,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2206
2285
  .slice(0, target.userOrdinal)
2207
2286
  .map((prompt) => prompt.displayText);
2208
2287
  conversationGenRef.current += 1;
2288
+ resetSubagentActivity();
2209
2289
  activeTurnTokenRef.current = null;
2210
2290
  goalActivation.clear();
2211
2291
  wakeGoalDriver();
@@ -2246,6 +2326,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2246
2326
  hasUnsettledTurn,
2247
2327
  nextId,
2248
2328
  pushMessage,
2329
+ resetSubagentActivity,
2249
2330
  resetTranscript,
2250
2331
  wakeGoalDriver,
2251
2332
  ]);
@@ -2259,15 +2340,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2259
2340
  feedback: permissionReviewRefusal(review.reason),
2260
2341
  });
2261
2342
  }
2343
+ const summary = buildPermissionSummary(review.text);
2262
2344
  return new Promise((resolve) => {
2263
2345
  permissionResolveRef.current = resolve;
2264
2346
  permissionOpenedAtRef.current = Date.now();
2265
2347
  setPermissionReviewOffset(0);
2266
- setPermission({ ...req, review: review.text });
2348
+ setPermissionDetailsOpen(!summary.complete);
2349
+ setPermission({ ...req, review: review.text, summary });
2267
2350
  setState('awaiting-permission');
2268
2351
  sendTerminalNotification({ kind: 'approval-required' });
2269
2352
  });
2270
- }, [sendTerminalNotification, setPermissionReviewOffset]);
2353
+ }, [sendTerminalNotification, setPermissionDetailsOpen, setPermissionReviewOffset]);
2271
2354
  // Render one agent event onto the transcript. Shared by the local turn loop
2272
2355
  // and the daemon-attach poller, so both paths produce identical output.
2273
2356
  // `st` carries the streaming-assistant bubble id + accumulated text across
@@ -2368,20 +2451,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2368
2451
  }
2369
2452
  case 'usage':
2370
2453
  setUsage({ totalTokens: event.totalTokens, cost: event.cost });
2371
- // Mirrored verbatim, absences included. A run that reports no
2372
- // window clears the gauge rather than leaving the last one up:
2373
- // a stale bar is read as current, and this bar's whole defect
2374
- // was being read as something it was not.
2375
- setContext({
2376
- ...(event.contextTokens !== undefined ? { tokens: event.contextTokens } : {}),
2377
- ...(event.contextWindowTokens !== undefined
2378
- ? { windowTokens: event.contextWindowTokens }
2379
- : {}),
2380
- ...(event.contextMeasuredBy !== undefined
2381
- ? { measuredBy: event.contextMeasuredBy }
2382
- : {}),
2383
- ...(event.windowSource !== undefined ? { windowSource: event.windowSource } : {}),
2384
- });
2385
2454
  break;
2386
2455
  case 'task':
2387
2456
  pushMessage('tool', event.subject, false, event.status === 'completed' ? '☑' : '☐');
@@ -2909,6 +2978,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2909
2978
  });
2910
2979
  return;
2911
2980
  }
2981
+ case 'agent-picker': {
2982
+ const agents = subagentsRef.current;
2983
+ if (agents.length === 0) {
2984
+ pushMessage('system', 'No delegated agents are available in this conversation yet. When an Agent tool starts one, /agent opens its live activity and retained transcript.');
2985
+ return;
2986
+ }
2987
+ setAgentSurface({
2988
+ kind: 'picker',
2989
+ selectedId: agents[0]?.viewId ?? '',
2990
+ });
2991
+ return;
2992
+ }
2912
2993
  case 'clear-screen':
2913
2994
  setMessages([]);
2914
2995
  resetTranscript();
@@ -3417,11 +3498,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3417
3498
  await replaceConversation(sessions, destination, result.messages);
3418
3499
  }
3419
3500
  modelHistoryRef.current = result.messages;
3420
- // The old gauge measured the history that was just replaced.
3421
- // Clear it only after the durable projection and live model
3422
- // history agree; until then the old measurement is still the
3423
- // truthful one. The next run publishes a fresh measurement.
3424
- setContext(null);
3425
3501
  // How many user/assistant turns survived the pass.
3426
3502
  // `keepRecentRows` explains why the transcript is trimmed
3427
3503
  // to match rather than rebuilt from `result.messages`.
@@ -3906,13 +3982,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3906
3982
  return;
3907
3983
  }
3908
3984
  if (key.home || key.end || key.pageUp || key.pageDown) {
3909
- setSelectedEdit((index) => moveSelection(index, editList.length, key.home
3910
- ? 'first'
3911
- : key.end
3912
- ? 'last'
3913
- : key.pageUp
3914
- ? 'previous-page'
3915
- : 'next-page'));
3985
+ setSelectedEdit((index) => moveSelection(index, editList.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
3916
3986
  return;
3917
3987
  }
3918
3988
  if (key.escape || key.leftArrow || key.upArrow) {
@@ -3939,13 +4009,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3939
4009
  if (resumeCommittedRef.current)
3940
4010
  return;
3941
4011
  if (key.home || key.end || key.pageUp || key.pageDown) {
3942
- setSelectedResume((index) => moveSelection(index, resumeList.length, key.home
3943
- ? 'first'
3944
- : key.end
3945
- ? 'last'
3946
- : key.pageUp
3947
- ? 'previous-page'
3948
- : 'next-page'));
4012
+ setSelectedResume((index) => moveSelection(index, resumeList.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
3949
4013
  }
3950
4014
  else if (key.upArrow)
3951
4015
  setSelectedResume((index) => moveSelection(index, resumeList.length, 'previous'));
@@ -3989,9 +4053,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3989
4053
  }
3990
4054
  // A pending permission prompt owns the keyboard: y/n/a decide it.
3991
4055
  if (permissionResolveRef.current) {
4056
+ const ch = input.toLowerCase();
4057
+ if (permission && ch === 'd') {
4058
+ setPermissionReviewOffset(0);
4059
+ setPermissionDetailsOpen(!permissionDetailsOpenRef.current);
4060
+ return;
4061
+ }
3992
4062
  if (permission &&
3993
4063
  (key.upArrow || key.downArrow || key.pageUp || key.pageDown || key.home || key.end)) {
3994
- const count = permissionReviewRows(permission.review, terminal.columns).length;
4064
+ const source = permissionDetailsOpenRef.current
4065
+ ? permission.review
4066
+ : permission.summary.text;
4067
+ const count = permissionReviewRows(source, terminal.columns).length;
3995
4068
  const maxOffset = Math.max(0, count - PERMISSION_REVIEW_PAGE_ROWS);
3996
4069
  const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
3997
4070
  const next = key.home
@@ -4008,7 +4081,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4008
4081
  setPermissionReviewOffset(next);
4009
4082
  return;
4010
4083
  }
4011
- const ch = input.toLowerCase();
4012
4084
  // Refusing is never deferred or gated — it is the direction a
4013
4085
  // mistake is recoverable in, so it answers on the first press.
4014
4086
  if (key.ctrl && input === 'c') {
@@ -4034,6 +4106,80 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4034
4106
  resolvePermission({ kind: 'approve-all' });
4035
4107
  return;
4036
4108
  }
4109
+ // Child observation owns navigation, but never outranks consent: the
4110
+ // permission branch above preempts it if a child or parent call asks.
4111
+ const agentView = agentSurfaceRef.current;
4112
+ if (agentView) {
4113
+ // The Return that opened `/agent` cannot also drill into the
4114
+ // highlighted child before the list has appeared on screen.
4115
+ if (agentSurfaceCommittedRef.current !== agentView)
4116
+ return;
4117
+ const agents = subagentsRef.current;
4118
+ const index = Math.max(0, agents.findIndex((agent) => agent.viewId === agentView.selectedId));
4119
+ const selected = agents[index];
4120
+ if (!selected) {
4121
+ setAgentSurface(null);
4122
+ return;
4123
+ }
4124
+ if (agentView.kind === 'picker') {
4125
+ if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
4126
+ setAgentSurface(null);
4127
+ return;
4128
+ }
4129
+ if (key.return) {
4130
+ setAgentSurface({
4131
+ kind: 'transcript',
4132
+ selectedId: selected.viewId,
4133
+ tailOffset: 0,
4134
+ });
4135
+ return;
4136
+ }
4137
+ if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
4138
+ const movement = key.home
4139
+ ? 'first'
4140
+ : key.end
4141
+ ? 'last'
4142
+ : key.pageUp
4143
+ ? 'previous-page'
4144
+ : key.pageDown
4145
+ ? 'next-page'
4146
+ : key.upArrow
4147
+ ? 'previous'
4148
+ : 'next';
4149
+ const next = moveSelection(index, agents.length, movement, agentPickerPageSize(terminal.rows));
4150
+ const target = agents[next];
4151
+ if (target)
4152
+ setAgentSurface({ kind: 'picker', selectedId: target.viewId });
4153
+ }
4154
+ return;
4155
+ }
4156
+ if (input.toLowerCase() === 'q' || (key.ctrl && input === 'c')) {
4157
+ setAgentSurface(null);
4158
+ return;
4159
+ }
4160
+ if (key.escape) {
4161
+ setAgentSurface({ kind: 'picker', selectedId: selected.viewId });
4162
+ return;
4163
+ }
4164
+ if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
4165
+ const page = agentTranscriptPageSize(terminal.rows);
4166
+ const max = maxAgentTranscriptTailOffset(selected, terminal.rows, terminal.columns);
4167
+ const offset = Math.min(agentView.tailOffset, max);
4168
+ const next = key.home
4169
+ ? max
4170
+ : key.end
4171
+ ? 0
4172
+ : key.pageUp
4173
+ ? Math.min(max, offset + page)
4174
+ : key.pageDown
4175
+ ? Math.max(0, offset - page)
4176
+ : key.upArrow
4177
+ ? Math.min(max, offset + 1)
4178
+ : Math.max(0, offset - 1);
4179
+ setAgentSurface({ ...agentView, tailOffset: next });
4180
+ }
4181
+ return;
4182
+ }
4037
4183
  // A host-owned text prompt has its own input hook. This synchronous ref
4038
4184
  // claims the interval before React paints it too, so App cannot read a
4039
4185
  // key repeat as an interrupt, exit ladder, or finite-menu choice.
@@ -4058,13 +4204,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4058
4204
  }
4059
4205
  if (key.home || key.end || key.pageUp || key.pageDown) {
4060
4206
  const pageSize = picker.kind === 'command' ? picker.windowSize : undefined;
4061
- setSelectedChoice((index) => moveSelection(index, options.length, key.home
4062
- ? 'first'
4063
- : key.end
4064
- ? 'last'
4065
- : key.pageUp
4066
- ? 'previous-page'
4067
- : 'next-page', pageSize));
4207
+ setSelectedChoice((index) => moveSelection(index, options.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page', pageSize));
4068
4208
  return;
4069
4209
  }
4070
4210
  if (key.upArrow) {
@@ -4090,19 +4230,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4090
4230
  // stays above it in this handler so an approval request that arrives
4091
4231
  // while the picker is open cannot have its keys stolen by the chooser.
4092
4232
  if (copyPickerRef.current) {
4233
+ // The Return that submitted `/copy` is an ownership fence, not a
4234
+ // selection made against a picker the operator has not seen yet.
4235
+ if (copyPickerCommittedRef.current !== copyPickerRef.current)
4236
+ return;
4093
4237
  const targets = copyPickerRef.current.targets;
4094
4238
  if (key.escape || (key.ctrl && input === 'c')) {
4095
4239
  setCopyPicker(null);
4096
4240
  return;
4097
4241
  }
4098
4242
  if (key.home || key.end || key.pageUp || key.pageDown) {
4099
- setSelectedCopy((index) => moveSelection(index, targets.length, key.home
4100
- ? 'first'
4101
- : key.end
4102
- ? 'last'
4103
- : key.pageUp
4104
- ? 'previous-page'
4105
- : 'next-page'));
4243
+ setSelectedCopy((index) => moveSelection(index, targets.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
4106
4244
  return;
4107
4245
  }
4108
4246
  if (key.upArrow) {
@@ -4213,21 +4351,71 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4213
4351
  // Background is left natural — we inherit the terminal's own background
4214
4352
  // and only theme the foreground. Forcing
4215
4353
  // a filled bg left mismatched patches around bordered areas, so we don't.
4216
- const lifecycleOwnsViewport = phase === 'trust' || phase === 'resume' || phase === 'edit' || phase === 'picker';
4217
- return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: window.settled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [_jsx(LiveActivity, { activeTools: activeTools, thinking: state === 'thinking' && !messages.some((m) => m.pending) }), permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
4354
+ const liveSubagentCount = subagents.filter((agent) => agent.status === 'starting' || agent.status === 'working').length;
4355
+ const selectedSubagent = agentSurface
4356
+ ? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
4357
+ : undefined;
4358
+ const lifecycleOwnsViewport = phase === 'trust' ||
4359
+ phase === 'resume' ||
4360
+ phase === 'edit' ||
4361
+ phase === 'picker' ||
4362
+ agentSurface !== null;
4363
+ const statusGoal = phase === 'ready' &&
4364
+ state === 'idle' &&
4365
+ conversationMutation === null &&
4366
+ !compacting &&
4367
+ permission === null &&
4368
+ textPrompt === null &&
4369
+ choicePicker === null &&
4370
+ copyPicker === null &&
4371
+ agentSurface === null
4372
+ ? goalStatusLabel(goalStatus)
4373
+ : null;
4374
+ const statusHint = conversationMutation === 'fork'
4375
+ ? 'forking conversation — input is paused'
4376
+ : conversationMutation === 'edit'
4377
+ ? 'branching before prompt — input is paused'
4378
+ : conversationMutation === 'archive'
4379
+ ? 'archiving conversation — input is paused'
4380
+ : conversationMutation === 'new'
4381
+ ? 'starting a fresh conversation — input is paused'
4382
+ : compacting
4383
+ ? 'compacting conversation — input is paused'
4384
+ : permission
4385
+ ? hintForPhase(phase, state, session?.hasProvider === true)
4386
+ : agentSurface?.kind === 'picker'
4387
+ ? 'agent list — enter inspect · esc return'
4388
+ : agentSurface?.kind === 'transcript'
4389
+ ? 'observing agent — esc agents · q parent'
4390
+ : textPrompt
4391
+ ? 'name editor open — enter save · esc cancel'
4392
+ : choicePicker
4393
+ ? 'choice open — ↑↓ / 1–9 select · enter apply · esc cancel'
4394
+ : copyPicker
4395
+ ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
4396
+ : goalStatus && phase === 'ready' && state === 'idle'
4397
+ ? undefined
4398
+ : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
4399
+ return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: activeTools, working: state === 'thinking' || state === 'tool', agentCount: liveSubagentCount, interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, permission === null && agentSurface?.kind === 'picker' ? (_jsx(AgentPicker, { agents: subagents, selectedId: agentSurface.selectedId, terminalRows: terminal.rows })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
4218
4400
  phase === 'ready' &&
4219
4401
  conversationMutation === null &&
4220
4402
  textPrompt === null &&
4221
4403
  choicePicker === null &&
4222
- copyPicker === null, hidden: permission !== null || choicePicker !== null || copyPicker !== null, children: [pendingSteers.length > 0 &&
4404
+ copyPicker === null &&
4405
+ agentSurface === null, hidden: permission !== null ||
4406
+ choicePicker !== null ||
4407
+ copyPicker !== null ||
4408
+ agentSurface !== null, children: [pendingSteers.length > 0 &&
4223
4409
  permission === null &&
4224
4410
  textPrompt === null &&
4225
4411
  choicePicker === null &&
4226
- copyPicker === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.accent.user, children: ["\u21B3 ", pendingSteers.length, " message", pendingSteers.length > 1 ? 's' : '', " steering the active turn \u2014 waiting for its next response boundary"] }) })) : null, queued.length > 0 &&
4412
+ copyPicker === null &&
4413
+ agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.accent.user, children: ["\u21B3 ", pendingSteers.length, " message", pendingSteers.length > 1 ? 's' : '', " steering the active turn \u2014 waiting for its next response boundary"] }) })) : null, queued.length > 0 &&
4227
4414
  permission === null &&
4228
4415
  textPrompt === null &&
4229
4416
  choicePicker === null &&
4230
- copyPicker === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
4417
+ copyPicker === null &&
4418
+ agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
4231
4419
  ? queuePause.outcome === 'paused'
4232
4420
  ? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
4233
4421
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
@@ -4235,42 +4423,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4235
4423
  state === 'awaiting-permission' ||
4236
4424
  compacting ||
4237
4425
  conversationMutation !== null ||
4238
- externalEditorRequest !== null, hidden: permission !== null ||
4426
+ externalEditorRequest !== null ||
4427
+ agentSurface !== null, hidden: permission !== null ||
4239
4428
  textPrompt !== null ||
4240
4429
  choicePicker !== null ||
4241
- copyPicker !== null,
4430
+ copyPicker !== null ||
4431
+ agentSurface !== null,
4242
4432
  // A turn is running, so Esc is the interrupt and not
4243
4433
  // the composer's clear.
4244
- escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), userCommands: userCommands, mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: phase === 'ready' &&
4245
- state === 'idle' &&
4246
- conversationMutation === null &&
4247
- !compacting &&
4248
- permission === null &&
4249
- textPrompt === null &&
4250
- choicePicker === null &&
4251
- copyPicker === null
4252
- ? goalStatusLabel(goalStatus)
4253
- : null, state: state, hint: conversationMutation === 'fork'
4254
- ? 'forking conversation — input is paused'
4255
- : conversationMutation === 'edit'
4256
- ? 'branching before prompt — input is paused'
4257
- : conversationMutation === 'archive'
4258
- ? 'archiving conversation — input is paused'
4259
- : conversationMutation === 'new'
4260
- ? 'starting a fresh conversation — input is paused'
4261
- : compacting
4262
- ? 'compacting conversation — input is paused'
4263
- : permission
4264
- ? hintForPhase(phase, state, session?.hasProvider === true)
4265
- : textPrompt
4266
- ? 'name editor open — enter save · esc cancel'
4267
- : choicePicker
4268
- ? 'choice open — ↑↓ / 1–9 select · enter apply · esc cancel'
4269
- : copyPicker
4270
- ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
4271
- : goalStatus && phase === 'ready' && state === 'idle'
4272
- ? undefined
4273
- : hintForPhase(phase, state, session?.hasProvider === true), usage: usage, context: context }) })] }) }));
4434
+ escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, userCommands: userCommands, mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: statusGoal, state: state, hint: statusHint }) })] }) }));
4274
4435
  }
4275
4436
  function Banner({ version, session, permissionMode, cwd, }) {
4276
4437
  const { columns } = useWindowSize();
@@ -4302,7 +4463,9 @@ function formatToolCall(toolName, summary, standalone = false) {
4302
4463
  }
4303
4464
  function hintForPhase(phase, state,
4304
4465
  /** Whether there is a working session behind the picker to go back to. */
4305
- canReturn = false) {
4466
+ canReturn = false,
4467
+ /** Whether the active-turn composer contains text, a paste, or an attachment. */
4468
+ hasDraft = false) {
4306
4469
  // Enter is absent because Enter grants nothing here, deliberately — see the
4307
4470
  // trust branch in the key handler above.
4308
4471
  if (phase === 'trust')
@@ -4328,7 +4491,7 @@ canReturn = false) {
4328
4491
  if (state === 'awaiting-permission')
4329
4492
  return 'y approve · n / esc reject · a approve all';
4330
4493
  if (state !== 'idle')
4331
- return 'enter steer · tab queue · esc interrupt';
4494
+ return hasDraft ? 'enter steer · tab queue · esc interrupt' : '';
4332
4495
  // The composer already points to /help. Keeping the complete key legend in
4333
4496
  // every idle frame made ordinary conversation read like a permanent setup
4334
4497
  // screen; only state-specific actions belong in the footer.