@pellux/goodvibes-tui 0.20.3 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +23 -2
  3. package/docs/foundation-artifacts/operator-contract.json +78 -1
  4. package/package.json +3 -2
  5. package/src/audio/spoken-turn-controller.ts +31 -1
  6. package/src/audio/spoken-turn-wiring.ts +26 -4
  7. package/src/cli/bundle-command.ts +1 -1
  8. package/src/cli/completions/generate.ts +662 -0
  9. package/src/cli/config-overrides.ts +68 -0
  10. package/src/cli/help.ts +4 -2
  11. package/src/cli/management-commands.ts +1 -1
  12. package/src/cli/management.ts +1 -8
  13. package/src/cli/parser.ts +14 -18
  14. package/src/cli/service-command.ts +1 -1
  15. package/src/cli/surface-command.ts +1 -1
  16. package/src/cli/tui-startup.ts +72 -10
  17. package/src/cli/types.ts +12 -3
  18. package/src/cli-flags.ts +1 -0
  19. package/src/config/atomic-write.ts +70 -0
  20. package/src/config/read-versioned.ts +115 -0
  21. package/src/core/conversation-rendering.ts +49 -15
  22. package/src/core/conversation.ts +101 -16
  23. package/src/core/format-user-error.ts +192 -0
  24. package/src/core/stream-event-wiring.ts +144 -0
  25. package/src/core/stream-stall-watchdog.ts +103 -0
  26. package/src/core/system-message-router.ts +5 -1
  27. package/src/export/cost-utils.ts +71 -0
  28. package/src/export/gist-uploader.ts +136 -0
  29. package/src/input/command-registry.ts +31 -1
  30. package/src/input/commands/control-room-runtime.ts +5 -5
  31. package/src/input/commands/experience-runtime.ts +5 -4
  32. package/src/input/commands/knowledge.ts +1 -1
  33. package/src/input/commands/local-auth-runtime.ts +27 -5
  34. package/src/input/commands/local-setup.ts +4 -6
  35. package/src/input/commands/memory-product-runtime.ts +8 -6
  36. package/src/input/commands/operator-panel-runtime.ts +1 -1
  37. package/src/input/commands/operator-runtime.ts +3 -10
  38. package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
  39. package/src/input/commands/recall-review.ts +26 -2
  40. package/src/input/commands/services-runtime.ts +2 -2
  41. package/src/input/commands/session-workflow.ts +3 -3
  42. package/src/input/commands/share-runtime.ts +99 -12
  43. package/src/input/commands/tts-runtime.ts +30 -4
  44. package/src/input/commands.ts +2 -2
  45. package/src/input/delete-key-policy.ts +46 -0
  46. package/src/input/feed-context-factory.ts +2 -0
  47. package/src/input/handler-feed.ts +3 -0
  48. package/src/input/handler-interactions.ts +2 -15
  49. package/src/input/handler-modal-routes.ts +91 -12
  50. package/src/input/handler-modal-token-routes.ts +3 -0
  51. package/src/input/handler-onboarding-cloudflare.ts +1 -1
  52. package/src/input/handler-onboarding.ts +55 -69
  53. package/src/input/handler-types.ts +163 -0
  54. package/src/input/handler.ts +5 -2
  55. package/src/input/input-history.ts +76 -6
  56. package/src/input/model-picker-filter.ts +265 -0
  57. package/src/input/model-picker-items.ts +208 -0
  58. package/src/input/model-picker.ts +92 -325
  59. package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
  60. package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
  61. package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
  62. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
  63. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
  64. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
  65. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
  66. package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
  67. package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
  68. package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
  69. package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
  70. package/src/input/onboarding/onboarding-wizard.ts +3 -3
  71. package/src/input/settings-modal-data.ts +304 -0
  72. package/src/input/settings-modal-mutations.ts +154 -0
  73. package/src/input/settings-modal.ts +182 -220
  74. package/src/main.ts +57 -57
  75. package/src/panels/builtin/agent.ts +4 -1
  76. package/src/panels/builtin/development.ts +4 -1
  77. package/src/panels/confirm-state.ts +27 -12
  78. package/src/panels/cost-tracker-panel.ts +23 -67
  79. package/src/panels/eval-panel.ts +10 -9
  80. package/src/panels/knowledge-panel.ts +3 -5
  81. package/src/panels/local-auth-panel.ts +124 -4
  82. package/src/panels/project-planning-panel.ts +42 -4
  83. package/src/panels/search-focus.ts +11 -5
  84. package/src/panels/subscription-panel.ts +33 -25
  85. package/src/panels/types.ts +28 -1
  86. package/src/panels/wrfc-panel.ts +224 -41
  87. package/src/renderer/agent-detail-modal.ts +11 -10
  88. package/src/renderer/code-block.ts +10 -2
  89. package/src/renderer/compositor.ts +18 -4
  90. package/src/renderer/context-inspector.ts +1 -5
  91. package/src/renderer/diff.ts +94 -21
  92. package/src/renderer/markdown.ts +29 -13
  93. package/src/renderer/settings-modal-helpers.ts +1 -1
  94. package/src/renderer/settings-modal.ts +77 -8
  95. package/src/renderer/syntax-highlighter.ts +10 -3
  96. package/src/renderer/term-caps.ts +318 -0
  97. package/src/renderer/theme.ts +158 -0
  98. package/src/renderer/tool-call.ts +12 -2
  99. package/src/renderer/ui-factory.ts +50 -6
  100. package/src/runtime/bootstrap-command-context.ts +1 -0
  101. package/src/runtime/bootstrap-command-parts.ts +14 -0
  102. package/src/runtime/bootstrap-core.ts +121 -13
  103. package/src/runtime/bootstrap.ts +2 -0
  104. package/src/runtime/onboarding/apply.ts +4 -6
  105. package/src/runtime/onboarding/index.ts +1 -0
  106. package/src/runtime/onboarding/markers.ts +42 -49
  107. package/src/runtime/onboarding/progress.ts +148 -0
  108. package/src/runtime/onboarding/state.ts +133 -55
  109. package/src/runtime/onboarding/types.ts +20 -0
  110. package/src/runtime/services.ts +21 -0
  111. package/src/runtime/wrfc-persistence.ts +237 -0
  112. package/src/shell/blocking-input.ts +20 -5
  113. package/src/tools/wrfc-agent-guard.ts +64 -3
  114. package/src/utils/format-elapsed.ts +30 -0
  115. package/src/utils/terminal-width.ts +45 -0
  116. package/src/version.ts +1 -1
  117. package/src/work-plans/work-plan-store.ts +4 -6
  118. package/src/planning/project-planning-coordinator.ts +0 -543
