@aragon-agent/cli 0.6.34 → 0.6.36

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 (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +173 -7
  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 +67 -3
  6. package/dist/agent/controller.js +106 -5
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/kit-apply.d.ts +56 -0
  9. package/dist/agent/kit-apply.js +209 -0
  10. package/dist/agent/model-profile-settings.d.ts +28 -0
  11. package/dist/agent/model-profile-settings.js +142 -29
  12. package/dist/agent/reducer.d.ts +13 -2
  13. package/dist/agent/reducer.js +1 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/agents-command.d.ts +15 -0
  16. package/dist/commands/agents-command.js +27 -0
  17. package/dist/commands/builtins.js +135 -4
  18. package/dist/commands/kit-command.d.ts +20 -0
  19. package/dist/commands/kit-command.js +131 -0
  20. package/dist/commands/perf.d.ts +11 -0
  21. package/dist/commands/perf.js +2 -1
  22. package/dist/commands/registry.d.ts +14 -0
  23. package/dist/config/app-paths.d.ts +15 -0
  24. package/dist/config/app-paths.js +33 -2
  25. package/dist/config/env.d.ts +4 -1
  26. package/dist/config/env.js +28 -2
  27. package/dist/config/kit-resolution.d.ts +93 -0
  28. package/dist/config/kit-resolution.js +226 -0
  29. package/dist/config/load.d.ts +10 -0
  30. package/dist/config/load.js +8 -1
  31. package/dist/config/model-kits.d.ts +224 -0
  32. package/dist/config/model-kits.js +302 -0
  33. package/dist/config/model-profile-store.d.ts +25 -0
  34. package/dist/config/model-profiles.d.ts +41 -3
  35. package/dist/config/model-profiles.js +46 -5
  36. package/dist/config/schema.d.ts +84 -0
  37. package/dist/config/schema.js +39 -0
  38. package/dist/config/store.js +12 -1
  39. package/dist/config/ui-state.d.ts +18 -0
  40. package/dist/config/ui-state.js +9 -0
  41. package/dist/exec/events.d.ts +25 -0
  42. package/dist/exec/events.js +4 -0
  43. package/dist/exec/index.js +16 -1
  44. package/dist/exec/runner.js +10 -0
  45. package/dist/input/keymap.js +18 -4
  46. package/dist/proc/limits.d.ts +16 -0
  47. package/dist/proc/limits.js +16 -0
  48. package/dist/proc/supervisor.d.ts +20 -1
  49. package/dist/proc/supervisor.js +42 -14
  50. package/dist/prompts/command.d.ts +34 -0
  51. package/dist/prompts/command.js +237 -0
  52. package/dist/prompts/copy.d.ts +89 -0
  53. package/dist/prompts/copy.js +131 -0
  54. package/dist/prompts/fill.d.ts +51 -0
  55. package/dist/prompts/fill.js +46 -0
  56. package/dist/prompts/limits.d.ts +53 -0
  57. package/dist/prompts/limits.js +53 -0
  58. package/dist/prompts/normalize.d.ts +53 -0
  59. package/dist/prompts/normalize.js +190 -0
  60. package/dist/prompts/resolve.d.ts +31 -0
  61. package/dist/prompts/resolve.js +60 -0
  62. package/dist/prompts/search.d.ts +58 -0
  63. package/dist/prompts/search.js +146 -0
  64. package/dist/prompts/store.d.ts +86 -0
  65. package/dist/prompts/store.js +453 -0
  66. package/dist/prompts/types.d.ts +204 -0
  67. package/dist/prompts/types.js +13 -0
  68. package/dist/session/checkpoint.d.ts +8 -1
  69. package/dist/session/checkpoint.js +6 -0
  70. package/dist/session/persist.d.ts +19 -1
  71. package/dist/session/persist.js +13 -1
  72. package/dist/session/startup-session.d.ts +7 -2
  73. package/dist/session/startup-session.js +5 -2
  74. package/dist/session/store.js +3 -0
  75. package/dist/session/validate-session.js +64 -1
  76. package/dist/team/limits.d.ts +25 -0
  77. package/dist/team/limits.js +26 -0
  78. package/dist/team/runtime.js +27 -2
  79. package/dist/team/settled.d.ts +57 -0
  80. package/dist/team/settled.js +87 -0
  81. package/dist/team/subagent.js +48 -2
  82. package/dist/team/timeline.d.ts +74 -0
  83. package/dist/team/timeline.js +101 -0
  84. package/dist/team/types.d.ts +24 -1
  85. package/dist/todo/acceptance.d.ts +120 -0
  86. package/dist/todo/acceptance.js +399 -0
  87. package/dist/todo/follow-through.d.ts +48 -23
  88. package/dist/todo/follow-through.js +110 -44
  89. package/dist/todo/limits.d.ts +62 -1
  90. package/dist/todo/limits.js +62 -1
  91. package/dist/todo/panel-rows.d.ts +29 -39
  92. package/dist/todo/panel-rows.js +37 -83
  93. package/dist/todo/plan-sections.d.ts +60 -0
  94. package/dist/todo/plan-sections.js +108 -0
  95. package/dist/todo/prompt.d.ts +17 -12
  96. package/dist/todo/prompt.js +59 -40
  97. package/dist/todo/row-window.d.ts +51 -0
  98. package/dist/todo/row-window.js +107 -0
  99. package/dist/todo/store.d.ts +139 -31
  100. package/dist/todo/store.js +271 -73
  101. package/dist/todo/todo-tool.d.ts +33 -8
  102. package/dist/todo/todo-tool.js +186 -30
  103. package/dist/todo/types.d.ts +58 -0
  104. package/dist/ui/App.js +414 -54
  105. package/dist/ui/Composer.d.ts +6 -2
  106. package/dist/ui/Composer.js +2 -2
  107. package/dist/ui/Header.d.ts +17 -1
  108. package/dist/ui/Header.js +8 -2
  109. package/dist/ui/PromptInput.d.ts +21 -1
  110. package/dist/ui/PromptInput.js +22 -2
  111. package/dist/ui/ScrollbackShell.d.ts +22 -7
  112. package/dist/ui/ScrollbackShell.js +8 -1
  113. package/dist/ui/StatusBar.d.ts +18 -0
  114. package/dist/ui/StatusBar.js +11 -3
  115. package/dist/ui/TeamPanel.d.ts +35 -41
  116. package/dist/ui/TeamPanel.js +53 -163
  117. package/dist/ui/TodoPanel.d.ts +17 -14
  118. package/dist/ui/TodoPanel.js +146 -38
  119. package/dist/ui/Transcript.js +23 -3
  120. package/dist/ui/composer-input.d.ts +10 -0
  121. package/dist/ui/composer-input.js +57 -24
  122. package/dist/ui/entries/TeamCard.d.ts +18 -2
  123. package/dist/ui/entries/TeamCard.js +47 -13
  124. package/dist/ui/entries/TodoCard.d.ts +26 -11
  125. package/dist/ui/entries/TodoCard.js +56 -15
  126. package/dist/ui/glyphs.d.ts +19 -0
  127. package/dist/ui/glyphs.js +4 -0
  128. package/dist/ui/layout/agents-board.d.ts +102 -0
  129. package/dist/ui/layout/agents-board.js +389 -0
  130. package/dist/ui/layout/cells.d.ts +61 -0
  131. package/dist/ui/layout/cells.js +156 -0
  132. package/dist/ui/layout/header-hints.d.ts +23 -3
  133. package/dist/ui/layout/header-hints.js +73 -7
  134. package/dist/ui/layout/kit-chip.d.ts +50 -0
  135. package/dist/ui/layout/kit-chip.js +81 -0
  136. package/dist/ui/layout/status-detail-layout.js +9 -0
  137. package/dist/ui/layout/status-layout.d.ts +13 -0
  138. package/dist/ui/layout/team-row.d.ts +139 -0
  139. package/dist/ui/layout/team-row.js +246 -0
  140. package/dist/ui/layout/todo-layout.d.ts +38 -0
  141. package/dist/ui/layout/todo-layout.js +75 -0
  142. package/dist/ui/layout/virtual-window.js +32 -3
  143. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  144. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  145. package/dist/ui/overlays/HelpOverlay.js +24 -2
  146. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  147. package/dist/ui/overlays/KitEditor.js +101 -0
  148. package/dist/ui/overlays/KitManager.d.ts +23 -0
  149. package/dist/ui/overlays/KitManager.js +104 -0
  150. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  151. package/dist/ui/overlays/KitPicker.js +58 -0
  152. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  153. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  154. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  155. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  156. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  157. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  158. package/dist/ui/overlays/PromptPicker.js +129 -0
  159. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  160. package/dist/ui/overlays/PromptTextArea.js +141 -0
  161. package/dist/ui/overlays/SettingsScreen.js +139 -16
  162. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  163. package/dist/ui/overlays/kit-picker-state.js +136 -0
  164. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  165. package/dist/ui/overlays/kit-settings-state.js +267 -0
  166. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  167. package/dist/ui/overlays/model-profile-state.js +59 -6
  168. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  169. package/dist/ui/overlays/prompt-library-state.js +335 -0
  170. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  171. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  172. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  173. package/dist/ui/scrollback/LiveTail.js +7 -2
  174. package/dist/ui/scrollback/band.js +14 -1
  175. package/dist/ui/scrollback/live-preview.js +10 -3
  176. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  177. package/dist/ui/scrollback/tail-window.js +17 -2
  178. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  179. package/dist/ui/team-status.d.ts +166 -0
  180. package/dist/ui/team-status.js +252 -0
  181. package/dist/ui/transcript-text.js +13 -4
  182. package/dist/ui/use-startup-notices.d.ts +35 -1
  183. package/dist/ui/use-startup-notices.js +29 -2
  184. package/dist/ui/use-wheel-routing.js +10 -2
  185. 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,9 +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';
51
+ import { isLiveTeamCard, lastTeamEntry, rosterCounts } from './team-status.js';
50
52
  import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
51
53
  import { useTerminalSize } from './layout/useTerminalSize.js';
52
54
  import { useHeightStore } from './use-height-store.js';
@@ -62,6 +64,16 @@ import { publishSessionView } from './session-bridge.js';
62
64
  import { OverlayFrame, overlayPageSize } from './layout/OverlayFrame.js';
63
65
  import { helpRows } from './overlays/HelpOverlay.js';
64
66
  import { ModelPicker } from './overlays/ModelPicker.js';
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';
75
+ import { INACTIVE_KIT } from '../config/kit-resolution.js';
76
+ import { kitApplyMessage, kitNoteMessages } from '../agent/kit-apply.js';
65
77
  import { createModelProbe } from '../agent/model-probe.js';
66
78
  import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, } from './overlays/SettingsScreen.js';
