@aragon-agent/cli 0.6.35 → 0.6.37

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 (146) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +3173 -3071
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +38 -3
  6. package/dist/agent/controller.js +64 -4
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/reducer.d.ts +13 -2
  9. package/dist/agent/reducer.js +1 -0
  10. package/dist/cli.js +12 -0
  11. package/dist/commands/agents-command.d.ts +15 -0
  12. package/dist/commands/agents-command.js +27 -0
  13. package/dist/commands/builtins.js +131 -4
  14. package/dist/commands/perf.d.ts +11 -0
  15. package/dist/commands/perf.js +2 -1
  16. package/dist/commands/registry.d.ts +14 -0
  17. package/dist/config/app-paths.d.ts +15 -0
  18. package/dist/config/app-paths.js +33 -2
  19. package/dist/config/env.d.ts +4 -1
  20. package/dist/config/env.js +28 -2
  21. package/dist/config/load.d.ts +10 -0
  22. package/dist/config/load.js +8 -1
  23. package/dist/config/schema.d.ts +68 -0
  24. package/dist/config/schema.js +39 -0
  25. package/dist/config/store.js +12 -1
  26. package/dist/config/ui-state.d.ts +18 -0
  27. package/dist/config/ui-state.js +9 -0
  28. package/dist/exec/events.d.ts +25 -0
  29. package/dist/exec/events.js +4 -0
  30. package/dist/exec/index.js +16 -1
  31. package/dist/exec/runner.js +10 -0
  32. package/dist/proc/limits.d.ts +16 -0
  33. package/dist/proc/limits.js +16 -0
  34. package/dist/proc/supervisor.d.ts +20 -1
  35. package/dist/proc/supervisor.js +42 -14
  36. package/dist/prompts/command.d.ts +34 -0
  37. package/dist/prompts/command.js +237 -0
  38. package/dist/prompts/copy.d.ts +89 -0
  39. package/dist/prompts/copy.js +131 -0
  40. package/dist/prompts/fill.d.ts +51 -0
  41. package/dist/prompts/fill.js +46 -0
  42. package/dist/prompts/limits.d.ts +53 -0
  43. package/dist/prompts/limits.js +53 -0
  44. package/dist/prompts/normalize.d.ts +53 -0
  45. package/dist/prompts/normalize.js +190 -0
  46. package/dist/prompts/resolve.d.ts +31 -0
  47. package/dist/prompts/resolve.js +60 -0
  48. package/dist/prompts/search.d.ts +58 -0
  49. package/dist/prompts/search.js +146 -0
  50. package/dist/prompts/store.d.ts +86 -0
  51. package/dist/prompts/store.js +453 -0
  52. package/dist/prompts/types.d.ts +204 -0
  53. package/dist/prompts/types.js +13 -0
  54. package/dist/session/checkpoint.d.ts +8 -1
  55. package/dist/session/checkpoint.js +6 -0
  56. package/dist/session/persist.d.ts +19 -1
  57. package/dist/session/persist.js +13 -1
  58. package/dist/session/startup-session.d.ts +7 -2
  59. package/dist/session/startup-session.js +5 -2
  60. package/dist/session/store.js +3 -0
  61. package/dist/session/validate-session.js +64 -1
  62. package/dist/team/limits.d.ts +25 -0
  63. package/dist/team/limits.js +26 -0
  64. package/dist/team/runtime.js +27 -2
  65. package/dist/team/settled.d.ts +57 -0
  66. package/dist/team/settled.js +87 -0
  67. package/dist/team/subagent.js +48 -2
  68. package/dist/team/timeline.d.ts +74 -0
  69. package/dist/team/timeline.js +101 -0
  70. package/dist/team/types.d.ts +24 -1
  71. package/dist/todo/acceptance.d.ts +120 -0
  72. package/dist/todo/acceptance.js +399 -0
  73. package/dist/todo/follow-through.d.ts +48 -23
  74. package/dist/todo/follow-through.js +110 -44
  75. package/dist/todo/limits.d.ts +62 -1
  76. package/dist/todo/limits.js +62 -1
  77. package/dist/todo/panel-rows.d.ts +29 -39
  78. package/dist/todo/panel-rows.js +37 -83
  79. package/dist/todo/plan-sections.d.ts +60 -0
  80. package/dist/todo/plan-sections.js +108 -0
  81. package/dist/todo/prompt.d.ts +17 -12
  82. package/dist/todo/prompt.js +59 -40
  83. package/dist/todo/row-window.d.ts +51 -0
  84. package/dist/todo/row-window.js +107 -0
  85. package/dist/todo/store.d.ts +139 -31
  86. package/dist/todo/store.js +271 -73
  87. package/dist/todo/todo-tool.d.ts +33 -8
  88. package/dist/todo/todo-tool.js +186 -30
  89. package/dist/todo/types.d.ts +58 -0
  90. package/dist/ui/App.js +310 -46
  91. package/dist/ui/Composer.d.ts +6 -2
  92. package/dist/ui/Composer.js +2 -2
  93. package/dist/ui/Header.d.ts +9 -1
  94. package/dist/ui/Header.js +2 -2
  95. package/dist/ui/PromptInput.d.ts +21 -1
  96. package/dist/ui/PromptInput.js +21 -1
  97. package/dist/ui/ScrollbackShell.d.ts +19 -19
  98. package/dist/ui/ScrollbackShell.js +8 -1
  99. package/dist/ui/StatusBar.d.ts +5 -0
  100. package/dist/ui/StatusBar.js +1 -0
  101. package/dist/ui/TeamPanel.d.ts +26 -44
  102. package/dist/ui/TeamPanel.js +46 -135
  103. package/dist/ui/TodoPanel.d.ts +17 -14
  104. package/dist/ui/TodoPanel.js +146 -38
  105. package/dist/ui/Transcript.js +1 -1
  106. package/dist/ui/composer-input.d.ts +10 -0
  107. package/dist/ui/composer-input.js +57 -24
  108. package/dist/ui/entries/TodoCard.d.ts +26 -11
  109. package/dist/ui/entries/TodoCard.js +56 -15
  110. package/dist/ui/glyphs.d.ts +19 -0
  111. package/dist/ui/glyphs.js +4 -0
  112. package/dist/ui/layout/agents-board.d.ts +102 -0
  113. package/dist/ui/layout/agents-board.js +389 -0
  114. package/dist/ui/layout/cells.d.ts +61 -0
  115. package/dist/ui/layout/cells.js +156 -0
  116. package/dist/ui/layout/header-hints.d.ts +13 -3
  117. package/dist/ui/layout/header-hints.js +48 -14
  118. package/dist/ui/layout/status-detail-layout.js +9 -0
  119. package/dist/ui/layout/status-layout.d.ts +13 -0
  120. package/dist/ui/layout/team-row.d.ts +139 -0
  121. package/dist/ui/layout/team-row.js +246 -0
  122. package/dist/ui/layout/todo-layout.d.ts +38 -0
  123. package/dist/ui/layout/todo-layout.js +75 -0
  124. package/dist/ui/layout/virtual-window.js +23 -3
  125. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  126. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  127. package/dist/ui/overlays/HelpOverlay.js +17 -2
  128. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  129. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  130. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  131. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  132. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  133. package/dist/ui/overlays/PromptPicker.js +129 -0
  134. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  135. package/dist/ui/overlays/PromptTextArea.js +141 -0
  136. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  137. package/dist/ui/overlays/prompt-library-state.js +335 -0
  138. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  139. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  140. package/dist/ui/scrollback/band.js +14 -1
  141. package/dist/ui/team-status.d.ts +25 -0
  142. package/dist/ui/team-status.js +31 -3
  143. package/dist/ui/use-startup-notices.d.ts +35 -1
  144. package/dist/ui/use-startup-notices.js +29 -2
  145. package/dist/ui/use-wheel-routing.js +9 -1
  146. package/package.json +2 -2
