@pellux/goodvibes-tui 1.7.0 → 1.9.1

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 (140) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +10 -9
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/config/credential-availability.ts +122 -0
  12. package/src/config/index.ts +15 -0
  13. package/src/core/composer-state.ts +11 -3
  14. package/src/core/conversation-line-cache.ts +27 -3
  15. package/src/core/conversation-rendering.ts +71 -14
  16. package/src/core/stream-event-wiring.ts +20 -1
  17. package/src/core/system-message-noise.ts +87 -0
  18. package/src/core/system-message-router.ts +68 -1
  19. package/src/core/turn-cancellation.ts +7 -2
  20. package/src/core/turn-event-wiring.ts +10 -2
  21. package/src/daemon/cli.ts +29 -2
  22. package/src/daemon/handlers/register.ts +8 -1
  23. package/src/daemon/service-commands.ts +329 -0
  24. package/src/input/autocomplete.ts +27 -1
  25. package/src/input/command-registry.ts +46 -4
  26. package/src/input/commands/codebase-runtime.ts +46 -6
  27. package/src/input/commands/config.ts +43 -3
  28. package/src/input/commands/health-runtime.ts +9 -1
  29. package/src/input/commands/memory.ts +68 -35
  30. package/src/input/commands/operator-panel-runtime.ts +31 -7
  31. package/src/input/commands/planning-runtime.ts +95 -6
  32. package/src/input/commands/qrcode-runtime.ts +25 -5
  33. package/src/input/commands/remote-runtime-setup.ts +5 -3
  34. package/src/input/commands/session-content.ts +20 -9
  35. package/src/input/commands/settings-sync-runtime.ts +15 -3
  36. package/src/input/commands/shell-core.ts +10 -1
  37. package/src/input/commands/workstream-runtime.ts +168 -18
  38. package/src/input/config-modal-types.ts +15 -1
  39. package/src/input/config-modal.ts +227 -12
  40. package/src/input/feed-context-factory.ts +3 -0
  41. package/src/input/handler-command-route.ts +10 -3
  42. package/src/input/handler-content-actions.ts +17 -2
  43. package/src/input/handler-feed-routes.ts +43 -121
  44. package/src/input/handler-feed.ts +32 -4
  45. package/src/input/handler-modal-routes.ts +78 -13
  46. package/src/input/handler-modal-stack.ts +11 -2
  47. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  48. package/src/input/handler-onboarding.ts +71 -59
  49. package/src/input/handler-picker-routes.ts +15 -8
  50. package/src/input/handler-shortcuts.ts +59 -9
  51. package/src/input/handler.ts +6 -1
  52. package/src/input/keybindings.ts +6 -5
  53. package/src/input/model-picker.ts +19 -2
  54. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  55. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  56. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  57. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  58. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  59. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  60. package/src/input/panel-mouse-geometry.ts +97 -0
  61. package/src/input/panel-paste-flood-guard.ts +86 -0
  62. package/src/input/selection-modal.ts +11 -0
  63. package/src/input/session-picker-modal.ts +44 -4
  64. package/src/input/settings-modal-data.ts +51 -2
  65. package/src/input/settings-modal-types.ts +4 -2
  66. package/src/main.ts +28 -27
  67. package/src/panels/builtin/shared.ts +9 -3
  68. package/src/panels/fleet-deep-link.ts +31 -0
  69. package/src/panels/fleet-panel-format.ts +62 -0
  70. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  71. package/src/panels/fleet-panel.ts +89 -131
  72. package/src/panels/fleet-read-model.ts +43 -0
  73. package/src/panels/fleet-steer.ts +35 -2
  74. package/src/panels/fleet-stop.ts +29 -1
  75. package/src/panels/modals/keybindings-modal.ts +16 -1
  76. package/src/panels/modals/modal-theme.ts +35 -29
  77. package/src/panels/modals/pairing-modal.ts +25 -6
  78. package/src/panels/modals/planning-modal.ts +43 -21
  79. package/src/panels/modals/work-plan-modal.ts +28 -0
  80. package/src/panels/panel-manager.ts +15 -4
  81. package/src/panels/polish-core.ts +38 -25
  82. package/src/panels/project-planning-answer-actions.ts +8 -13
  83. package/src/panels/types.ts +24 -0
  84. package/src/permissions/prompt.ts +160 -13
  85. package/src/renderer/autocomplete-overlay.ts +28 -2
  86. package/src/renderer/compositor.ts +2 -3
  87. package/src/renderer/config-modal.ts +19 -5
  88. package/src/renderer/footer-tips.ts +5 -1
  89. package/src/renderer/fullscreen-primitives.ts +32 -22
  90. package/src/renderer/git-status.ts +3 -1
  91. package/src/renderer/layout.ts +0 -4
  92. package/src/renderer/markdown.ts +7 -3
  93. package/src/renderer/modal-factory.ts +25 -20
  94. package/src/renderer/model-workspace.ts +18 -3
  95. package/src/renderer/overlay-box.ts +21 -17
  96. package/src/renderer/process-indicator.ts +14 -3
  97. package/src/renderer/selection-modal-overlay.ts +6 -1
  98. package/src/renderer/session-picker-modal.ts +196 -3
  99. package/src/renderer/settings-modal-helpers.ts +2 -0
  100. package/src/renderer/settings-modal.ts +7 -0
  101. package/src/renderer/shell-surface.ts +8 -1
  102. package/src/renderer/status-glyphs.ts +14 -15
  103. package/src/renderer/system-message.ts +15 -3
  104. package/src/renderer/terminal-bg-probe.ts +339 -0
  105. package/src/renderer/terminal-escapes.ts +20 -0
  106. package/src/renderer/theme-mode-config.ts +67 -0
  107. package/src/renderer/theme.ts +91 -1
  108. package/src/renderer/thinking.ts +11 -3
  109. package/src/renderer/tool-call.ts +15 -9
  110. package/src/renderer/tool-result-summary.ts +148 -0
  111. package/src/renderer/turn-injection.ts +22 -3
  112. package/src/renderer/ui-factory.ts +154 -85
  113. package/src/renderer/ui-primitives.ts +30 -129
  114. package/src/runtime/bootstrap-command-context.ts +6 -0
  115. package/src/runtime/bootstrap-command-parts.ts +8 -4
  116. package/src/runtime/bootstrap-core.ts +46 -24
  117. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  118. package/src/runtime/bootstrap-shell.ts +19 -1
  119. package/src/runtime/bootstrap.ts +118 -5
  120. package/src/runtime/code-index-services.ts +25 -2
  121. package/src/runtime/legacy-daemon-migration.ts +516 -0
  122. package/src/runtime/memory-fold.ts +55 -0
  123. package/src/runtime/onboarding/derivation.ts +7 -2
  124. package/src/runtime/onboarding/snapshot.ts +27 -1
  125. package/src/runtime/onboarding/types.ts +31 -1
  126. package/src/runtime/operator-token-cleanup.ts +82 -1
  127. package/src/runtime/orchestrator-core-services.ts +10 -0
  128. package/src/runtime/resume-notice.ts +209 -0
  129. package/src/runtime/services.ts +12 -8
  130. package/src/runtime/session-inbound-inputs.ts +252 -0
  131. package/src/runtime/session-spine-transport.ts +64 -0
  132. package/src/runtime/terminal-output-guard.ts +15 -8
  133. package/src/runtime/ui-services.ts +19 -3
  134. package/src/runtime/workstream-services.ts +160 -28
  135. package/src/runtime/wrfc-persistence.ts +124 -17
  136. package/src/shell/blocking-input.ts +46 -3
  137. package/src/shell/recovery-input-helpers.ts +170 -1
  138. package/src/shell/ui-openers.ts +42 -9
  139. package/src/utils/terminal-width.ts +52 -0
  140. package/src/version.ts +1 -1
