@pellux/goodvibes-tui 1.16.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/CHANGELOG.md +138 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5490 -1806
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/management-commands.ts +82 -15
  12. package/src/cli/management.ts +0 -2
  13. package/src/cli/parser.ts +5 -0
  14. package/src/cli/status.ts +13 -0
  15. package/src/cli/surface-command.ts +5 -5
  16. package/src/cli/tui-startup.ts +60 -97
  17. package/src/cli/types.ts +2 -0
  18. package/src/config/index.ts +1 -1
  19. package/src/config/tui-extension-settings.ts +33 -0
  20. package/src/core/composer-state.ts +1 -1
  21. package/src/core/consolidation-receipt.ts +41 -0
  22. package/src/core/conversation-line-cache.ts +3 -3
  23. package/src/core/conversation-rendering.ts +18 -9
  24. package/src/core/conversation.ts +2 -2
  25. package/src/core/long-task-notifier.ts +7 -7
  26. package/src/core/memory-provenance.ts +67 -0
  27. package/src/core/pairing-devices.ts +49 -0
  28. package/src/core/pairing-handoff.ts +101 -0
  29. package/src/core/pairing-offers.ts +83 -0
  30. package/src/core/pairing-origin.ts +78 -0
  31. package/src/core/pairing-tailscale-gateway.ts +56 -0
  32. package/src/core/power-chip-source.ts +91 -0
  33. package/src/core/power-status.ts +95 -0
  34. package/src/core/stable-host.ts +171 -0
  35. package/src/core/stream-event-wiring.ts +29 -6
  36. package/src/core/system-message-noise.ts +1 -1
  37. package/src/core/system-message-router.ts +6 -6
  38. package/src/core/turn-budget-outcome.ts +60 -0
  39. package/src/core/turn-cancellation.ts +40 -1
  40. package/src/core/turn-event-wiring.ts +14 -8
  41. package/src/daemon/cli.ts +64 -58
  42. package/src/daemon/lifecycle.ts +41 -0
  43. package/src/daemon/service-commands.ts +5 -5
  44. package/src/export/cost-utils.ts +128 -6
  45. package/src/input/autocomplete.ts +2 -2
  46. package/src/input/command-registry.ts +55 -9
  47. package/src/input/commands/acp-runtime.ts +72 -0
  48. package/src/input/commands/ci-runtime.ts +5 -1
  49. package/src/input/commands/codebase-runtime.ts +6 -6
  50. package/src/input/commands/cost-runtime.ts +1 -1
  51. package/src/input/commands/devices-runtime.ts +109 -0
  52. package/src/input/commands/git-runtime.ts +28 -4
  53. package/src/input/commands/graph-runtime.ts +45 -0
  54. package/src/input/commands/health-runtime.ts +2 -2
  55. package/src/input/commands/intelligence-runtime.ts +1 -1
  56. package/src/input/commands/local-auth-runtime.ts +1 -1
  57. package/src/input/commands/local-provider-runtime.ts +49 -10
  58. package/src/input/commands/local-runtime.ts +53 -5
  59. package/src/input/commands/memory.ts +2 -2
  60. package/src/input/commands/operator-panel-runtime.ts +5 -5
  61. package/src/input/commands/operator-rpc.ts +32 -11
  62. package/src/input/commands/permissions-provenance.ts +1 -1
  63. package/src/input/commands/permissions-runtime.ts +99 -8
  64. package/src/input/commands/planning-runtime.ts +3 -3
  65. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  66. package/src/input/commands/power-runtime.ts +56 -0
  67. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  68. package/src/input/commands/provider.ts +7 -5
  69. package/src/input/commands/qrcode-runtime.ts +14 -19
  70. package/src/input/commands/queue-runtime.ts +70 -0
  71. package/src/input/commands/recall-files-git.ts +13 -51
  72. package/src/input/commands/recall-files.ts +8 -11
  73. package/src/input/commands/recall-review.ts +2 -2
  74. package/src/input/commands/relay-runtime.ts +1 -1
  75. package/src/input/commands/remote-runtime.ts +1 -1
  76. package/src/input/commands/runtime-services.ts +4 -4
  77. package/src/input/commands/schedule-runtime.ts +14 -7
  78. package/src/input/commands/services-runtime.ts +1 -1
  79. package/src/input/commands/session-workflow.ts +2 -2
  80. package/src/input/commands/settings-sync-runtime.ts +2 -2
  81. package/src/input/commands/share-runtime.ts +1 -1
  82. package/src/input/commands/shell-core.ts +46 -68
  83. package/src/input/commands/subscription-runtime.ts +1 -1
  84. package/src/input/commands/update-runtime.ts +153 -96
  85. package/src/input/commands/work-plan-runtime.ts +1 -1
  86. package/src/input/commands/workstream-attempts.ts +6 -1
  87. package/src/input/commands/workstream-runtime.ts +46 -27
  88. package/src/input/commands/worktree-runtime.ts +35 -8
  89. package/src/input/commands.ts +10 -2
  90. package/src/input/config-modal-types.ts +14 -3
  91. package/src/input/config-modal.ts +75 -13
  92. package/src/input/feature-unit-layout.ts +93 -82
  93. package/src/input/feed-context-factory.ts +2 -2
  94. package/src/input/handler-command-route.ts +2 -2
  95. package/src/input/handler-content-actions.ts +1 -1
  96. package/src/input/handler-feed-routes.ts +7 -7
  97. package/src/input/handler-feed.ts +4 -4
  98. package/src/input/handler-interactions.ts +3 -2
  99. package/src/input/handler-modal-routes.ts +12 -4
  100. package/src/input/handler-modal-token-routes.ts +1 -1
  101. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  102. package/src/input/handler-onboarding.ts +3 -2
  103. package/src/input/handler-picker-routes.ts +26 -13
  104. package/src/input/handler-shortcuts.ts +29 -6
  105. package/src/input/handler.ts +2 -2
  106. package/src/input/keybindings.ts +21 -3
  107. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  108. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  109. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  110. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  111. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  112. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  113. package/src/input/panel-integration-actions.ts +2 -2
  114. package/src/input/panel-mouse-geometry.ts +1 -1
  115. package/src/input/panel-paste-flood-guard.ts +1 -1
  116. package/src/input/provider-key-intake.ts +167 -0
  117. package/src/input/sandbox-exec-config.ts +2 -2
  118. package/src/input/selection-modal.ts +1 -1
  119. package/src/input/settings-modal-activation.ts +5 -0
  120. package/src/input/settings-modal-adjustment.ts +4 -31
  121. package/src/input/settings-modal-data.ts +52 -57
  122. package/src/input/settings-modal-mutations.ts +12 -68
  123. package/src/input/settings-modal-types.ts +27 -17
  124. package/src/input/settings-modal.ts +74 -55
  125. package/src/main.ts +90 -90
  126. package/src/panels/agent-inspector-shared.ts +4 -4
  127. package/src/panels/base-panel.ts +2 -2
  128. package/src/panels/builtin/agent.ts +4 -4
  129. package/src/panels/builtin/development.ts +4 -4
  130. package/src/panels/builtin/knowledge.ts +1 -1
  131. package/src/panels/builtin/operations.ts +83 -11
  132. package/src/panels/builtin/session.ts +5 -5
  133. package/src/panels/builtin/shared.ts +16 -4
  134. package/src/panels/builtin-modals.ts +59 -42
  135. package/src/panels/builtin-panels.ts +1 -1
  136. package/src/panels/cost-tracker-panel.ts +124 -7
  137. package/src/panels/diff-panel.ts +39 -6
  138. package/src/panels/eval-registry.ts +2 -2
  139. package/src/panels/fleet-acts.ts +446 -0
  140. package/src/panels/fleet-deep-link.ts +1 -1
  141. package/src/panels/fleet-gateway.ts +117 -0
  142. package/src/panels/fleet-observed-render.ts +128 -0
  143. package/src/panels/fleet-panel-format.ts +163 -11
  144. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  145. package/src/panels/fleet-panel.ts +73 -57
  146. package/src/panels/fleet-read-model.ts +84 -14
  147. package/src/panels/fleet-spawn.ts +266 -0
  148. package/src/panels/fleet-steer.ts +2 -2
  149. package/src/panels/fleet-stop.ts +41 -5
  150. package/src/panels/fleet-tabs.ts +15 -16
  151. package/src/panels/fleet-transcript.ts +5 -5
  152. package/src/panels/git-panel.ts +2 -2
  153. package/src/panels/index.ts +4 -1
  154. package/src/panels/local-auth-panel.ts +1 -1
  155. package/src/panels/memory-consolidation-gateway.ts +94 -0
  156. package/src/panels/modals/devices-modal.ts +161 -0
  157. package/src/panels/modals/hooks-modal.ts +1 -1
  158. package/src/panels/modals/keybindings-modal.ts +2 -2
  159. package/src/panels/modals/knowledge-modal.ts +1 -1
  160. package/src/panels/modals/marketplace-modal.ts +4 -4
  161. package/src/panels/modals/memory-modal.ts +153 -9
  162. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  163. package/src/panels/modals/modal-theme.ts +1 -1
  164. package/src/panels/modals/pairing-modal.ts +198 -68
  165. package/src/panels/modals/planning-modal.ts +2 -2
  166. package/src/panels/modals/plugins-modal.ts +1 -1
  167. package/src/panels/modals/policy-modal.ts +1 -1
  168. package/src/panels/modals/security-modal.ts +7 -1
  169. package/src/panels/modals/skills-modal.ts +1 -1
  170. package/src/panels/modals/work-plan-modal.ts +3 -3
  171. package/src/panels/notifications-feed.ts +127 -0
  172. package/src/panels/notifications-panel.ts +160 -0
  173. package/src/panels/panel-manager.ts +9 -9
  174. package/src/panels/plugins-panel.ts +4 -4
  175. package/src/panels/project-planning-answer-actions.ts +1 -1
  176. package/src/panels/provider-health-routes.ts +2 -2
  177. package/src/panels/skills-panel.ts +4 -5
  178. package/src/panels/token-budget-panel.ts +11 -3
  179. package/src/panels/types.ts +5 -5
  180. package/src/panels/workstream-graph-render.ts +123 -0
  181. package/src/permissions/broker-approval-card.ts +201 -0
  182. package/src/permissions/hunk-selection.ts +28 -4
  183. package/src/permissions/prompt.ts +244 -29
  184. package/src/renderer/autocomplete-overlay.ts +107 -54
  185. package/src/renderer/code-block.ts +2 -2
  186. package/src/renderer/compaction-history-modal.ts +1 -1
  187. package/src/renderer/compaction-preview.ts +1 -1
  188. package/src/renderer/compaction-quality.ts +1 -1
  189. package/src/renderer/composer-fragments.ts +69 -0
  190. package/src/renderer/compositor.ts +2 -2
  191. package/src/renderer/config-modal.ts +1 -1
  192. package/src/renderer/conversation-overlays.ts +1 -1
  193. package/src/renderer/footer-tips.ts +3 -3
  194. package/src/renderer/hint-grammar.ts +1 -1
  195. package/src/renderer/modal-factory.ts +1 -1
  196. package/src/renderer/model-picker-overlay.ts +1 -1
  197. package/src/renderer/model-workspace.ts +7 -2
  198. package/src/renderer/panel-composite.ts +2 -2
  199. package/src/renderer/panel-workspace-bar.ts +1 -1
  200. package/src/renderer/process-indicator.ts +1 -1
  201. package/src/renderer/selection-modal-overlay.ts +1 -1
  202. package/src/renderer/session-picker-modal.ts +2 -2
  203. package/src/renderer/settings-modal-helpers.ts +23 -5
  204. package/src/renderer/settings-modal.ts +129 -39
  205. package/src/renderer/shell-surface.ts +6 -0
  206. package/src/renderer/terminal-bg-probe.ts +1 -1
  207. package/src/renderer/theme.ts +3 -3
  208. package/src/renderer/tool-call.ts +13 -7
  209. package/src/renderer/tool-result-summary.ts +1 -1
  210. package/src/renderer/turn-injection.ts +7 -6
  211. package/src/renderer/ui-factory.ts +38 -1
  212. package/src/runtime/bootstrap-command-context.ts +7 -2
  213. package/src/runtime/bootstrap-command-parts.ts +9 -5
  214. package/src/runtime/bootstrap-core.ts +41 -41
  215. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  216. package/src/runtime/bootstrap-shell.ts +13 -5
  217. package/src/runtime/bootstrap.ts +34 -32
  218. package/src/runtime/channel-composition.ts +46 -0
  219. package/src/runtime/code-index-services.ts +3 -4
  220. package/src/runtime/daemon-attach-notices.ts +120 -0
  221. package/src/runtime/durability-services.ts +90 -0
  222. package/src/runtime/embedded-service-factories.ts +40 -0
  223. package/src/runtime/feature-settings.ts +147 -0
  224. package/src/runtime/fleet-services.ts +41 -0
  225. package/src/runtime/idle-power-services.ts +85 -0
  226. package/src/runtime/index.ts +6 -21
  227. package/src/runtime/interaction-seams.ts +109 -0
  228. package/src/runtime/legacy-daemon-migration.ts +29 -19
  229. package/src/runtime/memory-fold.ts +1 -1
  230. package/src/runtime/notification-dispatch.ts +110 -0
  231. package/src/runtime/onboarding/apply.ts +3 -29
  232. package/src/runtime/onboarding/fast-path.ts +39 -6
  233. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  234. package/src/runtime/onboarding/snapshot.ts +0 -1
  235. package/src/runtime/onboarding/types.ts +9 -3
  236. package/src/runtime/power-keepawake-remote.ts +75 -0
  237. package/src/runtime/process-lifecycle.ts +3 -1
  238. package/src/runtime/provider-fallback.ts +55 -0
  239. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  240. package/src/runtime/release-artifacts.ts +19 -103
  241. package/src/runtime/resume-notice.ts +1 -1
  242. package/src/runtime/services.ts +113 -113
  243. package/src/runtime/session-continuity-hints.ts +38 -0
  244. package/src/runtime/surface-feature-flags.ts +40 -9
  245. package/src/runtime/terminal-output-guard.ts +2 -2
  246. package/src/runtime/trust/workspace-registration.ts +10 -2
  247. package/src/runtime/trust/workspace-trust.ts +58 -31
  248. package/src/runtime/ui-services.ts +6 -1
  249. package/src/runtime/update-check.ts +15 -69
  250. package/src/runtime/workstream-services.ts +2 -2
  251. package/src/shell/blocking-input.ts +108 -132
  252. package/src/shell/recovery-input-helpers.ts +53 -166
  253. package/src/shell/ui-openers.ts +38 -6
  254. package/src/utils/format-duration.ts +1 -1
  255. package/src/verification/verification-ledger.ts +21 -4
  256. package/src/version.ts +1 -1
  257. package/src/input/commands/flags-runtime.ts +0 -328