@@ -2,6 +2,7 @@ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
2
2
  import type { SelectionResult, SelectionAction } from './selection-modal.ts';
3
3
  import type { CommandContext } from './command-registry.ts';
4
4
  import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
5
+ import { isTextBackspace } from './delete-key-policy.ts';
5
6
 
6
7
  type SelectionRouteState = {
7
8
  selectionModal: {
@@ -136,10 +137,13 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
136
137
  getAdjustmentStep(selected, token.shift),
137
138
  );
138
139
  }
139
- } else if (token.logicalName === 'backspace') {
140
+ } else if (isTextBackspace(token.logicalName ?? '')) {
140
141
  if (state.selectionModal.allowSearch && state.selectionModal.searchFocused && state.selectionModal.query.length > 0) {
141
142
  state.selectionModal.setQuery(state.selectionModal.query.slice(0, -1));
142
143
  }
144
+ // 'delete' is intentionally absent here: modal search filters are
145
+ // end-anchored with no cursor, so forward-delete is a no-op per the
146
+ // delete-key policy (src/input/delete-key-policy.ts).
143
147
  } else if (state.selectionModal.allowSearch && !state.selectionModal.searchFocused && token.logicalName === '/') {
144
148
  state.selectionModal.focusSearch();
145
149
  } else if (!state.selectionModal.searchFocused && token.logicalName && token.logicalName.length === 1) {
@@ -212,9 +216,14 @@ type SettingsRouteState = {
212
216
  editingMode: boolean;
213
217
  currentCategory: string;
214
218
  focusPane?: 'categories' | 'settings';
219
+ /** True when the user is actively typing into the search input bar. */
220
+ searchFocused: boolean;
221
+ /** Current cross-category search query. */
222
+ searchQuery: string;
215
223
  commitEdit: () => void;
216
224
  toggleSelectedFlag: () => void;
217
225
  activateSelected: () => void;
226
+ handleSubscriptionLogoutKey?: (key: string) => 'confirmed' | 'cancelled' | 'absorbed' | 'inactive';
218
227
  adjustSelected: (direction: 'left' | 'right', step?: number) => void;
219
228
  moveFocusedUp?: () => void;
220
229
  moveFocusedDown?: () => void;
@@ -227,6 +236,16 @@ type SettingsRouteState = {
227
236
  prevCategory?: () => void;
228
237
  editBackspace: () => void;
229
238
  editChar: (char: string) => void;
239
+ /** Enter search mode (focus the search input bar). */
240
+ focusSearch: () => void;
241
+ /** Exit search mode without clearing the query. */
242
+ blurSearch: () => void;
243
+ /** Set search query and recompute results. */
244
+ setSearchQuery: (query: string) => void;
245
+ /** Clear search query, results, and exit search mode. */
246
+ clearSearch: () => void;
247
+ /** Cancel inline edit without saving (mirrors SettingsModal.cancelEdit). */
248
+ cancelEdit: () => void;
230
249
  pendingModelPickerTarget: import('./model-picker.ts').ModelPickerTarget | null;
231
250
  pendingProviderModelPickerTarget?: import('./model-picker.ts').ModelPickerTarget | null;
232
251
  pendingSettingsPickerAction?: 'tts-provider' | 'tts-voice' | null;
@@ -279,42 +298,104 @@ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
279
298
  export function handleSettingsModalToken(state: SettingsRouteState, token: InputToken): boolean {
280
299
  if (!state.settingsModal.active) return false;
281
300
 
301
+ // Subscription logout confirm gate: routes all keys through the unified
302
+ // confirm contract before normal dispatch when a confirm is pending.
303
+ if (state.settingsModal.handleSubscriptionLogoutKey) {
304
+ const key = token.type === 'key'
305
+ ? (token.logicalName ?? '')
306
+ : token.type === 'text'
307
+ ? token.value
308
+ : '';
309
+ const logoutResult = state.settingsModal.handleSubscriptionLogoutKey(key);
310
+ if (logoutResult !== 'inactive') {
311
+ state.requestRender();
312
+ return true;
313
+ }
314
+ }
315
+
282
316
  if (token.type === 'key') {
283
317
  const focusPane = state.settingsModal.focusPane ?? 'settings';
284
318
  if (token.logicalName === 'escape') {
319
+ // Cancel inline edit first — mirrors the global contract in handler-modal-stack.ts.
320
+ // Must check editingMode before searchFocused: the reachable path
321
+ // search→Enter(string/number)→Esc must cancel the edit, NOT just clear search.
322
+ if (state.settingsModal.editingMode) {
323
+ state.settingsModal.cancelEdit();
324
+ state.requestRender();
325
+ return true;
326
+ }
327
+ // Two-stage escape: if in search mode, first Esc exits search (clearSearch),
328
+ // second Esc closes the modal.
329
+ if (state.settingsModal.searchFocused) {
330
+ state.settingsModal.clearSearch();
331
+ state.requestRender();
332
+ return true;
333
+ }
285
334
  state.handleEscape();
286
335
  return true;
287
336
  }
288
- if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode)) {
337
+ if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused)) {
289
338
  if (state.settingsModal.editingMode) state.settingsModal.commitEdit();
290
- else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
339
+ else if (state.settingsModal.searchFocused) {
340
+ // Enter in search mode: activate the selected search result
341
+ state.settingsModal.activateSelected();
342
+ consumeSettingsPickerRequest(state);
343
+ } else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
291
344
  else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
292
345
  else {
293
346
  state.settingsModal.activateSelected();
294
347
  consumeSettingsPickerRequest(state);
295
348
  }
296
- } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode) {
349
+ } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
297
350
  if (token.logicalName === 'left') state.settingsModal.focusCategories?.();
298
351
  else state.settingsModal.focusSettings?.();
299
352
  } else if (token.logicalName === 'up') {
300
- if (state.settingsModal.moveFocusedUp) state.settingsModal.moveFocusedUp();
353
+ if (state.settingsModal.searchFocused) {
354
+ state.settingsModal.moveUp?.();
355
+ } else if (state.settingsModal.moveFocusedUp) state.settingsModal.moveFocusedUp();
301
356
  else state.settingsModal.moveUp?.();
302
357
  } else if (token.logicalName === 'down') {
303
- if (state.settingsModal.moveFocusedDown) state.settingsModal.moveFocusedDown();
358
+ if (state.settingsModal.searchFocused) {
359
+ state.settingsModal.moveDown?.();
360
+ } else if (state.settingsModal.moveFocusedDown) state.settingsModal.moveFocusedDown();
304
361
  else state.settingsModal.moveDown?.();
305
362
  }