@@ -75,6 +75,23 @@ export function handleGlobalShortcutToken(
75
75
  return true;
76
76
  }
77
77
 
78
+ // Bare F2 is also not in the keybinding table (hardcoded, like pageup/
79
+ // pagedown/escape above) — it must be handled here, GLOBALLY, rather than
80
+ // in handlePromptKeyToken (handler-feed-routes.ts) where it used to live.
81
+ // That location only ever runs when the panel workspace does NOT already
82
+ // own focus (handlePanelFocusToken, which runs before it, swallows every
83
+ // unclaimed key once panelFocused is true and reports it handled). The
84
+ // practical effect was that F2 could OPEN+focus the Fleet panel exactly
85
+ // once; every subsequent press while already focused vanished silently —
86
+ // "F2 pressed 4x never closed the panel" (UX-C evaluator finding). Routing
87
+ // it here, before handlePanelFocusToken ever sees the token, gives F2 the
88
+ // same toggle semantics as Ctrl+O below, matching how Ctrl+P/panel-picker
89
+ // was already reachable regardless of panelFocused.
90
+ if (token.logicalName === 'f2' && !token.ctrl && !token.meta) {
91
+ toggleFleetPanel(state);
92
+ return true;
93
+ }
94
+
78
95
  // O(1) lookup via inverted map.
79
96
  const kb = state.keybindingsManager;
80
97
  const action = kb.lookup(token);