67
79
  import { ConfirmDialog } from './overlays/ConfirmDialog.js';
@@ -74,6 +86,7 @@ import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
74
86
  import { createClipboardCoordinator } from './clipboard-task.js';
75
87
  import { QueuePanel } from './QueuePanel.js';
76
88
  import { QueueOverlay } from './overlays/QueueOverlay.js';
89
+ import { AgentsBoardOverlay } from './overlays/AgentsBoardOverlay.js';
77
90
  import { buildQueueLayout } from './layout/queue-layout.js';
78
91
  import { interactionCopy } from './interaction-copy.js';
79
92
  import { createRunMetrics, updateRunMetrics, freezeRunMetrics, projectRunMetrics } from './run-metrics.js';
@@ -138,6 +151,25 @@ function seedViewState(seed) {
138
151
  ? viewReducer(base, { type: 'restoreEntries', entries: seed.entries })
139
152
  : base;
140
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
+ }
141
173
  export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, scrollMode = 'app', scrollback, initialEntries, session, sessionBridge, sessionNotice, }) {
142
174
  const native = scrollMode === 'native' && scrollback !== undefined;
143
175
  /** Set when the native writer swallowed repeated tall frames; cleared by a resize. */
@@ -153,6 +185,19 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
153
185
  // nonce render (the async clear lands a frame later - see the timing
154
186
  // invariant beside the ladder itself).
155
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);
156
201
  const [draftClearedByCtrlC, setDraftClearedByCtrlC] = useState(false);