package/dist/ui/App.js CHANGED
@@ -16,13 +16,13 @@ import { computeCost } from '../agent/usage.js';
16
16
  import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
17
17
  import { updatePersistedConfig } from '../config/store.js';
18
18
  import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
19
- import { bumpSubmitCount, setMouseNoticeVersion, setScrollModeNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
19
+ import { bumpSubmitCount, setMouseNoticeVersion, setPromptKeyNoticeVersion, setScrollModeNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
20
20
  import { getLogger } from '../logging/logger.js';
21
21
  import { isTerminalStatus } from '../proc/types.js';
22
22
  import { getSecrets } from '../logging/secret-registry.js';
23
23
  import { redactText } from '../logging/redact.js';
24
24
  import { resolveTuiCapabilities } from './capabilities.js';
25
- import { useStartupNotices, MOUSE_NOTICE_VERSION, SCROLL_MODE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
25
+ import { useStartupNotices, MOUSE_NOTICE_VERSION, PROMPT_KEY_NOTICE_VERSION, SCROLL_MODE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
26
26
  import { getTheme } from './theme.js';
27
27
  import { pickGlyphs } from './glyphs.js';
28
28
  import { Header } from './Header.js';
@@ -44,10 +44,11 @@ import { buildExitSummary } from './scrollback/exit-summary.js';
44
44
  import { LiveTail } from './scrollback/LiveTail.js';
45
45
  import { SCROLLBACK_LIMITS } from './scrollback/limits.js';
46
46
  import { TODO_LIMITS } from '../todo/limits.js';
47
+ import { planIsResolved } from '../todo/acceptance.js';
47
48
  import { ScrollViewport } from './layout/ScrollViewport.js';
48
- import { buildTodoRailLayout } from './layout/todo-layout.js';
49
+ import { acceptanceChip, acceptanceRung, buildTodoRailLayout } from './layout/todo-layout.js';
49
50
  import { buildTeamPanelLayout } from './layout/team-panel.js';
50
- import { isLiveTeamCard, rosterCounts } from './team-status.js';
51
+ import { isLiveTeamCard, lastTeamEntry, rosterCounts } from './team-status.js';
51
52
  import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
52
53
  import { useTerminalSize } from './layout/useTerminalSize.js';
53
54
  import { useHeightStore } from './use-height-store.js';
@@ -64,6 +65,13 @@ import { OverlayFrame, overlayPageSize } from './layout/OverlayFrame.js';
64
65
  import { helpRows } from './overlays/HelpOverlay.js';
65
66
  import { ModelPicker } from './overlays/ModelPicker.js';
66
67
  import { KitPicker } from './overlays/KitPicker.js';
68
+ import { PromptLibraryOverlay } from './overlays/PromptLibraryOverlay.js';
69
+ import { PromptPicker } from './overlays/PromptPicker.js';
70
+ import { draftToInput } from './overlays/prompt-library-state.js';
71
+ import { createPrompt, deletePrompt, flushPromptStats, loadPromptLibrary, movePrompt, recordPromptUse, setPromptFavorite, takePromptRepairNotice, updatePrompt, } from '../prompts/store.js';
72
+ import { resolvePromptRef } from '../prompts/resolve.js';
73
+ import { promptLineCount } from '../prompts/fill.js';
74
+ import { PROMPT_CORRUPT_COPY, PROMPT_INSERTED_COPY, promptFilledCopy, promptSkippedCopy, } from '../prompts/copy.js';
67
75
  import { INACTIVE_KIT } from '../config/kit-resolution.js';
68
76
  import { kitApplyMessage, kitNoteMessages } from '../agent/kit-apply.js';
69
77
  import { createModelProbe } from '../agent/model-probe.js';
@@ -78,6 +86,7 @@ import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
78
86
  import { createClipboardCoordinator } from './clipboard-task.js';
79
87
  import { QueuePanel } from './QueuePanel.js';
80
88
  import { QueueOverlay } from './overlays/QueueOverlay.js';
89
+ import { AgentsBoardOverlay } from './overlays/AgentsBoardOverlay.js';
81
90
  import { buildQueueLayout } from './layout/queue-layout.js';
82
91
  import { interactionCopy } from './interaction-copy.js';
83
92
  import { createRunMetrics, updateRunMetrics, freezeRunMetrics, projectRunMetrics } from './run-metrics.js';
@@ -142,6 +151,25 @@ function seedViewState(seed) {
142
151
  ? viewReducer(base, { type: 'restoreEntries', entries: seed.entries })
143
152
  : base;
144
153
  }
154
+ /**
155
+ * One prompt-library mutation, translated into the store call it names.
156
+ *
157
+ * MODULE-LEVEL, so it is a table rather than a closure rebuilt on every render: the
158
+ * overlay sends an INTENT (`{kind:'create', draft}`), and this is the single place that
159
+ * intent becomes a write. Every branch returns the store's own result, which carries
160
+ * the new snapshot on success AND on `write_failed` (D-4).
161
+ */
162
+ function applyPromptMutation(action) {
163
+ if (action.kind === 'create')
164
+ return createPrompt(draftToInput(action.draft));
165
+ if (action.kind === 'update')
166
+ return updatePrompt(action.id, draftToInput(action.draft));
167
+ if (action.kind === 'delete')
168
+ return deletePrompt(action.id);
169
+ if (action.kind === 'move')
170
+ return movePrompt(action.id, action.delta);
171
+ return setPromptFavorite(action.id, action.favorite);
172
+ }
145
173
  export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, scrollMode = 'app', scrollback, initialEntries, session, sessionBridge, sessionNotice, }) {
146
174
  const native = scrollMode === 'native' && scrollback !== undefined;
147
175
  /** Set when the native writer swallowed repeated tall frames; cleared by a resize. */
@@ -157,6 +185,19 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
157
185
  // nonce render (the async clear lands a frame later - see the timing
158
186
  // invariant beside the ladder itself).
159
187
  const [draftClearNonce, setDraftClearNonce] = useState(0);
188
+ // --- Quick prompt library (quick-prompt-library §5.2) --------------------
189
+ //
190
+ // THE SNAPSHOT IS READ ONCE, from the store's own cache, and re-read only after a
191
+ // mutation or a command that might have written the file. `App` never edits it: the
192
+ // store is the only write authority (I-6), so the render can never disagree with the
193
+ // disk about what exists.
194
+ const [promptLibrary, setPromptLibrary] = useState(loadPromptLibrary);
195
+ const [promptIntent, setPromptIntent] = useState(null);
196
+ const promptIntentNonce = useRef(0);
197
+ // The fill channel's payload (D-6). `null` until the first fill, so the composer's
198
+ // effect has nothing to do on every mount.
199
+ const [draftFill, setDraftFill] = useState(null);
200
+ const draftFillNonce = useRef(0);
160
201
  const [draftClearedByCtrlC, setDraftClearedByCtrlC] = useState(false);
161
202
  const [popupRows, setPopupRows] = useState(0);
162
203
  const [statusExpanded, setStatusExpanded] = useState(false);
@@ -620,8 +661,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
620
661
  if (controller.isRunning() || controller.isCompactionBusy?.())
621
662
  return;
622
663
  const live = controller.getTodoSnapshot();
623
- if (!live || live.doneCount >= live.total)
624
- return; // the list moved
664
+ // `planIsResolved`, NOT `doneCount >= total` (todo-acceptance-criteria
665
+ // §6.7 / AC-20). With steps all done but criteria unchecked the old test
666
+ // said "no work left" and returned silently - one second after the notice
667
+ // promised "Continuing with 2 unchecked acceptance criteria in 3s". A
668
+ // promise that cancels itself is the worst failure this design has.
669
+ if (!live || planIsResolved(live.items, live.acceptance?.items ?? null))
670
+ return;
625
671
  budgetRef.current = { ...budgetRef.current, used: budgetRef.current.used + 1 };
626
672
  autoContinuationRef.current = true;
627
673
  // COUNTS ONLY, NEVER ITEM TEXT (D-19), and only at the fire point —
@@ -631,6 +677,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
631
677
  noProgressStreak: budgetRef.current.noProgressStreak,
632
678
  total: live.total,
633
679
  done: live.doneCount,
680
+ accTotal: live.acceptance?.total ?? 0,
681
+ accChecked: live.acceptance?.checkedCount ?? 0,
634
682
  mode: 'auto',
635
683
  });
636
684
  submitRef.current(decision.message, { userInitiated: false, todoPolicy: 'continue' });
@@ -1045,32 +1093,45 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1045
1093
  // the events must never be shown advice about a mode not in effect (§6.1).
1046
1094
  // `cli.tsx::wantMouse` now folds the platform check into the same expression,
1047
1095
  // so on a console that reports nothing there is no source and no advice.
1096
+ // SIX NOTICES, TWO ARGUMENTS (quick-prompt-library D-20 / P1-6). The hook took five
1097
+ // positional arguments and this feature added a sixth, which `CLAUDE.md` forbids; the
1098
+ // options object is also what makes an older caller that passes nothing byte-identical.
1048
1099
  useStartupNotices(dispatch, {
1049
- enabled: !!mouseSource,
1050
- // NOT re-derived here (see `MouseNoticeOptions`): `cli.tsx` computes
1051
- // `mouseFilter !== null && cfg.mouseSelect` once and four things read it,
1052
- // so a second derivation is a second chance for them to disagree.
1053
- selectEnabled: terminal?.mouseSelect ?? false,
1054
- // `state.json`, not `config.json`: whether a one-shot notice has been shown
1055
- // is bookkeeping, and the comment on the old persisted key said so.
1056
- //
1057
- // THE VERSION, NOT `true` (P1-8). The gate compares against
1058
- // `MOUSE_NOTICE_VERSION`, so storing anything else either replays the
1059
- // notice forever or silences a future revision of the text.
1060
- onSeen: () => setMouseNoticeVersion(MOUSE_NOTICE_VERSION),
1061
- },
1062
- // Present only when `loadConfig` had to raise the user's own
1063
- // `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
1064
- cfg.transcriptRetainRequested !== undefined
1065
- ? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
1066
- : undefined, vtInputWarning
1067
- ? {
1068
- nodeVersion: vtInputWarning.nodeVersion,
1069
- onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
1070
- }
1071
- : undefined, native
1072
- ? { onSeen: () => setScrollModeNoticeVersion(SCROLL_MODE_NOTICE_VERSION) }
1073
- : undefined);
1100
+ mouse: {
1101
+ enabled: !!mouseSource,
1102
+ // NOT re-derived here (see `MouseNoticeOptions`): `cli.tsx` computes
1103
+ // `mouseFilter !== null && cfg.mouseSelect` once and four things read it,
1104
+ // so a second derivation is a second chance for them to disagree.
1105
+ selectEnabled: terminal?.mouseSelect ?? false,
1106
+ // `state.json`, not `config.json`: whether a one-shot notice has been shown
1107
+ // is bookkeeping, and the comment on the old persisted key said so.
1108
+ //
1109
+ // THE VERSION, NOT `true` (P1-8). The gate compares against
1110
+ // `MOUSE_NOTICE_VERSION`, so storing anything else either replays the
1111
+ // notice forever or silences a future revision of the text.
1112
+ onSeen: () => setMouseNoticeVersion(MOUSE_NOTICE_VERSION),
1113
+ },
1114
+ // Present only when `loadConfig` had to raise the user's own
1115
+ // `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
1116
+ ...(cfg.transcriptRetainRequested !== undefined
1117
+ ? { retain: { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain } }
1118
+ : {}),
1119
+ ...(vtInputWarning
1120
+ ? {
1121
+ vtInput: {
1122
+ nodeVersion: vtInputWarning.nodeVersion,
1123
+ onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
1124
+ },
1125
+ }
1126
+ : {}),
1127
+ ...(native
1128
+ ? { scrollMode: { onSeen: () => setScrollModeNoticeVersion(SCROLL_MODE_NOTICE_VERSION) } }
1129
+ : {}),
1130
+ // THE KEY MIGRATION (D-20 / AC-32). Always supplied: the hook gates it on
1131
+ // `state.json::promptKeyNoticeVersion`, so a user who has already been told pays
1132
+ // nothing and `state.json` is never touched for a notice that will not be shown.
1133
+ promptKeys: { onSeen: () => setPromptKeyNoticeVersion(PROMPT_KEY_NOTICE_VERSION) },
1134
+ });
1074
1135
  // --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
