@pellux/goodvibes-tui 0.19.28 → 0.19.30

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 (34) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/surface-command.ts +46 -11
  6. package/src/core/orchestrator.ts +5 -1
  7. package/src/daemon/cli.ts +7 -0
  8. package/src/input/handler-onboarding.ts +151 -44
  9. package/src/input/onboarding/handler-onboarding-routes.ts +4 -0
  10. package/src/input/onboarding/onboarding-wizard-apply.ts +35 -8
  11. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -5
  12. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +93 -5
  13. package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -2
  14. package/src/input/onboarding/onboarding-wizard-rules.ts +22 -3
  15. package/src/input/onboarding/onboarding-wizard-state.ts +12 -7
  16. package/src/input/onboarding/onboarding-wizard-steps.ts +133 -59
  17. package/src/input/onboarding/onboarding-wizard-types.ts +10 -0
  18. package/src/input/onboarding/onboarding-wizard.ts +56 -4
  19. package/src/input/settings-modal-types.ts +2 -1
  20. package/src/input/settings-modal.ts +4 -0
  21. package/src/main.ts +33 -26
  22. package/src/renderer/compositor.ts +3 -3
  23. package/src/renderer/onboarding/onboarding-wizard.ts +38 -21
  24. package/src/renderer/settings-modal-helpers.ts +9 -0
  25. package/src/renderer/settings-modal.ts +3 -0
  26. package/src/runtime/bootstrap-core.ts +28 -3
  27. package/src/runtime/bootstrap.ts +20 -3
  28. package/src/runtime/onboarding/apply.ts +36 -8
  29. package/src/runtime/onboarding/derivation.ts +7 -7
  30. package/src/runtime/onboarding/snapshot.ts +1 -0
  31. package/src/runtime/onboarding/types.ts +4 -1
  32. package/src/runtime/onboarding/verify.ts +1 -1
  33. package/src/runtime/surface-feature-flags.ts +67 -0
  34. package/src/version.ts +1 -1
@@ -1,8 +1,14 @@
1
1
  import { NETWORK_MODE_OPTIONS, REASONING_OPTIONS, HITL_MODE_OPTIONS, GUIDANCE_MODE_OPTIONS, PERMISSION_MODE_OPTIONS, SECRET_POLICY_OPTIONS } from './onboarding-wizard-constants.ts';
2
- import { EXTERNAL_SURFACE_SPECS, type ExternalSurfaceSpec } from './onboarding-wizard-external-surfaces.ts';
2
+ import {
3
+ EXTERNAL_SURFACE_SPECS,
4
+ getExternalSurfaceAutoStartDefaultValue,
5
+ getExternalSurfaceAutoStartFieldId,
6
+ isExternalSurfaceSelectedByDefault,
7
+ type ExternalSurfaceSpec,
8
+ } from './onboarding-wizard-external-surfaces.ts';
3
9
  import { countSelected, modelSelectionLabel, normalizeText } from './onboarding-wizard-helpers.ts';
4
10
  import type { OnboardingWizardController } from './onboarding-wizard.ts';
5
- import type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardRadioFieldDefinition, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
11
+ import type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardRadioFieldDefinition, OnboardingWizardRadioOption, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
6
12
 
7
13
  export function buildOnboardingWizardSteps(controller: OnboardingWizardController): readonly OnboardingWizardStepDefinition[] {
8
14
  if (controller.hydrationPending || controller.hydrationError !== null) return [buildLoadingStep(controller)];
@@ -86,7 +92,7 @@ export function buildCapabilitiesStep(controller: OnboardingWizardController): O
86
92
  id: 'capabilities.select-all',
87
93
  action: 'select-all-capabilities',
88
94
  label: 'Select all server-backed capabilities',
89
- hint: 'Enable browser access, other-device LAN access, webhooks/events, and external integrations. Local TUI Only is turned off.',
95
+ hint: 'Enable browser access, LAN reachability, webhooks/events, and external app surfaces. Local TUI Only is turned off.',
90
96
  defaultValue: 'Action',
91
97
  },
