@pellux/goodvibes-tui 1.16.1 → 1.17.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 (219) hide show
  1. package/CHANGELOG.md +89 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1091 -271
  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/parser.ts +5 -0
  12. package/src/cli/status.ts +13 -0
  13. package/src/cli/surface-command.ts +5 -5
  14. package/src/cli/tui-startup.ts +60 -97
  15. package/src/cli/types.ts +2 -0
  16. package/src/config/index.ts +1 -1
  17. package/src/config/tui-extension-settings.ts +33 -0
  18. package/src/core/composer-state.ts +1 -1
  19. package/src/core/conversation-line-cache.ts +3 -3
  20. package/src/core/conversation-rendering.ts +4 -4
  21. package/src/core/conversation.ts +2 -2
  22. package/src/core/long-task-notifier.ts +7 -7
  23. package/src/core/stream-event-wiring.ts +8 -6
  24. package/src/core/system-message-noise.ts +1 -1
  25. package/src/core/system-message-router.ts +6 -6
  26. package/src/core/turn-cancellation.ts +1 -1
  27. package/src/core/turn-event-wiring.ts +8 -8
  28. package/src/daemon/cli.ts +20 -5
  29. package/src/daemon/lifecycle.ts +41 -0
  30. package/src/daemon/service-commands.ts +5 -5
  31. package/src/export/cost-utils.ts +128 -6
  32. package/src/input/autocomplete.ts +2 -2
  33. package/src/input/command-registry.ts +34 -9
  34. package/src/input/commands/ci-runtime.ts +5 -1
  35. package/src/input/commands/codebase-runtime.ts +6 -6
  36. package/src/input/commands/cost-runtime.ts +1 -1
  37. package/src/input/commands/health-runtime.ts +2 -2
  38. package/src/input/commands/intelligence-runtime.ts +1 -1
  39. package/src/input/commands/local-auth-runtime.ts +1 -1
  40. package/src/input/commands/local-provider-runtime.ts +49 -10
  41. package/src/input/commands/local-runtime.ts +53 -5
  42. package/src/input/commands/memory.ts +2 -2
  43. package/src/input/commands/operator-panel-runtime.ts +5 -5
  44. package/src/input/commands/permissions-provenance.ts +1 -1
  45. package/src/input/commands/permissions-runtime.ts +99 -8
  46. package/src/input/commands/planning-runtime.ts +3 -3
  47. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  48. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  49. package/src/input/commands/provider.ts +7 -5
  50. package/src/input/commands/recall-files-git.ts +13 -51
  51. package/src/input/commands/recall-files.ts +8 -11
  52. package/src/input/commands/recall-review.ts +2 -2
  53. package/src/input/commands/relay-runtime.ts +1 -1
  54. package/src/input/commands/remote-runtime.ts +1 -1
  55. package/src/input/commands/runtime-services.ts +4 -4
  56. package/src/input/commands/schedule-runtime.ts +14 -7
  57. package/src/input/commands/services-runtime.ts +1 -1
  58. package/src/input/commands/session-workflow.ts +2 -2
  59. package/src/input/commands/settings-sync-runtime.ts +2 -2
  60. package/src/input/commands/share-runtime.ts +1 -1
  61. package/src/input/commands/shell-core.ts +46 -68
  62. package/src/input/commands/subscription-runtime.ts +1 -1
  63. package/src/input/commands/update-runtime.ts +153 -96
  64. package/src/input/commands/work-plan-runtime.ts +1 -1
  65. package/src/input/commands/workstream-runtime.ts +2 -2
  66. package/src/input/commands.ts +0 -2
  67. package/src/input/config-modal-types.ts +3 -3
  68. package/src/input/config-modal.ts +10 -11
  69. package/src/input/feature-unit-layout.ts +93 -82
  70. package/src/input/feed-context-factory.ts +2 -2
  71. package/src/input/handler-command-route.ts +2 -2
  72. package/src/input/handler-content-actions.ts +1 -1
  73. package/src/input/handler-feed-routes.ts +7 -7
  74. package/src/input/handler-feed.ts +4 -4
  75. package/src/input/handler-interactions.ts +3 -2
  76. package/src/input/handler-modal-routes.ts +12 -4
  77. package/src/input/handler-modal-token-routes.ts +1 -1
  78. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  79. package/src/input/handler-onboarding.ts +3 -2
  80. package/src/input/handler-picker-routes.ts +26 -13
  81. package/src/input/handler-shortcuts.ts +6 -6
  82. package/src/input/handler.ts +2 -2
  83. package/src/input/keybindings.ts +2 -2
  84. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  85. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  86. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  87. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  88. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  89. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  90. package/src/input/panel-integration-actions.ts +2 -2
  91. package/src/input/panel-mouse-geometry.ts +1 -1
  92. package/src/input/panel-paste-flood-guard.ts +1 -1
  93. package/src/input/provider-key-intake.ts +167 -0
  94. package/src/input/sandbox-exec-config.ts +2 -2
  95. package/src/input/selection-modal.ts +1 -1
  96. package/src/input/settings-modal-activation.ts +5 -0
  97. package/src/input/settings-modal-adjustment.ts +4 -31
  98. package/src/input/settings-modal-data.ts +22 -21
  99. package/src/input/settings-modal-mutations.ts +12 -68
  100. package/src/input/settings-modal-types.ts +23 -16
  101. package/src/input/settings-modal.ts +74 -55
  102. package/src/main.ts +48 -48
  103. package/src/panels/agent-inspector-shared.ts +4 -4
  104. package/src/panels/base-panel.ts +2 -2
  105. package/src/panels/builtin/agent.ts +4 -4
  106. package/src/panels/builtin/development.ts +4 -4
  107. package/src/panels/builtin/knowledge.ts +1 -1
  108. package/src/panels/builtin/operations.ts +29 -10
  109. package/src/panels/builtin/session.ts +5 -5
  110. package/src/panels/builtin/shared.ts +4 -4
  111. package/src/panels/builtin-modals.ts +10 -10
  112. package/src/panels/builtin-panels.ts +1 -1
  113. package/src/panels/cost-tracker-panel.ts +124 -7
  114. package/src/panels/diff-panel.ts +5 -5
  115. package/src/panels/eval-registry.ts +2 -2
  116. package/src/panels/fleet-deep-link.ts +1 -1
  117. package/src/panels/fleet-panel-format.ts +44 -8
  118. package/src/panels/fleet-panel.ts +16 -16
  119. package/src/panels/fleet-read-model.ts +52 -13
  120. package/src/panels/fleet-steer.ts +2 -2
  121. package/src/panels/fleet-stop.ts +3 -3
  122. package/src/panels/fleet-tabs.ts +13 -14
  123. package/src/panels/fleet-transcript.ts +5 -5
  124. package/src/panels/git-panel.ts +2 -2
  125. package/src/panels/index.ts +4 -1
  126. package/src/panels/local-auth-panel.ts +1 -1
  127. package/src/panels/modals/hooks-modal.ts +1 -1
  128. package/src/panels/modals/keybindings-modal.ts +2 -2
  129. package/src/panels/modals/knowledge-modal.ts +1 -1
  130. package/src/panels/modals/marketplace-modal.ts +4 -4
  131. package/src/panels/modals/memory-modal.ts +4 -4
  132. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  133. package/src/panels/modals/modal-theme.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +2 -2
  135. package/src/panels/modals/planning-modal.ts +2 -2
  136. package/src/panels/modals/plugins-modal.ts +1 -1
  137. package/src/panels/modals/policy-modal.ts +1 -1
  138. package/src/panels/modals/security-modal.ts +1 -1
  139. package/src/panels/modals/skills-modal.ts +1 -1
  140. package/src/panels/modals/work-plan-modal.ts +3 -3
  141. package/src/panels/notifications-feed.ts +127 -0
  142. package/src/panels/notifications-panel.ts +160 -0
  143. package/src/panels/panel-manager.ts +9 -9
  144. package/src/panels/plugins-panel.ts +4 -4
  145. package/src/panels/project-planning-answer-actions.ts +1 -1
  146. package/src/panels/provider-health-routes.ts +2 -2
  147. package/src/panels/skills-panel.ts +4 -5
  148. package/src/panels/token-budget-panel.ts +11 -3
  149. package/src/panels/types.ts +5 -5
  150. package/src/permissions/broker-approval-card.ts +201 -0
  151. package/src/permissions/hunk-selection.ts +28 -4
  152. package/src/permissions/prompt.ts +244 -29
  153. package/src/renderer/autocomplete-overlay.ts +107 -54
  154. package/src/renderer/code-block.ts +2 -2
  155. package/src/renderer/compaction-history-modal.ts +1 -1
  156. package/src/renderer/compaction-preview.ts +1 -1
  157. package/src/renderer/compaction-quality.ts +1 -1
  158. package/src/renderer/compositor.ts +2 -2
  159. package/src/renderer/config-modal.ts +1 -1
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/footer-tips.ts +3 -3
  162. package/src/renderer/hint-grammar.ts +1 -1
  163. package/src/renderer/modal-factory.ts +1 -1
  164. package/src/renderer/model-picker-overlay.ts +1 -1
  165. package/src/renderer/model-workspace.ts +7 -2
  166. package/src/renderer/panel-composite.ts +2 -2
  167. package/src/renderer/panel-workspace-bar.ts +1 -1
  168. package/src/renderer/process-indicator.ts +1 -1
  169. package/src/renderer/selection-modal-overlay.ts +1 -1
  170. package/src/renderer/session-picker-modal.ts +2 -2
  171. package/src/renderer/settings-modal-helpers.ts +14 -5
  172. package/src/renderer/settings-modal.ts +127 -39
  173. package/src/renderer/shell-surface.ts +3 -0
  174. package/src/renderer/terminal-bg-probe.ts +1 -1
  175. package/src/renderer/theme.ts +3 -3
  176. package/src/renderer/tool-call.ts +1 -1
  177. package/src/renderer/tool-result-summary.ts +1 -1
  178. package/src/renderer/turn-injection.ts +7 -6
  179. package/src/renderer/ui-factory.ts +6 -1
  180. package/src/runtime/bootstrap-command-context.ts +4 -1
  181. package/src/runtime/bootstrap-command-parts.ts +6 -4
  182. package/src/runtime/bootstrap-core.ts +35 -35
  183. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  184. package/src/runtime/bootstrap-shell.ts +6 -5
  185. package/src/runtime/bootstrap.ts +34 -32
  186. package/src/runtime/channel-composition.ts +46 -0
  187. package/src/runtime/code-index-services.ts +3 -4
  188. package/src/runtime/daemon-attach-notices.ts +120 -0
  189. package/src/runtime/durability-services.ts +55 -0
  190. package/src/runtime/embedded-service-factories.ts +40 -0
  191. package/src/runtime/feature-settings.ts +147 -0
  192. package/src/runtime/index.ts +4 -21
  193. package/src/runtime/legacy-daemon-migration.ts +29 -19
  194. package/src/runtime/memory-fold.ts +1 -1
  195. package/src/runtime/notification-dispatch.ts +110 -0
  196. package/src/runtime/onboarding/apply.ts +3 -29
  197. package/src/runtime/onboarding/fast-path.ts +39 -6
  198. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  199. package/src/runtime/onboarding/snapshot.ts +0 -1
  200. package/src/runtime/onboarding/types.ts +9 -3
  201. package/src/runtime/provider-fallback.ts +55 -0
  202. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  203. package/src/runtime/release-artifacts.ts +19 -103
  204. package/src/runtime/resume-notice.ts +1 -1
  205. package/src/runtime/services.ts +81 -82
  206. package/src/runtime/session-continuity-hints.ts +38 -0
  207. package/src/runtime/surface-feature-flags.ts +40 -9
  208. package/src/runtime/terminal-output-guard.ts +2 -2
  209. package/src/runtime/trust/workspace-registration.ts +10 -2
  210. package/src/runtime/trust/workspace-trust.ts +58 -31
  211. package/src/runtime/ui-services.ts +3 -1
  212. package/src/runtime/update-check.ts +15 -69
  213. package/src/runtime/workstream-services.ts +2 -2
  214. package/src/shell/blocking-input.ts +102 -6
  215. package/src/shell/ui-openers.ts +38 -6
  216. package/src/utils/format-duration.ts +1 -1
  217. package/src/verification/verification-ledger.ts +21 -4
  218. package/src/version.ts +1 -1
  219. package/src/input/commands/flags-runtime.ts +0 -328