157
202
  const [popupRows, setPopupRows] = useState(0);
158
203
  const [statusExpanded, setStatusExpanded] = useState(false);
@@ -616,8 +661,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
616
661
  if (controller.isRunning() || controller.isCompactionBusy?.())
617
662
  return;
618
663
  const live = controller.getTodoSnapshot();
619
- if (!live || live.doneCount >= live.total)
620
- 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;
621
671
  budgetRef.current = { ...budgetRef.current, used: budgetRef.current.used + 1 };
622
672
  autoContinuationRef.current = true;
623
673
  // COUNTS ONLY, NEVER ITEM TEXT (D-19), and only at the fire point —
@@ -627,6 +677,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
627
677
  noProgressStreak: budgetRef.current.noProgressStreak,
628
678
  total: live.total,
629
679
  done: live.doneCount,
680
+ accTotal: live.acceptance?.total ?? 0,
681
+ accChecked: live.acceptance?.checkedCount ?? 0,
630
682
  mode: 'auto',
631
683
  });
632
684
  submitRef.current(decision.message, { userInitiated: false, todoPolicy: 'continue' });
@@ -1041,32 +1093,45 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1041
1093
  // the events must never be shown advice about a mode not in effect (§6.1).
1042
1094
  // `cli.tsx::wantMouse` now folds the platform check into the same expression,
