@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
@@ -3,6 +3,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
3
3
  import { summarizeError, openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
4
4
  import { getProviderIdFromModel } from '../config/provider-model.ts';
5
5
  import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
6
+ import { enrichProviderAccountsSnapshot } from '../runtime/onboarding/provider-key-capture.ts';
6
7
  import { handleConnectExistingDaemonForHandler, handleMigrateLegacyDaemonServiceForHandler } from './handler-onboarding-daemon-adopt.ts';
7
8
  import { detectLegacyUnit, resolveConfiguredServiceName } from '../runtime/legacy-daemon-migration.ts';
8
9
  import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
@@ -376,12 +377,12 @@ export async function refreshOnboardingHydrationForHandler(handler: InputHandler
376
377
  list: () => handler.uiServices.platform.surfaceRegistry.syncConfiguredSurfaces(),
377
378
  },
378
379
  providerAccounts: {
379
- loadSnapshot: () => buildProviderAccountSnapshot({
380
+ loadSnapshot: async () => enrichProviderAccountsSnapshot(await buildProviderAccountSnapshot({
380
381
  providerRegistry: handler.uiServices.providers.providerRegistry,
381
382
  serviceRegistry: handler.uiServices.platform.serviceRegistry,
382
383
  subscriptionManager: handler.uiServices.platform.subscriptionManager,
383
384
  secretsManager: handler.uiServices.platform.secretsManager,
384
- }),
385
+ }), handler.uiServices.providers.providerRegistry),
385
386
  },
386
387
  legacyDaemon: {
387
388
  detect: () => ({ ...detectLegacyUnit({ homeDir: handler.uiServices.environment.homeDirectory }), trackedServiceName: resolveConfiguredServiceName(handler.uiServices.platform.configManager) }),
@@ -3,6 +3,7 @@ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
3
3
  import type { CommandContext } from './command-registry.ts';
4
4
  import type { CapabilityFilter, CategoryFilter, ModelPickerModal } from './model-picker.ts';
5
5
  import { MODEL_PICKER_CHROME_LINES } from '../renderer/model-picker-overlay.ts';
6
+ import { ensureProviderKeyThenSelect } from './provider-key-intake.ts';
6
7
  import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils';
7
8
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
8
9
  import type { BlockActionId } from '../renderer/block-actions.ts';
@@ -66,15 +67,17 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
66
67
  } else {
67
68
  const target = state.modelPicker.target;
68
69
  const handled = state.onModelPickerCommit?.() ?? false;
69
- if (!handled) {
70
- state.commandContext?.completeModelSelection?.({
71
- model: selected,
72
- effort: currentEffort,
73
- target,
74
- });
75
- }
76
70
  state.modelPicker.close();
77
71
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
72
+ const ctx = state.commandContext;
73
+ if (!handled && ctx) {
74
+ // Selecting an unconfigured provider captures its key inline
75
+ // (concealed prompt → secrets manager → live re-register) and
76
+ // only then completes the original selection.
77
+ ensureProviderKeyThenSelect(ctx, selected.provider, () => {
78
+ ctx.completeModelSelection?.({ model: selected, effort: currentEffort, target });
79
+ });
80
+ }
78
81
  }
79
82
  }