92
98
  {
@@ -103,7 +109,7 @@ export function buildCapabilitiesStep(controller: OnboardingWizardController): O
103
109
  id: 'capabilities',
104
110
  title: 'Choose GoodVibes capabilities',
105
111
  shortLabel: 'Capabilities',
106
- description: 'Select one or more capabilities. Local TUI Only means no browser, daemon, listener, or network setup; any other choice turns on service mode and autostart.',
112
+ description: 'Choose what GoodVibes should be able to do. Local TUI Only avoids servers; any other choice enables service mode and autostart.',
107
113
  summaryTitle: 'Selected capabilities',
108
114
  summaryLines: [
109
115
  `${selectedCount}/${capabilities.length} option(s) selected`,
@@ -266,8 +272,11 @@ export function buildDefaultModelStep(controller: OnboardingWizardController): O
266
272
  }
267
273
 
268
274
  export function buildExternalServicesStep(controller: OnboardingWizardController): OnboardingWizardStepDefinition {
269
- const enabledCount = EXTERNAL_SURFACE_SPECS
270
- .filter((surface) => controller.getBooleanFieldValue(surface.enabledFieldId, surface.defaultEnabled(controller.runtimeSnapshot)))
275
+ const selectedCount = EXTERNAL_SURFACE_SPECS
276
+ .filter((surface) => controller.getBooleanFieldValue(
277
+ surface.enabledFieldId,
278
+ isExternalSurfaceSelectedByDefault(surface, controller.runtimeSnapshot),
279
+ ))
271
280
  .length;
272
281
  const fields: OnboardingWizardFieldDefinition[] = [];
273
282
 
@@ -276,8 +285,8 @@ export function buildExternalServicesStep(controller: OnboardingWizardController
276
285
  kind: 'checklist',
277
286
  id: surface.enabledFieldId,
278
287
  label: surface.label,
279
- hint: surface.hint,
280
- defaultValue: surface.defaultEnabled(controller.runtimeSnapshot),
288
+ hint: `${surface.hint} Selecting this opens a dedicated setup screen; auto-start is chosen on that screen.`,
289
+ defaultValue: isExternalSurfaceSelectedByDefault(surface, controller.runtimeSnapshot),
281
290
  });
282
291
  }
283
292
 
@@ -287,7 +296,7 @@ export function buildExternalServicesStep(controller: OnboardingWizardController
287
296
  id: 'external-services.select-all',
288
297
  action: 'select-all-external-surfaces',
289
298
  label: 'Select all external surfaces',
290
- hint: 'Enable every supported external surface so each one gets a setup screen.',
299
+ hint: 'Show setup screens for every supported external surface. Auto-start stays controlled per surface.',
291
300
  defaultValue: 'Action',
292
301
  },