1075
1136
  useEffect(() => {
1076
1137
  const merged = mergePendingEntries(state.entries, state.pendingSteering);
@@ -1206,6 +1267,102 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1206
1267
  // --- Helpers -----------------------------------------------------------
1207
1268
  const notify = (level, text) => dispatch({ type: 'notice', level, text });
1208
1269
  const toast = (level, text) => dispatch({ type: 'pushToast', level, text });
1270
+ // --- Quick prompt library: two one-shot channels and the store port ------
1271
+ const refreshPromptLibrary = useCallback(() => setPromptLibrary(loadPromptLibrary()), []);
1272
+ /**
1273
+ * Open the manager on a specific page (D-21).
1274
+ *
1275
+ * THE NONCE IS BUMPED BEFORE THE OVERLAY OPENS, and the overlay consumes it at most
1276
+ * once. Without that, two `/prompts edit 1` in a row would be one intent and the
1277
+ * second would look like a no-op (I-8).
1278
+ */
1279
+ const openPromptLibrary = useCallback((entry) => {
1280
+ promptIntentNonce.current += 1;
1281
+ setPromptIntent({ nonce: promptIntentNonce.current, entry });
1282
+ dispatch({ type: 'setOverlay', overlay: 'prompts' });
1283
+ }, []);
1284
+ /**
1285
+ * Hand the composer a body to insert (D-6 / §4.6).
1286
+ *
1287
+ * `App` DOES NOT TOUCH THE DRAFT. It only carries the payload and closes the overlay;
1288
+ * `PromptInput` owns the editor, the caret and the one submission path (P0-1).
1289
+ */
1290
+ const fillComposer = useCallback((text, submit = false) => {
1291
+ draftFillNonce.current += 1;
1292
+ setDraftFill({
1293
+ nonce: draftFillNonce.current,
1294
+ text,
1295
+ ...(submit ? { submit: true } : {}),
1296
+ });
1297
+ dispatch({ type: 'setOverlay', overlay: null });
1298
+ }, []);
1299
+ const handlePromptMutate = useCallback((action) => {
1300
+ const result = applyPromptMutation(action);
1301
+ // ON FAILURE TOO, and that is not a mistake: a `write_failed` still applied in
1302
+ // memory (D-4), so the user must be able to see the prompt they just created.
1303
+ setPromptLibrary(result.library);
1304
+ return result;
1305
+ }, []);
1306
+ /**
1307
+ * Closing the picker is when the usage counters reach the disk (D-7).
1308
+ *
1309
+ * The snapshot is refreshed FIRST so the next open shows the counts this session just
1310
+ * produced, then the stats are flushed - two independent concerns that happen to share
1311
+ * a moment.
1312
+ */
1313
+ const closePromptPicker = () => {
1314
+ refreshPromptLibrary();
1315
+ flushPromptStats();
1316
+ dispatch({ type: 'setOverlay', overlay: null });
1317
+ };
1318
+ const handlePromptFill = (id, submit) => {
1319
+ // THE FRESH LIBRARY, not the render snapshot: a `/prompts fill` can arrive in the
1320
+ // same tick as a mutation, and the snapshot would then be one render behind.
1321
+ const prompt = loadPromptLibrary().prompts.find((entry) => entry.id === id);
1322
+ if (!prompt)
1323
+ return;
1324
+ recordPromptUse(id);
1325
+ fillComposer(prompt.content, submit);
1326
+ // The usage counters change, so the list the picker re-opens with is the one with
1327
+ // the new counts (D-7: memory first, disk on a debounce).
1328
+ refreshPromptLibrary();
1329
+ // TWO ACKNOWLEDGEMENTS, ONE OF THEM HONEST ABOUT WHAT HAPPENED (D-6). When the draft
1330
+ // already had text and the mode is `insert`, the sentence has to say that the words
1331
+ // are still there - otherwise "filled" reads as "replaced" and the user retypes what
1332
+ // they had. `hasDraftRef` is the composer's own presence flag (the host has no copy
1333
+ // of the text), so this costs no new state.
1334
+ const keptDraft = cfg.prompts?.insertMode !== 'replace' && hasDraftRef.current;
1335
+ notify('info', keptDraft
1336
+ ? PROMPT_INSERTED_COPY
1337
+ : promptFilledCopy(prompt.title, promptLineCount(prompt.content)));
1338
+ };
1339
+ /**
1340
+ * One-shot report of a repaired or partially unreadable library (§8).
1341
+ *
1342
+ * A DURABLE `notice`, not a toast: the user has to be able to scroll back to the
1343
+ * sentence that explains why entries are missing, and a `.bak` file is only useful to
1344
+ * someone who knows it exists.
1345
+ */
1346
+ useEffect(() => {
1347
+ const repair = takePromptRepairNotice();
1348
+ if (!repair)
1349
+ return;
1350
+ if (repair.repaired !== 'entries')
1351
+ notify('warn', PROMPT_CORRUPT_COPY);
1352
+ if (repair.dropped > 0)
1353
+ notify('warn', promptSkippedCopy(repair.dropped));
1354
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1355
+ }, []);
1356
+ // Usage counters are written when the surface that produced them closes, and again
1357
+ // on unmount (D-7). The 1 s debounce inside the store covers the common case; these
1358
+ // two make a clean exit cost exactly one write instead of waiting for a timer.
1359
+ const promptOverlayOpen = state.overlay === 'prompts' || state.overlay === 'prompt-pick';
1360
+ useEffect(() => {
1361
+ if (!promptOverlayOpen)
1362
+ return undefined;
1363
+ return () => { flushPromptStats(); };
1364
+ }, [promptOverlayOpen]);
1365
+ useEffect(() => () => { flushPromptStats(); }, []);
1209
1366
  // THE BRANCH / `--save` NOTICE (D-13). Once, on mount, and only when the
1210
1367
  // caller supplied one: "the session you branched from is unchanged" is the
1211
1368
  // sentence that makes `--resume` safe to reach for, and it belongs in the
@@ -1524,6 +1681,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1524
1681
  }
1525
1682
  dispatch(action);
1526
1683
  },