80
83
  } else if (mode === 'provider') {
@@ -88,12 +91,16 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
88
91
  } else if (mode === 'effort') {
89
92
  const model = state.modelPicker.pendingModel;
90
93
  const effort = state.modelPicker.effortLevels[idx];
91
- if (model && effort) {
92
- const handled = state.onModelPickerCommit?.() ?? false;
93
- if (!handled) state.commandContext?.completeModelSelection?.({ model, effort, target: state.modelPicker.target });
94
- }
94
+ const target = state.modelPicker.target;
95
+ const handled = model && effort ? (state.onModelPickerCommit?.() ?? false) : false;
95
96
  state.modelPicker.close();
96
97
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
98
+ const ctx = state.commandContext;
99
+ if (model && effort && !handled && ctx) {
100
+ ensureProviderKeyThenSelect(ctx, model.provider, () => {
101
+ ctx.completeModelSelection?.({ model, effort, target });
102
+ });
103
+ }
97
104
  } else if (mode === 'embeddingProvider') {
98
105
  const selectedProvider = state.modelPicker.embeddingProviders[idx];
99
106
  if (selectedProvider) {
@@ -113,10 +120,16 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
113
120
  state.modelPicker.contextCapError = null;
114
121
  const validCap = parsedCap !== null && parsedCap > 0 && parsedCap <= 10_000_000 ? parsedCap : null;
115
122
  const effort = state.commandContext?.session.runtime.reasoningEffort ?? 'medium';
123
+ const target = state.modelPicker.target;
116
124
  const handled = state.onModelPickerCommit?.() ?? false;
117
- if (!handled) state.commandContext?.completeModelSelection?.({ model: capModel, effort, contextCap: validCap, target: state.modelPicker.target });
118
125
  state.modelPicker.close();
119
126
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
127
+ const ctx = state.commandContext;
128
+ if (!handled && ctx) {
129
+ ensureProviderKeyThenSelect(ctx, capModel.provider, () => {
130
+ ctx.completeModelSelection?.({ model: capModel, effort, contextCap: validCap, target });
131
+ });
132
+ }
120
133
  }
121
134
  } else {
122
135
  state.modelPicker.close();
@@ -214,7 +227,7 @@ function cycleCapabilityFilter(modelPicker: ModelPickerModal): void {
214
227
  modelPicker.setCapabilityFilter(cycle[(cur + 1) % cycle.length]!);
215
228
  }
216
229
 
217
- // W6.2 e / W6.1 retirement: the process-modal and live-tail-modal token routes
230
+ // e / retirement: the process-modal and live-tail-modal token routes
218
231
  // were removed with those modals (F2 now opens Fleet, which subsumes the live
219
232
  // process tree). ProcessModal/AgentDetailModal/LiveTailModal were structurally
220
233
  // unreachable after the F2 repoint and were deleted.
@@ -84,7 +84,7 @@ export function handleGlobalShortcutToken(
84
84
  // unclaimed key once panelFocused is true and reports it handled). The
85
85
  // practical effect was that F2 could OPEN+focus the Fleet panel exactly
86
86
  // once; every subsequent press while already focused vanished silently —
87
- // "F2 pressed 4x never closed the panel" (UX-C evaluator finding). Routing
87
+ // "F2 pressed 4x never closed the panel" (evaluator finding). Routing
88
88
  // it here, before handlePanelFocusToken ever sees the token, gives F2 the
89
89
  // same toggle semantics as Ctrl+O below, matching how Ctrl+P/panel-picker
90
90
  // was already reachable regardless of panelFocused.
@@ -139,7 +139,7 @@ export function handleGlobalShortcutToken(
139
139
  // in-panel action (FleetPanel session-tab detach) before it closes the
140
140
  // panel outright — see Panel.interceptPanelClose's doc comment.
141
141
  if (active?.interceptPanelClose?.()) {
142
- // UX-C fix: a consumed Ctrl+X (the Fleet panel's session-tab detach)
142
+ // fix: a consumed Ctrl+X (the Fleet panel's session-tab detach)
143
143
  // used to leave panelFocused untouched, so focus stayed on the panel
144
144
  // — the evaluator's "Ctrl+X detach landed focus in the panel and a
145
145
  // typed question became nav keys". Detach is a leave-taking action:
@@ -203,7 +203,7 @@ export function handleGlobalShortcutToken(
203
203
  // Alt+1..9: jump directly to the Nth workspace tab. Routed globally (like
204
204
  // panel-tab-next/prev) so the jump works whether focus is on the prompt or
205
205
  // the workspace; gated on visibility, matching cyclePanelTab semantics.
206
- // UX-C: a chord jump is "I'm going panel-driving" (focus rule 1a) — the
206
+ // a chord jump is "I'm going panel-driving" (focus rule 1a) — the
207
207
  // jump now also grabs keyboard focus, matching F2/Ctrl+O/Ctrl+P, so j/k
208
208
  // land in the newly-active tab immediately instead of the composer.
209
209
  const pm = state.panelManager;
@@ -218,9 +218,9 @@ export function handleGlobalShortcutToken(
218
218
  }
219
219
 
220
220
  case 'panel-ops': {
221
- // Ctrl+O: TOGGLE the Fleet panel (UX-C — same semantics as F2 above; see
221
+ // Ctrl+O: TOGGLE the Fleet panel (— same semantics as F2 above; see
222
222
  // toggleFleetPanel's doc comment). The former Ops Control panel was
223
- // retired to an 'ops-control' -> 'fleet' alias (W6.1); rather than route
223
+ // retired to an 'ops-control' -> 'fleet' alias; rather than route
224
224
  // through the now-aliased openOpsPanel callback (which opens without
225
225
  // transferring focus), this operates on 'fleet' directly.
226
226
  toggleFleetPanel(state);
@@ -403,7 +403,7 @@ export function handleGlobalShortcutToken(
403
403
  }
404
404
 
405
405
  /**
406
- * toggleFleetPanel — the shared F2 / Ctrl+O TOGGLE (UX-C item 2): if the
406
+ * toggleFleetPanel — the shared F2 / Ctrl+O TOGGLE (item 2): if the
407
407
  * Fleet panel is open AND the panel workspace currently owns keyboard focus
408
408
  * with Fleet as the active tab, the chord CLOSES it and returns focus to the
409
409
  * composer; if Fleet is open but not the focused/active tab, the chord brings
@@ -194,7 +194,7 @@ export class InputHandler implements InputHandlerLike {
194
194
  public blockActionsMenu = new BlockActionsMenu();
195
195
  public settingsModal = new SettingsModal();
196
196
  public configModal = new ConfigModal();
197
- /** DEBT-5 item 5 — paste-flood guard state, mutated in place across tokens (never reallocated). */
197
+ /** item 5 — paste-flood guard state, mutated in place across tokens (never reallocated). */
198
198
  private panelBurstGuard: PanelBurstGuardState = { timestamps: [], suspended: false, hintShown: false };
199
199
  public mcpWorkspace = new McpWorkspace();
200
200
  public onboardingWizard = new OnboardingWizardController();
@@ -277,7 +277,7 @@ export class InputHandler implements InputHandlerLike {
277
277
  uiServices.providers.benchmarkStore,
278
278
  uiServices.providers.providerRegistry,
279
279
  );
280
- // W6.1 retirement: ProcessModal/LiveTailModal/AgentDetailModal were removed
280
+ // retirement: ProcessModal/LiveTailModal/AgentDetailModal were removed
281
281
  // — F2 now opens the Fleet panel, which subsumes the live process tree.
282
282
  this.bookmarkModal = new BookmarkModal(uiServices.shell.bookmarkManager);
283
283
  this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager, uiServices.sessions.sessionBroker);
@@ -147,8 +147,8 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
147
147
  'panel-tab-8': [{ key: '8', alt: true }],
148
148
  'panel-tab-9': [{ key: '9', alt: true }],
149
149
  // Ctrl+O: TOGGLE the Fleet panel (open+focus / bring-to-front+focus / close
150
- // — see toggleFleetPanel in handler-shortcuts.ts, UX-C). The former Ops
151
- // Control panel was retired to an 'ops-control' -> 'fleet' alias (W6.1); the
150
+ // — see toggleFleetPanel in handler-shortcuts.ts). The former Ops
151
+ // Control panel was retired to an 'ops-control' -> 'fleet' alias; the
152
152
  // binding is KEPT (repointed, not removed) so the Ctrl+O muscle memory still
153
153
  // lands somewhere useful. Routed globally in handleGlobalShortcutToken.
154
154
  'panel-ops': [{ key: 'o', ctrl: true }],
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { ConfigKey } from '../../config/index.ts';
18
- import { FEATURE_FLAG_MAP } from '@pellux/goodvibes-sdk/platform/runtime/state';
18
+ import { getFeatureSetting, isFeatureDefaultEnabled } from '../../runtime/feature-settings.ts';
19
19
  import type {
20
20
  OnboardingWizardControllerLike,
21
21
  OnboardingWizardFieldDefinition,
@@ -94,12 +94,13 @@ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
94
94
  hint: 'When the exec sandbox leaves a command on ask, a model pass annotates the ask with stated reasons. Never converts allow to deny; never touches the frozen catastrophic block.',
95
95
  subOptions: [
96
96
  {
97
- key: 'auto-approve',
98
- configKey: 'sandbox.judgmentAutoApprove' as ConfigKey,
99
- label: 'Auto-approve looks-safe verdicts',
100
- hint: 'Yes lets a looks-safe judgment auto-approve the ask; No keeps annotate-only (default).',
101
- valueType: 'boolean',
102
- defaultValue: 'no',
97
+ key: 'mode',
98
+ configKey: 'sandbox.judgment' as ConfigKey,
99
+ label: 'Judgment mode',
100
+ hint: 'annotate (default) proposes a verdict with stated reasons and the human still decides; auto-approve additionally auto-approves looks-safe verdicts; off keeps plain asks.',
101
+ valueType: 'enum',
102
+ options: ['off', 'annotate', 'auto-approve'],
103
+ defaultValue: 'annotate',
103
104
  },
104
105
  ],
105
106
  },
@@ -283,23 +284,18 @@ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
283
284
  {
284
285
  flagId: 'integration-delivery-slo',
285
286
  label: 'Integration delivery SLO',
286
- hint: 'Retry with backoff and a dead-letter queue for Slack/Discord/webhook delivery. Exposes /notify dlq and /notify replay.',
287
- subOptions: [
288
- {
289
- key: 'slo',
290
- configKey: 'integrations.delivery.sloEnforced' as ConfigKey,
291
- label: 'Enforce delivery SLO',
292
- hint: 'Yes logs dead-letter events at error level and surfaces them in diagnostics; No is warn-level only (default).',
293
- valueType: 'boolean',
294
- defaultValue: 'no',
295
- },
296
- ],
287
+ hint: 'Retry with backoff and a dead-letter queue for Slack/Discord/webhook delivery, with dead-letter events surfaced in diagnostics. On by default; exposes /notify dlq and /notify replay. Retry bounds are tuned in /settings.',
297
288
  },
298
289
  {
299
290
  flagId: 'adaptive-execution-planner',
300
291
  label: 'Adaptive execution planner',
301
292
  hint: 'Score single/cohort/background/remote strategies each turn and pick the best. Exposes /plan commands.',
302
293
  },
294
+ {
295
+ flagId: 'daemon-auto-update',
296
+ label: 'Daemon auto-update',
297
+ hint: 'The daemon checks for a new release hourly, checksum-verifies it, swaps binaries only at a no-active-work moment, and keeps the previous binary for one-command rollback. On by default; update.auto turns it off and update.intervalMinutes tunes the cadence in /settings.',
298
+ },
303
299
  {
304
300
  flagId: 'relay-connect',
305
301
  label: 'Outbound zero-knowledge relay',
@@ -355,10 +351,10 @@ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
355
351
  key: 'backend',
356
352
  configKey: 'tools.overflowSpillBackend' as ConfigKey,
357
353
  label: 'Spill backend',
358
- hint: 'file (default), ledger, or diagnostics.',
354
+ hint: 'ledger (default when this feature is on) or diagnostics; file is the baseline used while the feature is off.',
359
355
  valueType: 'enum',
360
356
  options: ['file', 'ledger', 'diagnostics'],
361
- defaultValue: 'file',
357
+ defaultValue: 'ledger',
362
358
  },
363
359
  ],
364
360
  },
@@ -398,9 +394,9 @@ export function featureSubOptionFieldId(flagId: string, key: string): string {
398
394
  return `feature.${flagId}.${key}`;
399
395
  }
400
396
 
401
- /** Whether a flag defaults to enabled (drives the toggle's default-checked state). */
397
+ /** Whether a feature defaults to enabled (drives the toggle's default-checked state). */
402
398
  export function isFeatureDefaultOn(flagId: string): boolean {
403
- return FEATURE_FLAG_MAP.get(flagId)?.defaultState === 'enabled';
399
+ return isFeatureDefaultEnabled(flagId);
404
400
  }
405
401
 
406
402
  /** Every flag id reachable through the guided feature steps. */
@@ -498,10 +494,12 @@ export function buildFeatureUnitSteps(
498
494
 
499
495
  /**
500
496
  * Fold the guided feature selections into apply operations: for each unit, when
501
- * the user's choice differs from the flag's default, record the flag override;
502
- * when the feature is enabled, write its implied config, prerequisite flags, and
503
- * chosen sub-options. Flag overrides are collected into `overrides` (flushed by
504
- * the caller with the surface flags); config writes go straight to setConfig.
497
+ * the user's choice differs from the feature's default, record the desired
498
+ * state; when the feature is enabled, write its implied config, prerequisite
499
+ * features, and chosen sub-options. Desired states are collected into
500
+ * `overrides` (flushed by the caller as plain domain-settings writes on each
501
+ * feature's enablement key, together with the surface capabilities); other
502
+ * config writes go straight to setConfig.
505
503
  */
506
504
  export function applyFeatureUnitOperations(
507
505
  controller: OnboardingWizardControllerLike,
@@ -521,15 +519,24 @@ export function applyFeatureUnitOperations(
521
519
  for (const prerequisite of unit.requiresFlags ?? []) overrides.set(prerequisite, 'enabled');
522
520
  for (const implied of unit.impliedConfig ?? []) setConfig(implied.key, implied.value);
523
521
 
522
+ const enablementKey = getFeatureSetting(unit.flagId)?.enablement.key;
524
523
  for (const sub of unit.subOptions ?? []) {
525
524
  const raw = controller.getStringFieldValue(featureSubOptionFieldId(unit.flagId, sub.key), sub.defaultValue);
525
+ let wrote = true;
526
526
  if (sub.valueType === 'boolean') {
527
527
  setConfig(sub.configKey, raw === 'yes');
528
528
  } else if (sub.valueType === 'text') {
529
- if (raw.length > 0) setConfig(sub.configKey, raw);
529
+ wrote = raw.length > 0;
530
+ if (wrote) setConfig(sub.configKey, raw);
530
531
  } else {
531
532
  setConfig(sub.configKey, raw);
532
533
  }
534
+ // A sub-option that writes the unit's OWN enablement key (e.g. the
535
+ // provider-optimizer routing mode) is the authoritative enablement
536
+ // value — drop the pending override so the flush cannot clobber it.
537
+ if (wrote && enablementKey !== undefined && sub.configKey === enablementKey) {
538
+ overrides.delete(unit.flagId);
539
+ }
533
540
  }
534
541
  }
535
542
  }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Provider-agnostic API-key fields for the onboarding provider step.
3
+ *
4
+ * Kept out of onboarding-wizard-steps.ts so the step file stays focused on step
5
+ * assembly. Both pieces read from the same registration-truth targets so the
6
+ * inventory line and the per-provider masked fields never disagree.
7
+ */
8
+
9
+ import type { ProviderKeyCaptureTarget, SubscriptionStatusTarget } from '../../runtime/onboarding/provider-key-capture.ts';
10
+ import { deriveSubscriptionStatusTargets, providerKeyFieldId, subscriptionStatusFieldId } from '../../runtime/onboarding/provider-key-capture.ts';
11
+ import type { OnboardingProviderAccountRecord } from '../../runtime/onboarding/types.ts';
12
+ import type {
13
+ OnboardingWizardMaskedFieldDefinition,
14
+ OnboardingWizardStatusFieldDefinition,
15
+ } from './onboarding-wizard-types.ts';
16
+
17
+ /**
18
+ * Providers with a wired in-wizard subscription sign-in flow. This is DATA — the
19
+ * set of providers whose OAuth login the wizard can start/finish itself — not an
20
+ * assumption that any one vendor is the only subscription option.
21
+ */
22
+ const SUBSCRIPTION_SIGN_IN_PROVIDER_IDS = new Set<string>(['openai']);
23
+
24
+ /**
25
+ * One subscription-status row per subscription-capable provider. The row's copy
26
+ * names the provider from data — never a vendor hardcoded in the field set — so
27
+ * the status surface tracks whatever subscription-capable providers the runtime
28
+ * reports, not a single assumed one.
29
+ */
30
+ export function buildSubscriptionStatusFields(
31
+ targets: readonly SubscriptionStatusTarget[],
32
+ ): OnboardingWizardStatusFieldDefinition[] {
33
+ return targets.map((target) => ({
34
+ kind: 'status',
35
+ id: subscriptionStatusFieldId(target.providerId),
36
+ label: `${target.providerId} subscription status`,
37
+ hint: target.active
38
+ ? `A ${target.providerId} subscription session is already available.`
39
+ : target.pending
40
+ ? `A ${target.providerId} subscription login is pending.`
41
+ : `No ${target.providerId} subscription session was found in the current runtime state.`,
42
+ defaultValue: target.active ? 'Active' : target.pending ? 'Pending' : 'Not detected',
43
+ }));
44
+ }
45
+
46
+ /** The provider-agnostic subscription posture the provider step renders. */
47
+ export interface SubscriptionPosture {
48
+ readonly statusFields: OnboardingWizardStatusFieldDefinition[];
49
+ /** The provider whose in-wizard sign-in flow applies now, or null. */
50
+ readonly signInProviderId: string | null;
51
+ readonly signInPending: boolean;
52
+ readonly summaryLine: string;
53
+ }
54
+
55
+ /**
56
+ * Derive the subscription posture for the provider step: one status row per
57
+ * subscription-capable provider, plus which (if any) provider's in-wizard OAuth
58
+ * sign-in flow applies right now (a subscription-capable, sign-in-wired provider
59
+ * that is not already active).
60
+ */
61
+ export function buildSubscriptionPosture(
62
+ activeProviderIds: readonly string[],
63
+ pendingProviderIds: readonly string[],
64
+ providers: readonly OnboardingProviderAccountRecord[] | undefined,
65
+ ): SubscriptionPosture {
66
+ const targets = deriveSubscriptionStatusTargets(providers, activeProviderIds, pendingProviderIds);
67
+ const signIn = targets.find((target) => SUBSCRIPTION_SIGN_IN_PROVIDER_IDS.has(target.providerId) && !target.active);
68
+ const activeCount = targets.filter((target) => target.active).length;
69
+ const pendingCount = targets.filter((target) => target.pending).length;
70
+ return {
71
+ statusFields: buildSubscriptionStatusFields(targets),
72
+ signInProviderId: signIn?.providerId ?? null,
73
+ signInPending: signIn?.pending ?? false,
74
+ summaryLine: targets.length === 0
75
+ ? 'Subscriptions: none detected'
76
+ : `Subscriptions: ${activeCount} active, ${pendingCount} pending across ${targets.length} provider(s)`,
77
+ };
78
+ }
79
+
80
+ /** The status line summarizing how many key-accepting providers are configured. */
81
+ export function buildProviderKeyInventoryField(
82
+ targets: readonly ProviderKeyCaptureTarget[],
83
+ ): OnboardingWizardStatusFieldDefinition {
84
+ const configuredCount = targets.filter((target) => target.configured).length;
85
+ return {
86
+ kind: 'status',
87
+ id: 'providers.api-key-inventory',
88
+ label: 'Provider API keys',
89
+ hint: targets.length === 0
90
+ ? 'No key-accepting providers are registered yet in the current runtime state.'
91
+ : `${targets.length} provider(s) accept an API key; ${configuredCount} already configured. Values stay masked.`,
92
+ defaultValue: targets.length === 0
93
+ ? 'None detected'
94
+ : `${configuredCount}/${targets.length} configured`,
95
+ };
96
+ }
97
+
98
+ /** One masked field per key-accepting provider, keyed off registration truth. */
99
+ export function buildProviderKeyMaskedFields(
100
+ targets: readonly ProviderKeyCaptureTarget[],
101
+ ): OnboardingWizardMaskedFieldDefinition[] {
102
+ return targets.map((target) => ({
103
+ kind: 'masked',
104
+ id: providerKeyFieldId(target.providerId),
105
+ label: `${target.providerId} API key`,
106
+ hint: target.configured
107
+ ? `A key for ${target.providerId} is already stored. Leave blank to keep it; enter a new key to replace it through the secret manager.`
108
+ : `Optional: enter an API key for ${target.providerId} now. The value is stored through the secret manager, not in config.`,
109
+ placeholder: target.configured ? 'already configured' : 'paste key',
110
+ defaultValue: '',
111
+ }));
112
+ }
@@ -1,6 +1,7 @@
1
1
  import type { OnboardingAcknowledgementTarget, OnboardingApplyOperation, OnboardingApplyRequest } from '../../runtime/onboarding/index.ts';
2
2
  import { formatProviderModel } from '../../config/provider-model.ts';
3
3
  import { getServerSurfaceFeatureFlags } from '../../runtime/surface-feature-flags.ts';
4
+ import { featureEnablementWrite } from '../../runtime/feature-settings.ts';
4
5
  import {
5
6
  buildCloudflareApiTokenRef,
6
7
  buildCloudflareOperationalTokenRef,
@@ -17,13 +18,9 @@ import {
17
18
  } from './onboarding-wizard-external-surfaces.ts';
18
19
  import { applyFeatureUnitOperations } from './onboarding-feature-units.ts';
19
20
  import { buildGoodVibesSecretKey, buildGoodVibesSecretRef, isLoopbackAddress, isSecretReferenceValue } from './onboarding-wizard-helpers.ts';
21
+ import { deriveProviderKeyCaptureTargets, providerKeyFieldId } from '../../runtime/onboarding/provider-key-capture.ts';
20
22
  import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
21
23
 
22
- /** SDK schema default for behavior.hitlMode (schema-domain-core DEFAULT_CONFIG). */
23
- const HITL_MODE_SCHEMA_DEFAULT = 'balanced';
24
- /** Feature flag that gates behavior.hitlMode preset application in ModeManager. */
25
- const HITL_UX_MODES_FEATURE_FLAG = 'hitl-ux-modes';
26
-
27
24
  export function buildOnboardingApplyRequest(controller: OnboardingWizardControllerLike): OnboardingApplyRequest {
28
25
  const operations: OnboardingApplyOperation[] = [];
29
26
  // Accumulates feature-flag state that onboarding selections require, so the
@@ -132,21 +129,21 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
132
129
  setConfig('provider.reasoningEffort', controller.getStringFieldValue('default-model.reasoning', controller.runtimeSnapshot?.providerRouting.primaryReasoningEffort ?? 'medium'));
133
130
  const hitlMode = controller.getStringFieldValue('experience.hitl', controller.runtimeSnapshot?.runtimeDefaults.behavior.hitlMode ?? 'balanced');
134
131
  setConfig('behavior.hitlMode', hitlMode);
135
- // behavior.hitlMode is gated by the hitl-ux-modes feature flag: ModeManager
136
- // only applies the chosen preset to the notification router when the flag is
137
- // on. Writing hitlMode without enabling the flag leaves the user's choice
138
- // silently inert. When the user picks a non-default mode, enable the gating
139
- // flag in the same apply batch so the selection actually takes effect. The
140
- // SDK schema default is 'balanced' (schema-domain-core DEFAULT_CONFIG); a
141
- // choice equal to the default is left to the flag's own default-off state.
142
- if (hitlMode !== HITL_MODE_SCHEMA_DEFAULT) {
143
- featureFlagOverrides.set(HITL_UX_MODES_FEATURE_FLAG, 'enabled');
144
- }
132
+ // behavior.hitlMode IS the enablement key of the hitl-ux-modes capability
133
+ // (active for quiet/balanced/operator, off only at 'off'), so the write
134
+ // above already carries the enablement — no separate gating write exists,
135
+ // and adding one here would clobber a non-default choice with the stock
136
+ // mode when the batch flushes.
145
137
  setConfig('behavior.guidanceMode', controller.getStringFieldValue('experience.guidance', controller.runtimeSnapshot?.runtimeDefaults.behavior.guidanceMode ?? 'minimal'));
146
138
  setConfig('permissions.mode', controller.getStringFieldValue('experience.permissions', controller.runtimeSnapshot?.runtimeDefaults.permissionsMode ?? 'prompt'));
147
139
  setConfig('storage.secretPolicy', controller.getStringFieldValue('external-services.secret-policy', controller.runtimeSnapshot?.runtimeDefaults.secretStoragePolicy ?? 'preferred_secure'));
148
140
 
149
- setSecret('OPENAI_API_KEY', controller.getStringFieldValue('providers.openai-api-key', ''));
141
+ // Provider-agnostic key capture: store each entered key under the provider's
142
+ // own declared secret key. Empty fields are skipped (setSecret no-ops), so
143
+ // a blank field keeps any existing key untouched.
144
+ for (const target of deriveProviderKeyCaptureTargets(controller.runtimeSnapshot?.providerAccounts?.providers)) {
145
+ setSecret(target.apiKeyEnvVar, controller.getStringFieldValue(providerKeyFieldId(target.providerId), ''));
146
+ }
150
147
 
151
148
  if (shouldShowCloudflareStep(controller)) {
152
149
  addCloudflareOperations(controller, operations, setSecret);
@@ -190,12 +187,13 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
190
187
  }
191
188
  }
192
189
  // Feature units from the guided feature steps (safety, memory & context,
193
- // telemetry, automation, provider, advanced): enable/disable each flag the
194
- // user changed from its default and write its chosen sub-options.
190
+ // telemetry, automation, provider, advanced): enable/disable each feature
191
+ // the user changed from its default and write its chosen sub-options.
195
192
  applyFeatureUnitOperations(controller, setConfig, featureFlagOverrides);
196
193
 
197
- // Surface/server flags are authoritative for the network-exposing flags they
198
- // own — apply them last so they win over any feature-step override.
194
+ // Surface/server capabilities are authoritative for the network-exposing
195
+ // features they own — apply them last so they win over any feature-step
196
+ // override.
199
197
  for (const surfaceFlag of getServerSurfaceFeatureFlags({
200
198
  serverBacked: hasServers,
201
199
  web: browserAccess,
@@ -203,8 +201,11 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
203
201
  })) {
204
202
  featureFlagOverrides.set(surfaceFlag, 'enabled');
205
203
  }
204
+ // Each selection lands as a plain domain-settings write on the feature's
205
+ // real enablement key (e.g. sandbox.enabled, controlPlane.gateway).
206
206
  for (const flagId of [...featureFlagOverrides.keys()].sort((left, right) => left.localeCompare(right))) {
207
- setConfig(`featureFlags.${flagId}`, featureFlagOverrides.get(flagId)!);
207
+ const write = featureEnablementWrite(flagId, featureFlagOverrides.get(flagId)! === 'enabled');
208
+ if (write) setConfig(write.key, write.value);
208
209
  }
209
210
 
210
211
  acknowledge('providers', 'providers.reviewed');
@@ -282,6 +282,23 @@ const SURFACE_OVERLAYS: readonly SurfaceOverlay[] = [
282
282
  { configKey: 'surfaces.mattermost.defaultChannelId', label: 'Mattermost default channel ID', hint: 'Fallback Mattermost channel for outbound messages.', placeholder: 'channel id' },
283
283
  ],
284
284
  },
285
+ {
286
+ onboardingId: 'telephony',
287
+ sdkSurface: 'telephony',
288
+ label: 'Telephony surface',
289
+ hint: 'Enable SMS/voice notification delivery — direct Twilio, or a self-hosted bridge.',
290
+ fields: [
291
+ { configKey: 'surfaces.telephony.provider', label: 'Telephony provider', hint: 'twilio for direct Twilio delivery, bridge for a self-hosted telephony bridge.', placeholder: 'twilio' },
292
+ { configKey: 'surfaces.telephony.mode', label: 'Telephony mode', hint: 'sms, voice (call with spoken text), or bridge.', placeholder: 'sms' },
293
+ { configKey: 'surfaces.telephony.accountSid', label: 'Twilio account SID', hint: 'Account SID for direct Twilio delivery.', placeholder: 'AC...' },
294
+ { configKey: 'surfaces.telephony.authToken', label: 'Twilio auth token', hint: 'Auth token for direct Twilio delivery.', placeholder: 'auth token' },
295
+ { configKey: 'surfaces.telephony.fromNumber', label: 'From number', hint: 'The E.164 number outbound SMS/calls originate from.', placeholder: '+15551234567' },
296
+ { configKey: 'surfaces.telephony.defaultRecipient', label: 'Default recipient', hint: 'Fallback E.164 number for outbound delivery.', placeholder: '+15557654321' },
297
+ { configKey: 'surfaces.telephony.bridgeUrl', label: 'Bridge URL', hint: 'Base URL of the self-hosted telephony bridge (bridge provider only).', placeholder: 'https://telephony-bridge.example.test' },
298
+ { configKey: 'surfaces.telephony.token', label: 'Bridge token', hint: 'Token used to authenticate with the telephony bridge.', placeholder: 'bridge token' },
299
+ { configKey: 'surfaces.telephony.webhookSecret', label: 'Webhook secret', hint: 'Secret used to verify inbound telephony webhooks.', placeholder: 'webhook secret' },
300
+ ],
301
+ },
285
302
  {
286
303
  onboardingId: 'matrix',
287
304
  sdkSurface: 'matrix',
@@ -106,7 +106,7 @@ export function pushLegacyDaemonMigrationFields(
106
106
  fields.push({
107
107
  kind: 'status',
108
108
  id: 'network.migrate-legacy-daemon-detected',
109
- label: 'Legacy daemon service detected',
109
+ label: 'Separate install-script daemon service detected',
110
110
  // Follow-up: name the unit this host actually resolves (carried on the
111
111
  // snapshot from `resolveConfiguredServiceName` at collection time) — the
112
112
  // MANAGED_SERVICE_NAME fallback only covers snapshots built without it.
@@ -126,7 +126,7 @@ export function pushLegacyDaemonMigrationFields(
126
126
  fields.push({
127
127
  kind: 'action',
128
128
  id: 'network.migrate-legacy-daemon',
129
- label: confirmed ? 'Migrate legacy daemon service now' : 'Preview migration plan',
129
+ label: confirmed ? 'Migrate the install-script daemon service now' : 'Preview migration plan',
130
130
  hint: confirmed
131
131
  ? 'Executes the migration now: new-up-then-old-down, never touching the legacy unit until the new one is verified healthy. Result prints to the feed below.'
132
132
  : 'Prints the migration plan without changing anything. Check "Confirm migration execution" above to arm the real migration.',