@@ -108,6 +125,13 @@ export function handleGlobalShortcutToken(
108
125
  // in-panel action (FleetPanel session-tab detach) before it closes the
109
126
  // panel outright — see Panel.interceptPanelClose's doc comment.
110
127
  if (active?.interceptPanelClose?.()) {
128
+ // UX-C fix: a consumed Ctrl+X (the Fleet panel's session-tab detach)
129
+ // used to leave panelFocused untouched, so focus stayed on the panel
130
+ // — the evaluator's "Ctrl+X detach landed focus in the panel and a
131
+ // typed question became nav keys". Detach is a leave-taking action:
132
+ // like the ordinary close below, it hands control back to the
133
+ // composer rather than leaving the user stranded on the fleet tree.
134
+ state.panelFocused = false;
111
135
  state.requestRender();
112
136
  return true;
113
137
  }
@@ -160,27 +184,27 @@ export function handleGlobalShortcutToken(
160
184
  // Alt+1..9: jump directly to the Nth workspace tab. Routed globally (like
161
185
  // panel-tab-next/prev) so the jump works whether focus is on the prompt or
162
186
  // the workspace; gated on visibility, matching cyclePanelTab semantics.
187
+ // UX-C: a chord jump is "I'm going panel-driving" (focus rule 1a) — the
188
+ // jump now also grabs keyboard focus, matching F2/Ctrl+O/Ctrl+P, so j/k
189
+ // land in the newly-active tab immediately instead of the composer.
163
190
  const pm = state.panelManager;
164
191
  if (pm.isVisible()) {
165
192
  const index = Number(action.slice('panel-tab-'.length)) - 1;
166
193
  pm.activateWorkspaceIndex(index);
194
+ pm.focusPanels();
195
+ state.panelFocused = true;
167
196
  state.requestRender();
168
197
  }
169
198
  return true;
170
199
  }
171
200
 
172
201
  case 'panel-ops': {
173
- // Ctrl+O: open AND focus the Fleet panel. The former Ops Control panel was
202
+ // Ctrl+O: TOGGLE the Fleet panel (UX-C same semantics as F2 above; see
203
+ // toggleFleetPanel's doc comment). The former Ops Control panel was
174
204
  // retired to an 'ops-control' -> 'fleet' alias (W6.1); rather than route
175
205
  // through the now-aliased openOpsPanel callback (which opens without
176
- // transferring focus), open 'fleet' directly and grab focus — mirroring
177
- // the footer indicator's [Enter] -> openFleetPanel path so j/k/i/K land in
178
- // the panel immediately instead of the composer.
179
- const pm = state.panelManager;
180
- pm.open('fleet');
181
- pm.focusPanels();
182
- state.panelFocused = true;
183
- state.requestRender();
206
+ // transferring focus), this operates on 'fleet' directly.
207
+ toggleFleetPanel(state);
184
208
  return true;
185
209
  }
186
210
 
@@ -358,3 +382,29 @@ export function handleGlobalShortcutToken(
358
382
  return false;
359
383
  }
360
384
  }
385
+
386
+ /**
387
+ * toggleFleetPanel — the shared F2 / Ctrl+O TOGGLE (UX-C item 2): if the
388
+ * Fleet panel is open AND the panel workspace currently owns keyboard focus
389
+ * with Fleet as the active tab, the chord CLOSES it and returns focus to the
390
+ * composer; if Fleet is open but not the focused/active tab, the chord brings
391
+ * it to front and focuses it; if Fleet isn't open at all, the chord opens and
392
+ * focuses it. Uses `state.panelFocused` (not a panelManager query) for the
393
+ * focus check, consistent with every other case in this file and with what
394
+ * the mocked PanelManager test doubles in global-shortcuts.test.ts actually
395
+ * implement.
396
+ */
397
+ function toggleFleetPanel(state: GlobalShortcutRouteState): void {
398
+ const pm = state.panelManager;
399
+ const fleetOpen = pm.getAllOpen().some((p) => p.id === 'fleet');
400
+ const fleetIsFocusedActive = fleetOpen && state.panelFocused && pm.getActivePanel()?.id === 'fleet';
401
+ if (fleetIsFocusedActive) {
402
+ pm.close('fleet');
403
+ state.panelFocused = false;
404
+ } else {
405
+ pm.open('fleet');
406
+ pm.focusPanels();
407
+ state.panelFocused = true;
408
+ }
409
+ state.requestRender();
410
+ }
@@ -27,6 +27,7 @@ import { McpWorkspace } from './mcp-workspace.ts';
27
27
  import { SessionPickerModal } from './session-picker-modal.ts';
28
28
  import { ConfigModal } from './config-modal.ts';
29
29
  import { ProfilePickerModal } from './profile-picker-modal.ts';
30
+ import type { PanelBurstGuardState } from './panel-paste-flood-guard.ts';
30
31
  import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
31
32
  import {
32
33
  applyOnboardingRequest,
@@ -185,6 +186,8 @@ export class InputHandler implements InputHandlerLike {
185
186
  public blockActionsMenu = new BlockActionsMenu();
186
187
  public settingsModal = new SettingsModal();
187
188
  public configModal = new ConfigModal();
189
+ /** DEBT-5 item 5 — paste-flood guard state, mutated in place across tokens (never reallocated). */
190
+ private panelBurstGuard: PanelBurstGuardState = { timestamps: [], suspended: false, hintShown: false };
188
191
  public mcpWorkspace = new McpWorkspace();
189
192
  public onboardingWizard = new OnboardingWizardController();
190
193
  public onboardingModelPickerCancelSnapshot: OnboardingWizardSnapshot | null = null;
@@ -269,7 +272,7 @@ export class InputHandler implements InputHandlerLike {
269
272
  // W6.1 retirement: ProcessModal/LiveTailModal/AgentDetailModal were removed
270
273
  // — F2 now opens the Fleet panel, which subsumes the live process tree.
271
274
  this.bookmarkModal = new BookmarkModal(uiServices.shell.bookmarkManager);
272
- this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager);
275
+ this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager, uiServices.sessions.sessionBroker);
273
276
  this.profilePickerModal = new ProfilePickerModal(uiServices.shell.profileManager);
274
277
  this.initFeedContext();
275
278
  }
