@pellux/goodvibes-tui 0.20.3 → 0.22.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 (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +23 -2
  3. package/docs/foundation-artifacts/operator-contract.json +78 -1
  4. package/package.json +4 -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 +658 -0
  9. package/src/cli/config-overrides.ts +68 -0
  10. package/src/cli/entrypoint.ts +6 -0
  11. package/src/cli/help.ts +4 -2
  12. package/src/cli/management-commands.ts +1 -1
  13. package/src/cli/management.ts +1 -8
  14. package/src/cli/parser.ts +31 -18
  15. package/src/cli/service-command.ts +1 -1
  16. package/src/cli/surface-command.ts +1 -1
  17. package/src/cli/tui-startup.ts +72 -10
  18. package/src/cli/types.ts +14 -3
  19. package/src/cli-flags.ts +1 -0
  20. package/src/config/atomic-write.ts +70 -0
  21. package/src/config/goodvibes-home-audit.ts +2 -0
  22. package/src/config/read-versioned.ts +115 -0
  23. package/src/core/context-auto-compact.ts +77 -0
  24. package/src/core/conversation-rendering.ts +49 -15
  25. package/src/core/conversation.ts +101 -16
  26. package/src/core/format-user-error.ts +192 -0
  27. package/src/core/stream-event-wiring.ts +144 -0
  28. package/src/core/stream-stall-watchdog.ts +103 -0
  29. package/src/core/system-message-router.ts +5 -1
  30. package/src/core/turn-event-wiring.ts +124 -0
  31. package/src/daemon/cli.ts +5 -0
  32. package/src/export/cost-utils.ts +71 -0
  33. package/src/export/gist-uploader.ts +136 -0
  34. package/src/input/command-registry.ts +32 -1
  35. package/src/input/commands/control-room-runtime.ts +10 -10
  36. package/src/input/commands/experience-runtime.ts +5 -4
  37. package/src/input/commands/knowledge.ts +1 -1
  38. package/src/input/commands/local-auth-runtime.ts +27 -5
  39. package/src/input/commands/local-setup.ts +4 -6
  40. package/src/input/commands/memory-product-runtime.ts +8 -6
  41. package/src/input/commands/operator-panel-runtime.ts +1 -1
  42. package/src/input/commands/operator-runtime.ts +3 -10
  43. package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
  44. package/src/input/commands/provider.ts +57 -3
  45. package/src/input/commands/recall-review.ts +26 -2
  46. package/src/input/commands/services-runtime.ts +2 -2
  47. package/src/input/commands/session-workflow.ts +8 -16
  48. package/src/input/commands/session.ts +70 -20
  49. package/src/input/commands/share-runtime.ts +99 -12
  50. package/src/input/commands/tts-runtime.ts +30 -4
  51. package/src/input/commands.ts +2 -4
  52. package/src/input/delete-key-policy.ts +46 -0
  53. package/src/input/feed-context-factory.ts +2 -0
  54. package/src/input/handler-feed.ts +3 -0
  55. package/src/input/handler-interactions.ts +2 -15
  56. package/src/input/handler-modal-routes.ts +128 -12
  57. package/src/input/handler-modal-token-routes.ts +22 -5
  58. package/src/input/handler-onboarding-cloudflare.ts +1 -1
  59. package/src/input/handler-onboarding.ts +73 -69
  60. package/src/input/handler-types.ts +163 -0
  61. package/src/input/handler.ts +6 -2
  62. package/src/input/input-history.ts +76 -6
  63. package/src/input/model-picker-filter.ts +265 -0
  64. package/src/input/model-picker-items.ts +208 -0
  65. package/src/input/model-picker.ts +92 -325
  66. package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
  67. package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
  68. package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
  69. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
  70. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
  71. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
  72. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
  73. package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
  74. package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
  75. package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
  76. package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
  77. package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
  78. package/src/input/onboarding/onboarding-wizard.ts +3 -3
  79. package/src/input/settings-modal-behavior.ts +5 -0
  80. package/src/input/settings-modal-data.ts +378 -0
  81. package/src/input/settings-modal-mutations.ts +157 -0
  82. package/src/input/settings-modal-reset.ts +154 -0
  83. package/src/input/settings-modal.ts +236 -232
  84. package/src/main.ts +93 -85
  85. package/src/panels/agent-inspector-panel.ts +120 -18
  86. package/src/panels/agent-inspector-shared.ts +29 -0
  87. package/src/panels/builtin/agent.ts +4 -1
  88. package/src/panels/builtin/development.ts +5 -1
  89. package/src/panels/builtin/knowledge.ts +14 -13
  90. package/src/panels/builtin/operations.ts +22 -1
  91. package/src/panels/builtin/shared.ts +7 -0
  92. package/src/panels/cockpit-panel.ts +123 -3
  93. package/src/panels/cockpit-read-model.ts +232 -0
  94. package/src/panels/confirm-state.ts +27 -12
  95. package/src/panels/cost-tracker-panel.ts +23 -67
  96. package/src/panels/eval-panel.ts +10 -9
  97. package/src/panels/index.ts +1 -1
  98. package/src/panels/knowledge-graph-panel.ts +84 -0
  99. package/src/panels/local-auth-panel.ts +124 -4
  100. package/src/panels/memory-panel.ts +370 -40
  101. package/src/panels/project-planning-panel.ts +42 -4
  102. package/src/panels/search-focus.ts +11 -5
  103. package/src/panels/session-maintenance.ts +66 -15
  104. package/src/panels/subscription-panel.ts +33 -25
  105. package/src/panels/types.ts +28 -1
  106. package/src/panels/wrfc-panel.ts +224 -41
  107. package/src/renderer/agent-detail-modal.ts +118 -13
  108. package/src/renderer/code-block.ts +10 -2
  109. package/src/renderer/compositor.ts +18 -4
  110. package/src/renderer/context-inspector.ts +1 -5
  111. package/src/renderer/context-status-hint.ts +54 -0
  112. package/src/renderer/diff.ts +94 -21
  113. package/src/renderer/markdown.ts +29 -13
  114. package/src/renderer/settings-modal-helpers.ts +1 -1
  115. package/src/renderer/settings-modal.ts +90 -10
  116. package/src/renderer/shell-surface.ts +10 -0
  117. package/src/renderer/syntax-highlighter.ts +10 -3
  118. package/src/renderer/term-caps.ts +318 -0
  119. package/src/renderer/theme.ts +158 -0
  120. package/src/renderer/tool-call.ts +12 -2
  121. package/src/renderer/ui-factory.ts +50 -6
  122. package/src/runtime/bootstrap-command-context.ts +1 -0
  123. package/src/runtime/bootstrap-command-parts.ts +18 -0
  124. package/src/runtime/bootstrap-core.ts +145 -13
  125. package/src/runtime/bootstrap-shell.ts +11 -0
  126. package/src/runtime/bootstrap.ts +9 -0
  127. package/src/runtime/onboarding/apply.ts +4 -6
  128. package/src/runtime/onboarding/index.ts +1 -0
  129. package/src/runtime/onboarding/markers.ts +42 -49
  130. package/src/runtime/onboarding/progress.ts +148 -0
  131. package/src/runtime/onboarding/state.ts +133 -55
  132. package/src/runtime/onboarding/types.ts +20 -0
  133. package/src/runtime/services.ts +27 -1
  134. package/src/runtime/wrfc-persistence.ts +237 -0
  135. package/src/shell/blocking-input.ts +20 -5
  136. package/src/tools/wrfc-agent-guard.ts +64 -3
  137. package/src/utils/format-elapsed.ts +30 -0
  138. package/src/utils/terminal-width.ts +45 -0
  139. package/src/version.ts +1 -1
  140. package/src/work-plans/work-plan-store.ts +4 -6
  141. package/src/panels/knowledge-panel.ts +0 -345
  142. package/src/planning/project-planning-coordinator.ts +0 -543
@@ -1,6 +1,6 @@
1
1
  import { CLOUDFLARE_COMPONENT_IDS, DEFAULT_CLOUDFLARE_COMPONENT_SELECTION } from '../../runtime/cloudflare-control-plane.ts';
2
2
  import { normalizeText } from './onboarding-wizard-helpers.ts';
3
- import type { OnboardingWizardController } from './onboarding-wizard.ts';
3
+ import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
4
4
  import type { OnboardingWizardFieldDefinition, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
5
5
  import {
6
6
  CLOUDFLARE_BATCH_MODE_OPTIONS,
@@ -14,7 +14,7 @@ import {
14
14
  getCloudflareSetupSource,
15
15
  } from './onboarding-wizard-cloudflare.ts';
16
16
 
17
- export function buildCloudflareStep(controller: OnboardingWizardController): OnboardingWizardStepDefinition {
17
+ export function buildCloudflareStep(controller: OnboardingWizardControllerLike): OnboardingWizardStepDefinition {
18
18
  const config = controller.runtimeSnapshot?.config.cloudflare;
19
19
  const batch = controller.runtimeSnapshot?.config.batch;
20
20
  const enabledDefault = controller.isCapabilitySelected('cloudflare-batch') || config?.enabled === true;
@@ -257,6 +257,19 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
257
257
  );
258
258
  }
259
259
 
260
+ // Trust-proxy notice — shown when Tunnel is selected so the
261
+ // operator sees the security implication before applying.
262
+ const tunnelSelected = enabled && components.zeroTrustTunnel;
263
+ if (tunnelSelected) {
264
+ fields.push({
265
+ kind: 'status',
266
+ id: 'cloudflare.trust-proxy-notice',
267
+ label: 'trustProxy will be enabled for control plane and HTTP listener',
268
+ defaultValue: 'Notice',
269
+ hint: 'Selecting Zero Trust Tunnel auto-writes controlPlane.trustProxy=true and httpListener.trustProxy=true so the login rate-limiter keys on the real client IP (CF-Connecting-IP) rather than the tunnel egress address. RESIDUAL RISK: until the SDK validates CF-Connecting-IP against Cloudflare published IP ranges (handoff Item 5), a client that reaches the listener directly can spoof this header to bypass the per-IP rate-limiter. See docs/deployment-and-services.md for the full risk posture.',
270
+ });
271
+ }
272
+
260
273
  if (components.zeroTrustAccess) {
261
274
  fields.push(
262
275
  {
@@ -463,6 +476,7 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
463
476
  `Components: ${enabled ? componentCount : 0} selected`,
464
477
  `Token setup: ${enabled ? setupSource : 'not used'}`,
465
478
  `Provision on final apply: ${enabled ? controller.getStringFieldValue('cloudflare.provision-on-apply', 'no') : 'no'}`,
479
+ ...(enabled && components.zeroTrustTunnel ? ['trustProxy: enabled for control plane and HTTP listener (see security notice)'] : []),
466
480
  ],
467
481
  fields,
468
482
  };
@@ -8,7 +8,7 @@ import {
8
8
  type CloudflareProvisionRequest,
9
9
  } from '../../runtime/cloudflare-control-plane.ts';
10
10
  import { buildGoodVibesSecretRef, normalizeText } from './onboarding-wizard-helpers.ts';
11
- import type { OnboardingWizardController } from './onboarding-wizard.ts';
11
+ import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
12
12
  import type { OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
13
13
 
14
14
  export const CLOUDFLARE_SETUP_SOURCE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
@@ -90,7 +90,7 @@ export function cloudflareComponentLabel(component: CloudflareComponent): string
90
90
  return CLOUDFLARE_COMPONENT_LABELS[component];
91
91
  }
92
92
 
93
- export function isCloudflareConfigured(controller: OnboardingWizardController): boolean {
93
+ export function isCloudflareConfigured(controller: OnboardingWizardControllerLike): boolean {
94
94
  const config = controller.runtimeSnapshot?.config.cloudflare;
95
95
  if (!config) return false;
96
96
  return config.enabled
@@ -100,11 +100,11 @@ export function isCloudflareConfigured(controller: OnboardingWizardController):
100
100
  || normalizeText(config.workerName).length > 0;
101
101
  }
102
102
 
103
- export function shouldShowCloudflareStep(controller: OnboardingWizardController): boolean {
103
+ export function shouldShowCloudflareStep(controller: OnboardingWizardControllerLike): boolean {
104
104
  return controller.isCapabilitySelected('cloudflare-batch') || isCloudflareConfigured(controller);
105
105
  }
106
106
 
107
- export function getCloudflareSetupSource(controller: OnboardingWizardController): CloudflareSetupSource {
107
+ export function getCloudflareSetupSource(controller: OnboardingWizardControllerLike): CloudflareSetupSource {
108
108
  const configuredTokenRef = controller.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
109
109
  const defaultValue = configuredTokenRef.startsWith('goodvibes://secrets/env/') ? 'operational-env' : 'save-only';
110
110
  const value = controller.getStringFieldValue('cloudflare.setup-source', defaultValue);
@@ -120,7 +120,7 @@ export function getCloudflareSetupSource(controller: OnboardingWizardController)
120
120
  return 'save-only';
121
121
  }
122
122
 
123
- export function getCloudflareComponentSelection(controller: OnboardingWizardController): Record<CloudflareComponent, boolean> {
123
+ export function getCloudflareComponentSelection(controller: OnboardingWizardControllerLike): Record<CloudflareComponent, boolean> {
124
124
  const selected: Record<CloudflareComponent, boolean> = { ...DEFAULT_CLOUDFLARE_COMPONENT_SELECTION };
125
125
  const configured = controller.runtimeSnapshot?.config.cloudflare;
126
126
  for (const component of CLOUDFLARE_COMPONENT_IDS) {
@@ -132,11 +132,11 @@ export function getCloudflareComponentSelection(controller: OnboardingWizardCont
132
132
  return selected;
133
133
  }
134
134
 
135
- export function getSelectedCloudflareComponents(controller: OnboardingWizardController): CloudflareComponentSelection {
135
+ export function getSelectedCloudflareComponents(controller: OnboardingWizardControllerLike): CloudflareComponentSelection {
136
136
  return getCloudflareComponentSelection(controller);
137
137
  }
138
138
 
139
- export function getCloudflareBatchMode(controller: OnboardingWizardController): CloudflareBatchMode {
139
+ export function getCloudflareBatchMode(controller: OnboardingWizardControllerLike): CloudflareBatchMode {
140
140
  const value = controller.getStringFieldValue('cloudflare.batch-mode', controller.runtimeSnapshot?.config.batch.mode ?? 'off');
141
141
  return value === 'explicit' || value === 'eligible-by-default' ? value : 'off';
142
142
  }
@@ -146,7 +146,7 @@ export function buildCloudflareApiTokenRef(envName: string): string {
146
146
  return `goodvibes://secrets/env/${encodeURIComponent(normalized)}`;
147
147
  }
148
148
 
149
- export function buildCloudflareProvisionRequest(controller: OnboardingWizardController, options: {
149
+ export function buildCloudflareProvisionRequest(controller: OnboardingWizardControllerLike, options: {
150
150
  readonly includeTransientSecrets?: boolean;
151
151
  } = {}): CloudflareProvisionRequest {
152
152
  const components = getCloudflareComponentSelection(controller);
@@ -1,4 +1,4 @@
1
- import type { ExternalSurfaceSpec } from './onboarding-wizard-external-surfaces.ts';
1
+ import type { ExternalSurfaceSpec } from './onboarding-wizard-types.ts';
2
2
 
3
3
  export const WEBHOOK_SURFACE_SPEC: ExternalSurfaceSpec = {
4
4
  id: 'webhook',
@@ -8,36 +8,9 @@ import { DEFAULT_CONFIG, type ConfigKey } from '../../config/index.ts';
8
8
  import type { OnboardingSnapshotState } from '../../runtime/onboarding/index.ts';
9
9
  import { TELEGRAM_MODE_OPTIONS, WHATSAPP_PROVIDER_OPTIONS } from './onboarding-wizard-constants.ts';
10
10
  import { HOME_ASSISTANT_SURFACE_SPEC, WEBHOOK_SURFACE_SPEC } from './onboarding-wizard-external-surface-extra-specs.ts';
11
- import type { OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
11
+ import type { ExternalSurfaceSetupFieldSpec, ExternalSurfaceSpec, OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
12
12
 
13
- export interface ExternalSurfaceSetupFieldSpec {
14
- readonly id: string;
15
- readonly configKey: ConfigKey;
16
- readonly kind: 'text' | 'masked' | 'radio';
17
- readonly valueType?: 'string' | 'number';
18
- readonly label: string;
19
- readonly hint: string;
20
- readonly placeholder: string;
21
- readonly options?: readonly OnboardingWizardRadioOption[];
22
- readonly defaultNumber?: number;
23
- readonly min?: number;
24
- readonly max?: number;
25
- readonly defaultValue: (snapshot: OnboardingSnapshotState | null) => string;
26
- }
27
-
28
- export interface ExternalSurfaceSpec {
29
- readonly id: string;
30
- readonly enabledFieldId: string;
31
- readonly enabledConfigKey: ConfigKey;
32
- readonly label: string;
33
- readonly hint: string;
34
- /**
35
- * Existing SDK config key. In onboarding this maps to the per-surface
36
- * auto-start choice, not to whether setup fields are shown.
37
- */
38
- readonly defaultEnabled: (snapshot: OnboardingSnapshotState | null) => boolean;
39
- readonly fields: readonly ExternalSurfaceSetupFieldSpec[];
40
- }
13
+ export type { ExternalSurfaceSetupFieldSpec, ExternalSurfaceSpec };
41
14
 
42
15
  function normalizeConfigValue(value: unknown): string {
43
16
  if (value === null || value === undefined) return '';
@@ -7,10 +7,10 @@ import {
7
7
  isExternalSurfaceSelectedByDefault,
8
8
  } from './onboarding-wizard-external-surfaces.ts';
9
9
  import { getExternalSurfaceSpecByFieldId, normalizeText, uniqueNonEmpty } from './onboarding-wizard-helpers.ts';
10
- import type { OnboardingWizardController } from './onboarding-wizard.ts';
10
+ import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
11
11
 
12
12
  export function getSharedIpDefault(
13
- controller: OnboardingWizardController,
13
+ controller: OnboardingWizardControllerLike,
14
14
  enabled: { readonly controlPlane: boolean; readonly httpListener: boolean; readonly web: boolean },
15
15
  ): boolean {
16
16
  if (!controller.runtimeSnapshot) return true;
@@ -23,7 +23,7 @@ export function getSharedIpDefault(
23
23
  }
24
24
 
25
25
  export function getSharedIpHostDefault(
26
- controller: OnboardingWizardController,
26
+ controller: OnboardingWizardControllerLike,
27
27
  enabled: { readonly controlPlane: boolean; readonly httpListener: boolean; readonly web: boolean },
28
28
  ): string {
29
29
  if (!controller.runtimeSnapshot) return '0.0.0.0';
@@ -35,7 +35,7 @@ export function getSharedIpHostDefault(
35
35
  return hosts[0] ?? '0.0.0.0';
36
36
  }
37
37
 
38
- export function toggleCapability(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId): void {
38
+ export function toggleCapability(controller: OnboardingWizardControllerLike, capabilityId: OnboardingStep1CapabilityId): void {
39
39
  if (capabilityId === 'local-tui-only') {
40
40
  for (const capability of controller.getCurrentCapabilities()) {
41
41
  controller.toggleState.set(`capabilities.${capability.id}`, capability.id === 'local-tui-only');
@@ -58,31 +58,31 @@ export function toggleCapability(controller: OnboardingWizardController, capabil
58
58
  if (!anyServerCapability) controller.toggleState.set('capabilities.local-tui-only', true);
59
59
  }
60
60
 
61
- export function selectAllServerCapabilities(controller: OnboardingWizardController): void {
61
+ export function selectAllServerCapabilities(controller: OnboardingWizardControllerLike): void {
62
62
  for (const capability of controller.getCurrentCapabilities()) {
63
63
  controller.toggleState.set(`capabilities.${capability.id}`, capability.id !== 'local-tui-only');
64
64
  }
65
65
  }
66
66
 
67
- export function selectLocalTuiOnly(controller: OnboardingWizardController): void {
67
+ export function selectLocalTuiOnly(controller: OnboardingWizardControllerLike): void {
68
68
  for (const capability of controller.getCurrentCapabilities()) {
69
69
  controller.toggleState.set(`capabilities.${capability.id}`, capability.id === 'local-tui-only');
70
70
  }
71
71
  }
72
72
 
73
- export function selectAllExternalSurfaces(controller: OnboardingWizardController): void {
73
+ export function selectAllExternalSurfaces(controller: OnboardingWizardControllerLike): void {
74
74
  for (const surface of EXTERNAL_SURFACE_SPECS) {
75
75
  controller.toggleState.set(surface.enabledFieldId, true);
76
76
  }
77
77
  }
78
78
 
79
- export function clearExternalSurfaces(controller: OnboardingWizardController): void {
79
+ export function clearExternalSurfaces(controller: OnboardingWizardControllerLike): void {
80
80
  for (const surface of EXTERNAL_SURFACE_SPECS) {
81
81
  controller.toggleState.set(surface.enabledFieldId, false);
82
82
  }
83
83
  }
84
84
 
85
- export function setCapabilityValue(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId, selected: boolean): void {
85
+ export function setCapabilityValue(controller: OnboardingWizardControllerLike, capabilityId: OnboardingStep1CapabilityId, selected: boolean): void {
86
86
  if (capabilityId === 'local-tui-only') {
87
87
  if (selected) {
88
88
  for (const capability of controller.getCurrentCapabilities()) {
@@ -112,20 +112,20 @@ export function setCapabilityValue(controller: OnboardingWizardController, capab
112
112
  if (!anyServerCapability) controller.toggleState.set('capabilities.local-tui-only', true);
113
113
  }
114
114
 
115
- export function isCapabilitySelected(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId): boolean {
115
+ export function isCapabilitySelected(controller: OnboardingWizardControllerLike, capabilityId: OnboardingStep1CapabilityId): boolean {
116
116
  return controller.getCapabilitySelectionState().some((capability) => capability.id === capabilityId && capability.selected);
117
117
  }
118
118
 
119
- export function hasServerCapabilitiesSelected(controller: OnboardingWizardController): boolean {
119
+ export function hasServerCapabilitiesSelected(controller: OnboardingWizardControllerLike): boolean {
120
120
  return controller.getCapabilitySelectionState().some((capability) => capability.id !== 'local-tui-only' && capability.selected);
121
121
  }
122
122
 
123
- export function shouldEnableBrowserSurface(controller: OnboardingWizardController): boolean {
123
+ export function shouldEnableBrowserSurface(controller: OnboardingWizardControllerLike): boolean {
124
124
  return controller.hasServerCapabilitiesSelected()
125
125
  && (controller.isCapabilitySelected('browser-access') || controller.isCapabilitySelected('network-access'));
126
126
  }
127
127
 
128
- export function hasSelectedInboundExternalSurface(controller: OnboardingWizardController): boolean {
128
+ export function hasSelectedInboundExternalSurface(controller: OnboardingWizardControllerLike): boolean {
129
129
  if (!controller.isCapabilitySelected('external-integrations')) return false;
130
130
  return EXTERNAL_SURFACE_SPECS.some((surface) => (
131
131
  INBOUND_EXTERNAL_SURFACE_IDS.has(surface.id)
@@ -140,7 +140,7 @@ export function hasSelectedInboundExternalSurface(controller: OnboardingWizardCo
140
140
  ));
141
141
  }
142
142
 
143
- export function isRequiredExternalSetupField(controller: OnboardingWizardController, fieldId: string): boolean {
143
+ export function isRequiredExternalSetupField(controller: OnboardingWizardControllerLike, fieldId: string): boolean {
144
144
  if (!REQUIRED_EXTERNAL_SETUP_FIELD_IDS.has(fieldId)) return false;
145
145
  const surface = getExternalSurfaceSpecByFieldId(fieldId);
146
146
  if (!surface) return false;
@@ -163,7 +163,7 @@ export function isRequiredExternalSetupField(controller: OnboardingWizardControl
163
163
  return true;
164
164
  }
165
165
 
166
- export function getSelectedSecretMedium(controller: OnboardingWizardController): 'secure' | 'plaintext' {
166
+ export function getSelectedSecretMedium(controller: OnboardingWizardControllerLike): 'secure' | 'plaintext' {
167
167
  const policy = controller.getStringFieldValue(
168
168
  'external-services.secret-policy',
169
169
  controller.runtimeSnapshot?.runtimeDefaults.secretStoragePolicy ?? 'preferred_secure',
@@ -174,12 +174,12 @@ export function getSelectedSecretMedium(controller: OnboardingWizardController):
174
174
  return 'plaintext';
175
175
  }
176
176
 
177
- export function shouldEnableHttpListener(controller: OnboardingWizardController): boolean {
177
+ export function shouldEnableHttpListener(controller: OnboardingWizardControllerLike): boolean {
178
178
  return controller.hasServerCapabilitiesSelected()
179
179
  && (controller.isCapabilitySelected('webhook-events') || controller.hasSelectedInboundExternalSurface());
180
180
  }
181
181
 
182
- export function shouldExposeHttpListenerNetworkFields(controller: OnboardingWizardController): boolean {
182
+ export function shouldExposeHttpListenerNetworkFields(controller: OnboardingWizardControllerLike): boolean {
183
183
  return controller.hasServerCapabilitiesSelected()
184
184
  && (
185
185
  controller.isCapabilitySelected('webhook-events')
@@ -188,31 +188,31 @@ export function shouldExposeHttpListenerNetworkFields(controller: OnboardingWiza
188
188
  );
189
189
  }
190
190
 
191
- export function shouldExposeControlPlaneNetwork(controller: OnboardingWizardController): boolean {
191
+ export function shouldExposeControlPlaneNetwork(controller: OnboardingWizardControllerLike): boolean {
192
192
  return controller.hasServerCapabilitiesSelected()
193
193
  && (controller.isCapabilitySelected('browser-access') || controller.isCapabilitySelected('network-access'));
194
194
  }
195
195
 
196
- export function requiresAuthBootstrap(controller: OnboardingWizardController): boolean {
196
+ export function requiresAuthBootstrap(controller: OnboardingWizardControllerLike): boolean {
197
197
  return controller.hasServerCapabilitiesSelected()
198
198
  && (!controller.hasLocalAuthUser() || controller.hasBootstrapCredentialPresent());
199
199
  }
200
200
 
201
- export function hasAdminAuthUser(controller: OnboardingWizardController): boolean {
201
+ export function hasAdminAuthUser(controller: OnboardingWizardControllerLike): boolean {
202
202
  return (controller.runtimeSnapshot?.auth.snapshot.users ?? [])
203
203
  .some((user) => user.roles.includes('admin'));
204
204
  }
205
205
 
206
- export function hasLocalAuthUser(controller: OnboardingWizardController): boolean {
206
+ export function hasLocalAuthUser(controller: OnboardingWizardControllerLike): boolean {
207
207
  return (controller.runtimeSnapshot?.auth.snapshot.userCount ?? 0) > 0
208
208
  || (controller.runtimeSnapshot?.auth.snapshot.users ?? []).length > 0;
209
209
  }
210
210
 
211
- export function hasBootstrapCredentialPresent(controller: OnboardingWizardController): boolean {
211
+ export function hasBootstrapCredentialPresent(controller: OnboardingWizardControllerLike): boolean {
212
212
  return controller.runtimeSnapshot?.auth.snapshot.bootstrapCredentialPresent === true;
213
213
  }
214
214
 
215
- export function getDefaultAdminUsername(controller: OnboardingWizardController): string {
215
+ export function getDefaultAdminUsername(controller: OnboardingWizardControllerLike): string {
216
216
  const users = controller.runtimeSnapshot?.auth.snapshot.users ?? [];
217
217
  if (controller.hasBootstrapCredentialPresent()) {
218
218
  const existingAdmin = users.find((user) => user.roles.includes('admin'));
@@ -225,29 +225,29 @@ export function getDefaultAdminUsername(controller: OnboardingWizardController):
225
225
  return candidate ?? `goodvibes-admin-${users.length + 1}`;
226
226
  }
227
227
 
228
- export function getBooleanFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: boolean): boolean {
228
+ export function getBooleanFieldValue(controller: OnboardingWizardControllerLike, fieldId: string, fallback: boolean): boolean {
229
229
  return controller.toggleState.get(fieldId) ?? fallback;
230
230
  }
231
231
 
232
- export function getStringFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: string): string {
232
+ export function getStringFieldValue(controller: OnboardingWizardControllerLike, fieldId: string, fallback: string): string {
233
233
  const value = controller.textState.get(fieldId) ?? controller.radioState.get(fieldId);
234
234
  return normalizeText(value ?? fallback);
235
235
  }
236
236
 
237
- export function parseIntegerFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: number): number | null {
237
+ export function parseIntegerFieldValue(controller: OnboardingWizardControllerLike, fieldId: string, fallback: number): number | null {
238
238
  const raw = controller.getStringFieldValue(fieldId, String(fallback));
239
239
  if (!/^-?\d+$/.test(raw)) return null;
240
240
  const parsed = Number.parseInt(raw, 10);
241
241
  return Number.isInteger(parsed) ? parsed : null;
242
242
  }
243
243
 
244
- export function getPortFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: number): number {
244
+ export function getPortFieldValue(controller: OnboardingWizardControllerLike, fieldId: string, fallback: number): number {
245
245
  const parsed = controller.parseIntegerFieldValue(fieldId, fallback);
246
246
  if (parsed === null || parsed < 1 || parsed > 65535) return fallback;
247
247
  return parsed;
248
248
  }
249
249
 
250
- export function getNumberFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: number, min?: number, max?: number): number {
250
+ export function getNumberFieldValue(controller: OnboardingWizardControllerLike, fieldId: string, fallback: number, min?: number, max?: number): number {
251
251
  const parsed = controller.parseIntegerFieldValue(fieldId, fallback);
252
252
  if (parsed === null) return fallback;
253
253
  if (min !== undefined && parsed < min) return fallback;
@@ -2,16 +2,16 @@ import type { ModelPickerTarget } from '../model-picker.ts';
2
2
  import type { OnboardingStep1CapabilityItem } from '../../runtime/onboarding/index.ts';
3
3
  import { DEFAULT_CAPABILITIES, NETWORK_HOST_FIELD_IDS } from './onboarding-wizard-constants.ts';
4
4
  import { areSelectionsEqual, clamp, cloneSelection, getExternalSurfaceSetupFieldSpec, isMalformedGoodVibesSecretReferenceValue, isValidHostValue, normalizeText } from './onboarding-wizard-helpers.ts';
5
- import type { OnboardingWizardController } from './onboarding-wizard.ts';
5
+ import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
6
6
  import type { OnboardingWizardFieldDefinition, OnboardingWizardModelSelection, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
7
7
 
8
- export function getToggleFieldCount(controller: OnboardingWizardController, stepIndex: number): number {
8
+ export function getToggleFieldCount(controller: OnboardingWizardControllerLike, stepIndex: number): number {
9
9
  const step = controller.steps[stepIndex];
10
10
  if (!step) return 0;
11
11
  return step.fields.filter((field) => field.kind === 'checklist' || field.kind === 'acknowledgement').length;
12
12
  }
13
13
 
14
- export function getCompletedToggleCount(controller: OnboardingWizardController, stepIndex: number): number {
14
+ export function getCompletedToggleCount(controller: OnboardingWizardControllerLike, stepIndex: number): number {
15
15
  const step = controller.steps[stepIndex];
16
16
  if (!step) return 0;
17
17
 
@@ -21,27 +21,27 @@ export function getCompletedToggleCount(controller: OnboardingWizardController,
21
21
  )).length;
22
22
  }
23
23
 
24
- export function getStepFieldCount(controller: OnboardingWizardController, stepIndex: number): number {
24
+ export function getStepFieldCount(controller: OnboardingWizardControllerLike, stepIndex: number): number {
25
25
  return controller.steps[stepIndex]?.fields.length ?? 0;
26
26
  }
27
27
 
28
- export function getCompletedFieldCount(controller: OnboardingWizardController, stepIndex: number): number {
28
+ export function getCompletedFieldCount(controller: OnboardingWizardControllerLike, stepIndex: number): number {
29
29
  const step = controller.steps[stepIndex];
30
30
  if (!step) return 0;
31
31
  return step.fields.filter((field) => controller.isFieldSatisfied(field)).length;
32
32
  }
33
33
 
34
- export function isStepDirty(controller: OnboardingWizardController, stepIndex: number): boolean {
34
+ export function isStepDirty(controller: OnboardingWizardControllerLike, stepIndex: number): boolean {
35
35
  const stepId = controller.steps[stepIndex]?.id;
36
36
  return stepId ? controller.dirtyStepIds.has(stepId) : false;
37
37
  }
38
38
 
39
- export function isFieldDirty(controller: OnboardingWizardController, fieldId: string): boolean {
39
+ export function isFieldDirty(controller: OnboardingWizardControllerLike, fieldId: string): boolean {
40
40
  const field = controller.getFieldById(fieldId);
41
41
  return field ? controller.isFieldDirtyByDefinition(field) : false;
42
42
  }
43
43
 
44
- export function getBlockingFieldLabels(controller: OnboardingWizardController): readonly string[] {
44
+ export function getBlockingFieldLabels(controller: OnboardingWizardControllerLike): readonly string[] {
45
45
  const labels: string[] = [];
46
46
  if (controller.hydrationPending) {
47
47
  labels.push('Loading: Current runtime settings are still being collected.');
@@ -65,7 +65,7 @@ export function getBlockingFieldLabels(controller: OnboardingWizardController):
65
65
  }
66
66
 
67
67
  export function getFieldValidationError(
68
- controller: OnboardingWizardController,
68
+ controller: OnboardingWizardControllerLike,
69
69
  step: OnboardingWizardStepDefinition,
70
70
  field: OnboardingWizardFieldDefinition,
71
71
  ): string | null {
@@ -127,7 +127,7 @@ export function getFieldValidationError(
127
127
  return null;
128
128
  }
129
129
 
130
- export function getFieldById(controller: OnboardingWizardController, fieldId: string): OnboardingWizardFieldDefinition | null {
130
+ export function getFieldById(controller: OnboardingWizardControllerLike, fieldId: string): OnboardingWizardFieldDefinition | null {
131
131
  for (const step of controller.steps) {
132
132
  const field = step.fields.find((entry) => entry.id === fieldId);
133
133
  if (field) return field;
@@ -135,7 +135,7 @@ export function getFieldById(controller: OnboardingWizardController, fieldId: st
135
135
  return null;
136
136
  }
137
137
 
138
- export function ensureSelectionVisible(controller: OnboardingWizardController, visibleFields: number): void {
138
+ export function ensureSelectionVisible(controller: OnboardingWizardControllerLike, visibleFields: number): void {
139
139
  const total = controller.currentStep.fields.length;
140
140
  if (total === 0) {
141
141
  controller.scrollOffsets[controller.stepIndex] = 0;
@@ -154,7 +154,7 @@ export function ensureSelectionVisible(controller: OnboardingWizardController, v
154
154
  controller.scrollOffsets[controller.stepIndex] = clamp(nextOffset, 0, maxStart);
155
155
  }
156
156
 
157
- export function reconcileStepCursor(controller: OnboardingWizardController, stepIndex: number): void {
157
+ export function reconcileStepCursor(controller: OnboardingWizardControllerLike, stepIndex: number): void {
158
158
  const total = controller.steps[stepIndex]?.fields.length ?? 0;
159
159
  if (total === 0) {
160
160
  controller.scrollOffsets[stepIndex] = 0;
@@ -166,7 +166,7 @@ export function reconcileStepCursor(controller: OnboardingWizardController, step
166
166
  controller.scrollOffsets[stepIndex] = clamp(controller.scrollOffsets[stepIndex] ?? 0, 0, total - 1);
167
167
  }
168
168
 
169
- export function resetValuesFromCurrentDefinitions(controller: OnboardingWizardController): void {
169
+ export function resetValuesFromCurrentDefinitions(controller: OnboardingWizardControllerLike): void {
170
170
  controller.toggleState.clear();
171
171
  controller.baselineToggleState.clear();
172
172
  controller.radioState.clear();
@@ -213,7 +213,7 @@ export function resetValuesFromCurrentDefinitions(controller: OnboardingWizardCo
213
213
  }
214
214
  }
215
215
 
216
- export function reconcileStateWithCurrentDefinitions(controller: OnboardingWizardController): void {
216
+ export function reconcileStateWithCurrentDefinitions(controller: OnboardingWizardControllerLike): void {
217
217
  const nextToggleKeys = new Set<string>();
218
218
  const nextRadioKeys = new Set<string>();
219
219
  const nextTextKeys = new Set<string>();
@@ -282,7 +282,7 @@ export function reconcileStateWithCurrentDefinitions(controller: OnboardingWizar
282
282
  }
283
283
  }
284
284
 
285
- export function recalculateDirtyState(controller: OnboardingWizardController): void {
285
+ export function recalculateDirtyState(controller: OnboardingWizardControllerLike): void {
286
286
  controller.reconcileStateWithCurrentDefinitions();
287
287
  controller.dirtyStepIds.clear();
288
288
 
@@ -293,7 +293,7 @@ export function recalculateDirtyState(controller: OnboardingWizardController): v
293
293
  }
294
294
  }
295
295
 
296
- export function isFieldDirtyByDefinition(controller: OnboardingWizardController, field: OnboardingWizardFieldDefinition): boolean {
296
+ export function isFieldDirtyByDefinition(controller: OnboardingWizardControllerLike, field: OnboardingWizardFieldDefinition): boolean {
297
297
  if (field.kind === 'checklist' || field.kind === 'acknowledgement') {
298
298
  return (controller.toggleState.get(field.id) ?? field.defaultValue)
299
299
  !== (controller.baselineToggleState.get(field.id) ?? field.defaultValue);
@@ -317,7 +317,7 @@ export function isFieldDirtyByDefinition(controller: OnboardingWizardController,
317
317
  );
318
318
  }
319
319
 
320
- export function isFieldSatisfied(controller: OnboardingWizardController, field: OnboardingWizardFieldDefinition): boolean {
320
+ export function isFieldSatisfied(controller: OnboardingWizardControllerLike, field: OnboardingWizardFieldDefinition): boolean {
321
321
  if (field.kind === 'checklist') {
322
322
  return true;
323
323
  }
@@ -341,20 +341,20 @@ export function isFieldSatisfied(controller: OnboardingWizardController, field:
341
341
  return selection.providerId.length > 0 || selection.modelId.length > 0;
342
342
  }
343
343
 
344
- export function getCurrentCapabilities(controller: OnboardingWizardController): readonly OnboardingStep1CapabilityItem[] {
344
+ export function getCurrentCapabilities(controller: OnboardingWizardControllerLike): readonly OnboardingStep1CapabilityItem[] {
345
345
  return controller.runtimeDerived.step1Capabilities.length > 0
346
346
  ? controller.runtimeDerived.step1Capabilities
347
347
  : DEFAULT_CAPABILITIES;
348
348
  }
349
349
 
350
- export function getCapabilitySelectionState(controller: OnboardingWizardController): readonly OnboardingStep1CapabilityItem[] {
350
+ export function getCapabilitySelectionState(controller: OnboardingWizardControllerLike): readonly OnboardingStep1CapabilityItem[] {
351
351
  return controller.getCurrentCapabilities().map((capability) => ({
352
352
  ...capability,
353
353
  selected: controller.toggleState.get(`capabilities.${capability.id}`) ?? capability.selected,
354
354
  }));
355
355
  }
356
356
 
357
- export function hasExistingAccessState(controller: OnboardingWizardController): boolean {
357
+ export function hasExistingAccessState(controller: OnboardingWizardControllerLike): boolean {
358
358
  const auth = controller.runtimeSnapshot?.auth.snapshot;
359
359
  return controller.mode !== 'new'
360
360
  || (controller.runtimeSnapshot?.subscriptions.active.length ?? 0) > 0