@pellux/goodvibes-tui 0.19.24 → 0.19.26

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 (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/bin/goodvibes +10 -0
  4. package/bin/goodvibes-daemon +10 -0
  5. package/docs/foundation-artifacts/operator-contract.json +1 -1
  6. package/package.json +3 -2
  7. package/src/cli/bundle-command.ts +225 -0
  8. package/src/cli/completion.ts +90 -0
  9. package/src/cli/config-overrides.ts +159 -0
  10. package/src/cli/endpoints.ts +63 -0
  11. package/src/cli/entrypoint.ts +169 -0
  12. package/src/cli/help.ts +301 -0
  13. package/src/cli/index.ts +11 -0
  14. package/src/cli/management-commands.ts +426 -0
  15. package/src/cli/management.ts +719 -0
  16. package/src/cli/network-posture.ts +46 -0
  17. package/src/cli/package-verification.ts +119 -0
  18. package/src/cli/parser.ts +369 -0
  19. package/src/cli/provider-classification.ts +107 -0
  20. package/src/cli/redaction.ts +105 -0
  21. package/src/cli/service-command.ts +45 -0
  22. package/src/cli/service-posture.ts +247 -0
  23. package/src/cli/status.ts +382 -0
  24. package/src/cli/surface-command.ts +248 -0
  25. package/src/cli/tui-startup.ts +32 -0
  26. package/src/cli/types.ts +69 -0
  27. package/src/cli-flags.ts +18 -55
  28. package/src/config/index.ts +1 -1
  29. package/src/config/secrets.ts +44 -0
  30. package/src/daemon/cli.ts +62 -11
  31. package/src/input/command-registry.ts +3 -0
  32. package/src/input/commands/guidance-runtime.ts +9 -4
  33. package/src/input/commands/local-runtime.ts +21 -7
  34. package/src/input/commands/local-setup.ts +31 -38
  35. package/src/input/commands/onboarding-runtime.ts +14 -0
  36. package/src/input/commands/runtime-services.ts +9 -0
  37. package/src/input/commands.ts +2 -0
  38. package/src/input/feed-context-factory.ts +8 -1
  39. package/src/input/handler-feed.ts +13 -8
  40. package/src/input/handler-interactions.ts +266 -0
  41. package/src/input/handler-modal-stack.ts +23 -3
  42. package/src/input/handler-modal-token-routes.ts +23 -1
  43. package/src/input/handler-onboarding.ts +696 -0
  44. package/src/input/handler-picker-routes.ts +15 -7
  45. package/src/input/handler-ui-state.ts +58 -0
  46. package/src/input/handler.ts +120 -246
  47. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  48. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  49. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  50. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  51. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  52. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  53. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  54. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  55. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  56. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  57. package/src/main.ts +32 -39
  58. package/src/panels/builtin/operations.ts +0 -10
  59. package/src/panels/index.ts +0 -1
  60. package/src/renderer/conversation-overlays.ts +6 -0
  61. package/src/renderer/help-overlay.ts +1 -1
  62. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  63. package/src/runtime/bootstrap-core.ts +1 -0
  64. package/src/runtime/bootstrap.ts +123 -0
  65. package/src/runtime/onboarding/apply.ts +685 -0
  66. package/src/runtime/onboarding/derivation.ts +495 -0
  67. package/src/runtime/onboarding/index.ts +7 -0
  68. package/src/runtime/onboarding/markers.ts +161 -0
  69. package/src/runtime/onboarding/snapshot.ts +400 -0
  70. package/src/runtime/onboarding/state.ts +140 -0
  71. package/src/runtime/onboarding/types.ts +402 -0
  72. package/src/runtime/onboarding/verify.ts +233 -0
  73. package/src/runtime/ui-services.ts +16 -0
  74. package/src/shell/ui-openers.ts +12 -2
  75. package/src/version.ts +1 -1
  76. package/src/panels/welcome-panel.ts +0 -64
@@ -53,6 +53,7 @@ import { registerLocalAuthRuntimeCommands } from './commands/local-auth-runtime.
53
53
  import { registerIntelligenceRuntimeCommands } from './commands/intelligence-runtime.ts';
54
54
  import { registerConversationRuntimeCommands } from './commands/conversation-runtime.ts';
55
55
  import { registerQrcodeRuntimeCommands } from './commands/qrcode-runtime.ts';
56
+ import { registerOnboardingRuntimeCommands } from './commands/onboarding-runtime.ts';
56
57
 
57
58
  /**
58
59
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -100,6 +101,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
100
101
  registerIntelligenceRuntimeCommands(registry);
101
102
  registerConversationRuntimeCommands(registry);
102
103
  registerQrcodeRuntimeCommands(registry);
104
+ registerOnboardingRuntimeCommands(registry);
103
105
  registerLocalRuntimeCommands(registry);
104
106
  registerSessionWorkflowCommands(registry);
105
107
  registerDiscoveryRuntimeCommands(registry);
@@ -31,10 +31,12 @@ import type { BookmarkModal } from './bookmark-modal.ts';
31
31
  import type { SettingsModal } from './settings-modal.ts';
32
32
  import type { SessionPickerModal } from './session-picker-modal.ts';
33
33
  import type { ProfilePickerModal } from './profile-picker-modal.ts';
34
+ import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
34
35
  import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
35
36
  import type { Panel } from '../panels/types.ts';
36
37
  import type { PanelManager } from '../panels/panel-manager.ts';
37
38
  import type { KeybindingsManager } from './keybindings.ts';
39
+ import type { ModelPickerTarget } from './model-picker.ts';
38
40
 
39
41
  /**
40
42
  * Initial mutable scalar values for InputFeedContext.
@@ -77,7 +79,7 @@ export interface FeedContextMutableInit {
77
79
  * `profilePickerModal` — modal objects constructed once
78
80
  * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`,
79
81
  * `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`,
80
- * `searchManager`, `historySearch` — service objects constructed once
82
+ * `searchManager`, `historySearch`, `onboardingWizard` — service objects constructed once
81
83
  * - `panelManager`, `keybindingsManager` — from uiServices, stable
82
84
  * - `modalStack` — reference to the handler's shared array
83
85
  * - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — callbacks
@@ -104,6 +106,7 @@ export interface FeedContextStableRefs {
104
106
  autocomplete: AutocompleteEngine | null;
105
107
  filePicker: FilePickerModal;
106
108
  modelPicker: ModelPickerModal;
109
+ onboardingWizard: OnboardingWizardController;
107
110
  processModal: ProcessModal;
108
111
  liveTailModal: LiveTailModal;
109
112
  agentDetailModal: AgentDetailModal;
@@ -148,6 +151,9 @@ export interface FeedContextClosures {
148
151
  findMarkerAtPos: (pos: number) => { start: number; end: number } | null;
149
152
  cleanupMarkerRegistry: (text: string) => void;
150
153
  expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
154
+ openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
155
+ onModelPickerCommit: () => boolean;
156
+ onOnboardingAction: (action: import('./onboarding/onboarding-wizard.ts').OnboardingWizardAction) => void;
151
157
  }
152
158
 
153
159
  /**
@@ -233,4 +239,5 @@ export function syncFeedContextMutableFields(
233
239
  ctx.nextImageId = fields.nextImageId;
234
240
  ctx.mouseDownRow = fields.mouseDownRow;
235
241
  ctx.mouseDownCol = fields.mouseDownCol;
242
+ ctx.contentWidth = fields.contentWidth;
236
243
  }
@@ -18,6 +18,8 @@ import { BookmarkModal } from './bookmark-modal.ts';
18
18
  import { SettingsModal } from './settings-modal.ts';
19
19
  import { SessionPickerModal } from './session-picker-modal.ts';
20
20
  import { ProfilePickerModal } from './profile-picker-modal.ts';
21
+ import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
22
+ import type { OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
21
23
  import {
22
24
  IMAGE_EXTENSIONS,
23
25
  formatFileSize,
@@ -39,6 +41,7 @@ import { handlePanelIntegrationAction } from './panel-integration-actions.ts';
39
41
  import { SelectionManager } from './selection.ts';
40
42
  import type { PanelManager } from '../panels/panel-manager.ts';
41
43
  import type { KeybindingsManager } from './keybindings.ts';
44
+ import type { ModelPickerTarget } from './model-picker.ts';
42
45
 
43
46
  /**
44
47
  * InputFeedContext — The single long-lived context object passed to feedInputTokens
@@ -66,8 +69,8 @@ import type { KeybindingsManager } from './keybindings.ts';
66
69
  * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
67
70
  * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
68
71
  * `profilePickerModal` — modal objects constructed once in InputHandler constructor
69
- * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`, `agentDetailModal`,
70
- * `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
72
+ * - `filePicker`, `modelPicker`, `onboardingWizard`, `processModal`, `liveTailModal`,
73
+ * `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
71
74
  * service objects constructed once
72
75
  * - `panelManager`, `keybindingsManager` — from uiServices, stable for app lifetime
73
76
  * - `modalStack` — reference to the handler's shared array (mutated in place)
@@ -108,6 +111,7 @@ export interface InputFeedContext {
108
111
  autocomplete: AutocompleteEngine | null;
109
112
  readonly filePicker: FilePickerModal;
110
113
  readonly modelPicker: ModelPickerModal;
114
+ readonly onboardingWizard: OnboardingWizardController;
111
115
  readonly processModal: ProcessModal;
112
116
  readonly liveTailModal: LiveTailModal;
113
117
  readonly agentDetailModal: AgentDetailModal;
@@ -148,6 +152,9 @@ export interface InputFeedContext {
148
152
  readonly findMarkerAtPos: (pos: number) => { start: number; end: number } | null;
149
153
  readonly cleanupMarkerRegistry: (text: string) => void;
150
154
  readonly expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
155
+ readonly openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
156
+ readonly onModelPickerCommit: () => boolean;
157
+ readonly onOnboardingAction: (action: OnboardingWizardAction) => void;
151
158
  readonly exitApp: () => void;
152
159
  }
153
160
 
@@ -173,6 +180,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
173
180
  settingsModal: context.settingsModal,
174
181
  sessionPickerModal: context.sessionPickerModal,
175
182
  profilePickerModal: context.profilePickerModal,
183
+ onboardingWizard: context.onboardingWizard,
176
184
  helpOverlayActive: context.helpOverlayActive,
177
185
  helpScrollOffset: context.helpScrollOffset,
178
186
  shortcutsOverlayActive: context.shortcutsOverlayActive,
@@ -204,12 +212,9 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
204
212
  searchManager: context.searchManager,
205
213
  scroll: context.scroll,
206
214
  getScrollTop: context.getScrollTop,
207
- openModelPickerWithTarget: context.commandContext?.openModelPicker
208
- ? (target: import('./model-picker.ts').ModelPickerTarget) => {
209
- context.modelPicker.target = target;
210
- context.commandContext!.openModelPicker!();
211
- }
212
- : undefined,
215
+ openModelPickerWithTarget: context.openModelPickerWithTarget,
216
+ onModelPickerCommit: context.onModelPickerCommit,
217
+ onOnboardingAction: context.onOnboardingAction,
213
218
  }, token);
214
219
  context.selectionCallback = modalRoute.selectionCallback;
215
220
  context.helpOverlayActive = modalRoute.helpOverlayActive;
@@ -0,0 +1,266 @@
1
+ import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
2
+ import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
3
+ import { collectOnboardingSnapshot } from '../runtime/onboarding/index.ts';
4
+ import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy, handleBlockRerun, handleBlockSave, handleBlockToggle, handleBookmark, handleClipboardPaste, handleCopy, handleCtrlC, handleDiffApply, registerPaste } from './handler-content-actions.ts';
5
+ import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
6
+ import { openOnboardingWizardState, type OpenOnboardingWizardOptions } from './handler-ui-state.ts';
7
+ import type { InputHandler } from './handler.ts';
8
+
9
+ export function openOnboardingWizardForHandler(
10
+ handler: InputHandler,
11
+ modeOrOptions: OnboardingWizardMode | OpenOnboardingWizardOptions = 'new',
12
+ ): void {
13
+ const options = typeof modeOrOptions === 'string' ? { mode: modeOrOptions } : modeOrOptions;
14
+ if (!handler.modalStack.includes('onboarding')) handler.modalOpened('onboarding');
15
+ handler.clearOnboardingModelPickerCancelState();
16
+ openOnboardingWizardState(handler.onboardingWizard, options);
17
+ const hydrationSerial = ++handler.onboardingHydrationSerial;
18
+ if (options.preload === undefined) {
19
+ handler.onboardingWizard.beginRuntimeHydration();
20
+ void handler.hydrateOnboardingWizardFromRuntime(hydrationSerial);
21
+ }
22
+ handler.requestRender();
23
+ }
24
+
25
+ export async function hydrateOnboardingWizardFromRuntimeForHandler(handler: InputHandler, hydrationSerial: number): Promise<void> {
26
+ try {
27
+ const snapshot = await collectOnboardingSnapshot({
28
+ config: handler.uiServices.platform.configManager,
29
+ shellPaths: handler.uiServices.environment.shellPaths,
30
+ acknowledgementScope: 'project',
31
+ subscriptions: handler.uiServices.platform.subscriptionManager,
32
+ secrets: handler.uiServices.platform.secretsManager,
33
+ auth: handler.uiServices.platform.localUserAuthManager,
34
+ services: handler.uiServices.platform.serviceRegistry,
35
+ surfaces: {
36
+ list: () => handler.uiServices.platform.surfaceRegistry.syncConfiguredSurfaces(),
37
+ },
38
+ providerAccounts: {
39
+ loadSnapshot: () => buildProviderAccountSnapshot({
40
+ providerRegistry: handler.uiServices.providers.providerRegistry,
41
+ serviceRegistry: handler.uiServices.platform.serviceRegistry,
42
+ subscriptionManager: handler.uiServices.platform.subscriptionManager,
43
+ secretsManager: handler.uiServices.platform.secretsManager,
44
+ }),
45
+ },
46
+ });
47
+ if (!handler.onboardingWizard.active || hydrationSerial !== handler.onboardingHydrationSerial) return;
48
+ handler.onboardingWizard.hydrateRuntimeState({ snapshot }, { resetValues: true });
49
+ handler.requestRender();
50
+ } catch (error) {
51
+ if (!handler.onboardingWizard.active || hydrationSerial !== handler.onboardingHydrationSerial) return;
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ handler.onboardingWizard.failRuntimeHydration(message);
54
+ handler.commandContext?.print?.(`Onboarding runtime snapshot failed: ${message}`);
55
+ handler.requestRender();
56
+ }
57
+ }
58
+
59
+ export function registerPasteForHandler(handler: InputHandler, content: string): string {
60
+ const result = registerPaste({
61
+ pasteRegistry: handler.pasteRegistry,
62
+ nextPasteId: handler.nextPasteId,
63
+ imageRegistry: handler.imageRegistry,
64
+ nextImageId: handler.nextImageId,
65
+ }, content, handler.uiServices.environment.shellPaths.workingDirectory);
66
+ handler.nextPasteId = result.nextPasteId;
67
+ handler.nextImageId = result.nextImageId;
68
+ return result.marker;
69
+ }
70
+
71
+ /**
72
+ * expandPrompt - Replaces paste markers with actual content.
73
+ * If image markers are present, returns ContentPart[] for multimodal delivery.
74
+ * Otherwise returns a plain string.
75
+ */
76
+ export function expandPromptForHandler(handler: InputHandler, text: string) {
77
+ return expandPrompt(handler.pasteRegistry, handler.imageRegistry, text, handler.uiServices.environment.shellPaths.workingDirectory);
78
+ }
79
+
80
+ /**
81
+ * getImageAttachments - Returns a copy of the current image registry.
82
+ * Callers can use this to attach images when building LLM messages.
83
+ */
84
+ export function getImageAttachmentsForHandler(handler: InputHandler): Map<string, { data: string; mediaType: string }> {
85
+ return new Map(handler.imageRegistry);
86
+ }
87
+
88
+ /**
89
+ * findMarkerAtPos - Returns the start/end of an atomic marker if pos is inside one.
90
+ * Used to make backspace/delete/arrow treat markers as single units.
91
+ */
92
+ /**
93
+ * cleanupMarkerRegistry - If the given marker text is an IMAGE marker,
94
+ * parses its ID and removes it from imageRegistry.
95
+ */
96
+ export function cleanupMarkerRegistryForHandler(handler: InputHandler, markerText: string): void {
97
+ cleanupMarkerRegistry(handler.imageRegistry, markerText);
98
+ }
99
+
100
+ export function findMarkerAtPosForHandler(handler: InputHandler, pos: number): { start: number; end: number } | null {
101
+ return findMarkerAtPos(handler.prompt, pos);
102
+ }
103
+
104
+ export function handleCopyForHandler(handler: InputHandler): void {
105
+ handleCopy(handler.selection, handler.getHistory, handler.requestRender, () => {
106
+ handler.lastCopyTime = Date.now();
107
+ });
108
+ }
109
+
110
+ /**
111
+ * handleBlockCopy - Ctrl+Y: Copy the content of the nearest code/tool block.
112
+ */
113
+ export function handleBlockCopyForHandler(handler: InputHandler): void {
114
+ handleBlockCopy(handler.conversationManager, handler.getScrollTop, handler.requestRender, () => {
115
+ handler.lastBlockCopyTime = Date.now();
116
+ });
117
+ }
118
+
119
+ /**
120
+ * handleBookmark - Ctrl+B: Toggle bookmark on the nearest block.
121
+ */
122
+ export function handleBookmarkForHandler(handler: InputHandler): void {
123
+ handleBookmark(handler.conversationManager, handler.getScrollTop, handler.requestRender, handler.uiServices.shell.bookmarkManager);
124
+ }
125
+
126
+ /**
127
+ * handleBlockSave - Ctrl+S: Save nearest block content to a file.
128
+ */
129
+ export function handleBlockSaveForHandler(handler: InputHandler): void {
130
+ handleBlockSave(handler.conversationManager, handler.getScrollTop, handler.requestRender, handler.uiServices.shell.bookmarkManager);
131
+ }
132
+
133
+ /**
134
+ * executeBlockAction - Execute a block action ID on the nearest block.
135
+ * Called when the user selects an action from the BlockActionsMenu.
136
+ */
137
+ export function executeBlockActionForHandler(handler: InputHandler, actionId: string): void {
138
+ switch (actionId) {
139
+ case 'copy': handler.handleBlockCopy(); break;
140
+ case 'bookmark': handler.handleBookmark(); break;
141
+ case 'toggle': handler.handleBlockToggle(); break;
142
+ case 'apply': handler.handleDiffApply(); break;
143
+ case 'rerun': handler.handleBlockRerun(); break;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * handleBlockRerun - Re-run the tool call for the nearest tool block.
149
+ * Emits a tool-rerun event for the orchestrator to handle.
150
+ */
151
+ export function handleBlockRerunForHandler(handler: InputHandler): void {
152
+ handleBlockRerun(handler.conversationManager, handler.getScrollTop, handler.requestRender);
153
+ }
154
+
155
+ /**
156
+ * handleBlockToggle - Tab (non-command mode): Toggle collapse of nearest block.
157
+ */
158
+ export function handleBlockToggleForHandler(handler: InputHandler): void {
159
+ handleBlockToggle(handler.conversationManager, handler.getScrollTop, handler.requestRender);
160
+ }
161
+
162
+ /**
163
+ * handleDiffApply - Ctrl+A when a diff block is nearest: request approval and apply the diff.
164
+ * Returns true if a diff was found and applied (so caller can skip default Ctrl+A).
165
+ */
166
+ export function handleDiffApplyForHandler(handler: InputHandler): boolean {
167
+ return handleDiffApply(
168
+ handler.conversationManager,
169
+ handler.getScrollTop,
170
+ handler.commandContext,
171
+ handler.requestRender,
172
+ () => `diff-apply-${Date.now()}`,
173
+ 'write',
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Handle Ctrl+C:
179
+ * - If prompt has text: clear it
180
+ * - If prompt is empty and LLM is thinking: cancel generation
181
+ * - If prompt is empty and idle: show exit notice (double = exit)
182
+ */
183
+ export function handleCtrlCForHandler(handler: InputHandler): void {
184
+ handleCtrlC(
185
+ handler.prompt,
186
+ () => handler.saveUndoState(),
187
+ (value) => { handler.prompt = value; },
188
+ (value) => { handler.cursorPos = value; },
189
+ handler.commandContext?.cancelGeneration,
190
+ handler.exitApp,
191
+ handler.requestRender,
192
+ handler.lastCtrlCTime,
193
+ (value) => { handler.lastCtrlCTime = value; },
194
+ (value) => { handler.showExitNotice = value; },
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Handle Escape:
200
+ * - If prompt has text: clear it
201
+ * - If prompt is empty: cancel generation (double-tap not needed)
202
+ */
203
+ /**
204
+ * Record that a modal has been opened and push it onto the navigation stack.
205
+ * Call this EVERY time a modal opens (except inside openModal()).
206
+ *
207
+ * @param name - The modal identifier (e.g. 'settings', 'help', 'process').
208
+ */
209
+ export function modalOpenedForHandler(handler: InputHandler, name: string): void {
210
+ modalOpened(handler, name);
211
+ }
212
+
213
+ /**
214
+ * Clear the modal navigation stack on non-modal user input (e.g. submit).
215
+ */
216
+ export function clearModalStackForHandler(handler: InputHandler): void {
217
+ clearModalStack(handler.modalStack);
218
+ }
219
+
220
+ export function handleEscapeForHandler(handler: InputHandler): void {
221
+ const result = handleEscape({
222
+ helpOverlayActive: handler.helpOverlayActive,
223
+ shortcutsOverlayActive: handler.shortcutsOverlayActive,
224
+ bookmarkModal: handler.bookmarkModal,
225
+ agentDetailModal: handler.agentDetailModal,
226
+ liveTailModal: handler.liveTailModal,
227
+ settingsModal: handler.settingsModal,
228
+ sessionPickerModal: handler.sessionPickerModal,
229
+ profilePickerModal: handler.profilePickerModal,
230
+ contextInspectorModal: handler.contextInspectorModal,
231
+ processModal: handler.processModal,
232
+ modelPicker: handler.modelPicker,
233
+ filePicker: handler.filePicker,
234
+ blockActionsMenu: handler.blockActionsMenu,
235
+ selectionModal: handler.selectionModal,
236
+ onboardingWizard: handler.onboardingWizard,
237
+ commandMode: handler.commandMode,
238
+ modalStack: handler.modalStack,
239
+ modalReturnFocus: handler.modalReturnFocus,
240
+ panelFocused: handler.panelFocused,
241
+ indicatorFocused: handler.indicatorFocused,
242
+ prompt: handler.prompt,
243
+ cursorPos: handler.cursorPos,
244
+ requestRender: handler.requestRender,
245
+ saveUndoState: () => handler.saveUndoState(),
246
+ cancelGeneration: handler.commandContext?.cancelGeneration,
247
+ selectionCallback: handler.selectionCallback,
248
+ autocompleteReset: () => handler.autocomplete?.reset(),
249
+ autocompleteUpdate: (query: string) => handler.autocomplete?.update(query),
250
+ helpScrollOffset: handler.helpScrollOffset,
251
+ shortcutsScrollOffset: handler.shortcutsScrollOffset,
252
+ clearOnboardingModelPickerCancelState: () => handler.clearOnboardingModelPickerCancelState(),
253
+ restoreOnboardingModelPickerCancelState: () => handler.restoreOnboardingModelPickerCancelState(),
254
+ });
255
+ handler.prompt = result.prompt;
256
+ handler.cursorPos = result.cursorPos;
257
+ handler.commandMode = result.commandMode;
258
+ handler.helpOverlayActive = result.helpOverlayActive;
259
+ handler.helpScrollOffset = result.helpScrollOffset;
260
+ handler.shortcutsOverlayActive = result.shortcutsOverlayActive;
261
+ handler.shortcutsScrollOffset = result.shortcutsScrollOffset;
262
+ handler.selectionCallback = result.selectionCallback;
263
+ handler.panelFocused = result.panelFocused;
264
+ handler.indicatorFocused = result.indicatorFocused;
265
+ handler.modalReturnFocus = result.modalReturnFocus;
266
+ }
@@ -17,6 +17,8 @@ export function modalOpened(state: ModalStackState, name: string): void {
17
17
  if (getActiveModalName(state) === null && state.modalStack.length > 0) {
18
18
  state.modalStack.length = 0;
19
19
  }
20
+ if (state.modalStack[state.modalStack.length - 1] === name) return;
21
+ if (state.modalStack.includes(name)) return;
20
22
  if (state.modalStack.length === 0) {
21
23
  state.modalReturnFocus = state.indicatorFocused ? 'indicator' : state.panelFocused ? 'panel' : 'prompt';
22
24
  }
@@ -46,6 +48,8 @@ export type EscapeState = ModalStackState & {
46
48
  selectionModal: ModalStackState['selectionModal'];
47
49
  autocompleteReset: () => void;
48
50
  autocompleteUpdate?: (query: string) => void;
51
+ clearOnboardingModelPickerCancelState?: () => void;
52
+ restoreOnboardingModelPickerCancelState?: () => void;
49
53
  };
50
54
 
51
55
  export function handleEscape(state: EscapeState): {
@@ -59,6 +63,7 @@ export function handleEscape(state: EscapeState): {
59
63
  selectionCallback: ((result: SelectionResult | null) => void) | null;
60
64
  panelFocused: boolean;
61
65
  indicatorFocused: boolean;
66
+ modalReturnFocus: NonNullable<ModalStackState['modalReturnFocus']>;
62
67
  } {
63
68
  let prompt = state.prompt;
64
69
  let cursorPos = state.cursorPos;
@@ -70,6 +75,7 @@ export function handleEscape(state: EscapeState): {
70
75
  let selectionCallback = state.selectionCallback;
71
76
  let panelFocused = state.panelFocused;
72
77
  let indicatorFocused = state.indicatorFocused;
78
+ let modalReturnFocus: NonNullable<ModalStackState['modalReturnFocus']> = state.modalReturnFocus ?? 'prompt';
73
79
 
74
80
  const restoreFocus = (): void => {
75
81
  if (state.modalStack.length > 0 || getActiveModalName({
@@ -78,8 +84,9 @@ export function handleEscape(state: EscapeState): {
78
84
  shortcutsOverlayActive,
79
85
  commandMode,
80
86
  }) !== null) return;
81
- panelFocused = state.modalReturnFocus === 'panel';
82
- indicatorFocused = state.modalReturnFocus === 'indicator';
87
+ panelFocused = modalReturnFocus === 'panel';
88
+ indicatorFocused = modalReturnFocus === 'indicator';
89
+ modalReturnFocus = 'prompt';
83
90
  state.modalReturnFocus = 'prompt';
84
91
  };
85
92
 
@@ -97,6 +104,7 @@ export function handleEscape(state: EscapeState): {
97
104
  selectionCallback,
98
105
  panelFocused,
99
106
  indicatorFocused,
107
+ modalReturnFocus,
100
108
  };
101
109
  }
102
110
 
@@ -118,7 +126,10 @@ export function handleEscape(state: EscapeState): {
118
126
  closeProfilePicker: () => state.profilePickerModal.close(),
119
127
  closeContextInspector: () => state.contextInspectorModal.close(),
120
128
  closeProcess: () => state.processModal.close(),
121
- closeModelPicker: () => state.modelPicker.close(),
129
+ closeModelPicker: () => {
130
+ state.modelPicker.close();
131
+ state.restoreOnboardingModelPickerCancelState?.();
132
+ },
122
133
  closeFilePicker: () => state.filePicker.close(),
123
134
  closeBlockActions: () => state.blockActionsMenu.close(),
124
135
  closeSelection: () => {
@@ -127,6 +138,10 @@ export function handleEscape(state: EscapeState): {
127
138
  state.selectionModal.close();
128
139
  cb?.(null);
129
140
  },
141
+ closeOnboarding: () => {
142
+ state.onboardingWizard?.close();
143
+ state.clearOnboardingModelPickerCancelState?.();
144
+ },
130
145
  closeCommandMode: () => {
131
146
  commandMode = false;
132
147
  for (let i = state.modalStack.length - 1; i >= 0; i--) {
@@ -146,6 +161,7 @@ export function handleEscape(state: EscapeState): {
146
161
  openBookmark: () => state.bookmarkModal.open(),
147
162
  openProcess: () => state.processModal.open(),
148
163
  openContextInspector: () => state.contextInspectorModal.open(),
164
+ openOnboarding: () => state.onboardingWizard?.reopen(),
149
165
  openCommandMode: () => {
150
166
  commandMode = true;
151
167
  prompt = '/';
@@ -176,6 +192,7 @@ export function handleEscape(state: EscapeState): {
176
192
  selectionCallback,
177
193
  panelFocused,
178
194
  indicatorFocused,
195
+ modalReturnFocus,
179
196
  };
180
197
  }
181
198
 
@@ -200,6 +217,7 @@ export function handleEscape(state: EscapeState): {
200
217
  selectionCallback,
201
218
  panelFocused,
202
219
  indicatorFocused,
220
+ modalReturnFocus,
203
221
  };
204
222
  }
205
223
 
@@ -218,6 +236,7 @@ export function handleEscape(state: EscapeState): {
218
236
  selectionCallback,
219
237
  panelFocused,
220
238
  indicatorFocused,
239
+ modalReturnFocus,
221
240
  };
222
241
  }
223
242
 
@@ -233,5 +252,6 @@ export function handleEscape(state: EscapeState): {
233
252
  selectionCallback,
234
253
  panelFocused,
235
254
  indicatorFocused,
255
+ modalReturnFocus,
236
256
  };
237
257
  }
@@ -15,7 +15,9 @@ import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
15
15
  import type { FilePickerModal } from './file-picker.ts';
16
16
  import type { BlockActionsMenu, BlockActionId } from '../renderer/block-actions.ts';
17
17
  import type { SearchManager } from './search.ts';
18
+ import type { OnboardingWizardAction, OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
18
19
  import { handleHistorySearchToken, handleOverlayToken, handleSearchModeToken } from './handler-ui-state.ts';
20
+ import { handleOnboardingWizardToken } from './onboarding/handler-onboarding-routes.ts';
19
21
  import {
20
22
  handleBookmarkModalToken,
21
23
  handleProfilePickerToken,
@@ -41,6 +43,7 @@ export type ModalTokenRouteState = {
41
43
  settingsModal: SettingsModal;
42
44
  sessionPickerModal: SessionPickerModal;
43
45
  profilePickerModal: ProfilePickerModal;
46
+ onboardingWizard: OnboardingWizardController;
44
47
  helpOverlayActive: boolean;
45
48
  helpScrollOffset: number;
46
49
  shortcutsOverlayActive: boolean;
@@ -73,7 +76,14 @@ export type ModalTokenRouteState = {
73
76
  scroll: (delta: number) => void;
74
77
  getScrollTop: () => number;
75
78
  /** Callback to open the model picker with a specific target (helper or tool). Optional — only wired when available. */
76
- openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => void;
79
+ openModelPickerWithTarget?: (
80
+ target: import('./model-picker.ts').ModelPickerTarget,
81
+ source?: 'settings' | 'onboarding',
82
+ ) => boolean;
83
+ clearOnboardingModelPickerCancelState?: () => void;
84
+ restoreOnboardingModelPickerCancelState?: () => void;
85
+ onModelPickerCommit?: () => boolean;
86
+ onOnboardingAction?: (action: OnboardingWizardAction) => void;
77
87
  };
78
88
 
79
89
  export function handleModalTokenRoutes(state: ModalTokenRouteState, token: InputToken): {
@@ -173,6 +183,18 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
173
183
  getViewportHeight: state.getViewportHeight,
174
184
  requestRender: state.requestRender,
175
185
  handleEscape: state.handleEscape,
186
+ onModelPickerCommit: state.onModelPickerCommit,
187
+ }, token)) {
188
+ return withState(state, true);
189
+ }
190
+
191
+ if (handleOnboardingWizardToken({
192
+ onboardingWizard: state.onboardingWizard,
193
+ getViewportHeight: state.getViewportHeight,
194
+ requestRender: state.requestRender,
195
+ handleEscape: state.handleEscape,
196
+ openModelPickerWithTarget: state.openModelPickerWithTarget,
197
+ onAction: state.onOnboardingAction,
176
198
  }, token)) {
177
199
  return withState(state, true);
178
200
  }