1684
+ // The prompt library's three capabilities (quick-prompt-library §4.3 / P1-3).
1685
+ // Present even when `prompts.enabled` is false: `/prompts` has to be able to SAY
1686
+ // that it is off, and a missing port would answer "not available in this build"
1687
+ // instead (AC-30). `resolve` reads the store's cache, so it always sees the last
1688
+ // mutation rather than this render's snapshot.
1689
+ prompts: {
1690
+ openPicker: () => dispatch({ type: 'setOverlay', overlay: 'prompt-pick' }),
1691
+ openManager: (entry) => openPromptLibrary(entry),
1692
+ resolve: (ref) => resolvePromptRef(loadPromptLibrary(), ref),
1693
+ },
1527
1694
  setOverlay: (overlay) => dispatch({ type: 'setOverlay', overlay }),
1528
1695
  notify,
1529
1696
  toast,
@@ -1685,6 +1852,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1685
1852
  notify('error', `Command failed: ${redactText(reason, getSecrets())}\nInput: ${safe}`);
1686
1853
  }
1687
1854
  finally {
1855
+ // A command may have written the prompt library (`/prompts add|rm|move|sort`).
1856
+ // The store's cache already holds the new snapshot, so this is a `setState`, not
1857
+ // a disk read - and without it the list would show the library as it was when
1858
+ // the overlay opened.
1859
+ refreshPromptLibrary();
1688
1860
  if (switching) {
1689
1861
  switchingConversation.current = false;
1690
1862
  drainCompactionQueue.current();
@@ -2004,6 +2176,53 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2004
2176
  setStatusExpanded(value => !value);
2005
2177
  return;
2006
2178
  }
2179
+ // Ctrl+B: the Agents board (tui-subagent-panel D-6 / §6.5).
2180
+ //
2181
+ // THE GUARD ORDER IS Ctrl+G's EXACTLY, plus one rung: pressing it again CLOSES the
2182
+ // board, and any OTHER overlay keeps the keyboard (a second overlay drawn over the
2183
+ // first would leave two `isActive` input handlers fighting for Up/Down).
2184
+ //
2185
+ // Two byte forms, like Ctrl+K: the raw 0x02 byte and `key.ctrl` + `b`. 0x02 is
2186
+ // unbound everywhere in this app, and `Alt+B` (keymap.ts's `wordLeft`) is a
2187
+ // different modifier with a different prefix, so neither collides.
2188
+ const isCtrlB = (key.ctrl && (input === 'b' || input === 'B')) || input === '\x02';
2189
+ if (isCtrlB) {
2190
+ if (tooSmall)
2191
+ return;
2192
+ const openOverlay = stateRef.current.overlay;
2193
+ if (openOverlay === 'agents') {
2194
+ dispatch({ type: 'setOverlay', overlay: null });
2195
+ return;
2196
+ }
2197
+ if (openOverlay)
2198
+ return;
2199
+ // ONE GUARD, SHARED WITH `/agents` (AC-18): an empty board would read as a broken
2200
+ // feature, and the honest answer is the same notice the command gives.
2201
+ if (stateRef.current.team === null && lastTeamEntry(stateRef.current.entries) === null) {
2202
+ toast('info', 'No subagent dispatch yet.');
2203
+ return;
2204
+ }
2205
+ dispatch({ type: 'setOverlay', overlay: 'agents' });
2206
+ return;
2207
+ }
2208
+ // Ctrl+P: the quick prompt library (quick-prompt-library D-1).
2209
+ //
2210
+ // THIS KEY USED TO BE THE MODE FALLBACK and now opens a library, because the
2211
+ // requirement names it as a first-class entry point. The guards are exactly the
2212
+ // ones Ctrl+K and Ctrl+G use: an overlay owns the keyboard, and on a too-small
2213
+ // terminal the frame has nowhere to go. `prompts.enabled=false` is read at KEY
2214
+ // TIME rather than at mount, so the switch takes effect without a restart - and
2215
+ // the command stays registered so `/prompts` can say it is off (D-19 / AC-30).
2216
+ if (key.ctrl && (input === 'p' || input === 'P')) {
2217
+ if (tooSmall || stateRef.current.overlay)
2218
+ return;
2219
+ // `!== false`, so a hand-built `CliConfig` without the section behaves like the
2220
+ // default (enabled) rather than like a dead key.
2221
+ if (controller.getConfig().prompts?.enabled === false)
2222
+ return;
2223
+ openPromptLibrary({ kind: 'list' });
2224
+ return;
2225
+ }
2007
2226
  // Ctrl+K: the Agent-kit picker (agent-model-kits D-15 / §4.7).
2008
2227
  //
2009
2228
  // NOT AN INPUT FRAME, unlike Ctrl+I below. Ctrl+I collides with the Tab byte,
@@ -2060,7 +2279,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2060
2279
  // Modes A only. `model` / `confirm` manage their own keys; registering an
2061
2280
  // arrow handler for them would double every keypress (R-12).
2062
2281
  const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan'
2063
- || overlay === 'queue' || overlay === 'kit';
2282
+ || overlay === 'queue' || overlay === 'kit'
2283
+ // The Agents board pages through `overlayScroll` too, but keeps Up/Down for its
2284
+ // CHILD CURSOR - the same trade `settings` makes with its field cursor.
2285
+ || overlay === 'agents'
2286
+ // The prompt surfaces page themselves through `overlayScroll` too
2287
+ // (quick-prompt-library appendix B-1). Without them a `PgDn` in the list does
2288
+ // nothing and the wheel falls through to the transcript behind it.
2289
+ || overlay === 'prompts' || overlay === 'prompt-pick';
2064
2290
  if (key.pageUp) {
2065
2291
  if (controlled)
2066
2292
  setOverlayScroll((n) => Math.max(0, n - overlayPage));
@@ -2085,11 +2311,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2085
2311
  // everything else falls through (AC-P2).
2086
2312
  //
2087
2313
  // TWO KEYS, ONE BRANCH (shift-tab-mode-toggle-still-dead-on-windows, C2).
2088
- // `Ctrl+P` is not a Windows-only alias bolted on beside the real binding: it
2089
- // is the rung that survives when `Shift+Tab` cannot be delivered at all, and
2090
- // it shares this branch precisely so the two can never drift into different
2091
- // behaviour. See `MODE_TOGGLE_KEYS` for why this key and not another.
2092
- if ((key.tab && key.shift) || (key.ctrl && (input === 'p' || input === 'P'))) {
2314
+ // `Ctrl+N` is not a Windows-only alias bolted on beside the real binding: it is the
2315
+ // rung that survives when `Shift+Tab` cannot be delivered at all, and it shares this
2316
+ // branch precisely so the two can never drift into different behaviour. See
2317
+ // `MODE_TOGGLE_KEYS` for why this key and not another - and for why it is `Ctrl+N`
2318
+ // now rather than the `Ctrl+P` this branch used to own (quick-prompt-library D-1:
2319
+ // `Ctrl+P` opens the prompt library, and one key may not mean two things).
2320
+ if ((key.tab && key.shift) || (key.ctrl && (input === 'n' || input === 'N'))) {
2093
2321
  if (stateRef.current.overlay)
2094
2322
  return;
2095
2323
  applyMode(nextMode(controller.getAgentMode()));
@@ -2428,7 +2656,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2428
2656
  // ALWAYS TRUE (agent-model-kits §4.7 / RV-P1-2): the requirement is that
2429
2657
  // `ctrl+G` reveals `ctrl+K`, and the user with an empty library needs that
2430
2658
  // hint most - the picker itself carries the "create one in /settings" row.
2431
- kitHint: true }));
2659
+ kitHint: true,
2660
+ // `prompts.hint=false` removes the `^P prompts` clause and nothing else; the key
2661
+ // still works (AC-3).
2662
+ promptsHint: cfg.prompts?.hint !== false }));
2432
2663
  // The header and the opener are SLOTS OF THE SHELL, not printed bytes (D3/D4): at