@@ -1,9 +1,9 @@
1
1
  import type { ModalSectionStyle } from '../renderer/modal-factory.ts';
2
2
 
3
3
  /**
4
- * Config-modal host — the shared seam every W6.1 MIGRATE-TO-MODAL surface
4
+ * Config-modal host — the shared seam every MIGRATE-TO-MODAL surface
5
5
  * (provider-health, services, subscription, remote, local-auth, settings-sync,
6
- * sandbox, and the WO-B ecosystem/governance set) is expressed through.
6
+ * sandbox, and the ecosystem/governance set) is expressed through.
7
7
  *
8
8
  * A surface is PURE DATA + ACTIONS: it hands the host a `buildView()` that maps
9
9
  * live read-models to a tabbed, list-structured view, plus a declarative action
@@ -106,6 +106,17 @@ export interface ConfigModalActionContext {
106
106
  readonly setStatus: (message: string) => void;
107
107
  /** Close the modal (focus returns to whatever opened it). */
108
108
  readonly close: () => void;
109
+ /**
110
+ * Switch the active tab and select a row within it, by id — the in-surface
111
+ * "jump" affordance (e.g. the Memory modal's Proposals tab jumping to an
112
+ * affected record in the Review Queue tab, rather than opening a second
113
+ * modal). Host-owned, populated by ConfigModal itself (like `close`/
114
+ * `setStatus`) — a surface's `onAction` calls it, never constructs it. A
115
+ * no-op if either id is not present in the surface's current structure; the
116
+ * surface should check reachability itself and print an honest status line
117
+ * when it isn't, rather than relying on a silent no-op.
118
+ */
119
+ readonly jumpToRow?: (tabId: string, rowId: string) => void;
109
120
  }