@@ -2,7 +2,7 @@
2
2
  // workstream-runtime.ts — /workstream
3
3
  //
4
4
  // UX over the OrchestrationEngine (@pellux/goodvibes-sdk/platform/orchestration,
5
- // wo701/W4.1) via its command-facing facade wired onto RuntimeServices as
5
+ //) via its command-facing facade wired onto RuntimeServices as
6
6
  // `workstreamCommands` (src/runtime/workstream-services.ts) and threaded
7
7
  // through CommandContext.session.workstreamEngine exactly like wrfcController
8
8
  // already is (bootstrap-command-context.ts).
@@ -21,7 +21,7 @@
21
21
  // pre-creation "draft" concept, so approval happens on a TUI-held draft
22
22
  // (workstream-services.ts's WorkstreamDraft) before anything is created in
23
23
  // the engine at all — see that module's header doc for the full reality-wins
24
- // divergence from the wo703 design brief.
24
+ // divergence from the design brief.
25
25
  // ---------------------------------------------------------------------------
26
26
 
27
27
  import { AdaptivePlanner } from '@pellux/goodvibes-sdk/platform/core';
@@ -36,7 +36,6 @@ import { registerRemoteRuntimeCommands } from './commands/remote-runtime.ts';
36
36
  import { registerTeleportRuntimeCommands } from './commands/teleport-runtime.ts';