1043
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.
1044
1099
  useStartupNotices(dispatch, {
1045
- enabled: !!mouseSource,
1046
- // NOT re-derived here (see `MouseNoticeOptions`): `cli.tsx` computes
1047
- // `mouseFilter !== null && cfg.mouseSelect` once and four things read it,
1048
- // so a second derivation is a second chance for them to disagree.
1049
- selectEnabled: terminal?.mouseSelect ?? false,
1050
- // `state.json`, not `config.json`: whether a one-shot notice has been shown
1051
- // is bookkeeping, and the comment on the old persisted key said so.
1052
- //
1053
- // THE VERSION, NOT `true` (P1-8). The gate compares against
1054
- // `MOUSE_NOTICE_VERSION`, so storing anything else either replays the
1055
- // notice forever or silences a future revision of the text.
1056
- onSeen: () => setMouseNoticeVersion(MOUSE_NOTICE_VERSION),
1057
- },
1058
- // Present only when `loadConfig` had to raise the user's own
1059
- // `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
1060
- cfg.transcriptRetainRequested !== undefined
1061
- ? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
1062
- : undefined, vtInputWarning
1063
- ? {
1064
- nodeVersion: vtInputWarning.nodeVersion,
1065
- onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
1066
- }
1067
- : undefined, native
1068
- ? { onSeen: () => setScrollModeNoticeVersion(SCROLL_MODE_NOTICE_VERSION) }
1069
- : 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
+ });
1070
1135
  // --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
1071
1136
  useEffect(() => {
1072
1137
  const merged = mergePendingEntries(state.entries, state.pendingSteering);
@@ -1202,6 +1267,102 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1202
1267
  // --- Helpers -----------------------------------------------------------
1203
1268
  const notify = (level, text) => dispatch({ type: 'notice', level, text });
1204
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(); }, []);
1205
1366
  // THE BRANCH / `--save` NOTICE (D-13). Once, on mount, and only when the
1206
1367
  // caller supplied one: "the session you branched from is unchanged" is the
1207
1368
  // sentence that makes `--resume` safe to reach for, and it belongs in the
@@ -1520,6 +1681,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1520
1681
  }
1521
1682
  dispatch(action);
1522
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
+ },
1523
1694
  setOverlay: (overlay) => dispatch({ type: 'setOverlay', overlay }),
1524
1695
  notify,
1525
1696
  toast,
@@ -1681,6 +1852,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1681
1852
  notify('error', `Command failed: ${redactText(reason, getSecrets())}\nInput: ${safe}`);
