@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.
- package/CHANGELOG.md +11 -0
- package/README.md +10 -9
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/config/credential-availability.ts +122 -0
- package/src/config/index.ts +15 -0
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +71 -14
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +68 -1
- package/src/core/turn-cancellation.ts +7 -2
- package/src/core/turn-event-wiring.ts +10 -2
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +46 -4
- package/src/input/commands/codebase-runtime.ts +46 -6
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/health-runtime.ts +9 -1
- package/src/input/commands/memory.ts +68 -35
- package/src/input/commands/operator-panel-runtime.ts +31 -7
- package/src/input/commands/planning-runtime.ts +95 -6
- package/src/input/commands/qrcode-runtime.ts +25 -5
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/settings-sync-runtime.ts +15 -3
- package/src/input/commands/shell-core.ts +10 -1
- package/src/input/commands/workstream-runtime.ts +168 -18
- package/src/input/config-modal-types.ts +15 -1
- package/src/input/config-modal.ts +227 -12
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +10 -3
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +43 -121
- package/src/input/handler-feed.ts +32 -4
- package/src/input/handler-modal-routes.ts +78 -13
- package/src/input/handler-modal-stack.ts +11 -2
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +15 -8
- package/src/input/handler-shortcuts.ts +59 -9
- package/src/input/handler.ts +6 -1
- package/src/input/keybindings.ts +6 -5
- package/src/input/model-picker.ts +19 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +51 -2
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +28 -27
- package/src/panels/builtin/shared.ts +9 -3
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +89 -131
- package/src/panels/fleet-read-model.ts +43 -0
- package/src/panels/fleet-steer.ts +35 -2
- package/src/panels/fleet-stop.ts +29 -1
- package/src/panels/modals/keybindings-modal.ts +16 -1
- package/src/panels/modals/modal-theme.ts +35 -29
- package/src/panels/modals/pairing-modal.ts +25 -6
- package/src/panels/modals/planning-modal.ts +43 -21
- package/src/panels/modals/work-plan-modal.ts +28 -0
- package/src/panels/panel-manager.ts +15 -4
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -13
- package/src/panels/types.ts +24 -0
- package/src/permissions/prompt.ts +160 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +19 -5
- package/src/renderer/footer-tips.ts +5 -1
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +3 -1
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +18 -3
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +22 -3
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +8 -4
- package/src/runtime/bootstrap-core.ts +46 -24
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +19 -1
- package/src/runtime/bootstrap.ts +118 -5
- package/src/runtime/code-index-services.ts +25 -2
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +55 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +10 -0
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +12 -8
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +19 -3
- package/src/runtime/workstream-services.ts +160 -28
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +46 -3
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +42 -9
- package/src/utils/terminal-width.ts +52 -0
- 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:
|
|
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),
|
|
177
|
-
|
|
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
|
+
}
|
package/src/input/handler.ts
CHANGED
|
@@ -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 {
|
package/src/input/keybindings.ts
CHANGED
|
@@ -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
|
|
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:
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|