2433
2664
  // `H = rows - 1` a printed row is pushed off the screen by the very next frame, so
2434
2665
  // `printBanner()` was a way of making the banner invisible. Neither is passed here.
@@ -2482,7 +2713,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2482
2713
  const overlayNode = overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: viewCols, rows: helpRows(theme, caps, native), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { isActive: !tooSmall, registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'kit' ? (_jsx(KitPicker, { isActive: !tooSmall, library: cfg.modelProfiles, config: cfg, ...(kitDiskConfig ? { diskConfig: kitDiskConfig } : {}), active: activeKit,
2483
2714
  // ONLY THE APPLY IS BLOCKED while work is in flight (§4.7): a list a
2484
2715
  // user cannot even OPEN reads as a broken key.
2485
- busy: controller.isModelSettingsBusy(), maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onChoose: handleKitChoose, onClose: () => dispatch({ type: 'setOverlay', overlay: null }) })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
2716
+ busy: controller.isModelSettingsBusy(), maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onChoose: handleKitChoose, onClose: () => dispatch({ type: 'setOverlay', overlay: null }) })) : overlay === 'prompts' ? (_jsx(PromptLibraryOverlay, { isActive: !tooSmall, library: promptLibrary, ...(promptIntent ? { intent: promptIntent } : {}), cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onMutate: handlePromptMutate, onNotice: notify, onClose: () => dispatch({ type: 'setOverlay', overlay: null }) })) : overlay === 'prompt-pick' ? (_jsx(PromptPicker, { isActive: !tooSmall, library: promptLibrary, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onFill: handlePromptFill, onManage: (entry) => openPromptLibrary(entry), onClose: closePromptPicker })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
2486
2717
  provider: cfg.provider,
2487
2718
  model: cfg.model,
2488
2719
  baseUrl: cfg.baseUrl ?? '',
@@ -2519,7 +2750,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2519
2750
  }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft, probe: probe, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