37
37
  import { registerSubscriptionRuntimeCommands } from './commands/subscription-runtime.ts';
38
38
  import { registerHooksRuntimeCommands } from './commands/hooks-runtime.ts';
39
- import { registerFlagsRuntimeCommands } from './commands/flags-runtime.ts';
40
39
  import { registerEditorRuntimeCommands } from './commands/editor-runtime.ts';
41
40
  import { registerControlRoomRuntimeCommands } from './commands/control-room-runtime.ts';
42
41
  import { registerMcpRuntimeCommands } from './commands/mcp-runtime.ts';
@@ -121,7 +120,6 @@ export const BUILTIN_COMMAND_GROUPS: readonly BuiltinCommandGroup[] = [
121
120
  { category: 'Remote', register: registerTeleportRuntimeCommands },
122
121
  { category: 'Subscriptions', register: registerSubscriptionRuntimeCommands },
123
122
  { category: 'Hooks', register: registerHooksRuntimeCommands },
124
- { category: 'Feature Flags', register: registerFlagsRuntimeCommands },
125
123
  { category: 'Editor', register: registerEditorRuntimeCommands },
126
124
  { category: 'Control Room', register: registerControlRoomRuntimeCommands },
127
125
  { category: 'MCP', register: registerMcpRuntimeCommands },
@@ -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
@@ -114,7 +114,7 @@ export interface ConfigModalAction {
114
114
  * Trigger key: a single printable char ('r', 'd') or a named key ('enter').
115
115
  * Must not collide with the host-reserved nav keys (up/down/left/right/tab/
116
116
  * 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
117
+ * consulted. '/' arms the host's type-to-filter (item 1); while
118
118
  * filtering, EVERY printable key (not just j/k) is captured by the query
119
119
  * instead of firing an action — see handleConfigModalToken.
120
120
  */
@@ -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();
@@ -395,8 +395,7 @@ export class ConfigModal {
395
395
  *
396
396
  * `labelWrapWidth` is the wrap column ModalFactory's list section will use
397
397
  * 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
398
+ * 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
399
  * mid-tick — a structural change (an extra visible line) without an
401
400
  * interaction — by measuring wrapped line counts here, before the label
402
401
  * ever reaches the renderer.
@@ -445,7 +444,7 @@ export class ConfigModal {
445
444
  const visible = this.visibleRows;
446
445
  const windowed = allRows.slice(this.scrollOffset, this.scrollOffset + visible);
447
446
 
448
- // DEBT-5 item 1: while filtering, the surface's own action/tab hints are
447
+ // item 1: while filtering, the surface's own action/tab hints are
449
448
  // unreliable (their printable-letter keys are captured by the filter
450
449
  // instead of firing — see handleConfigModalToken), so the footer swaps
451
450
  // to just the filter status (query + a truthful match count) and the
@@ -483,7 +482,7 @@ export class ConfigModal {
483
482
  // ── internals ────────────────────────────────────────────────────────────────
484
483
 
485
484
  /**
486
- * DEBT-5 item 2 (wrap-clamp overlay): keep a live label within the FROZEN
485
+ * item 2 (wrap-clamp overlay): keep a live label within the FROZEN
487
486
  * row's line footprint until the next interaction boundary re-freezes it.
488
487
  * `frozenLabel` is what the row wrapped to when the structure was captured
489
488
  * (open/nav/filter keystroke); `liveLabel` is this tick's value. If the
@@ -526,7 +525,7 @@ export class ConfigModal {
526
525
  * empty-state copy, warning banners) always pass through unfiltered —
527
526
  * they're context, not data. When the query excludes every selectable row
528
527
  * 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
528
+ * (item 1's empty-result case) instead of silently showing nothing
530
529
  * or misleadingly falling back to the surface's own emptyText (which
531
530
  * describes "no data at all", not "no data matches your filter").
532
531
  */
@@ -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;
@@ -95,7 +95,7 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
95
95
  dispatchSelectionAction(action, selected);
96
96
  }
97
97
  } else if (state.selectionModal.allowSearch) {
98
- // UX-C instant filter: an unclaimed keystroke arms search AND starts
98
+ // instant filter: an unclaimed keystroke arms search AND starts
99
99
  // the query immediately, instead of silently doing nothing until the
100
100
  // user discovers '/' first (the "help search needs '/' arming while
101
101
  // the palette filters instantly" evaluator finding — /help has no
@@ -108,7 +108,7 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
108
108
  }
109
109
  } else if (token.type === 'key') {
110
110
  if (token.logicalName === 'escape') {
111
- // UX-C: ONE Escape always closes the modal, regardless of search state.
111
+ // ONE Escape always closes the modal, regardless of search state.
112
112
  // Clearing an in-progress query is Backspace's job, not Esc's — the old
113
113
  // two-stage contract (1st Esc clears query, 2nd blurs search, 3rd
114
114
  // finally closes) was the "help modal took 3 Escapes after searching"
@@ -234,6 +234,8 @@ type SettingsRouteState = {
234
234
  searchQuery: string;
235
235
  commitEdit: () => void;
236
236
  toggleSelectedFlag: () => void;
237
+ /** Scroll the documentation pane (PgUp/PgDn); the renderer clamps and shows honest markers. */
238
+ scrollContext?: (delta: number) => void;
237
239
  activateSelected: () => void;
238
240
  handleSubscriptionLogoutKey?: (key: string) => 'confirmed' | 'cancelled' | 'absorbed' | 'inactive';
239
241
  adjustSelected: (direction: 'left' | 'right', step?: number) => void;
@@ -415,6 +417,12 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
415
417
  const reset = state.settingsModal.resetSelected?.();
416
418
  if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
417
419
  }
420
+ else if (token.logicalName === 'pageup' && !state.settingsModal.editingMode) {
421
+ state.settingsModal.scrollContext?.(-3);
422
+ }
423
+ else if (token.logicalName === 'pagedown' && !state.settingsModal.editingMode) {
424
+ state.settingsModal.scrollContext?.(3);
425
+ }
418
426
  else if (token.logicalName === 'tab' && !state.settingsModal.searchFocused) {
419
427
  if (state.settingsModal.toggleFocusPane) state.settingsModal.toggleFocusPane();
420
428
  else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
@@ -556,9 +564,9 @@ type ConfigModalRouteState = {
556
564
  };
557
565
 
558
566
  /**
559
- * Route a key to the generic config-modal host (W6.1 MIGRATE-TO-MODAL
567
+ * Route a key to the generic config-modal host (MIGRATE-TO-MODAL
560
568
  * surfaces). The host owns the reserved navigation keys (Esc, up/down/j/k tab
561
- * left/right, and now '/' — DEBT-5 item 1's type-to-filter, armed the same
569
+ * left/right, and now '/' — item 1's type-to-filter, armed the same
562
570
  * way as scrollable-list-panel.ts's opt-in filter and SettingsModal's own
563
571
  * '/'-armed search); every other key is offered to the active surface's
564
572
  * declarative action table (fireAction handles the two-press confirm for
@@ -227,7 +227,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
227
227
  return withState(state, true);
228
228
  }
229
229
 
230
- // W6.1 retirement: the live-tail, agent-detail, and process-modal token
230
+ // retirement: the live-tail, agent-detail, and process-modal token
231
231
  // routes were removed with those modals (F2 now opens Fleet, which subsumes
232
232
  // the live process tree; the three modals were structurally unreachable after
233
233
  // the F2 repoint and were deleted).
@@ -138,10 +138,10 @@ export async function handleMigrateLegacyDaemonServiceForHandler(handler: InputH
138
138
  legacy,
139
139
  );
140
140
  handler.commandContext?.print?.(
141
- ['Migrate legacy daemon service:', ...result.lines.map((line: string) => ` ${line}`)].join('\n'),
141
+ ['Migrate install-script daemon service:', ...result.lines.map((line: string) => ` ${line}`)].join('\n'),
142
142
  );
143
143
  } catch (error) {
144
- handler.commandContext?.print?.(`Migrate legacy daemon service failed: ${summarizeError(error)}`);
144
+ handler.commandContext?.print?.(`Migrate install-script daemon service failed: ${summarizeError(error)}`);
145
145
  } finally {
146
146
  handler.onboardingApplyPending = false;
147
147
  handler.requestRender();