1682
1853
  }
1683
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();
1684
1860
  if (switching) {
1685
1861
  switchingConversation.current = false;
1686
1862
  drainCompactionQueue.current();
@@ -1724,6 +1900,39 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1724
1900
  dispatch({ type: 'setOverlay', overlay: null });
1725
1901
  toast('info', `Model set to ${provider}:${model}.`);
1726
1902
  };
1903
+ /**
1904
+ * `ctrl+K` chose a row (agent-model-kits §4.7 / AC-25).
1905
+ *
1906
+ * `null` is `Current setup`, which is the RELEASE action, not a no-op: it runs
1907
+ * `clearSessionKit` so the live connections go back to the file's bindings.
1908
+ * Both paths close the overlay, and both report through the same toast funnel -
1909
+ * including every `KitNote`, because a kit that turns the fast tier on spends
1910
+ * money and must never do it silently (§4.2 / F-23).
1911
+ */
1912
+ const handleKitChoose = (kitId) => {
1913
+ // NO KIT PORT, NO CRASH: a legacy host can still press the key, and the only
1914
+ // honest answer is a sentence rather than a stack trace.
1915
+ const port = controller;
1916
+ if (typeof port.applySessionKit !== 'function' || typeof port.clearSessionKit !== 'function') {
1917
+ dispatch({ type: 'setOverlay', overlay: null });
1918
+ toast('error', 'This build cannot switch kits.');
1919
+ return;
1920
+ }
1921
+ const result = kitId === null
1922
+ ? controller.clearSessionKit() : controller.applySessionKit(kitId);
1923
+ dispatch({ type: 'setOverlay', overlay: null });
1924
+ // The config is already updated synchronously, so this reads the kit that is
1925
+ // in effect RIGHT NOW rather than the id the user asked for - if the apply
1926
+ // fell back to anything, the toast says so.
1927
+ const applied = controller.getActiveKit().name;
1928
+ const text = !result.ok ? kitApplyMessage(result)
1929
+ : kitId === null ? "Kit released; using the config file's settings."
1930
+ : `Kit ${applied} applied for this session.`;
1931
+ const notes = kitNoteMessages(result.notes);
1932
+ // A NOTE IS A WARNING, NOT A SUCCESS (F-23): the toast colour is the only
1933
+ // signal a user gets that applying a kit can start spending money.
1934
+ toast(result.ok ? (notes.length ? 'warn' : 'success') : 'error', [text, ...notes].join(' '));
1935
+ };
1727
1936
  const closeConfirm = () => {
1728
1937
  setConfirmState(null);
1729
1938
  dispatch({ type: 'setOverlay', overlay: null });
@@ -1967,6 +2176,71 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1967
2176
  setStatusExpanded(value => !value);
1968
2177
  return;
1969
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
+ }
2226
+ // Ctrl+K: the Agent-kit picker (agent-model-kits D-15 / §4.7).
2227
+ //
2228
+ // NOT AN INPUT FRAME, unlike Ctrl+I below. Ctrl+I collides with the Tab byte,
2229
+ // so a stdin translator has to disambiguate it; 0x0B is unambiguous in every
2230
+ // encoding this app supports (utf8 byte, win32-input-mode's pass-through of
2231
+ // control chars, and CSI-u's `ctrlByte` fold), so the raw byte IS the key.
2232
+ //
2233
+ // `killToEnd` HAS MOVED TO Alt+K FIRST (keymap.ts): `PromptInput`'s
2234
+ // recognizer runs before its `key.ctrl` early-return, so leaving the old
2235
+ // binding would delete the draft in one listener while this one opened the
2236
+ // overlay. Two listeners both see the event; `return` only leaves one.
2237
+ const isCtrlK = (key.ctrl && (input === 'k' || input === 'K')) || input === '\x0b';
2238
+ if (isCtrlK) {
2239
+ if (tooSmall || stateRef.current.overlay)
2240
+ return;
2241
+ dispatch({ type: 'setOverlay', overlay: 'kit' });
2242
+ return;
2243
+ }
1970
2244
  // Ctrl+I arrives as INDEX_KEY_FRAME, never as key.ctrl + 'i': legacy