110
121
 
111
122
  /** A declarative action bound to a key. */
@@ -114,7 +125,7 @@ export interface ConfigModalAction {
114
125
  * Trigger key: a single printable char ('r', 'd') or a named key ('enter').
115
126
  * Must not collide with the host-reserved nav keys (up/down/left/right/tab/
116
127
  * j/k/escape/'/') — those are consumed by the host before actions are
117
- * consulted. '/' arms the host's type-to-filter (DEBT-5 item 1); while
128
+ * consulted. '/' arms the host's type-to-filter (item 1); while
118
129
  * filtering, EVERY printable key (not just j/k) is captured by the query
119
130
  * instead of firing an action — see handleConfigModalToken.
120
131
  */
@@ -66,7 +66,7 @@ const DEFAULT_VISIBLE_ROWS = 10;
66
66
  const DEFAULT_LABEL_WRAP_WIDTH = 70;
67
67
 
68
68
  /**
69
- * Stable id for the synthesized "no matches" row DEBT-5 item 1 injects when a
69
+ * Stable id for the synthesized "no matches" row item 1 injects when a
70
70
  * filter query excludes every selectable row in a tab. Non-selectable (info
71
71
  * rows are never filtered — see ConfigModal's filter doc) so this id can
72
72
  * never collide with a real surface row id.
@@ -97,7 +97,7 @@ export class ConfigModal {
97
97
  private pendingConfirmKey: string | null = null;
98
98
 
99
99
  /**
100
- * DEBT-5 item 1 — the host's own type-to-filter, armed with '/' (matching
100
+ * item 1 — the host's own type-to-filter, armed with '/' (matching
101
101
  * the pre-existing "'/' to filter" convention: scrollable-list-panel.ts's
102
102
  * opt-in filter, and SettingsModal's own '/'-armed search). `filterActive`
103
103
  * gates whether printable keys go to the query instead of nav/actions;
@@ -111,11 +111,11 @@ export class ConfigModal {
111
111
  * - '/'-ARMED here (config-modal host surfaces): these surfaces bind PLAIN
112
112
  * single keys to ACTION HOTKEYS (e.g. 'r' refresh, 'd' delete). A key
113
113
  * can't be both an action and a filter character, so filtering must be
114
- * explicitly armed with '/' first. This is DEBT-5's design.
114
+ * explicitly armed with '/' first. This is a deliberate design choice.
115
115
  * - INSTANT filtering (pure pickers — help overlay, command palette,
116
116
  * selection-modal): these have no single-key actions, so every printable
117
117
  * key is unambiguously a filter character and narrows the list on the
118
- * first keystroke. This is UX-C's design (selection-modal-overlay.ts).
118
+ * first keystroke. This is a deliberate design choice (selection-modal-overlay.ts).
119
119
  * - Both keep single-Esc-close semantics. The only extra step is here: a
120
120
  * two-stage Esc (first Esc clears a NON-EMPTY query, second Esc closes)
121
121
  * — see the Esc branch in handleConfigModalToken. With an empty query,
@@ -221,7 +221,7 @@ export class ConfigModal {
221
221
  this._clampScroll();
222
222
  }
223
223
 
224
- // ── Filter (DEBT-5 item 1 — each mutation is an interaction boundary) ──────
224
+ // ── Filter (item 1 — each mutation is an interaction boundary) ──────
225
225
 
226
226
  isFilterActive(): boolean {
227
227
  return this.filterActive;
@@ -306,7 +306,7 @@ export class ConfigModal {
306
306
  private _switchTab(dir: 1 | -1): void {
307
307
  this._clearConfirm();
308
308
  // A filter query is scoped to the tab it was typed against — switching
309
- // tabs resets it (DEBT-5 item 1), same as statusMessage below.
309
+ // tabs resets it (item 1), same as statusMessage below.
310
310
  this.filterActive = false;
311
311
  this.filterQuery = '';
312
312
  this.syncStructure();
@@ -339,7 +339,7 @@ export class ConfigModal {
339
339
  * action (consumed). Handles the two-press confirm for destructive actions.
340
340
  * This is an interaction boundary (syncs structure first).
341
341
  */
342
- fireAction(key: string, ctx: Omit<ConfigModalActionContext, 'row' | 'tabId' | 'setStatus' | 'close' | 'requestRender'>): boolean {
342
+ fireAction(key: string, ctx: Omit<ConfigModalActionContext, 'row' | 'tabId' | 'setStatus' | 'close' | 'requestRender' | 'jumpToRow'>): boolean {
343
343
  this.syncStructure();
344
344
  const action = this.resolveAction(key);
345
345
  if (!action) return false;
@@ -359,11 +359,36 @@ export class ConfigModal {
359
359
  requestRender: this.requestRender,
360
360
  setStatus: (m: string) => { this.statusMessage = m; },
361
361
  close: () => this.close(),
362
+ jumpToRow: (tabId: string, rowId: string) => this.jumpToRow(tabId, rowId),
362
363
  };
363
364
  this.surface?.onAction?.(action.id, fullCtx);
364
365
  return true;
365
366
  }
366
367
 
368
+ /**
369
+ * In-surface "jump": switch the active tab (by id) and select a specific
370
+ * row (by id) within it — the mechanism a surface's own action handler
371
+ * uses to move the user from one tab to a specific row in another (e.g.
372
+ * the Memory modal's Proposals tab jumping to an affected record in the
373
+ * Review Queue tab). An interaction boundary (re-syncs structure first,
374
+ * same as `_switchTab`). A no-op if either id is absent from the
375
+ * freshly-synced structure.
376
+ */
377
+ jumpToRow(tabId: string, rowId: string): void {
378
+ this._clearConfirm();
379
+ this.filterActive = false;
380
+ this.filterQuery = '';
381
+ this.syncStructure();
382
+ const tab = this.frozenView?.tabs.find((t) => t.id === tabId);
383
+ if (!tab) return;
384
+ if (!tab.rows.some((r) => r.id === rowId && r.selectable !== false)) return;
385
+ this.activeTabId = tabId;
386
+ this.selectedRowId = rowId;
387
+ this.scrollOffset = 0;
388
+ this.statusMessage = '';
389
+ this._clampScroll();
390
+ }
391
+
367
392
  /** Clear a pending confirm when the user navigates or presses an unrelated key. */
368
393
  clearConfirmOnMiss(): void {
369
394
  this._clearConfirm();
@@ -395,8 +420,7 @@ export class ConfigModal {
395
420
  *
396
421
  * `labelWrapWidth` is the wrap column ModalFactory's list section will use
397
422
  * for row labels (renderConfigModal computes and passes the real one for
398
- * the current terminal size). It exists so the wrap-clamp below (DEBT-5
399
- * item 2) can pre-empt a live label growing past ModalFactory's wrap width
423
+ * the current terminal size). It exists so the wrap-clamp below (* item 2) can pre-empt a live label growing past ModalFactory's wrap width
400
424
  * mid-tick — a structural change (an extra visible line) without an
401
425
  * interaction — by measuring wrapped line counts here, before the label
402
426
  * ever reaches the renderer.
@@ -443,9 +467,9 @@ export class ConfigModal {
443
467
  });
444
468
 
445
469
  const visible = this.visibleRows;
446
- const windowed = allRows.slice(this.scrollOffset, this.scrollOffset + visible);
470
+ const windowed = this._windowRowsByLineBudget(allRows, this.scrollOffset, visible, labelWrapWidth);
447
471
 
448
- // DEBT-5 item 1: while filtering, the surface's own action/tab hints are
472
+ // item 1: while filtering, the surface's own action/tab hints are
449
473
  // unreliable (their printable-letter keys are captured by the filter
450
474
  // instead of firing — see handleConfigModalToken), so the footer swaps
451
475
  // to just the filter status (query + a truthful match count) and the
@@ -483,7 +507,7 @@ export class ConfigModal {
483
507
  // ── internals ────────────────────────────────────────────────────────────────
484
508
 
485
509
  /**
486
- * DEBT-5 item 2 (wrap-clamp overlay): keep a live label within the FROZEN
510
+ * item 2 (wrap-clamp overlay): keep a live label within the FROZEN
487
511
  * row's line footprint until the next interaction boundary re-freezes it.
488
512
  * `frozenLabel` is what the row wrapped to when the structure was captured
489
513
  * (open/nav/filter keystroke); `liveLabel` is this tick's value. If the
@@ -506,6 +530,44 @@ export class ConfigModal {
506
530
  return clamped.join('\n');
507
531
  }
508
532
 
533
+ /**
534
+ * Modal sizing rule (owner, zero tolerance: a modal/list must never clip
535
+ * its full descriptive text — size to content or scroll, never clip).
536
+ * `scrollOffset`/`visible` are ROW-count based, which is exactly right when
537
+ * every row wraps to one line — but ModalFactory renders each row's WRAPPED
538
+ * lines and then bounds the whole section to a fixed content-row budget
539
+ * computed from `visible` (see renderConfigModal's targetContentRows). A
540
+ * row whose label wraps to MULTIPLE lines can therefore push the total past
541
+ * that budget, and the LAST row handed to the renderer gets cut off
542
+ * mid-line instead of being deferred to the next scroll page — the
543
+ * clipping hazard this closes.
544
+ *
545
+ * Windows `rows` starting at `scrollOffset`, greedily including whole rows
546
+ * while their CUMULATIVE wrapped-line count (at `wrapWidth`) stays within
547
+ * the `visible` line budget. Always includes at least the first candidate
548
+ * row so the view is never empty — a single row that alone exceeds the
549
+ * budget is shown in full rather than replaced with nothing; only a
550
+ * SUBSEQUENT row that would overflow is deferred to scrolling instead of
551
+ * being rendered partially.
552
+ */
553
+ private _windowRowsByLineBudget(
554
+ rows: readonly ConfigModalRenderRow[],
555
+ scrollOffset: number,
556
+ visible: number,
557
+ wrapWidth: number,
558
+ ): ConfigModalRenderRow[] {
559
+ const candidates = rows.slice(scrollOffset);
560
+ const out: ConfigModalRenderRow[] = [];
561
+ let usedLines = 0;
562
+ for (const row of candidates) {
563
+ const lineCount = Math.max(1, wrapText(row.label, wrapWidth).length);
564
+ if (out.length > 0 && usedLines + lineCount > visible) break;
565
+ out.push(row);
566
+ usedLines += lineCount;
567
+ }
568
+ return out;
569
+ }
570
+
509
571
  /** Footer text for the active filter: the query + a truthful match count. */
510
572
  private _filterStatusHint(matched: number, total: number): string {
511
573
  const q = this.filterQuery;
@@ -526,7 +588,7 @@ export class ConfigModal {
526
588
  * empty-state copy, warning banners) always pass through unfiltered —
527
589
  * they're context, not data. When the query excludes every selectable row
528
590
  * but the tab genuinely had some, append the honest "no rows match" line
529
- * (DEBT-5 item 1's empty-result case) instead of silently showing nothing
591
+ * (item 1's empty-result case) instead of silently showing nothing
530
592
  * or misleadingly falling back to the surface's own emptyText (which
531
593
  * describes "no data at all", not "no data matches your filter").
532
594
  */
@@ -1,83 +1,84 @@
1
1
  /**
2
- * feature-unit-layout — turns the flat "44 flags in one bucket + their config
3
- * keys scattered across namespace categories" layout into feature UNITS: each
4
- * feature flag renders as a toggle header immediately followed by the config
5
- * keys that tune it, hosted in the topical category its config lives under
6
- * (sandbox with Runtime & Data, surfaces with Surfaces & Cloud, and so on).
2
+ * feature-unit-layout — every platform capability renders as a feature UNIT in
3
+ * its settings DOMAIN: the feature's real enablement row (its domain settings
4
+ * key, with the feature's name and full description attached) immediately
5
+ * followed by the settings keys that tune it.
7
6
  *
8
- * Grouping metadata is sourced entirely from the SDK's FEATURE_FLAG_CONFIG map
9
- * (the single source of truth, completeness test-guarded SDK-side) so the three
10
- * surfaces that present features — the settings modal, the onboarding wizard,
11
- * and /flags — agree on which config keys belong to which flag.
7
+ * Grouping metadata is sourced entirely from the SDK's FEATURE_SETTINGS
8
+ * surface (the single source of truth, completeness test-guarded SDK-side):
9
+ * `domain` is the top-level settings namespace the feature lives in, the
10
+ * enablement binding carries the real option shape (boolean switch, enum mode
11
+ * choices, or a constant capability), and `settings` lists every key that
12
+ * configures the feature.
12
13
  *
13
- * A flag whose FEATURE_FLAG_CONFIG entry has no config keys is hosted in the
14
- * Advanced Features bucket as a simple toggle (genuinely internal / startup-gated
15
- * plumbing flags with nothing to tune).
14
+ * A feature-unit header is the REAL config row for the feature's enablement
15
+ * key — boolean headers toggle, enum headers cycle their mode choices through
16
+ * the ordinary settings interactions. Two features can share one enablement
17
+ * key (e.g. behavior.compactionStrategy drives both compaction features);
18
+ * each still gets its own header row so every capability keeps its own name,
19
+ * description, and state, all reading the same underlying key.
16
20
  */
17
21
 
18
- import { FEATURE_FLAG_CONFIG, FEATURE_FLAGS } from '@pellux/goodvibes-sdk/platform/runtime/state';
19
- import type { ConfigKey, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
20
- import type { FeatureFlag } from '@/runtime/index.ts';
22
+ import { FEATURE_SETTINGS } from '@pellux/goodvibes-sdk/platform/runtime/state';
23
+ import type { FeatureSetting } from '@pellux/goodvibes-sdk/platform/runtime/state';
24
+ import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
25
+ import { getConfigSchemaSetting } from '../runtime/feature-settings.ts';
21
26
  import type { FlagEntry, SettingEntry, SettingsCategory } from './settings-modal-types.ts';
22
27
 
23
- /**
24
- * The category that hosts flags with no tunable config keys. Repurposed from the
25
- * old flat "Feature Flags" tab — it now holds only the internal/plumbing flags
26
- * that have nothing to configure, as simple toggles.
27
- */
28
- export const ADVANCED_FEATURES_CATEGORY: SettingsCategory = 'flags';
28
+ /** Every enablement key — these rows render as feature headers, never as plain sub-rows. */
29
+ const ENABLEMENT_KEYS: ReadonlySet<string> = new Set(
30
+ FEATURE_SETTINGS.map((feature) => feature.enablement.key),
31
+ );
29
32
 
30
33
  /**
31
- * Host category for a flag's feature unit: the single top-level namespace its
32
- * config keys live under (verified single-namespace SDK-side), or the Advanced
33
- * Features bucket when the flag has no config keys.
34
+ * Host category for a feature unit: its settings domain (the top-level
35
+ * namespace of its enablement key), which is always a real settings category.
34
36
  */
35
- export function getFeatureUnitHostCategory(flagId: string): SettingsCategory {
36
- const assoc = FEATURE_FLAG_CONFIG[flagId];
37
- const namespace = assoc?.configCategories[0];
38
- return (namespace ?? ADVANCED_FEATURES_CATEGORY) as SettingsCategory;
37
+ export function getFeatureUnitHostCategory(featureId: string): SettingsCategory | null {
38
+ const feature = FEATURE_SETTINGS.find((candidate) => candidate.id === featureId);
39
+ return feature ? (feature.domain as SettingsCategory) : null;
39
40
  }
40
41
 
41
42
  /**
42
- * Map every config key claimed by any flag to the flag that OWNS it — the first
43
- * flag (in FEATURE_FLAGS declaration order) whose FEATURE_FLAG_CONFIG entry lists
44
- * it. A key claimed by two flags (e.g. behavior.compactionStrategy, shared by
45
- * session-compaction and compaction-distiller-strategy) is listed once, under its
46
- * owner, so no config key double-lists across feature units.
43
+ * Map every non-enablement settings key claimed by any feature to the feature
44
+ * that OWNS it — the first feature (in FEATURE_SETTINGS declaration order)
45
+ * whose settings list contains it. A key claimed by two features is listed
46
+ * once, under its owner, so no settings key double-lists across feature units.
47
47
  */
48
48
  export function buildConfigKeyOwnership(): Map<string, string> {
49
49
  const owner = new Map<string, string>();
50
- for (const flag of FEATURE_FLAGS) {
51
- const assoc = FEATURE_FLAG_CONFIG[flag.id];
52
- if (!assoc) continue;
53
- for (const key of assoc.configKeys) {
54
- if (!owner.has(key)) owner.set(key, flag.id);
50
+ for (const feature of FEATURE_SETTINGS) {
51
+ for (const key of feature.settings) {
52
+ if (ENABLEMENT_KEYS.has(key)) continue;
53
+ if (!owner.has(key)) owner.set(key, feature.id);
55
54
  }
56
55
  }
57
56
  return owner;
58
57
  }
59
58
 
60
59
  /**
61
- * Build the synthetic SettingEntry that is a feature unit's toggle header. Its
62
- * key is `featureFlags.<id>` (the same override path persistFlagState writes), so
63
- * it round-trips through the config layer; type boolean so the existing
64
- * activate/adjust toggle paths drive it. currentValue mirrors the live runtime
65
- * state; isDefault is true when the flag sits at its declared default state (so
66
- * the "modified" marker shows only for an actual override). The attached
67
- * `flag` marks the row as a feature-unit header for rendering and interaction.
60
+ * Build a feature unit's header row: the REAL config row for the feature's
61
+ * enablement key (so booleans toggle and enums cycle their mode choices
62
+ * through the ordinary settings paths), cloned with the feature attached.
63
+ * When the category has no row for the key (it should always have one — every
64
+ * binding key is CONFIG_SCHEMA-registered), the schema descriptor is used
65
+ * directly so the header still renders honestly.
68
66
  */
69
- export function buildFlagHeaderEntry(entry: FlagEntry): SettingEntry {
70
- const { flag, state } = entry;
71
- const setting: ConfigSetting = {
72
- key: `featureFlags.${flag.id}` as ConfigKey,
73
- type: 'boolean',
74
- default: flag.defaultState === 'enabled',
75
- description: flag.description,
76
- };
67
+ export function buildFeatureHeaderEntry(entry: FlagEntry, base: SettingEntry | null): SettingEntry {
68
+ const feature = entry.feature;
69
+ const setting: ConfigSetting = base?.setting
70
+ ?? getConfigSchemaSetting(feature.enablement.key)
71
+ ?? {
72
+ key: feature.enablement.key,
73
+ type: 'boolean',
74
+ default: feature.defaultEnabled,
75
+ description: feature.description,
76
+ };
77
77
  return {
78
+ ...(base ?? {}),
78
79
  setting,
79
- currentValue: state === 'enabled',
80
- isDefault: state === flag.defaultState,
80
+ currentValue: base ? base.currentValue : setting.default,
81
+ isDefault: base ? base.isDefault : true,
81
82
  flag: entry,
82
83
  };
83
84
  }
@@ -88,13 +89,14 @@ function cloneOwnedEntry(entry: SettingEntry, ownerFlagId: string): SettingEntry
88
89
  }
89
90
 
90
91
  /**
91
- * Rewrite `groups` in place so each host category leads with its feature units
92
- * (flag toggle header + its owned config sub-options, in FEATURE_FLAGS order),
93
- * followed by the category's remaining un-owned config rows. The Advanced
94
- * Features category is filled with the no-config flags as bare toggles.
92
+ * Rewrite `groups` in place so each settings domain leads with its feature
93
+ * units (feature header + its owned settings sub-rows, in FEATURE_SETTINGS
94
+ * declaration order), followed by the category's remaining un-owned rows.
95
+ * The plain rows for enablement keys are consumed into the headers; cross
96
+ * listed copies in the combined network view keep their plain form.
95
97
  *
96
- * No-op when there are no flag entries (the flagless test path), leaving the
97
- * plain namespace-bucketed layout intact.
98
+ * No-op when there are no feature entries (the featureless test path),
99
+ * leaving the plain namespace-bucketed layout intact.
98
100
  */
99
101
  export function applyFeatureUnitLayout(
100
102
  groups: Map<SettingsCategory, SettingEntry[]>,
@@ -102,39 +104,48 @@ export function applyFeatureUnitLayout(
102
104
  ): void {
103
105
  if (flagEntries.length === 0) return;
104
106
  const owner = buildConfigKeyOwnership();
105
- const flagState = new Map<string, FlagEntry>(flagEntries.map((entry) => [entry.flag.id, entry]));
107
+ const entryById = new Map<string, FlagEntry>(flagEntries.map((entry) => [entry.feature.id, entry]));
106
108
 
107
- // Group the flags this runtime actually registered by their host category,
108
- // preserving FEATURE_FLAGS declaration order within each category.
109
- const flagsByCategory = new Map<SettingsCategory, FeatureFlag[]>();
110
- for (const flag of FEATURE_FLAGS) {
111
- if (!flagState.has(flag.id)) continue; // not registered in this runtime
112
- const category = getFeatureUnitHostCategory(flag.id);
113
- if (!flagsByCategory.has(category)) flagsByCategory.set(category, []);
114
- flagsByCategory.get(category)!.push(flag);
109
+ // Group the features this runtime actually registered by their domain,
110
+ // preserving FEATURE_SETTINGS declaration order within each category.
111
+ const featuresByCategory = new Map<SettingsCategory, FeatureSetting[]>();
112
+ for (const feature of FEATURE_SETTINGS) {
113
+ if (!entryById.has(feature.id)) continue; // not registered in this runtime
114
+ const category = feature.domain as SettingsCategory;
115
+ if (!featuresByCategory.has(category)) featuresByCategory.set(category, []);
116
+ featuresByCategory.get(category)!.push(feature);
115
117
  }
116
118
 
117
- for (const [category, flags] of flagsByCategory) {
119
+ for (const [category, features] of featuresByCategory) {
118
120
  const existing = groups.get(category) ?? [];
119
- // Split existing config rows into those owned by a feature unit hosted here
120
- // and the remaining orphan rows. An owned row's owner is guaranteed to be a
121
- // flag hosted in THIS category (owned keys share the flag's namespace).
122
- const ownedByFlag = new Map<string, SettingEntry[]>();
121
+ const rowByKey = new Map<string, SettingEntry>();
122
+ for (const entry of existing) {
123
+ if (!rowByKey.has(entry.setting.key)) rowByKey.set(entry.setting.key, entry);
124
+ }
125
+
126
+ // Split existing config rows: enablement rows are consumed into headers,
127
+ // rows owned by a feature hosted here become indented sub-rows, and the
128
+ // rest stay as orphan rows after the feature units.
129
+ const hostedEnablementKeys = new Set(features.map((feature) => feature.enablement.key));
130
+ const ownedByFeature = new Map<string, SettingEntry[]>();
123
131
  const orphans: SettingEntry[] = [];
124
132
  for (const entry of existing) {
125
- const ownerId = owner.get(entry.setting.key);
126
- if (ownerId !== undefined && flagState.has(ownerId)) {
127
- if (!ownedByFlag.has(ownerId)) ownedByFlag.set(ownerId, []);
128
- ownedByFlag.get(ownerId)!.push(cloneOwnedEntry(entry, ownerId));
133
+ const key = entry.setting.key;
134
+ if (hostedEnablementKeys.has(key)) continue; // consumed into header(s)
135
+ const ownerId = owner.get(key);
136
+ if (ownerId !== undefined && entryById.has(ownerId)) {
137
+ if (!ownedByFeature.has(ownerId)) ownedByFeature.set(ownerId, []);
138
+ ownedByFeature.get(ownerId)!.push(cloneOwnedEntry(entry, ownerId));
129
139
  } else {
130
140
  orphans.push(entry);
131
141
  }
132
142
  }
133
143
 
134
144
  const rebuilt: SettingEntry[] = [];
135
- for (const flag of flags) {
136
- rebuilt.push(buildFlagHeaderEntry(flagState.get(flag.id)!));
137
- for (const sub of ownedByFlag.get(flag.id) ?? []) rebuilt.push(sub);
145
+ for (const feature of features) {
146
+ const base = rowByKey.get(feature.enablement.key) ?? null;
147
+ rebuilt.push(buildFeatureHeaderEntry(entryById.get(feature.id)!, base));
148
+ for (const sub of ownedByFeature.get(feature.id) ?? []) rebuilt.push(sub);
138
149
  }
139
150
  rebuilt.push(...orphans);
140
151
  groups.set(category, rebuilt);
@@ -90,7 +90,7 @@ export interface FeedContextMutableInit {
90
90
  * - `commandRegistry`, `commandContext`, `autocomplete`, `inputHistory`,
91
91
  * `conversationManager` — wired after construction; synced at feed() entry only
92
92
  * (not per-action) since no in-feed action changes them
93
- * - `focusTracker` (W2.3) — shared instance from uiServices.platform, mutated
93
+ * - `focusTracker` — shared instance from uiServices.platform, mutated
94
94
  * directly by feedInputTokens() on 'focus' tokens; never reallocated
95
95
  *
96
96
  * **Rationale:** per-feed mutation on a single object avoids per-keystroke GC pressure
@@ -126,7 +126,7 @@ export interface FeedContextStableRefs {
126
126
  keybindingsManager: KeybindingsManager;
127
127
  killRing: KillRing;
128
128
  focusTracker: FocusTracker;
129
- /** DEBT-5 item 5 — paste-flood guard state, mutated in place (never reallocated). */
129
+ /** item 5 — paste-flood guard state, mutated in place (never reallocated). */
130
130
  panelBurstGuard: PanelBurstGuardState;
131
131
  getHistory: () => InfiniteBuffer;
132
132
  getViewportHeight: () => number;
@@ -145,13 +145,13 @@ function withPanelFocusSync(context: CommandContext, state: CommandModeRouteStat
145
145
  showPanel: context.showPanel
146
146
  ? (panelId, pane, target, opts) => {
147
147
  context.showPanel?.(panelId, pane, target, opts);
148
- // UX-C: showPanel no longer transfers focus by default — the command
148
+ // showPanel no longer transfers focus by default — the command
149
149
  // path leaves the composer focused (ui-openers.ts's showPanel only
150
150
  // calls focusPanels() when opts.focus is set). Only flip the local
151
151
  // panelFocused mirror when the caller explicitly asked for focus;
152
152
  // otherwise leave it exactly as it was (the composer, since you can
153
153
  // only type a command from there in the first place). `target` is the
154
- // DEBT-5 deep-link jump target, forwarded verbatim.
154
+ // deep-link jump target, forwarded verbatim.
155
155
  if (opts?.focus) state.panelFocused = true;
156
156
  }
157
157
  : undefined,
@@ -440,7 +440,7 @@ export function handleCtrlC(
440
440
  // double-press window from advancing. A throw from the async TTS-stop path
441
441
  // (e.g. killing the audio subprocess) would otherwise swallow this press and
442
442
  // strand the user unable to quit while TTS is speaking. Decouple the two: the
443
- // press below is recorded no matter how speech-stop fares. (UX-B item 6b.)
443
+ // press below is recorded no matter how speech-stop fares. (item 6b.)
444
444
  let stoppedSpeech = false;
445
445
  try {
446
446
  stoppedSpeech = cancelGeneration?.() === true;
@@ -45,7 +45,7 @@ export type PanelFocusRouteState = {
45
45
  * no text capture (Invariant A: focus must not silently flip to the composer).
46
46
  */
47
47
  onPasteDropped?: (panelName: string) => void;
48
- /** Wall-clock time (ms) for this token; `burstGuard` is DEBT-5 item 5's flood-guard state, mutated in place across tokens (see panel-paste-flood-guard.ts). */
48
+ /** Wall-clock time (ms) for this token; `burstGuard` is item 5's flood-guard state, mutated in place across tokens (see panel-paste-flood-guard.ts). */
49
49
  now: number;
50
50
  burstGuard: PanelBurstGuardState;
51
51
  /**
@@ -92,7 +92,7 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
92
92
  // (e.g. dismiss a confirm dialog or clear search). Only unfocus if the
93
93
  // panel returns false (unconsumed) or there is no active panel.
94
94
  if (token.logicalName === 'escape') {
95
- // I6.1/replay R5: while streaming, a focused panel must not swallow the
95
+ // While streaming, a focused panel must not swallow the
96
96
  // only way to cancel — Escape's first job is always cancel-turn; the
97
97
  // panel's own two-stage consume-or-unfocus contract only runs once no
98
98
  // turn is active, so a *second* Escape falls through to it normally
@@ -148,12 +148,12 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
148
148
 
149
149
  if (token.type === 'text' && token.value) {
150
150
  const activePanel = state.panelManager.getActive();
151
- // UX-C: '/' is an explicit transfer verb back to the composer (a capturing panel, isCapturingTextBurst, keeps '/' for itself).
151
+ // '/' is an explicit transfer verb back to the composer (a capturing panel, isCapturingTextBurst, keeps '/' for itself).
152
152
  if (token.value === '/' && !activePanel?.isCapturingTextBurst?.()) {
153
153
  panelFocused = false;
154
154
  return { handled: false, panelFocused };
155
155
  }
156
- // Invariant A/B (W6.2): a paste (isPasteToken, one multi-char token) into
156
+ // Invariant A/B: a paste (isPasteToken, one multi-char token) into
157
157
  // a capturing panel forwards verbatim below; elsewhere it is DROPPED with
158
158
  // a one-shot hint (never exploded into hotkeys, never a silent focus
159
159
  // flip — focus moves only on an explicit verb). Discrete keystrokes fall
@@ -164,7 +164,7 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
164
164
  return { handled: true, panelFocused };
165
165
  }
166
166
 
167
- // DEBT-5 item 5 — paste flood guard (rate-based; see panel-paste-flood-guard.ts).
167
+ // item 5 — paste flood guard (rate-based; see panel-paste-flood-guard.ts).
168
168
  if (!state.isPasteToken && !activePanel?.isCapturingTextBurst?.()) {
169
169
  const guard = trackPanelPasteFloodGuard(state.burstGuard, state.now);
170
170
  if (!guard.dispatch) {
@@ -211,7 +211,7 @@ export function handleIndicatorFocusToken(state: IndicatorFocusRouteState, token
211
211
  }
212
212
  if (token.logicalName === 'enter') {
213
213
  indicatorFocused = false;
214
- // W2.2: opens the Fleet panel (see openFleetPanel's own doc, line ~352).
214
+ // opens the Fleet panel (see openFleetPanel's own doc, line ~352).
215
215
  state.openFleetPanel();
216
216
  state.requestRender();
217
217
  return { handled: true, indicatorFocused };
@@ -626,7 +626,7 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
626
626
  return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
627
627
  }
628
628
 
629
- // UX-C: F2 is now handled globally with toggle semantics (handler-shortcuts.ts)
629
+ // F2 is now handled globally with toggle semantics (handler-shortcuts.ts)
630
630
  // BEFORE it ever reaches this route; this branch is dead in the real
631
631
  // pipeline but stays for panel-entry-points-reachable.test.ts's direct test.
632
632
  if (token.logicalName === 'f2') {
@@ -129,9 +129,9 @@ export interface InputFeedContext {
129
129
  inputHistory: InputHistory | null;
130
130
  conversationManager: ConversationManager | null;
131
131
  readonly killRing: KillRing;
132
- /** Terminal focus tracker (W2.3) — updated here from 'focus' tokens, read by the unfocused-alert notifiers in core/. */
132
+ /** Terminal focus tracker — updated here from 'focus' tokens, read by the unfocused-alert notifiers in core/. */
133
133
  readonly focusTracker: FocusTracker;
134
- /** DEBT-5 item 5 — the paste-flood guard's persistent state, mutated in place across tokens by handlePanelFocusToken (see panel-paste-flood-guard.ts). Never reallocated. */
134
+ /** item 5 — the paste-flood guard's persistent state, mutated in place across tokens by handlePanelFocusToken (see panel-paste-flood-guard.ts). Never reallocated. */
135
135
  readonly panelBurstGuard: PanelBurstGuardState;
136
136
  readonly getHistory: () => InfiniteBuffer;
137
137
  readonly getViewportHeight: () => number;
@@ -204,13 +204,13 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
204
204
  // several batched into one feed() by render-tick latency — arrive as
205
205
  // separate 1-char 'text' tokens. The old per-feed sum misread two quick nav
206
206
  // keystrokes (e.g. j then k in one drain) as a "burst" and yanked focus.
207
- // DEBT-5 item 5's flood guard reuses that same per-token model but adds
207
+ // item 5's flood guard reuses that same per-token model but adds
208
208
  // real timing: one `now` per feed() call (not per token) is intentional —
209
209
  // a genuine flood delivers many tokens in one drain, and they should all
210
210
  // measure as arriving "at once", not spread across meaningless sub-ms noise.
211
211
  const now = Date.now();
212
212
  for (const token of tokens) {
213
- // Focus-reporting tokens (CSI ?1004h, W2.3) never reach the composer or any
213
+ // Focus-reporting tokens (CSI ?1004h) never reach the composer or any
214
214
  // modal route — consumed here, first, unconditionally. No render needed.
215
215
  if (token.type === 'focus') {
216
216
  context.focusTracker.setFocused(token.action === 'in');
@@ -1,4 +1,5 @@
1
1
  import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
2
+ import { enrichProviderAccountsSnapshot } from '../runtime/onboarding/provider-key-capture.ts';
2
3
  import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
3
4
  import { collectOnboardingSnapshot } from '../runtime/onboarding/index.ts';
4
5
  import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy, handleBlockRerun, handleBlockSave, handleBlockToggle, handleBookmark, handleClipboardPaste, handleCopy, handleCtrlC, handleDiffApply, registerPaste } from './handler-content-actions.ts';
@@ -37,12 +38,12 @@ export async function hydrateOnboardingWizardFromRuntimeForHandler(handler: Inpu
37
38
  list: () => handler.uiServices.platform.surfaceRegistry.syncConfiguredSurfaces(),
38
39
  },
39
40
  providerAccounts: {
40
- loadSnapshot: () => buildProviderAccountSnapshot({
41
+ loadSnapshot: async () => enrichProviderAccountsSnapshot(await buildProviderAccountSnapshot({
41
42
  providerRegistry: handler.uiServices.providers.providerRegistry,
42
43
  serviceRegistry: handler.uiServices.platform.serviceRegistry,
43
44
  subscriptionManager: handler.uiServices.platform.subscriptionManager,
44
45
  secretsManager: handler.uiServices.platform.secretsManager,
45
- }),
46
+ }), handler.uiServices.providers.providerRegistry),
46
47
  },
47
48
  });
48
49
  if (!handler.onboardingWizard.active || hydrationSerial !== handler.onboardingHydrationSerial) return;