293
302
  {
@@ -295,7 +304,7 @@ export function buildExternalServicesStep(controller: OnboardingWizardController
295
304
  id: 'external-services.clear',
296
305
  action: 'clear-external-surfaces',
297
306
  label: 'Clear all external surfaces',
298
- hint: 'Disable all external surfaces. The HTTP listener can still be enabled separately by webhook/event capabilities.',
307
+ hint: 'Hide all external surface setup screens. The HTTP listener can still be enabled separately by webhook/event capabilities.',
299
308
  defaultValue: 'Action',
300
309
  },
301
310
  {
@@ -312,12 +321,12 @@ export function buildExternalServicesStep(controller: OnboardingWizardController
312
321
  id: 'external-services',
313
322
  title: 'Choose external surfaces',
314
323
  shortLabel: 'Services',
315
- description: 'Select the apps and integration surfaces GoodVibes should prepare. Setup fields are not shown here; each selected surface opens as its own screen.',
324
+ description: 'Select the apps and integration surfaces GoodVibes should prepare. Each selected surface gets its own setup screen and its own auto-start choice.',
316
325
  summaryTitle: 'External surfaces',
317
326
  summaryLines: [
318
- `${enabledCount} external surface(s) selected`,
327
+ `${selectedCount} external surface(s) selected for setup`,
319
328
  `Secret policy: ${controller.getStringFieldValue('external-services.secret-policy', controller.runtimeSnapshot?.runtimeDefaults.secretStoragePolicy ?? 'preferred_secure')}`,
320
- enabledCount > 0 ? 'Selected surfaces appear as separate setup screens.' : 'No external surfaces selected.',
329
+ selectedCount > 0 ? 'Selected surfaces appear as separate setup screens.' : 'No external surfaces selected.',
321
330
  ],
322
331
  fields,
323
332
  };
@@ -325,28 +334,47 @@ export function buildExternalServicesStep(controller: OnboardingWizardController
325
334
 
326
335
  function getSelectedExternalSurfaceSpecs(controller: OnboardingWizardController): readonly ExternalSurfaceSpec[] {
327
336
  return EXTERNAL_SURFACE_SPECS.filter((surface) => (
328
- controller.getBooleanFieldValue(surface.enabledFieldId, surface.defaultEnabled(controller.runtimeSnapshot))
337
+ controller.getBooleanFieldValue(
338
+ surface.enabledFieldId,
339
+ isExternalSurfaceSelectedByDefault(surface, controller.runtimeSnapshot),
340
+ )
329
341
  ));
330
342
  }
331
343
 
344
+ const SURFACE_AUTO_START_OPTIONS: readonly OnboardingWizardRadioOption[] = [
345
+ {
346
+ id: 'yes',
347
+ label: 'Yes',
348
+ hint: 'Start this surface automatically when the GoodVibes service starts.',
349
+ },
350
+ {
351
+ id: 'no',
352
+ label: 'No',
353
+ hint: 'Save these settings but leave the surface idle until it is enabled from Settings > Surfaces.',
354
+ },
355
+ ];
356
+
332
357
  function buildExternalSurfaceStep(
333
358
  controller: OnboardingWizardController,
334
359
  surface: ExternalSurfaceSpec,
335
360
  ): OnboardingWizardStepDefinition {
336
- let requiredCount = 0;
337
- let requiredCompleteCount = 0;
361
+ let setupCount = 0;
362
+ let setupCompleteCount = 0;
363
+ const autoStartFieldId = getExternalSurfaceAutoStartFieldId(surface);
364
+ const autoStartDefault = getExternalSurfaceAutoStartDefaultValue(surface, controller.runtimeSnapshot);
365
+ const autoStartValue = controller.getStringFieldValue(autoStartFieldId, autoStartDefault);
338
366
  const setupFields = surface.fields.map((setupField): OnboardingWizardFieldDefinition => {
339
- const required = controller.isRequiredExternalSetupField(setupField.id);
340
- if (required) {
341
- requiredCount += 1;
367
+ const suggested = controller.isRequiredExternalSetupField(setupField.id);
368
+ if (suggested) {
369
+ setupCount += 1;
342
370
  if (normalizeText(setupField.defaultValue(controller.runtimeSnapshot)).length > 0
343
371
  || normalizeText(controller.getStringFieldValue(setupField.id, '')).length > 0) {
344
- requiredCompleteCount += 1;
372
+ setupCompleteCount += 1;
345
373
  }
346
374
  }
347
375
 
348
- const hint = required
349
- ? `${setupField.hint} Required because ${surface.label} is selected.`
376
+ const hint = suggested
377
+ ? `${setupField.hint} Recommended because ${surface.label} is selected, but it will not block saving.`
350
378
  : setupField.hint;
351
379
 
352
380
  if (setupField.kind === 'radio') {
@@ -367,26 +395,46 @@ function buildExternalSurfaceStep(
367
395
  hint,
368
396
  placeholder: setupField.placeholder,
369
397
  defaultValue: setupField.defaultValue(controller.runtimeSnapshot),
370
- required,
371
398
  };
372
399
  });
400
+ const ntfyTopicSummary = surface.id === 'ntfy'
401
+ ? [
402
+ `Chat topic: ${controller.getStringFieldValue('external-services.ntfy.chat-topic', 'goodvibes-chat')}`,
403
+ `Agent topic: ${controller.getStringFieldValue('external-services.ntfy.agent-topic', 'goodvibes-agent')}`,
404
+ `Daemon-only remote topic: ${controller.getStringFieldValue('external-services.ntfy.remote-topic', 'goodvibes-ntfy')}`,
405
+ ]
406
+ : [];
373
407
  const title = `${surface.label.replace(/ surface$/i, '')} setup`;
374
- const requiredSummary = requiredCount === 0
375
- ? 'Required setup: none'
376
- : `Required setup: ${requiredCompleteCount}/${requiredCount}`;
408
+ const setupSummary = setupCount === 0
409
+ ? 'Suggested setup: none'
410
+ : `Suggested setup entered: ${setupCompleteCount}/${setupCount}`;
377
411
 
378
412
  return {
379
413
  id: `external-surface:${surface.id}` as OnboardingWizardExternalSurfaceStepId,
380
414
  title,
381
415
  shortLabel: surface.label.replace(/ surface$/i, ''),
382
- description: `Configure ${surface.label}. This screen only appears because that surface is selected on the previous screen.`,
416
+ description: `Configure ${surface.label}. Settings are saved either way; auto-start controls whether this surface starts with the background service.`,
383
417
  summaryTitle: `${surface.label} setup`,
384
418
  summaryLines: [
385
- requiredSummary,
419
+ `Auto-start: ${autoStartValue === 'yes' ? 'yes' : 'no'}`,
420
+ ...ntfyTopicSummary,
421
+ setupSummary,
386
422
  `Secret policy: ${controller.getStringFieldValue('external-services.secret-policy', controller.runtimeSnapshot?.runtimeDefaults.secretStoragePolicy ?? 'preferred_secure')}`,
387
- 'Leave optional fields blank to keep defaults or existing values.',
423
+ autoStartValue === 'yes'
424
+ ? 'The surface will be enabled after apply.'
425
+ : 'Start it later from Settings > Surfaces by turning Enabled on.',
426
+ ],
427
+ fields: [
428
+ {
429
+ kind: 'radio',
430
+ id: autoStartFieldId,
431
+ label: 'Auto-start this surface',
432
+ hint: `Yes turns on ${surface.enabledConfigKey}. No saves setup values but keeps the surface off until Settings > Surfaces enables it.`,
433
+ options: SURFACE_AUTO_START_OPTIONS,
434
+ defaultValue: autoStartDefault,
435
+ },
436
+ ...setupFields,
388
437
  ],
389
- fields: setupFields,
390
438
  };
391
439
  }
392
440
 
@@ -574,32 +622,34 @@ export function buildAccountsStep(controller: OnboardingWizardController): Onboa
574
622
  && !needsAuthBootstrap
575
623
  && controller.hasLocalAuthUser();
576
624
  const fields: OnboardingWizardFieldDefinition[] = [];
625
+ const defaultAdminUsername = controller.getDefaultAdminUsername();
577
626
 
578
- if (needsAuthBootstrap) {
579
- const defaultAdminUsername = controller.getDefaultAdminUsername();
580
- fields.push(
581
- {
582
- kind: 'text',
583
- id: 'accounts.admin-username',
584
- label: 'Local auth admin username',
585
- hint: 'Required before any background service, browser surface, or listener is exposed.',
586
- placeholder: defaultAdminUsername,
587
- defaultValue: defaultAdminUsername,
588
- required: true,
589
- },
590
- {
591
- kind: 'masked',
592
- id: 'accounts.admin-password',
593
- label: 'Local auth admin password',
594
- hint: controller.hasBootstrapCredentialPresent()
595
- ? 'Creates a new local admin, removes the bootstrap credential file, and retires the bootstrap admin before LAN/server settings are applied.'
596
- : 'Creates the first local admin user and an initial session before LAN/server settings are applied.',
597
- placeholder: 'password required',
598
- defaultValue: '',
599
- required: true,
600
- },
601
- );
602
- }
627
+ fields.push(
628
+ {
629
+ kind: 'text',
630
+ id: 'accounts.admin-username',
631
+ label: 'Local auth admin username',
632
+ hint: needsAuthBootstrap
633
+ ? 'Required before any background service, browser surface, or listener is exposed.'
634
+ : 'Optional. Enter an existing admin username to rotate its password, or a new username to create another admin.',
635
+ placeholder: defaultAdminUsername,
636
+ defaultValue: defaultAdminUsername,
637
+ required: needsAuthBootstrap,
638
+ },
639
+ {
640
+ kind: 'masked',
641
+ id: 'accounts.admin-password',
642
+ label: 'Local auth admin password',
643
+ hint: needsAuthBootstrap
644
+ ? controller.hasBootstrapCredentialPresent()
645
+ ? 'Creates or updates the named local admin, removes the bootstrap credential file, and retires the bootstrap admin when it is a different user.'
646
+ : 'Creates the first local admin user and an initial session before LAN/server settings are applied.'
647
+ : 'Optional. Leave blank to keep existing local auth unchanged; enter a password to create or rotate the named admin user.',
648
+ placeholder: needsAuthBootstrap ? 'password required' : 'leave blank to keep unchanged',
649
+ defaultValue: '',
650
+ required: needsAuthBootstrap,
651
+ },
652
+ );
603
653
 
604
654
  fields.push(
605
655
  {
@@ -668,6 +718,29 @@ export function buildAccountsStep(controller: OnboardingWizardController): Onboa
668
718
  }
669
719
 
670
720
  export function buildReviewStep(controller: OnboardingWizardController): OnboardingWizardStepDefinition {
721
+ const feedback = controller.applyFeedback;
722
+ const feedbackFields: OnboardingWizardFieldDefinition[] = feedback
723
+ ? [
724
+ {
725
+ kind: 'status',
726
+ id: 'review.feedback',
727
+ label: feedback.title,
728
+ hint: feedback.summary,
729
+ defaultValue: feedback.severity === 'error' ? 'Needs attention' : feedback.severity === 'warning' ? 'Warning' : 'Info',
730
+ },
731
+ ...feedback.messages.slice(0, 8).map((message, index): OnboardingWizardFieldDefinition => ({
732
+ kind: 'status',
733
+ id: `review.feedback.${index}`,
734
+ label: message,
735
+ hint: message,
736
+ defaultValue: feedback.severity === 'error' ? 'Error' : feedback.severity === 'warning' ? 'Warning' : 'Info',
737
+ })),
738
+ ]
739
+ : [];
740
+ const unsavedLabel = controller.dirtyStepCount === 1
741
+ ? '1 screen has unapplied changes'
742
+ : `${controller.dirtyStepCount} screens have unapplied changes`;
743
+
671
744
  return {
672
745
  id: 'review',
673
746
  title: 'Review and apply',
@@ -675,12 +748,13 @@ export function buildReviewStep(controller: OnboardingWizardController): Onboard
675
748
  description: 'Review the selected settings and apply them directly from the wizard.',
676
749
  summaryTitle: 'Review posture',
677
750
  summaryLines: [
678
- `${controller.dirtyStepCount} dirty step(s)`,
679
- `${controller.buildApplyRequest().operations.length} operation(s) ready to apply`,
680
- `Pending picker: ${controller.pendingModelPickerTarget ?? 'none'}`,
681
- controller.isEditingTextField() ? `Editing: ${controller.editingFieldId}` : 'Ready for apply/verify',
751
+ unsavedLabel,
752
+ `${controller.buildApplyRequest().operations.length} settings change(s) ready to apply`,
753
+ feedback ? `Last apply: ${feedback.title}` : 'No apply errors reported',
754
+ controller.isEditingTextField() ? `Editing: ${controller.editingFieldId}` : 'Ready to apply',
682
755
  ],
683
756
  fields: [
757
+ ...feedbackFields,
684
758
  {
685
759
  kind: 'status',
686
760
  id: 'review.global-marker',
@@ -49,6 +49,15 @@ export type OnboardingWizardAction =
49
49
  | 'start-openai-subscription'
50
50
  | 'finish-openai-subscription';
51
51
 
52
+ export type OnboardingWizardApplyFeedbackSeverity = 'info' | 'warning' | 'error';
53
+
54
+ export interface OnboardingWizardApplyFeedback {
55
+ readonly severity: OnboardingWizardApplyFeedbackSeverity;
56
+ readonly title: string;
57
+ readonly summary: string;
58
+ readonly messages: readonly string[];
59
+ }
60
+
52
61
  export interface OnboardingWizardRadioOption {
53
62
  readonly id: string;
54
63
  readonly label: string;
@@ -164,6 +173,7 @@ export interface OnboardingWizardSnapshot {
164
173
  readonly editBuffer: string;
165
174
  readonly hydrationPending: boolean;
166
175
  readonly hydrationError: string | null;
176
+ readonly applyFeedback: OnboardingWizardApplyFeedback | null;
167
177
  readonly hydration: OnboardingWizardRuntimeHydration;
168
178
  }
169
179
 
@@ -34,9 +34,9 @@ import {
34
34
  toggleCapability as toggleCapabilityForController,
35
35
  } from './onboarding-wizard-rules.ts';
36
36
  import { ensureSelectionVisible as ensureSelectionVisibleForController, getBlockingFieldLabels as getBlockingFieldLabelsForController, getCapabilitySelectionState as getCapabilitySelectionStateForController, getCompletedFieldCount as getCompletedFieldCountForController, getCompletedToggleCount as getCompletedToggleCountForController, getCurrentCapabilities as getCurrentCapabilitiesForController, getFieldById as getFieldByIdForController, getFieldValidationError as getFieldValidationErrorForController, getStepFieldCount as getStepFieldCountForController, getToggleFieldCount as getToggleFieldCountForController, hasExistingAccessState as hasExistingAccessStateForController, isFieldDirty as isFieldDirtyForController, isFieldDirtyByDefinition as isFieldDirtyByDefinitionForController, isFieldSatisfied as isFieldSatisfiedForController, isStepDirty as isStepDirtyForController, recalculateDirtyState as recalculateDirtyStateForController, reconcileStateWithCurrentDefinitions as reconcileStateWithCurrentDefinitionsForController, reconcileStepCursor as reconcileStepCursorForController, resetValuesFromCurrentDefinitions as resetValuesFromCurrentDefinitionsForController } from './onboarding-wizard-state.ts';
37
- import type { MutableModelSelectionMap, OnboardingWizardAction, OnboardingWizardFieldDefinition, OnboardingWizardFieldWindow, OnboardingWizardMode, OnboardingWizardModelSelection, OnboardingWizardRuntimeHydration, OnboardingWizardSnapshot, OnboardingWizardStepDefinition, OnboardingWizardStepId } from './onboarding-wizard-types.ts';
37
+ import type { MutableModelSelectionMap, OnboardingWizardAction, OnboardingWizardApplyFeedback, OnboardingWizardFieldDefinition, OnboardingWizardFieldWindow, OnboardingWizardMode, OnboardingWizardModelSelection, OnboardingWizardRuntimeHydration, OnboardingWizardSnapshot, OnboardingWizardStepDefinition, OnboardingWizardStepId } from './onboarding-wizard-types.ts';
38
38
 
39
- export type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardAction, OnboardingWizardActionFieldDefinition, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardFieldKind, OnboardingWizardFieldWindow, OnboardingWizardMaskedFieldDefinition, OnboardingWizardMode, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardModelSelection, OnboardingWizardRadioFieldDefinition, OnboardingWizardRadioOption, OnboardingWizardRuntimeHydration, OnboardingWizardSnapshot, OnboardingWizardStatusFieldDefinition, OnboardingWizardStepDefinition, OnboardingWizardStepId, OnboardingWizardTextFieldDefinition } from './onboarding-wizard-types.ts';
39
+ export type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardAction, OnboardingWizardActionFieldDefinition, OnboardingWizardApplyFeedback, OnboardingWizardApplyFeedbackSeverity, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardFieldKind, OnboardingWizardFieldWindow, OnboardingWizardMaskedFieldDefinition, OnboardingWizardMode, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardModelSelection, OnboardingWizardRadioFieldDefinition, OnboardingWizardRadioOption, OnboardingWizardRuntimeHydration, OnboardingWizardSnapshot, OnboardingWizardStatusFieldDefinition, OnboardingWizardStepDefinition, OnboardingWizardStepId, OnboardingWizardTextFieldDefinition } from './onboarding-wizard-types.ts';
40
40
  export { getOnboardingWizardBodyRows, getOnboardingWizardVisibleFieldCount } from './onboarding-wizard-helpers.ts';
41
41
 
42
42
  export class OnboardingWizardController {
@@ -58,6 +58,7 @@ export class OnboardingWizardController {
58
58
  public editingFieldId: string | null = null;
59
59
  public editBuffer = '';
60
60
  public hydrationError: string | null = null;
61
+ public applyFeedback: OnboardingWizardApplyFeedback | null = null;
61
62
 
62
63
  public readonly baselineToggleState = new Map<string, boolean>();
63
64
  public readonly baselineRadioState = new Map<string, string>();
@@ -102,6 +103,7 @@ export class OnboardingWizardController {
102
103
  this.stepIndex = 0;
103
104
  this.pendingModelPickerTarget = null;
104
105
  this.pendingAction = null;
106
+ this.applyFeedback = null;
105
107
  this.editingFieldId = null;
106
108
  this.editBuffer = '';
107
109
  this.scrollOffsets.fill(0);
@@ -115,6 +117,7 @@ export class OnboardingWizardController {
115
117
  this.hydrationError = null;
116
118
  this.pendingModelPickerTarget = null;
117
119
  this.pendingAction = null;
120
+ this.applyFeedback = null;
118
121
  this.cancelEdit();
119
122
  }
120
123
 
@@ -178,6 +181,7 @@ export class OnboardingWizardController {
178
181
  editBuffer: this.editBuffer,
179
182
  hydrationPending: this.hydrationPending,
180
183
  hydrationError: this.hydrationError,
184
+ applyFeedback: this.applyFeedback,
181
185
  hydration: this.captureHydratedState(),
182
186
  };
183
187
  }
@@ -191,6 +195,7 @@ export class OnboardingWizardController {
191
195
  this.stepIndex = clamp(snapshot.stepIndex, 0, Math.max(0, this.steps.length - 1));
192
196
  this.pendingModelPickerTarget = snapshot.pendingModelPickerTarget;
193
197
  this.pendingAction = snapshot.pendingAction;
198
+ this.applyFeedback = snapshot.applyFeedback;
194
199
  this.dirtyStepIds.clear();
195
200
  for (const stepId of snapshot.dirtyStepIds) this.dirtyStepIds.add(stepId);
196
201
 
@@ -223,6 +228,7 @@ export class OnboardingWizardController {
223
228
  public beginRuntimeHydration(): void {
224
229
  this.hydrationPending = true;
225
230
  this.hydrationError = null;
231
+ this.applyFeedback = null;
226
232
  this.stepIndex = 0;
227
233
  this.pendingModelPickerTarget = null;
228
234
  this.pendingAction = null;
@@ -232,6 +238,7 @@ export class OnboardingWizardController {
232
238
  public finishRuntimeHydration(): void {
233
239
  this.hydrationPending = false;
234
240
  this.hydrationError = null;
241
+ this.applyFeedback = null;
235
242
  this.stepIndex = clamp(this.stepIndex, 0, Math.max(0, this.steps.length - 1));
236
243
  this.reconcileStepCursor(this.stepIndex);
237
244
  }
@@ -239,6 +246,12 @@ export class OnboardingWizardController {
239
246
  public failRuntimeHydration(message: string): void {
240
247
  this.hydrationPending = false;
241
248
  this.hydrationError = message;
249
+ this.applyFeedback = {
250
+ severity: 'error',
251
+ title: 'Current settings could not load',
252
+ summary: message,
253
+ messages: [message],
254
+ };
242
255
  this.stepIndex = 0;
243
256
  this.pendingModelPickerTarget = null;
244
257
  this.pendingAction = null;
@@ -344,6 +357,14 @@ export class OnboardingWizardController {
344
357
  this.pendingAction = null;
345
358
  }
346
359
 
360
+ public setApplyFeedback(feedback: OnboardingWizardApplyFeedback): void {
361
+ this.applyFeedback = feedback;
362
+ }
363
+
364
+ public clearApplyFeedback(): void {
365
+ this.applyFeedback = null;
366
+ }
367
+
347
368
  public consumePendingAction(): OnboardingWizardAction | null {
348
369
  const action = this.pendingAction;
349
370
  this.pendingAction = null;
@@ -360,6 +381,7 @@ export class OnboardingWizardController {
360
381
  this.toggleCapability(field.capabilityId);
361
382
  this.pendingModelPickerTarget = null;
362
383
  this.pendingAction = null;
384
+ this.applyFeedback = null;
363
385
  this.recalculateDirtyState();
364
386
  return;
365
387
  }
@@ -369,6 +391,7 @@ export class OnboardingWizardController {
369
391
  this.toggleState.set(field.id, !current);
370
392
  this.pendingModelPickerTarget = null;
371
393
  this.pendingAction = null;
394
+ this.applyFeedback = null;
372
395
  this.recalculateDirtyState();
373
396
  return;
374
397
  }
@@ -382,6 +405,7 @@ export class OnboardingWizardController {
382
405
  this.radioState.set(field.id, next.id);
383
406
  this.pendingModelPickerTarget = null;
384
407
  this.pendingAction = null;
408
+ this.applyFeedback = null;
385
409
  this.recalculateDirtyState();
386
410
  return;
387
411
  }
@@ -397,6 +421,7 @@ export class OnboardingWizardController {
397
421
  this.selectAllServerCapabilities();
398
422
  this.pendingAction = null;
399
423
  this.pendingModelPickerTarget = null;
424
+ this.applyFeedback = null;
400
425
  this.recalculateDirtyState();
401
426
  return;
402
427
  }
@@ -405,6 +430,7 @@ export class OnboardingWizardController {
405
430
  this.selectLocalTuiOnly();
406
431
  this.pendingAction = null;
407
432
  this.pendingModelPickerTarget = null;
433
+ this.applyFeedback = null;
408
434
  this.recalculateDirtyState();
409
435
  return;
410
436
  }
@@ -413,6 +439,7 @@ export class OnboardingWizardController {
413
439
  this.selectAllExternalSurfaces();
414
440
  this.pendingAction = null;
415
441
  this.pendingModelPickerTarget = null;
442
+ this.applyFeedback = null;
416
443
  this.recalculateDirtyState();
417
444
  return;
418
445
  }
@@ -421,6 +448,7 @@ export class OnboardingWizardController {
421
448
  this.clearExternalSurfaces();
422
449
  this.pendingAction = null;
423
450
  this.pendingModelPickerTarget = null;
451
+ this.applyFeedback = null;
424
452
  this.recalculateDirtyState();
425
453
  return;
426
454
  }
@@ -433,6 +461,7 @@ export class OnboardingWizardController {
433
461
  this.pendingModelPickerTarget = field.target;
434
462
  this.pendingAction = null;
435
463
  this.touchedActionFields.add(field.id);
464
+ this.applyFeedback = null;
436
465
  }
437
466
 
438
467
  public beginEdit(fieldId: string): void {
@@ -458,6 +487,7 @@ export class OnboardingWizardController {
458
487
  const field = this.getFieldById(fieldId);
459
488
  if (field && (field.kind === 'text' || field.kind === 'masked')) {
460
489
  this.textState.set(fieldId, this.editBuffer);
490
+ this.applyFeedback = null;
461
491
  this.recalculateDirtyState();
462
492
  }
463
493
  this.editingFieldId = null;
@@ -479,6 +509,21 @@ export class OnboardingWizardController {
479
509
  this.editBuffer = this.editBuffer.slice(0, -1);
480
510
  }
481
511
 
512
+ public clearEditingValue(): void {
513
+ if (this.editingFieldId === null) return;
514
+ this.editBuffer = '';
515
+ }
516
+
517
+ public clearSelectedTextField(): boolean {
518
+ const field = this.getSelectedField();
519
+ if (!field || (field.kind !== 'text' && field.kind !== 'masked')) return false;
520
+ this.textState.set(field.id, '');
521
+ if (this.editingFieldId === field.id) this.editBuffer = '';
522
+ this.applyFeedback = null;
523
+ this.recalculateDirtyState();
524
+ return true;
525
+ }
526
+
482
527
  public setFieldValue(fieldId: string, value: boolean | string): void {
483
528
  const field = this.getFieldById(fieldId);
484
529
  if (!field) return;
@@ -487,6 +532,7 @@ export class OnboardingWizardController {
487
532
  if (typeof value === 'boolean') {
488
533
  if (field.kind === 'checklist' && field.capabilityId) this.setCapabilityValue(field.capabilityId, value);
489
534
  else this.toggleState.set(fieldId, value);
535
+ this.applyFeedback = null;
490
536
  this.recalculateDirtyState();
491
537
  }
492
538
  return;
@@ -495,6 +541,7 @@ export class OnboardingWizardController {
495
541
  if (field.kind === 'radio') {
496
542
  if (typeof value === 'string' && field.options.some((option) => option.id === value)) {
497
543
  this.radioState.set(fieldId, value);
544
+ this.applyFeedback = null;
498
545
  this.recalculateDirtyState();
499
546
  }
500
547
  return;
@@ -504,6 +551,7 @@ export class OnboardingWizardController {
504
551
  if (typeof value === 'string') {
505
552
  this.textState.set(fieldId, value);
506
553
  if (this.editingFieldId === fieldId) this.editBuffer = value;
554
+ this.applyFeedback = null;
507
555
  this.recalculateDirtyState();
508
556
  }
509
557
  }
@@ -559,13 +607,15 @@ export class OnboardingWizardController {
559
607
 
560
608
  if (field.kind === 'text') {
561
609
  const value = normalizeText(this.getFieldValue(field) as string);
562
- if (value.length === 0 && (field.required === true || this.isRequiredExternalSetupField(field.id))) return 'Missing';
610
+ if (value.length === 0 && field.required === true) return 'Missing';
611
+ if (value.length === 0 && this.isRequiredExternalSetupField(field.id)) return 'Not set';
563
612
  return value.length > 0 ? value : field.placeholder;
564
613
  }
565
614
 
566
615
  if (field.kind === 'masked') {
567
616
  const value = normalizeText(this.getFieldValue(field) as string);
568
- if (value.length === 0 && (field.required === true || this.isRequiredExternalSetupField(field.id))) return 'Missing';
617
+ if (value.length === 0 && field.required === true) return 'Missing';
618
+ if (value.length === 0 && this.isRequiredExternalSetupField(field.id)) return 'Not set';
569
619
  return value.length > 0 ? maskValue(value) : field.placeholder;
570
620
  }
571
621
 
@@ -584,6 +634,7 @@ export class OnboardingWizardController {
584
634
  enabled: selection.enabled ?? true,
585
635
  });
586
636
  this.pendingModelPickerTarget = null;
637
+ this.applyFeedback = null;
587
638
  this.recalculateDirtyState();
588
639
  }
589
640
 
@@ -601,6 +652,7 @@ export class OnboardingWizardController {
601
652
  this.baselineModelSelectionState.clear();
602
653
  for (const [key, value] of this.modelSelectionState) this.baselineModelSelectionState.set(key, cloneSelection(value));
603
654
  this.dirtyStepIds.clear();
655
+ this.applyFeedback = null;
604
656
  }
605
657
 
606
658
  public getSharedIpDefault(enabled: { readonly controlPlane: boolean; readonly httpListener: boolean; readonly web: boolean }): boolean {
@@ -2,7 +2,7 @@ import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config/schema
2
2
  import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
3
3
  import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
4
4
 
5
- export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'danger' | 'tools' | 'flags' | 'network';
5
+ export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'danger' | 'tools' | 'flags' | 'network';
6
6
 
7
7
  export const SETTINGS_CATEGORIES: SettingsCategory[] = [
8
8
  'display',
@@ -14,6 +14,7 @@ export const SETTINGS_CATEGORIES: SettingsCategory[] = [
14
14
  'permissions',
15
15
  'mcp',
16
16
  'sandbox',
17
+ 'surfaces',
17
18
  'danger',
18
19
  'tools',
19
20
  'flags',
@@ -519,6 +519,8 @@ export class SettingsModal {
519
519
  cat = 'tools';
520
520
  } else if (rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') {
521
521
  cat = 'network';
522
+ } else if (rawCat === 'surfaces') {
523
+ cat = 'surfaces';
522
524
  } else {
523
525
  cat = rawCat as SettingsCategory;
524
526
  }
@@ -746,6 +748,8 @@ export class SettingsModal {
746
748
  if (isRestartKey && previousValue !== value) {
747
749
  this.lastSaveTriggeredRestart = 'web';
748
750
  }
751
+ } else if (rawCat === 'surfaces') {
752
+ cat = 'surfaces';
749
753
  } else {
750
754
  cat = rawCat as SettingsCategory;
751
755
  }