1971
2245
  // terminals encode it as the Tab byte and the stdin filter's translators
1972
2246
  // are the only layer that can tell them apart (see input/limits.ts).
@@ -2004,7 +2278,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2004
2278
  const overlay = stateRef.current.overlay;
2005
2279
  // Modes A only. `model` / `confirm` manage their own keys; registering an
2006
2280
  // arrow handler for them would double every keypress (R-12).
2007
- const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan' || overlay === 'queue';
2281
+ const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan'
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';
2008
2290
  if (key.pageUp) {
2009
2291
  if (controlled)
2010
2292
  setOverlayScroll((n) => Math.max(0, n - overlayPage));
@@ -2029,11 +2311,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2029
2311
  // everything else falls through (AC-P2).
2030
2312
  //
2031
2313
  // TWO KEYS, ONE BRANCH (shift-tab-mode-toggle-still-dead-on-windows, C2).
2032
- // `Ctrl+P` is not a Windows-only alias bolted on beside the real binding: it
2033
- // is the rung that survives when `Shift+Tab` cannot be delivered at all, and
2034
- // it shares this branch precisely so the two can never drift into different
2035
- // behaviour. See `MODE_TOGGLE_KEYS` for why this key and not another.
2036
- 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'))) {
2037
2321
  if (stateRef.current.overlay)
2038
2322
  return;
2039
2323
  applyMode(nextMode(controller.getAgentMode()));
@@ -2114,10 +2398,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2114
2398
  const entries = stateRef.current.entries;
2115
2399
  const card = [...entries]
2116
2400
  .reverse()
2117
- .find((e) => e.kind === 'tool' ||
2118
- e.kind === 'team' ||
2119
- e.kind === 'compaction' ||
2120
- (e.kind === 'service' && !e.terminal));
2401
+ .find((e) =>
2402
+ // THE LIVE DISPATCH CARD IS NOT EXPANDABLE (tui-subagent-double-roster
2403
+ // A-2'): it is suppressed on screen, so expanding it would print a frozen
2404
+ // copy of a card that is still being rewritten. Once it settles it becomes
2405
+ // an ordinary history card and this key reaches it again.
2406
+ !isLiveTeamCard(e) &&
2407
+ (e.kind === 'tool' ||
2408
+ e.kind === 'team' ||
2409
+ e.kind === 'compaction' ||
2410
+ (e.kind === 'service' && !e.terminal)));
2121
2411
  // NATIVE MODE ONLY. The fallback exists because native mode cannot scroll inside the
2122
2412
  // frame - the answer's head is reachable only if it is printed. Full-screen mode has a
2123
2413
  // viewport that already reaches it, and it has no `expanded` rendering for an answer,
@@ -2224,6 +2514,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2224
2514
  const overlay = state.overlay;
2225
2515
  const settingsDraft = useMemo(() => overlay === 'settings'
2226
2516
  ? controller.createModelSettingsDraft() : undefined, [controller, overlay]);
2517
+ // THE FILE'S BINDINGS, FOR THE KIT PICKER'S FIRST ROW (§4.6 / D-13). While a
2518
+ // session kit is held the live config mirrors the KIT (I-6), so `Current setup`
2519
+ // can only describe what choosing it restores by reading the file - one read per
2520
+ // opening, the same price `settingsDraft` one line up already pays.
2521
+ const kitDiskConfig = useMemo(() => overlay === 'kit'
2522
+ ? controller.createModelSettingsDraft().baseline : undefined, [controller, overlay]);
2227
2523
  // ONE PROBE PER APP (model-profile-connection-test 5 row 9): the port needs the
2228
2524
  // controller's provider registry, which the overlay has no business owning.
2229
2525
  // Built only while the settings overlay is open, so no other screen pays for it.
@@ -2237,6 +2533,22 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2237
2533
  return createModelProbe({ registry: controller.getProviderRegistry() });
2238
2534
  }, [controller, overlay]);