@@ -319,6 +322,7 @@ export class InputHandler implements InputHandlerLike {
319
322
  keybindingsManager: this.uiServices.shell.keybindingsManager,
320
323
  killRing: this.killRing,
321
324
  focusTracker: this.uiServices.platform.focusTracker,
325
+ panelBurstGuard: this.panelBurstGuard,
322
326
  getHistory: this.getHistory,
323
327
  getViewportHeight: this.getViewportHeight,
324
328
  getScrollTop: this.getScrollTop,
@@ -399,6 +403,7 @@ export class InputHandler implements InputHandlerLike {
399
403
  preSelectId?: string;
400
404
  allowSearch?: boolean;
401
405
  customActions?: Map<string, SelectionAction>;
406
+ primaryVerbLabel?: string;
402
407
  } | undefined,
403
408
  callback: SelectionModalCallback,
404
409
  ): void {
@@ -88,7 +88,7 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
88
88
  'panel-tab-7': 'Jump to workspace panel tab 7',
89
89
  'panel-tab-8': 'Jump to workspace panel tab 8',
90
90
  'panel-tab-9': 'Jump to workspace panel tab 9',
91
- 'panel-ops': 'Open and focus the Fleet panel',
91
+ 'panel-ops': 'Open, focus, or close the Fleet panel (toggle)',
92
92
  'panel-focus-toggle': 'Switch keyboard focus between top and bottom pane',
93
93
  'history-search': 'Reverse input history search',
94
94
  'search': 'Toggle conversation search',
@@ -140,10 +140,11 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
140
140
  'panel-tab-7': [{ key: '7', alt: true }],
141
141
  'panel-tab-8': [{ key: '8', alt: true }],
142
142
  'panel-tab-9': [{ key: '9', alt: true }],
143
- // Ctrl+O: open and focus the Fleet panel. The former Ops Control panel was
144
- // retired to an 'ops-control' -> 'fleet' alias (W6.1); the binding is KEPT
145
- // (repointed, not removed) so the Ctrl+O muscle memory still lands somewhere
146
- // useful. Routed globally in handleGlobalShortcutToken.
143
+ // Ctrl+O: TOGGLE the Fleet panel (open+focus / bring-to-front+focus / close
144
+ // see toggleFleetPanel in handler-shortcuts.ts, UX-C). The former Ops
145
+ // Control panel was retired to an 'ops-control' -> 'fleet' alias (W6.1); the
146
+ // binding is KEPT (repointed, not removed) so the Ctrl+O muscle memory still
147
+ // lands somewhere useful. Routed globally in handleGlobalShortcutToken.
147
148
  'panel-ops': [{ key: 'o', ctrl: true }],
148
149
  // Ctrl+G: toggle keyboard focus between the top and bottom panes. Ctrl+G is
149
150
  // otherwise unbound in the default table.
@@ -261,7 +261,22 @@ export class ModelPickerModal {
261
261
  }
262
262
  }
263
263
 