2520
2751
  // Read from `cfg` (render scope, re-read every frame), so `/retry off`
2521
2752
  // while the screen is open shows the new value on the next paint.
2522
- retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { isActive: !tooSmall, state: confirmState, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { isActive: !tooSmall, questions: humanRequest.questions, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { isActive: !tooSmall, plan: humanRequest.plan, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
2753
+ retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'agents' ? (
2754
+ // THE BRANCH AND THE `Overlay` MEMBER SHIP TOGETHER (R-11): a missing branch is not
2755
+ // a type error, it is a SILENT BLANK FRAME, because `active={overlay === null}` has
2756
+ // already disabled the transcript underneath.
2757
+ _jsx(AgentsBoardOverlay, { team: state.team, entries: state.entries, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, isActive: !tooSmall, native: native, theme: theme, caps: caps })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { isActive: !tooSmall, state: confirmState, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { isActive: !tooSmall, questions: humanRequest.questions, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { isActive: !tooSmall, plan: humanRequest.plan, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
2523
2758
  const density = cfg.density;
2524
2759
  const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
2525
2760
  // The fixed status bar remains visible underneath every overlay and toast.
@@ -2554,6 +2789,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2554
2789
  windowStart: scrollbackView.windowStart,
2555
2790
  windowEntries: scrollbackView.tailEntries.length,
2556
2791
  headRows: scrollbackView.headRows,
2792
+ // WHICH RUNG THE ACCEPTANCE SECTION REACHED, from the same function the rail
2793
+ // rendered with (§6.9.6). `n/a` is the honest answer when the plan has no
2794
+ // criteria at all, which is a different fact from `absent`.
2795
+ acceptance: state.todos?.acceptance
2796
+ ? acceptanceRung({
2797
+ snapshot: state.todos,
2798
+ railRows: nativeBudget.band.railRows,
2799
+ width: railWidth,
2800
+ })
2801
+ : 'n/a',
2557
2802
  }
2558
2803
  : null,
2559
2804
  // A getter: the bridge's `frameReady` and geometry change outside React renders.
@@ -2602,6 +2847,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2602
2847
  toast: latestToast ? { text: latestToast.text,
2603
2848
  level: latestToast.level === 'success' ? 'info' : latestToast.level } : undefined,
2604
2849
  });
2850
+ // THE ACCEPTANCE CHIP, COMPUTED ONCE for both status surfaces (P1-8 / C-9).
2851
+ // Whether the rail is already reporting the count is a question about the rail, so
2852
+ // it is asked of the module that owns the rail's geometry - both here and in the
2853
+ // detail row. `App` contributes the two numbers only it knows: which rail was drawn
2854
+ // and how wide it was.
2855
+ const acceptanceActive = acceptanceChip({
2856
+ snapshot: state.todos,
2857
+ railRows: native ? (nativeBudget?.band.railRows ?? 0) : (fullscreenRailVisible ? railRows : 0),
2858
+ width: native ? (nativeBudget?.band.railCols ?? 0) : railWidth,
2859
+ });
2605
2860
  const statusInput = {
2606
2861
  columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
2607
2862
  context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
@@ -2622,6 +2877,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2622
2877
  ? { running: rosterCounts(state.team.runs).running, total: state.team.runs.length }
2623
2878
  : undefined,
2624
2879
  todoActive: state.todos ? { done: state.todos.doneCount, total: state.todos.total } : undefined,
2880
+ // ONE LINE, NO ROW ALGEBRA HERE (P1-8 / C-9): whether the acceptance count still
2881
+ // needs the second status row was decided once, above, by `acceptanceChip`.
2882
+ ...(acceptanceActive ? { acceptanceActive } : {}),
2625
2883
  fastActive: state.fast?.live ? { inFlight: state.fast.inFlight } : undefined,
2626
2884
  ...feedbackProjection,
2627
2885
  };
@@ -2633,7 +2891,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2633
2891
  hintsEnabled: cfg.hints, modeToggleKey,
2634
2892
  interruptPhase: interruptGesture.current.phase,
2635
2893
  updateAvailable: !!updateSnapshot && shouldRenderUpdateLine(updateSnapshot) } });
2636
- const composer = (_jsx(Composer, { deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: frameBudget.composerCols, terminalRows: rows, statusExpanded: statusExpanded, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: false, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: frameBudget.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onDraftRows: onDraftRows, onDraftPresence: onDraftPresence, draftClearNonce: draftClearNonce, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
2894
+ const composer = (_jsx(Composer, { deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: frameBudget.composerCols, terminalRows: rows, statusExpanded: statusExpanded, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: false, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: frameBudget.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onDraftRows: onDraftRows, onDraftPresence: onDraftPresence, draftClearNonce: draftClearNonce, ...(draftFill ? { draftFill } : {}), insertMode: cfg.prompts?.insertMode ?? 'insert', onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
2637
2895
  const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2638
2896
  // --- Native panel band (release-gate-and-native-panels §6.3/§6.4) -------------
2639
2897
  //
@@ -2644,12 +2902,18 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2644
2902
  // panel that mounts on one value and lays out against another renders nothing and
2645
2903
  // reports nothing (TodoPanel's own I-10 / P1-3 note).
2646
2904
  const nativeBand = nativeBudget ? nativeBudget.band : null;
2905
+ // The band's FULL width, from the budget rather than from the band object: `band`
2906
+ // carries `previewCols` (which `rail.ts` narrows) but `contentCols` lives on the
2907
+ // budget, and the roster spans the whole thing (tui-subagent-panel D-2).
2908
+ const nativeBandCols = nativeBudget ? nativeBudget.contentCols : 0;
2647
2909
  const nativeRail = state.todos && nativeBand && nativeBand.railRows >= TODO_LIMITS.panelMinRows
2648
2910
  ? (_jsx(TodoPanel, { snapshot: state.todos, width: nativeBand.railCols, rows: nativeBand.railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2649
- // `cols` is the band's left column, not `contentCols`: `TeamPanel.activityBudget` is
2650
- // continuous in `cols`, so a roster laid out against a column it is not in is exactly
2651
- // the bug that pushes the elapsed column off an 80-column terminal.
2652
- const nativeTeam = state.team && nativeBand ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: nativeBand.previewCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2911
+ // `cols` is the WHOLE band, not its left column (tui-subagent-panel §4.1.2 / D-2): the
2912
+ // roster is a full-width slot spanning the TODO rail's columns as well, so a row laid
2913
+ // out against `previewCols` would stop its clock one rail short of the screen's edge.
2914
+ // `teamRowGeometry` is continuous in `cols`, so the number that decides whether the
2915
+ // panel mounts IS the number it lays out against.
2916
+ const nativeTeam = state.team && nativeBand ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: nativeBandCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2653
2917
  const rail = fullscreenRailVisible && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2654
2918
  const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: overlayNode, footer: _jsxs(_Fragment, { children: [team, _jsx(QueuePanel, { layout: queueLayout, theme: theme })] }), rail: rail, scrollbar: terminal?.scrollbar, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, onGeometryChange: onGeometryChange, geometryKey: `${rows}:${cols}:${viewportBudget}:${frameBudget.composerSlotRows}:${frameBudget.statusRows}:${contentCols}:${overlay}:${tooSmall}`, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: transcriptEntries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
2655
2919
  const detailsNode = _jsx(BottomStatusRow, { plan: details, columns: viewCols, theme: theme });
@@ -2674,7 +2938,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2674
2938
  ? (nativeBudget?.band.railRows ?? 0) > 0
2675
2939
  : fullscreenRailVisible)
2676
2940
  ? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
2677
- : {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
2941
+ : {}), ...(acceptanceActive ? { acceptanceActive } : {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
2678
2942
  // Manual compaction starts before its next snapshot is published.
2679
2943
  compactionActive: { inFlight: statusInput.compacting ?? false }, ...(controller.isCompactionEnabled()
2680
2944
  ? {
@@ -5,7 +5,7 @@ import type { Theme } from './theme.js';
5
5
  import type { TermCapabilities } from './capabilities.js';
6
6
  import { pickGlyphs } from './glyphs.js';
7
7
  import type { ComposerSubmitResult } from './composer-input.js';
8
- import { type CommandOption } from './PromptInput.js';
8
+ import { type CommandOption, type DraftFill } from './PromptInput.js';
9
9
  import type { RunActivity } from './run-status-row.js';
10
10
  import { type AgentMode } from '../agent/agent-mode.js';
11
11
  /** Legacy caller shape retained for source compatibility; Composer no longer mounts it. */
@@ -77,6 +77,10 @@ export interface ComposerProps {
77
77
  * PromptInput unchanged.
78
78
  */
79
79
  draftClearNonce?: number;
80
+ /** Downward half of the fill channel; forwarded to `PromptInput` unchanged. */
81
+ draftFill?: DraftFill;
82
+ /** `prompts.insertMode`, forwarded unchanged. */
83
+ insertMode?: 'insert' | 'replace';
80
84
  /** Paste refusals from `PromptInput`, routed to `App`'s notice dispatch. */
81
85
  onNotice?: (level: 'warn' | 'error', text: string) => void;
82
86
  theme: Theme;
@@ -101,5 +105,5 @@ declare function hintText(opts: {
101
105
  }): string;
102
106
  /** Compatibility export for existing hint contract tests. */
103
107
  export declare const hintTextForTest: typeof hintText;
104
- export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onDraftPresence, draftClearNonce, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
108
+ export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onDraftPresence, draftClearNonce, draftFill, insertMode, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
105
109
  export {};
@@ -36,7 +36,7 @@ function hintText(opts) {
36
36
  }
37
37
  /** Compatibility export for existing hint contract tests. */
38
38
  export const hintTextForTest = hintText;
39
- export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onDraftPresence, draftClearNonce, onNotice, theme, caps, onSubmit, onHelp, }) {
39
+ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onDraftPresence, draftClearNonce, draftFill, insertMode, onNotice, theme, caps, onSubmit, onHelp, }) {
40
40
  const [hasDraft, setHasDraft] = useState(false);
41
41
  const onDraftChange = useCallback((next) => {
42
42
  setHasDraft(next.hasDraft);
@@ -57,5 +57,5 @@ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols,
57
57
  : hasDraft
58
58
  ? theme.focusBorder ?? theme.primary
59
59
  : theme.idleBorder ?? theme.border;
60
- return (_jsx(Box, { ref: measureRef, width: cols, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, terminalRows: terminalRows, statusExpanded: statusExpanded, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, borderColor: borderColor, onDraftChange: onDraftChange, draftClearNonce: draftClearNonce, onNotice: onNotice }) }));
60
+ return (_jsx(Box, { ref: measureRef, width: cols, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, terminalRows: terminalRows, statusExpanded: statusExpanded, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, borderColor: borderColor, onDraftChange: onDraftChange, draftClearNonce: draftClearNonce, draftFill: draftFill, insertMode: insertMode, onNotice: onNotice }) }));
61
61
  }
@@ -23,6 +23,14 @@ interface HeaderProps {
23
23
  * forgets the prop silently teaching nothing.
24
24
  */
25
25
  kitHint?: boolean;
26
+ /**
27
+ * Whether the collapsed row teaches `ctrl+P` (quick-prompt-library D-15).
28
+ *
29
+ * DEFAULTS TO TRUE for the same reason `kitHint` does: the requirement is that the
30
+ * key be visible, and a default of `false` would leave the one caller that forgets
31
+ * the prop silently teaching nothing. `cfg.prompts.hint` is what turns it off.
32
+ */
33
+ promptsHint?: boolean;
26
34
  }
27
- export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns, statusExpanded, scrolledLines, overlayOpen, kitHint }: HeaderProps): React.ReactElement;
35
+ export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns, statusExpanded, scrolledLines, overlayOpen, kitHint, promptsHint }: HeaderProps): React.ReactElement;
28
36
  export {};
package/dist/ui/Header.js CHANGED
@@ -3,7 +3,7 @@ import { basename } from 'node:path';
3
3
  import { Box, Text } from 'ink';
4
4
  import { Logo } from './Logo.js';
5
5
  import { planHeaderHints, HEADER_HINT_LIMITS } from './layout/header-hints.js';
6
- export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false, kitHint = true }) {
6
+ export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false, kitHint = true, promptsHint = true }) {
7
7
  // The right-hand hints are planned, not assembled (tui-branch-terminal-shortcut
8
8
  // 3.9 / D-11). `plan.text` is exactly `plan.cells` wide and already carries the
9
9
  // scroll chip's own trailing spacing, so this component can no longer clip a
@@ -14,7 +14,7 @@ export function Header({ cwd, provider, model, hasKey, variant, theme, caps, col
14
14
  // passes `true` always, because the requirement is that `ctrl+G` reveals the
15
15
  // key - and the user with no kits needs that hint most.
16
16
  const plan = planHeaderHints({
17
- columns, statusExpanded, overlayOpen, scrolledLines, kitHint,
17
+ columns, statusExpanded, overlayOpen, scrolledLines, kitHint, promptsHint,
18
18
  });
19
19
  // THE TWO HALVES ARE SEPARATED BY CELLS, NOT BY LUCK (defect 12). The left half
20
20
  // is `wrap="truncate"`, which draws its ellipsis in the LAST cell it owns; with