2239
2535
  const hasKey = controller.hasApiKey();
2536
+ // THE ACTIVE KIT, DERIVED PER FRAME (D-5 / F-22): `controller.getActiveKit()` is
2537
+ // pure, so it cannot go stale the way a cached copy would after `ctrl+K`,
2538
+ // `/model` or a saved default. Absent when no kit is in effect, which is what
2539
+ // keeps the status row byte-identical for users who own none (I-9).
2540
+ //
2541
+ // A TEST DOUBLE OR AN EMBEDDED HOST MAY PREDATE THIS PORT: the same tolerance
2542
+ // the probe port gets one screen over ("absent means the rows read
2543
+ // unresolved") applies here - no kit, no crash.
2544
+ const activeKit = typeof controller.getActiveKit === 'function'
2545
+ ? controller.getActiveKit() : INACTIVE_KIT;
2546
+ const kitChip = activeKit.source === 'none' ? undefined : {
2547
+ name: activeKit.name,
2548
+ modified: activeKit.modified,
2549
+ otherNames: (cfg.modelProfiles?.kits ?? [])
2550
+ .filter((kit) => kit.id !== activeKit.id).map((kit) => kit.name),
2551
+ };
2240
2552
  const empty = state.entries.length === 0;
2241
2553
  // RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
2242
2554
  // handler will do on the very next keystroke.
@@ -2340,7 +2652,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2340
2652
  // ALWAYS CALLED, INERT WHEN `enabled` IS FALSE: hooks cannot be conditional, and
2341
2653
  // an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
2342
2654
  const viewCols = native ? Math.max(1, cols - 1) : cols;
2343
- const header = (_jsx(Header, { columns: viewCols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
2655
+ const header = (_jsx(Header, { columns: viewCols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps,
2656
+ // ALWAYS TRUE (agent-model-kits §4.7 / RV-P1-2): the requirement is that
2657
+ // `ctrl+G` reveals `ctrl+K`, and the user with an empty library needs that
2658
+ // hint most - the picker itself carries the "create one in /settings" row.
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 }));
2344
2663
  // The header and the opener are SLOTS OF THE SHELL, not printed bytes (D3/D4): at
2345
2664
  // `H = rows - 1` a printed row is pushed off the screen by the very next frame, so
2346
2665
  // `printBanner()` was a way of making the banner invisible. Neither is passed here.
@@ -2391,7 +2710,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2391
2710
  const openerCols = nativeBudget ? nativeBudget.previewCols : contentCols;
2392
2711
  const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(openerRows, openerCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: openerRows, theme: theme, caps: caps })) : null;
2393
2712
  const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
2394
- 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 === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
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,
2714
+ // ONLY THE APPLY IS BLOCKED while work is in flight (§4.7): a list a
2715
+ // user cannot even OPEN reads as a broken key.
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: {
2395
2717
  provider: cfg.provider,
2396
2718
  model: cfg.model,
2397
2719
  baseUrl: cfg.baseUrl ?? '',
@@ -2428,7 +2750,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2428
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 }),
2429
2751
  // Read from `cfg` (render scope, re-read every frame), so `/retry off`
2430
2752
  // while the screen is open shows the new value on the next paint.
2431
- 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;
2432
2758
  const density = cfg.density;
2433
2759
  const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
2434
2760
  // The fixed status bar remains visible underneath every overlay and toast.
@@ -2463,6 +2789,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2463
2789
  windowStart: scrollbackView.windowStart,
2464
2790
  windowEntries: scrollbackView.tailEntries.length,
2465
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',
2466
2802
  }
2467
2803
  : null,
2468
2804
  // A getter: the bridge's `frameReady` and geometry change outside React renders.
@@ -2511,6 +2847,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2511
2847
  toast: latestToast ? { text: latestToast.text,
2512
2848
  level: latestToast.level === 'success' ? 'info' : latestToast.level } : undefined,