264
- /** Open showing all models — entry point for /model */
264
+ /**
265
+ * Open showing all models — entry point for /model.
266
+ *
267
+ * W3-T2: search starts FOCUSED. Live tmux repro (cc-w3-t2) showed the actual
268
+ * friction wasn't the filter itself (it already works — fuzzy multi-word
269
+ * substring over id/displayName/provider) but that with search unfocused by
270
+ * default, typing a search term went character-by-character into single-key
271
+ * shortcuts instead (g=group, c=capability, a=available-only, b=benchmark) —
272
+ * e.g. typing "claude" silently cycled the capability filter (on 'c') and
273
+ * toggled available-only off (on 'a') while the visible list never
274
+ * filtered, with no error and no indication anything happened. Reaching
275
+ * search required already knowing to press '/' or Up at the top row.
276
+ * Starting focused makes "just start typing" work the way every other
277
+ * picker in this codebase (and everywhere else) already behaves; Down
278
+ * still blurs search into list navigation (see handleModelPickerToken).
279
+ */
265
280
  openAllModels(models: ModelDefinition[], currentModelId: string): void {
266
281
  this.models = models;
267
282
  this.mode = 'model';
@@ -269,6 +284,7 @@ export class ModelPickerModal {
269
284
  this.pendingModel = null;
270
285
  this.focusPane = 'items';
271
286
  this.searchFocused = false;
287
+ this.focusSearch();
272
288
  this.query = '';
273
289
  this.categoryFilter = 'all';
274
290
  this.capabilityFilter = 'none';
@@ -279,7 +295,7 @@ export class ModelPickerModal {
279
295
  this.scrollOffset = 0;
280
296
  }
281
297
 
282
- /** Open showing providers first — entry point for /provider */
298
+ /** Open showing providers first — entry point for /provider. W3-T2: search starts focused — see openAllModels(). */
283
299
  openProviders(providers: string[], currentProvider: string): void {
284
300
  this.previousMode = null;
285
301
  this.providers = providers;
@@ -288,6 +304,7 @@ export class ModelPickerModal {
288
304
  this.pendingModel = null;
289
305
  this.focusPane = 'items';
290
306
  this.searchFocused = false;
307
+ this.focusSearch();
291
308
  this.query = '';
292
309
  this.categoryFilter = 'all';
293
310
  this.capabilityFilter = 'none';
@@ -27,6 +27,13 @@ export function runtimePortDiagnostic(
27
27
  if (status.mode === 'unavailable') {
28
28
  return `The configured endpoint ${status.baseUrl} is unavailable after startup or restart.${reason}`;
29
29
  }
30
+ if (status.mode === 'incompatible') {
31
+ // A GoodVibes daemon IS on the port, but it speaks an incompatible wire
32
+ // version, so this TUI neither adopted it nor started a competing daemon.
33
+ // status.reason already names the port and both versions (SDK side).
34
+ const version = status.version ? ` version ${status.version}` : '';
35
+ return `A GoodVibes service${version} is running at ${status.baseUrl} but is not wire-compatible with this TUI, so it was not adopted.${reason}`;
36
+ }
30
37
  if (status.mode === 'external') {
31
38
  const version = status.version ? ` version ${status.version}` : '';
32
39
  return `An existing GoodVibes service was verified at ${status.baseUrl}${version}.`;
@@ -62,7 +69,9 @@ export function isRuntimeEndpointOccupyingConfiguredPort(
62
69
  endpoint: OnboardingRuntimeEndpoint,
63
70
  ): boolean {
64
71
  const status = getRuntimeEndpointStatus(state, endpoint);
65
- if (status) return status.mode === 'embedded' || status.mode === 'external' || status.mode === 'blocked';
72
+ // 'incompatible' means a GoodVibes daemon holds the configured port (we just
73
+ // did not adopt it) — the port IS occupied, so report it as such.
74
+ if (status) return status.mode === 'embedded' || status.mode === 'external' || status.mode === 'blocked' || status.mode === 'incompatible';
66
75
  return endpoint === 'daemon'
67
76
  ? state?.daemonRunning === true || state?.daemonPortInUse === true
68
77
  : state?.httpListenerRunning === true || state?.httpListenerPortInUse === true;
@@ -85,7 +85,14 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
85
85
  setConfig('service.enabled', hasServers);
86
86
  setConfig('service.autostart', hasServers);
87
87
  setConfig('service.restartOnFailure', true);
88
- setConfig('danger.daemon', hasServers);
88
+ // One-Platform daemon-by-default (SDK docs/decisions/2026-07-05-daemon-by-default.md):
89
+ // the loopback-only cross-surface session daemon is safe-by-design (Wave 1:
90
+ // loopback bind, auth-gated, rate-limited) and runs regardless of whether
91
+ // browser/LAN/webhook/external-app capabilities are selected here — it is no
92
+ // longer bundled with those network-exposing surfaces, so onboarding leaves
93
+ // daemon.enabled untouched and lets the SDK's own default-true (or an existing
94
+ // explicit user override) govern. (The deprecated danger.daemon alias this
95
+ // comment used to also name was removed in Wave 6.)
89
96
  setConfig('controlPlane.enabled', hasServers);
90
97
  setConfig('danger.httpListener', httpListener);
91
98
  setConfig('web.enabled', browserAccess);
@@ -18,7 +18,10 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
18
18
  id: 'local-tui-only',
19
19
  label: 'Local TUI Only (No Servers)',
20
20
  selected: true,
21
- detail: 'Use GoodVibes only in this terminal. No browser access, background service, HTTP listener, external app surface, or network setup.',
21
+ // Honest wording (One-Platform daemon-by-default): this avoids browser/LAN/
22
+ // webhook/external-app exposure, but the loopback-only GoodVibes daemon
23
+ // still runs by default for cross-surface visibility on this machine.
24
+ detail: 'Use GoodVibes only in this terminal. No browser access, HTTP listener, external app surface, or network setup. A loopback-only background daemon still runs by default so sessions here stay visible to other GoodVibes surfaces on this machine — turn it off in Settings > daemon if you want none at all.',
22
25
  },
23
26
  {
24
27
  id: 'browser-access',
@@ -0,0 +1,136 @@
1
+ /**
2
+ * F1 (One-Platform Wave 2 friction fix): the Network step's daemon-source choice
3
+ * — start a new daemon owned by this TUI (default) vs connect to one that is
4
+ * already running elsewhere with a known token. Extracted from
5
+ * onboarding-wizard-steps.ts to keep that file under the architecture line cap.
6
+ */
7
+ import { legacyUnitNote, MANAGED_SERVICE_NAME } from '../../runtime/legacy-daemon-migration.ts';
8
+ import { normalizeText } from './onboarding-wizard-helpers.ts';
9
+ import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
10
+ import type { OnboardingWizardFieldDefinition } from './onboarding-wizard-types.ts';
11
+
12
+ export const DAEMON_SOURCE_FIELD_ID = 'network.daemon-source';
13
+
14
+ /** Current daemon-source selection ('start' default, or 'adopt'). */
15
+ export function getDaemonSource(controller: OnboardingWizardControllerLike): string {
16
+ return controller.getStringFieldValue(DAEMON_SOURCE_FIELD_ID, 'start');
17
+ }
18
+
19
+ /**
20
+ * Push the daemon-source radio, plus the host/port/token/connect fields when
21
+ * 'adopt' is selected, onto the Network step's field list. Independent of the
22
+ * local/custom network mode — adopting someone else's daemon is a client-side
23
+ * choice about which daemon THIS TUI talks to, not about how THIS TUI's own
24
+ * daemon binds to the network.
25
+ */
26
+ export function pushDaemonAdoptionFields(
27
+ fields: OnboardingWizardFieldDefinition[],
28
+ controller: OnboardingWizardControllerLike,
29
+ daemonSource: string,
30
+ bindSettings: { readonly controlPlane: { readonly host?: string | undefined; readonly port?: number | undefined } } | undefined,
31
+ ): void {
32
+ fields.push({
33
+ kind: 'radio',
34
+ id: DAEMON_SOURCE_FIELD_ID,
35
+ label: 'GoodVibes daemon source',
36
+ hint: 'Start a new daemon owned by this TUI (default), or connect to one that is already running elsewhere with a known token.',
37
+ options: [
38
+ { id: 'start', label: 'Start a new daemon', hint: 'This TUI starts and owns its own daemon (default).' },
39
+ { id: 'adopt', label: 'Connect to an existing running daemon', hint: 'Point this TUI at a daemon someone already started, using its host, port, and token.' },
40
+ ],
41
+ defaultValue: 'start',
42
+ });
43
+ if (daemonSource !== 'adopt') return;
44
+ fields.push(
45
+ {
46
+ kind: 'text',
47
+ id: 'network.adopt-daemon-host',
48
+ label: 'Existing daemon host',
49
+ hint: 'Host or IP address of the already-running GoodVibes daemon.',
50
+ placeholder: '127.0.0.1',
51
+ defaultValue: controller.getStringFieldValue('network.adopt-daemon-host', normalizeText(bindSettings?.controlPlane.host) || '127.0.0.1'),
52
+ },
53
+ {
54
+ kind: 'text',
55
+ id: 'network.adopt-daemon-port',
56
+ label: 'Existing daemon port',
57
+ hint: 'Port the already-running GoodVibes daemon is bound to.',
58
+ placeholder: '3421',
59
+ defaultValue: controller.getStringFieldValue('network.adopt-daemon-port', String(bindSettings?.controlPlane.port ?? 3421)),
60
+ },
61
+ {
62
+ kind: 'masked',
63
+ id: 'network.adopt-daemon-token',
64
+ label: 'Existing daemon token',
65
+ hint: "Bearer token that daemon was started with. Connecting writes it into this home directory's operator-tokens.json so this TUI authenticates as that daemon instead of minting its own.",
66
+ placeholder: 'gv_...',
67
+ defaultValue: controller.getStringFieldValue('network.adopt-daemon-token', ''),
68
+ },
69
+ {
70
+ kind: 'action',
71
+ id: 'network.adopt-daemon-connect',
72
+ label: 'Connect to this daemon now',
73
+ hint: 'Apply the host/port above, install the token, and verify the connection immediately (before Apply at the end of the wizard). Result prints to the feed below.',
74
+ action: 'connect-existing-daemon',
75
+ defaultValue: 'Connect',
76
+ },
77
+ );
78
+ }
79
+
80
+ /**
81
+ * W4-D1: the guided, visible entry point for migrating a detected legacy
82
+ * `goodvibes-daemon.service` unit — closes the fast-follow-up flagged when
83
+ * `handleMigrateLegacyDaemonServiceForHandler` (handler-onboarding-daemon-adopt.ts)
84
+ * shipped fully built but not wired to any onboarding control. Independent of
85
+ * the daemon-source radio above: this offers to retire an old unmanaged unit
86
+ * regardless of whether the wizard is set to start a new daemon or adopt an
87
+ * existing one.
88
+ *
89
+ * Visible ONLY when the runtime snapshot's read-only `legacyDaemon` detection
90
+ * (collected via `detectLegacyUnit`, see `../../runtime/legacy-daemon-migration.ts`)
91
+ * found a unit file present — never rendered speculatively. Confirm-gated on
92
+ * top of the engine's own consent requirement: a checklist toggle must be
93
+ * checked before the action executes the real migration (`confirmMigration:
94
+ * true`); left unchecked, pressing the action still works but only prints the
95
+ * engine's dry-run plan (`confirmMigration: false`) — see
96
+ * `runLegacyDaemonMigration`'s honest dry-run branch. The action label and
97
+ * hint change to make which mode is armed unambiguous before it is pressed.
98
+ */
99
+ export function pushLegacyDaemonMigrationFields(
100
+ fields: OnboardingWizardFieldDefinition[],
101
+ controller: OnboardingWizardControllerLike,
102
+ ): void {
103
+ const legacy = controller.runtimeSnapshot?.legacyDaemon;
104
+ if (!legacy || !legacy.present) return;
105
+
106
+ fields.push({
107
+ kind: 'status',
108
+ id: 'network.migrate-legacy-daemon-detected',
109
+ label: 'Legacy daemon service detected',
110
+ // F2 follow-up: name the unit this host actually resolves (carried on the
111
+ // snapshot from `resolveConfiguredServiceName` at collection time) — the
112
+ // MANAGED_SERVICE_NAME fallback only covers snapshots built without it.
113
+ hint: legacyUnitNote(legacy, legacy.trackedServiceName ?? MANAGED_SERVICE_NAME),
114
+ defaultValue: legacy.active ? 'Installed and running' : 'Installed (not active)',
115
+ });
116
+
117
+ const confirmFieldId = 'network.migrate-legacy-daemon-confirm';
118
+ const confirmed = controller.getBooleanFieldValue(confirmFieldId, false);
119
+ fields.push({
120
+ kind: 'checklist',
121
+ id: confirmFieldId,
122
+ label: 'Confirm migration execution',
123
+ hint: 'When checked, the action below performs the real migration: install and start the new managed service, verify it comes up healthy, and only then stop, disable, and remove the legacy unit. Leave unchecked to preview the plan without changing anything.',
124
+ defaultValue: false,
125
+ });
126
+ fields.push({
127
+ kind: 'action',
128
+ id: 'network.migrate-legacy-daemon',
129
+ label: confirmed ? 'Migrate legacy daemon service now' : 'Preview migration plan',
130
+ hint: confirmed
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
+ : 'Prints the migration plan without changing anything. Check "Confirm migration execution" above to arm the real migration.',
133
+ action: 'migrate-legacy-daemon-service',
134
+ defaultValue: confirmed ? 'Migrate' : 'Preview',
135
+ });
136
+ }
@@ -9,6 +9,7 @@ import {
9
9
  type ExternalSurfaceSpec,
10
10
  } from './onboarding-wizard-external-surfaces.ts';
11
11
  import { countSelected, modelSelectionLabel, normalizeText } from './onboarding-wizard-helpers.ts';
12
+ import { getDaemonSource, pushDaemonAdoptionFields, pushLegacyDaemonMigrationFields } from './onboarding-wizard-network-adopt.ts';
12
13
  import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
13
14
  import type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardActionFieldDefinition, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardRadioFieldDefinition, OnboardingWizardRadioOption, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
14
15
 
@@ -123,7 +124,7 @@ export function buildCapabilitiesStep(controller: OnboardingWizardControllerLike
123
124
  id: 'capabilities.clear',
124
125
  action: 'clear-capabilities',
125
126
  label: 'Use Local TUI Only (No Servers)',
126
- hint: 'Clear all server-backed capabilities and keep GoodVibes in this terminal only.',
127
+ hint: 'Clear all server-backed capabilities: no browser access, LAN reachability, webhooks/events, or external app surfaces. The loopback-only background daemon still runs by default.',
127
128
  defaultValue: 'Action',
128
129
  },
129
130
  ];
@@ -518,11 +519,7 @@ export function buildNetworkStep(controller: OnboardingWizardControllerLike): On
518
519
  const listenerEnabled = controller.shouldExposeHttpListenerNetworkFields();
519
520
  const listenerWillApply = controller.shouldEnableHttpListener();
520
521
  const controlPlaneRemote = controller.shouldExposeControlPlaneNetwork();
521
- const networkEnabled = {
522
- controlPlane: controlPlaneRemote,
523
- httpListener: listenerEnabled,
524
- web: browserEnabled,
525
- };
522
+ const networkEnabled = { controlPlane: controlPlaneRemote, httpListener: listenerEnabled, web: browserEnabled };
526
523
  const mode = controller.getStringFieldValue('network.mode', controller.runtimeDerived.step1_5NetworkMode);
527
524
  const custom = mode === 'custom';
528
525
  const fields: OnboardingWizardFieldDefinition[] = [
@@ -536,6 +533,11 @@ export function buildNetworkStep(controller: OnboardingWizardControllerLike): On
536
533
  },
537
534
  ];
538
535
 
536
+ // F1: recognize an adoptable already-running daemon (see onboarding-wizard-network-adopt.ts).
537
+ const daemonSource = getDaemonSource(controller);
538
+ pushDaemonAdoptionFields(fields, controller, daemonSource, bindSettings);
539
+ pushLegacyDaemonMigrationFields(fields, controller);
540
+
539
541
  if (custom) {
540
542
  const sharedIpField: OnboardingWizardChecklistFieldDefinition = {
541
543
  kind: 'checklist',
@@ -635,6 +637,7 @@ export function buildNetworkStep(controller: OnboardingWizardControllerLike): On
635
637
  summaryTitle: 'Bind posture',
636
638
  summaryLines: [
637
639
  `Mode: ${custom ? 'custom' : 'local network default'}`,
640
+ `Daemon source: ${daemonSource === 'adopt' ? 'connect to an existing running daemon' : 'start a new daemon'}`,
638
641
  `Browser surface: ${browserEnabled ? 'enabled' : 'not selected'}`,
639
642
  `HTTP listener: ${listenerWillApply ? 'enabled' : listenerEnabled ? 'available for selected external apps' : 'not selected'}`,
640
643
  ],
@@ -53,7 +53,9 @@ export type OnboardingWizardAction =
53
53
  | 'cloudflare-verify'
54
54
  | 'cloudflare-disable'
55
55
  | 'start-openai-subscription'
56
- | 'finish-openai-subscription';
56
+ | 'finish-openai-subscription'
57
+ | 'connect-existing-daemon'
58
+ | 'migrate-legacy-daemon-service';
57
59
 
58
60
  export type OnboardingWizardApplyFeedbackSeverity = 'info' | 'warning' | 'error';
59
61
 
@@ -0,0 +1,97 @@
1
+ // ---------------------------------------------------------------------------
2
+ // panel-mouse-geometry.ts
3
+ //
4
+ // Pure mouse-geometry helpers for the panel workspace, extracted from
5
+ // handler-feed-routes.ts so that file stays under the 800-line architecture
6
+ // cap once the UX-C focus verbs and the DEBT-5 paste-flood guard both live in
7
+ // it. These map a terminal (row, col) onto the panel/pane/workspace-tab under
8
+ // the cursor. No dependency on the feed-route state types, so no import cycle.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ import type { PanelManager } from '../panels/panel-manager.ts';
12
+ import { renderPanelWorkspaceBar } from '../renderer/panel-workspace-bar.ts';
13
+ import type { TabHitRegion } from '../renderer/tab-strip.ts';
14
+
15
+ /** Screen rectangle the panel workspace occupies, plus its vertical split. */
16
+ export type PanelMouseLayout = {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ height: number;
21
+ hasBottomPane: boolean;
22
+ verticalSplitRatio: number;
23
+ };
24
+
25
+ function clampRatio(value: number): number {
26
+ return Math.max(0.2, Math.min(0.8, value));
27
+ }
28
+
29
+ export function getActivePanelInPane(panelManager: PanelManager, pane: 'top' | 'bottom') {
30
+ const target = pane === 'top' ? panelManager.getTopPane() : panelManager.getBottomPane();
31
+ return target.panels[target.activeIndex] ?? null;
32
+ }
33
+
34
+ export function getPanelUnderMouse(
35
+ panelManager: PanelManager,
36
+ layout: PanelMouseLayout | null,
37
+ row: number,
38
+ col: number,
39
+ ) {
40
+ if (
41
+ layout === null
42
+ || !panelManager.isVisible()
43
+ || panelManager.getAllOpen().length === 0
44
+ || col < layout.x
45
+ || col >= layout.x + layout.width
46
+ || row < layout.y
47
+ || row >= layout.y + layout.height
48
+ ) {
49
+ return null;
50
+ }
51
+
52
+ const panelRow = row - layout.y;
53
+ if (!layout.hasBottomPane) {
54
+ return getActivePanelInPane(panelManager, 'top');
55
+ }
56
+
57
+ // Single consolidated workspace bar (row 0) + h-separator; the rest splits
58
+ // between the two panes' content.
59
+ const panelAreaRows = Math.max(0, layout.height - 1);
60
+ const contentRows = Math.max(0, panelAreaRows - 1);
61
+ const topContentRows = contentRows <= 1
62
+ ? contentRows
63
+ : Math.max(1, Math.floor(contentRows * clampRatio(layout.verticalSplitRatio)));
64
+ // panelRow 0 = workspace bar; rows 1..topContentRows = top pane; rest = bottom.
65
+ return panelRow <= topContentRows
66
+ ? getActivePanelInPane(panelManager, 'top')
67
+ : getActivePanelInPane(panelManager, 'bottom');
68
+ }
69
+
70
+ /**
71
+ * If the mouse is over the consolidated workspace tab bar (the first panel
72
+ * row), return the index of the tab under the cursor, else null. Recomputes the
73
+ * tab hit regions by rendering the bar with a layout callback — cheap and keeps
74
+ * the click geometry in lockstep with what was drawn.
75
+ */
76
+ export function workspaceTabAtMouse(
77
+ panelManager: PanelManager,
78
+ layout: PanelMouseLayout | null,
79
+ row: number,
80
+ col: number,
81
+ ): number | null {
82
+ if (
83
+ layout === null
84
+ || !panelManager.isVisible()
85
+ || panelManager.getAllOpen().length === 0
86
+ || row !== layout.y // workspace bar is the first panel row
87
+ || col < layout.x
88
+ || col >= layout.x + layout.width
89
+ ) {
90
+ return null;
91
+ }
92
+ let regions: readonly TabHitRegion[] = [];
93
+ renderPanelWorkspaceBar(panelManager.getWorkspaceTabs(), layout.width, true, (r) => { regions = r; });
94
+ const relCol = col - layout.x;
95
+ const hit = regions.find((rg) => relCol >= rg.startCol && relCol < rg.endCol);
96
+ return hit ? hit.index : null;
97
+ }