306
- else if (token.logicalName === 'r' && !state.settingsModal.editingMode) {
363
+ else if (token.logicalName === 'r' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
307
364
  const reset = state.settingsModal.resetSelected?.();
308
365
  if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
309
366
  }
310
- else if (token.logicalName === 'tab') {
367
+ else if (token.logicalName === 'tab' && !state.settingsModal.searchFocused) {
311
368
  if (state.settingsModal.toggleFocusPane) state.settingsModal.toggleFocusPane();
312
369
  else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
313
370
  else state.settingsModal.focusCategories?.();
314
371
  }
315
- else if (token.logicalName === 'backspace' && state.settingsModal.editingMode) state.settingsModal.editBackspace();
372
+ else if (isTextBackspace(token.logicalName ?? '')) {
373
+ if (state.settingsModal.editingMode) {
374
+ state.settingsModal.editBackspace();
375
+ } else if (state.settingsModal.searchFocused) {
376
+ // Backspace in search mode: trim query
377
+ const trimmed = state.settingsModal.searchQuery.slice(0, -1);
378
+ state.settingsModal.setSearchQuery(trimmed);
379
+ }
380
+ }
381
+ // token.logicalName === 'delete' is intentionally absent: search filters
382
+ // are end-anchored with no cursor, so forward-delete is a no-op per
383
+ // delete-key policy (src/input/delete-key-policy.ts).
384
+ else if (!state.settingsModal.editingMode && !state.settingsModal.searchFocused && token.logicalName === '/') {
385
+ state.settingsModal.focusSearch();
386
+ }
316
387
  } else if (token.type === 'text') {
317
- if (token.value === ' ' && !state.settingsModal.editingMode) {
388
+ if (state.settingsModal.editingMode) {
389
+ // editingMode takes priority over search — Enter on a string/number search
390
+ // result enters inline edit; subsequent chars must go to editChar, not the query.
391
+ state.settingsModal.editChar(token.value);
392
+ } else if (state.settingsModal.searchFocused) {
393
+ // Any printable char in search mode appends to the query
394
+ state.settingsModal.setSearchQuery(state.settingsModal.searchQuery + token.value);
395
+ } else if (token.value === '/' && !state.settingsModal.editingMode) {
396
+ // / enters search mode
397
+ state.settingsModal.focusSearch();
398
+ } else if (token.value === ' ' && !state.settingsModal.editingMode) {
318
399
  const focusPane = state.settingsModal.focusPane ?? 'settings';
319
400
  if (focusPane === 'categories') state.settingsModal.focusSettings?.();
320
401
  else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
@@ -322,8 +403,6 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
322
403
  state.settingsModal.activateSelected();
323
404
  consumeSettingsPickerRequest(state);
324
405
  }
325
- } else if (state.settingsModal.editingMode) {
326
- state.settingsModal.editChar(token.value);
327
406
  } else if (token.value === 'r') {
328
407
  const reset = state.settingsModal.resetSelected?.();
329
408
  if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
@@ -92,6 +92,8 @@ export type ModalTokenRouteState = {
92
92
  restoreOnboardingModelPickerCancelState?: () => void;
93
93
  onModelPickerCommit?: () => boolean;
94
94
  onOnboardingAction?: (action: OnboardingWizardAction) => void;
95
+ /** Called after any wizard step navigation so the handler can persist progress. */
96
+ onStepChange?: () => void;
95
97
  };
96
98
 
97
99
  export function handleModalTokenRoutes(state: ModalTokenRouteState, token: InputToken): {
@@ -216,6 +218,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
216
218
  handleEscape: state.handleEscape,
217
219
  openModelPickerWithTarget: state.openModelPickerWithTarget,
218
220
  onAction: state.onOnboardingAction,
221
+ onStepChange: state.onStepChange,
219
222
  }, token)) {
220
223
  return withState(state, true);
221
224
  }
@@ -12,7 +12,7 @@ import {
12
12
  type CloudflareVerifyResult,
13
13
  } from '../runtime/cloudflare-control-plane.ts';
14
14
  import type { OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
15
- import type { InputHandler } from './handler.ts';
15
+ import type { InputHandlerLike as InputHandler } from './handler-types.ts';
16
16
  import type { OnboardingWizardAction, OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
17
17
  import {
18
18
  buildCloudflareApiTokenRef,
@@ -5,11 +5,17 @@ import { getProviderIdFromModel } from '../config/provider-model.ts';
5
5
  import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
6
6
  import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
7
7
  import { handleCloudflareOnboardingActionForHandler, maybeProvisionCloudflareOnFinalApplyForHandler } from './handler-onboarding-cloudflare.ts';
8
- import { applyOnboardingRequest, collectOnboardingSnapshot, verifyOnboardingRequest } from '../runtime/onboarding/index.ts';
8
+ import { applyOnboardingRequest, collectOnboardingSnapshot, deleteWizardProgress, verifyOnboardingRequest, writeOnboardingCheckMarker, writeWizardProgress } from '../runtime/onboarding/index.ts';
9
9
  import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
10
+ import {
11
+ dedupeOnboardingVerificationItems,
12
+ extractAuthorizationCode,
13
+ formatOnboardingApplyCompletionMessage,
14
+ isLoopbackHostValue,
15
+ } from './onboarding/onboarding-verification-helpers.ts';
10
16
  import type { ModelPickerTarget } from './model-picker.ts';
11
17
  import { captureOnboardingWizardSnapshot, restoreOnboardingWizardSnapshot } from './handler-ui-state.ts';
12
- import type { InputHandler } from './handler.ts';
18
+ import type { InputHandlerLike as InputHandler, OnboardingRuntimePosture } from './handler-types.ts';
13
19
  import {
14
20
  formatRuntimeActiveSuccessMessage,
15
21
  getRuntimeEndpointStatus,
@@ -20,73 +26,6 @@ import {
20
26
  type OnboardingRuntimeEndpoint,
21
27
  } from './onboarding/onboarding-runtime-status.ts';
22
28
 
23
- export interface OnboardingRuntimePosture {
24
- readonly serviceEnabled: boolean;
25
- readonly serviceAutostart: boolean;
26
- readonly restartOnFailure: boolean;
27
- readonly expectedDaemon: boolean;
28
- readonly expectedHttpListener: boolean;
29
- readonly serverBacked: boolean;
30
- readonly remoteExposure: boolean;
31
- }
32
-
33
- function extractAuthorizationCode(input: string): string | null {
34
- const trimmed = input.trim();
35
- if (!trimmed) return null;
36
-
37
- try {
38
- const url = new URL(trimmed);
39
- return url.searchParams.get('code');
40
- } catch {
41
- return trimmed;
42
- }
43
- }
44
-
45
- function isLoopbackHostValue(value: string | null | undefined): boolean {
46
- const normalized = (value ?? '').trim().toLowerCase();
47
- if (normalized.length === 0) return false;
48
- return normalized === 'localhost'
49
- || normalized === '::1'
50
- || normalized === '[::1]'
51
- || normalized === '0:0:0:0:0:0:0:1'
52
- || /^127(?:\.\d{1,3}){3}$/.test(normalized);
53
- }
54
-
55
- function onboardingVerificationStatusRank(item: OnboardingVerificationItem): number {
56
- if (item.status === 'fail') return 3;
57
- if (item.status === 'warn') return 2;
58
- return 1;
59
- }
60
-
61
- function dedupeOnboardingVerificationItems(
62
- items: readonly OnboardingVerificationItem[],
63
- ): OnboardingVerificationItem[] {
64
- const order: string[] = [];
65
- const byId = new Map<string, OnboardingVerificationItem>();
66
- for (const item of items) {
67
- const existing = byId.get(item.id);
68
- if (!existing) {
69
- order.push(item.id);
70
- byId.set(item.id, item);
71
- continue;
72
- }
73
- if (onboardingVerificationStatusRank(item) > onboardingVerificationStatusRank(existing)) {
74
- byId.set(item.id, item);
75
- }
76
- }
77
- return order.map((id) => byId.get(id)).filter((item): item is OnboardingVerificationItem => Boolean(item));
78
- }
79
-
80
- function formatOnboardingApplyCompletionMessage(items: readonly OnboardingVerificationItem[]): string {
81
- const warnings = items.filter((item) => item.status === 'warn');
82
- if (warnings.length === 0) return `Onboarding applied and verified ${items.length} item(s).`;
83
- const passed = items.filter((item) => item.status === 'pass').length;
84
- return [
85
- `Onboarding settings applied. ${passed} verification item(s) passed; ${warnings.length} warning(s) need attention.`,
86
- ...warnings.map((warning) => ` warning ${warning.id}: ${warning.message}`),
87
- ].join('\n');
88
- }
89
-
90
29
  function getRuntimeEndpointBinding(
91
30
  handler: InputHandler,
92
31
  request: OnboardingApplyRequest,
@@ -292,6 +231,18 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
292
231
  }
293
232
 
294
233
  if (appliedErrors.length === 0) {
234
+ try {
235
+ writeOnboardingCheckMarker(handler.uiServices.environment.shellPaths, {
236
+ scope: 'user',
237
+ source: 'wizard',
238
+ mode: request.mode,
239
+ });
240
+ deleteWizardProgress(handler.uiServices.environment.shellPaths);
241
+ } catch (markerError) {
242
+ handler.commandContext?.print?.(
243
+ `Onboarding check marker could not be written: ${markerError instanceof Error ? markerError.message : String(markerError)}`,
244
+ );
245
+ }
295
246
  const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
296
247
  runtimeWarnings = dedupeOnboardingVerificationItems([...activationVerification, ...handler.verifyOnboardingRuntimePosture(request)]
297
248
  .map((item): OnboardingVerificationItem => item.status === 'fail'
@@ -540,6 +491,41 @@ export async function handleOpenAiSubscriptionFinishForHandler(handler: InputHan
540
491
  }
541
492
  }
542
493
 
494
+ /**
495
+ * Persist the current wizard field state to onboarding-progress.json.
496
+ *
497
+ * Called after each step navigation so the user can resume a partially
498
+ * completed wizard after a restart. Masked fields (kind === 'masked') are
499
+ * excluded from the persisted textState to avoid writing secrets to disk.
500
+ *
501
+ * This is a best-effort write: if it fails the wizard continues normally.
502
+ */
503
+ export function saveWizardProgressForHandler(handler: InputHandler): void {
504
+ const wizard = handler.onboardingWizard;
505
+ if (!wizard.active) return;
506
+ try {
507
+ // Exclude masked fields to avoid persisting secrets.
508
+ const maskedFieldIds = new Set<string>();
509
+ for (const step of wizard.steps) {
510
+ for (const field of step.fields) {
511
+ if (field.kind === 'masked') maskedFieldIds.add(field.id);
512
+ }
513
+ }
514
+ const safeTextState = [...wizard.textState.entries()]
515
+ .filter(([id]) => !maskedFieldIds.has(id)) as Array<[string, string]>;
516
+
517
+ writeWizardProgress(handler.uiServices.environment.shellPaths, {
518
+ mode: wizard.mode,
519
+ stepIndex: wizard.stepIndex,
520
+ toggleState: [...wizard.toggleState.entries()],
521
+ radioState: [...wizard.radioState.entries()],
522
+ textState: safeTextState,
523
+ });
524
+ } catch {
525
+ // Best-effort: never block wizard interaction on a progress write failure.
526
+ }
527
+ }
528
+
543
529
  export function syncRuntimeFromOnboardingRequestForHandler(handler: InputHandler, request: ReturnType<OnboardingWizardController['buildApplyRequest']>): void {
544
530
  const runtime = handler.commandContext?.session.runtime;
545
531
  if (!runtime) return;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * handler-types.ts — Leaf interface for InputHandler.
3
+ *
4
+ * Extracted from handler.ts to break circular import chains between handler.ts
5
+ * and handler-interactions.ts / handler-onboarding.ts / handler-onboarding-cloudflare.ts.
6
+ *
7
+ * The interface is the union of all `handler.*` accesses in those three files.
8
+ * InputHandler declares `implements InputHandlerLike`; no cycle is created
9
+ * because this file imports only from leaf modules (no import from handler.ts).
10
+ */
11
+ import { type createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
12
+ import type { OnboardingWizardController, OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
13
+ import type { OnboardingWizardSnapshot, OpenOnboardingWizardOptions } from './handler-ui-state.ts';
14
+ import type {
15
+ OnboardingApplyRequest,
16
+ OnboardingVerificationItem,
17
+ } from '../runtime/onboarding/index.ts';
18
+ import type { UiRuntimeServices } from '../runtime/ui-services.ts';
19
+ import type { CommandContext } from './command-registry.ts';
20
+ import type { ConversationManager } from '../core/conversation';
21
+ import type { ModelPickerModal, ModelPickerTarget } from './model-picker.ts';
22
+ import type { SelectionManager } from './selection.ts';
23
+ import type { InfiniteBuffer } from '../core/history.ts';
24
+ import type { AutocompleteEngine } from './autocomplete.ts';
25
+ import type { BookmarkModal } from './bookmark-modal.ts';
26
+ import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
27
+ import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
28
+ import type { SettingsModal } from './settings-modal.ts';
29
+ import type { McpWorkspace } from './mcp-workspace.ts';
30
+ import type { SessionPickerModal } from './session-picker-modal.ts';
31
+ import type { ProfilePickerModal } from './profile-picker-modal.ts';
32
+ import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
33
+ import type { ProcessModal } from '../renderer/process-modal.ts';
34
+ import type { FilePickerModal } from './file-picker.ts';
35
+ import type { BlockActionsMenu } from '../renderer/block-actions.ts';
36
+ import type { SelectionModal } from './selection-modal.ts';
37
+ import type { SelectionResult } from './selection-modal.ts';
38
+ export interface OnboardingRuntimePosture {
39
+ readonly serviceEnabled: boolean;
40
+ readonly serviceAutostart: boolean;
41
+ readonly restartOnFailure: boolean;
42
+ readonly expectedDaemon: boolean;
43
+ readonly expectedHttpListener: boolean;
44
+ readonly serverBacked: boolean;
45
+ readonly remoteExposure: boolean;
46
+ }
47
+
48
+ type SelectionModalCallback = (result: SelectionResult | null) => void;
49
+
50
+ /**
51
+ * Public surface of InputHandler consumed by handler-interactions.ts,
52
+ * handler-onboarding.ts, and handler-onboarding-cloudflare.ts.
53
+ */
54
+ export interface InputHandlerLike {
55
+ // ── Core render / lifecycle ──────────────────────────────────────────────
56
+ requestRender: () => void;
57
+ exitApp: () => void;
58
+
59
+ // ── Services ─────────────────────────────────────────────────────────────
60
+ readonly uiServices: Pick<UiRuntimeServices,
61
+ | 'agents'
62
+ | 'environment'
63
+ | 'platform'
64
+ | 'providers'
65
+ | 'sessions'
66
+ | 'shell'
67
+ >;
68
+ commandContext: CommandContext | undefined;
69
+
70
+ // ── Prompt / cursor state ────────────────────────────────────────────────
71
+ prompt: string;
72
+ cursorPos: number;
73
+ showExitNotice: boolean;
74
+
75
+ // ── Selection / history ──────────────────────────────────────────────────
76
+ selection: SelectionManager;
77
+ getHistory: () => InfiniteBuffer;
78
+ getScrollTop: () => number;
79
+ conversationManager: ConversationManager | null;
80
+
81
+ // ── Paste / image registries ─────────────────────────────────────────────
82
+ pasteRegistry: Map<string, string>;
83
+ nextPasteId: number;
84
+ imageRegistry: Map<string, { data: string; mediaType: string }>;
85
+ nextImageId: number;
86
+
87
+ // ── Timing ───────────────────────────────────────────────────────────────
88
+ lastCopyTime: number;
89
+ lastBlockCopyTime: number;
90
+ lastCtrlCTime: number;
91
+
92
+ // ── Modal state ───────────────────────────────────────────────────────────
93
+ commandMode: boolean;
94
+ modalStack: string[];
95
+ modalReturnFocus: 'prompt' | 'panel' | 'indicator';
96
+ panelFocused: boolean;
97
+ indicatorFocused: boolean;
98
+ helpOverlayActive: boolean;
99
+ helpScrollOffset: number;
100
+ shortcutsOverlayActive: boolean;
101
+ shortcutsScrollOffset: number;
102
+ selectionCallback: SelectionModalCallback | null;
103
+ autocomplete: AutocompleteEngine | null;
104
+
105
+ // ── Modal objects ─────────────────────────────────────────────────────────
106
+ bookmarkModal: BookmarkModal;
107
+ agentDetailModal: AgentDetailModal;
108
+ liveTailModal: LiveTailModal;
109
+ settingsModal: SettingsModal;
110
+ mcpWorkspace: McpWorkspace;
111
+ sessionPickerModal: SessionPickerModal;
112
+ profilePickerModal: ProfilePickerModal;
113
+ contextInspectorModal: ContextInspectorModal;
114
+ processModal: ProcessModal;
115
+ modelPicker: ModelPickerModal;
116
+ filePicker: FilePickerModal;
117
+ blockActionsMenu: BlockActionsMenu;
118
+ selectionModal: SelectionModal;
119
+
120
+ // ── Onboarding ────────────────────────────────────────────────────────────
121
+ onboardingWizard: OnboardingWizardController;
122
+ onboardingModelPickerCancelSnapshot: OnboardingWizardSnapshot | null;
123
+ onboardingHydrationSerial: number;
124
+ onboardingApplyPending: boolean;
125
+ onboardingOpenAiListenerSerial: number;
126
+
127
+ // ── Methods: modal lifecycle ──────────────────────────────────────────────
128
+ modalOpened(name: string): void;
129
+ saveUndoState(): void;
130
+
131
+ // ── Methods: block actions (dispatched in executeBlockAction) ────────────
132
+ handleBlockCopy(): void;
133
+ handleBookmark(): void;
134
+ handleBlockToggle(): void;
135
+ handleDiffApply(): boolean;
136
+ handleBlockRerun(): void;
137
+
138
+ // ── Methods: onboarding ───────────────────────────────────────────────────
139
+ hydrateOnboardingWizardFromRuntime(hydrationSerial: number): Promise<void>;
140
+ clearOnboardingModelPickerCancelState(): void;
141
+ restoreOnboardingModelPickerCancelState(): void;
142
+ clearOnboardingPendingModelPickerTarget(): void;
143
+ refreshOnboardingHydration(options?: { readonly preserveValues?: boolean; readonly targetStepId?: string }): Promise<void>;
144
+ handleOpenAiSubscriptionStart(): Promise<void>;
145
+ handleOpenAiSubscriptionFinish(): Promise<void>;
146
+ syncRuntimeFromOnboardingRequest(request: ReturnType<OnboardingWizardController['buildApplyRequest']>): void;
147
+ getOnboardingConfigValue(request: OnboardingApplyRequest, key: string): unknown;
148
+ getOnboardingRuntimePosture(request: OnboardingApplyRequest): OnboardingRuntimePosture;
149
+ restartOnboardingExternalServicesIfNeeded(request: OnboardingApplyRequest): Promise<OnboardingVerificationItem[]>;
150
+ verifyOnboardingRuntimePosture(request: OnboardingApplyRequest): OnboardingVerificationItem[];
151
+
152
+ // ── Method: model picker ──────────────────────────────────────────────────
153
+ openModelPickerWithTarget(target: ModelPickerTarget, source?: 'settings' | 'onboarding'): boolean;
154
+ openProviderModelPickerWithTarget(target: ModelPickerTarget, source?: 'settings' | 'onboarding'): boolean;
155
+
156
+ // ── Method: onboarding action ─────────────────────────────────────────────
157
+ completeOpenAiSubscriptionFromListener(
158
+ listener: Awaited<ReturnType<typeof createOAuthLocalListener>>,
159
+ verifier: string,
160
+ serial: number,
161
+ ): Promise<void>;
162
+ handleOnboardingAction(action: OnboardingWizardAction): Promise<void>;
163
+ }
@@ -3,7 +3,8 @@ import { dirname } from 'node:path';
3
3
  import { InputTokenizer } from '@pellux/goodvibes-sdk/platform/core';
4
4
  import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
5
5
  import { clearModalStackForHandler, cleanupMarkerRegistryForHandler, executeBlockActionForHandler, expandPromptForHandler, findMarkerAtPosForHandler, getImageAttachmentsForHandler, handleBlockCopyForHandler, handleBlockRerunForHandler, handleBlockSaveForHandler, handleBlockToggleForHandler, handleBookmarkForHandler, handleCopyForHandler, handleCtrlCForHandler, handleDiffApplyForHandler, handleEscapeForHandler, hydrateOnboardingWizardFromRuntimeForHandler, modalOpenedForHandler, openOnboardingWizardForHandler, registerPasteForHandler } from './handler-interactions.ts';
6
- import { clearOnboardingModelPickerCancelStateForHandler, clearOnboardingPendingModelPickerTargetForHandler, completeOpenAiSubscriptionFromListenerForHandler, getOnboardingConfigValueForHandler, getOnboardingRuntimePostureForHandler, handleModelPickerCommitForHandler, handleOnboardingActionForHandler, handleOpenAiSubscriptionFinishForHandler, handleOpenAiSubscriptionStartForHandler, openModelPickerWithTargetForHandler, openProviderModelPickerWithTargetForHandler, refreshOnboardingHydrationForHandler, restartOnboardingExternalServicesIfNeededForHandler, restoreOnboardingModelPickerCancelStateForHandler, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler, type OnboardingRuntimePosture } from './handler-onboarding.ts';
6
+ import { clearOnboardingModelPickerCancelStateForHandler, clearOnboardingPendingModelPickerTargetForHandler, completeOpenAiSubscriptionFromListenerForHandler, getOnboardingConfigValueForHandler, getOnboardingRuntimePostureForHandler, handleModelPickerCommitForHandler, handleOnboardingActionForHandler, handleOpenAiSubscriptionFinishForHandler, handleOpenAiSubscriptionStartForHandler, openModelPickerWithTargetForHandler, openProviderModelPickerWithTargetForHandler, refreshOnboardingHydrationForHandler, restartOnboardingExternalServicesIfNeededForHandler, restoreOnboardingModelPickerCancelStateForHandler, saveWizardProgressForHandler, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler, } from './handler-onboarding.ts';
7
+ import type { OnboardingRuntimePosture } from './handler-types.ts';
7
8
  import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
8
9
  import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
9
10
  import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
@@ -109,6 +110,7 @@ import {
109
110
  } from './handler-picker-routes.ts';
110
111
  import { handleGlobalShortcutToken } from './handler-shortcuts.ts';
111
112
  import { feedInputTokens } from './handler-feed.ts';
113
+ import type { InputHandlerLike } from './handler-types.ts';
112
114
  import { buildInitialFeedContext, syncFeedContextMutableFields } from './feed-context-factory.ts';
113
115
  import { handlePanelIntegrationAction as runPanelIntegrationAction } from './panel-integration-actions.ts';
114
116
  import type { Panel } from '../panels/types.ts';
@@ -123,7 +125,7 @@ type SelectionModalCallback = (result: SelectionResult | null) => void;
123
125
  * InputHandler - Owns prompt text, paste registry, and keyboard/mouse handling.
124
126
  * Extracted from main.ts and StateManager.
125
127
  */
126
- export class InputHandler {
128
+ export class InputHandler implements InputHandlerLike {
127
129
  public prompt = '';
128
130
  public cursorPos = 0;
129
131
  public showExitNotice = false;
@@ -335,6 +337,7 @@ export class InputHandler {
335
337
  this.openProviderModelPickerWithTarget(target, source),
336
338
  onModelPickerCommit: () => this.handleModelPickerCommit(),
337
339
  onOnboardingAction: (action: OnboardingWizardAction) => { void this.handleOnboardingAction(action); },
340
+ onStepChange: () => { saveWizardProgressForHandler(this); },
338
341
  },
339
342
  );
340
343
  }