2513
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
+ });
2514
2860
  const statusInput = {
2515
2861
  columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
2516
2862
  context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
@@ -2524,9 +2870,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2524
2870
  compacting: compactionLive || !!state.compaction?.inFlight,
2525
2871
  mode: state.agentMode, pendingMode: state.pendingAgentMode,
2526
2872
  servicesActive: { live: liveServices }, ecoRung: governor.rung,
2527
- teamActive: state.team ? { running: state.team.runs.filter(item => !['queued', 'done', 'failed', 'aborted'].includes(item.phase)).length,
2528
- total: state.team.runs.length } : undefined,
2873
+ // THE STATUS-DETAIL ROW COUNTS LIKE THE ROSTER AND THE CHIP (A-1): this was the
2874
+ // last inline phase filter, left behind when the StatusBar chip converged, and it
2875
+ // feeds the `Team n/m` on the second status row.
2876
+ teamActive: state.team
2877
+ ? { running: rosterCounts(state.team.runs).running, total: state.team.runs.length }
2878
+ : undefined,
2529
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 } : {}),
2530
2883
  fastActive: state.fast?.live ? { inFlight: state.fast.inFlight } : undefined,
2531
2884
  ...feedbackProjection,
2532
2885
  };
@@ -2538,7 +2891,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2538
2891
  hintsEnabled: cfg.hints, modeToggleKey,
2539
2892
  interruptPhase: interruptGesture.current.phase,
2540
2893
  updateAvailable: !!updateSnapshot && shouldRenderUpdateLine(updateSnapshot) } });
2541
- 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' }) }));
2542
2895
  const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2543
2896
  // --- Native panel band (release-gate-and-native-panels §6.3/§6.4) -------------
2544
2897
  //
@@ -2549,16 +2902,22 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2549
2902
  // panel that mounts on one value and lays out against another renders nothing and
2550
2903
  // reports nothing (TodoPanel's own I-10 / P1-3 note).
2551
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;
2552
2909
  const nativeRail = state.todos && nativeBand && nativeBand.railRows >= TODO_LIMITS.panelMinRows
2553
2910
  ? (_jsx(TodoPanel, { snapshot: state.todos, width: nativeBand.railCols, rows: nativeBand.railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2554
- // `cols` is the band's left column, not `contentCols`: `TeamPanel.activityBudget` is
2555
- // continuous in `cols`, so a roster laid out against a column it is not in is exactly
2556
- // the bug that pushes the elapsed column off an 80-column terminal.
2557
- 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;
2558
2917
  const rail = fullscreenRailVisible && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2559
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 })] }));
2560
2919
  const detailsNode = _jsx(BottomStatusRow, { plan: details, columns: viewCols, theme: theme });
2561
- const statusNode = (_jsx(StatusBar, { columns: viewCols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, ...(wait ? { wait } : {}), runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
2920
+ const statusNode = (_jsx(StatusBar, { columns: viewCols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, ...(wait ? { wait } : {}), runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), ...(kitChip ? { kit: kitChip } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
2562
2921
  ? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
2563
2922
  // ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
2564
2923
  // projection of "many sources of truth" into the props layer, and a
@@ -2568,9 +2927,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2568
2927
  // `contextWindow` override reach the bar.
2569
2928
  context: state.context, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
2570
2929
  ? {
2930
+ // `rosterCounts`, NOT a fourth inline phase filter (A-1): the chip and
2931
+ // the roster header above it must never disagree.
2571
2932
  teamActive: {
2572
- running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
2573
- r.phase !== 'failed' && r.phase !== 'aborted').length,
2933
+ running: rosterCounts(state.team.runs).running,
2574
2934
  total: state.team.runs.length,
2575
2935
  },
2576
2936
  }
@@ -2578,7 +2938,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2578
2938
  ? (nativeBudget?.band.railRows ?? 0) > 0
2579
2939
  : fullscreenRailVisible)
2580
2940
  ? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
2581
- : {}), ...(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 } } : {}),
2582
2942
  // Manual compaction starts before its next snapshot is published.
2583
2943
  compactionActive: { inFlight: statusInput.compacting ?? false }, ...(controller.isCompactionEnabled()
2584
2944
  ? {