@pellux/goodvibes-tui 0.19.33 → 0.19.35

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 (41) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -3
  3. package/docs/foundation-artifacts/operator-contract.json +284 -112
  4. package/package.json +2 -2
  5. package/src/cli/management.ts +2 -2
  6. package/src/input/command-registry.ts +1 -0
  7. package/src/input/commands/cloudflare-runtime.ts +370 -0
  8. package/src/input/commands/local-auth-runtime.ts +4 -4
  9. package/src/input/commands/tts-runtime.ts +93 -10
  10. package/src/input/commands.ts +2 -0
  11. package/src/input/feed-context-factory.ts +1 -0
  12. package/src/input/handler-feed.ts +6 -0
  13. package/src/input/handler-modal-routes.ts +23 -10
  14. package/src/input/handler-modal-token-routes.ts +9 -0
  15. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  16. package/src/input/handler-onboarding.ts +33 -0
  17. package/src/input/handler-picker-routes.ts +1 -1
  18. package/src/input/handler.ts +4 -1
  19. package/src/input/model-picker-types.ts +125 -0
  20. package/src/input/model-picker.ts +144 -135
  21. package/src/input/onboarding/onboarding-wizard-apply.ts +85 -0
  22. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
  23. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
  24. package/src/input/onboarding/onboarding-wizard-constants.ts +12 -1
  25. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +117 -0
  26. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +3 -41
  27. package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
  28. package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
  29. package/src/input/settings-modal-types.ts +2 -1
  30. package/src/input/settings-modal.ts +30 -8
  31. package/src/renderer/buffer.ts +40 -2
  32. package/src/renderer/compositor.ts +25 -17
  33. package/src/renderer/model-picker-overlay.ts +70 -0
  34. package/src/renderer/settings-modal-helpers.ts +9 -0
  35. package/src/runtime/cloudflare-control-plane.ts +349 -0
  36. package/src/runtime/onboarding/apply.ts +9 -8
  37. package/src/runtime/onboarding/derivation.ts +26 -1
  38. package/src/runtime/onboarding/snapshot.ts +2 -0
  39. package/src/runtime/onboarding/types.ts +5 -1
  40. package/src/shell/ui-openers.ts +10 -1
  41. package/src/version.ts +1 -1
@@ -19,6 +19,8 @@ type SelectionRouteState = {
19
19
  close: () => void;
20
20
  };
21
21
  selectionCallback: ((result: SelectionResult | null) => void) | null;
22
+ getSelectionCallback?: () => ((result: SelectionResult | null) => void) | null;
23
+ setSelectionCallback?: (callback: ((result: SelectionResult | null) => void) | null) => void;
22
24
  modalStack: string[];
23
25
  requestRender: () => void;
24
26
  handleEscape: () => void;
@@ -53,11 +55,13 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
53
55
  }
54
56
  const cb = state.selectionCallback;
55
57
  state.selectionCallback = null;
58
+ state.setSelectionCallback?.(null);
56
59
  state.selectionModal.close();
57
60
  if (state.modalStack.length > 0 && state.modalStack[state.modalStack.length - 1] === 'selection') {
58
61
  state.modalStack.pop();
59
62
  }
60
63
  cb?.({ item: selected, action, step });
64
+ state.selectionCallback = state.getSelectionCallback?.() ?? state.selectionCallback;
61
65
  };
62
66
 
63
67
  const getAdjustmentStep = (
@@ -216,13 +220,30 @@ type SettingsRouteState = {
216
220
  editBackspace: () => void;
217
221
  editChar: (char: string) => void;
218
222
  pendingModelPickerTarget: import('./model-picker.ts').ModelPickerTarget | null;
223
+ pendingProviderModelPickerTarget?: import('./model-picker.ts').ModelPickerTarget | null;
219
224
  };
220
225
  /** Called when the settings modal requests the model picker for a non-main target. */
221
226
  openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => void;
227
+ /** Called when the settings modal requests provider selection before model selection. */
228
+ openProviderModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => void;
222
229
  requestRender: () => void;
223
230
  handleEscape: () => void;
224
231
  };
225
232
 
233
+ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
234
+ const providerModelTarget = state.settingsModal.pendingProviderModelPickerTarget ?? null;
235
+ if (providerModelTarget !== null) {
236
+ state.settingsModal.pendingProviderModelPickerTarget = null;
237
+ state.openProviderModelPickerWithTarget?.(providerModelTarget);
238
+ return;
239
+ }
240
+ const pickerTarget = state.settingsModal.pendingModelPickerTarget;
241
+ if (pickerTarget !== null) {
242
+ state.settingsModal.pendingModelPickerTarget = null;
243
+ state.openModelPickerWithTarget?.(pickerTarget);
244
+ }
245
+ }
246
+
226
247
  export function handleSettingsModalToken(state: SettingsRouteState, token: InputToken): boolean {
227
248
  if (!state.settingsModal.active) return false;
228
249
 
@@ -236,11 +257,7 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
236
257
  else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
237
258
  else {
238
259
  state.settingsModal.activateSelected();
239
- const pickerTarget = state.settingsModal.pendingModelPickerTarget;
240
- if (pickerTarget !== null) {
241
- state.settingsModal.pendingModelPickerTarget = null;
242
- state.openModelPickerWithTarget?.(pickerTarget);
243
- }
260
+ consumeSettingsPickerRequest(state);
244
261
  }
245
262
  } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode) {
246
263
  state.settingsModal.adjustSelected(token.logicalName, token.shift ? 10 : 1);
@@ -253,11 +270,7 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
253
270
  if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
254
271
  else {
255
272
  state.settingsModal.activateSelected();
256
- const pickerTarget = state.settingsModal.pendingModelPickerTarget;
257
- if (pickerTarget !== null) {
258
- state.settingsModal.pendingModelPickerTarget = null;
259
- state.openModelPickerWithTarget?.(pickerTarget);
260
- }
273
+ consumeSettingsPickerRequest(state);
261
274
  }
262
275
  } else if (state.settingsModal.editingMode) {
263
276
  state.settingsModal.editChar(token.value);
@@ -39,6 +39,8 @@ export type ModalTokenRouteState = {
39
39
  searchShortcutMatch: boolean;
40
40
  selectionModal: SelectionModal;
41
41
  selectionCallback: ((result: SelectionResult | null) => void) | null;
42
+ getSelectionCallback?: () => ((result: SelectionResult | null) => void) | null;
43
+ setSelectionCallback?: (callback: ((result: SelectionResult | null) => void) | null) => void;
42
44
  bookmarkModal: BookmarkModal;
43
45
  settingsModal: SettingsModal;
44
46
  sessionPickerModal: SessionPickerModal;
@@ -80,6 +82,10 @@ export type ModalTokenRouteState = {
80
82
  target: import('./model-picker.ts').ModelPickerTarget,
81
83
  source?: 'settings' | 'onboarding',
82
84
  ) => boolean;
85
+ openProviderModelPickerWithTarget?: (
86
+ target: import('./model-picker.ts').ModelPickerTarget,
87
+ source?: 'settings' | 'onboarding',
88
+ ) => boolean;
83
89
  clearOnboardingModelPickerCancelState?: () => void;
84
90
  restoreOnboardingModelPickerCancelState?: () => void;
85
91
  onModelPickerCommit?: () => boolean;
@@ -110,6 +116,8 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
110
116
  const selectionState = {
111
117
  selectionModal: state.selectionModal,
112
118
  selectionCallback: state.selectionCallback,
119
+ getSelectionCallback: state.getSelectionCallback,
120
+ setSelectionCallback: state.setSelectionCallback,
113
121
  modalStack: state.modalStack,
114
122
  requestRender: state.requestRender,
115
123
  handleEscape: state.handleEscape,
@@ -130,6 +138,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
130
138
  if (handleSettingsModalToken({
131
139
  settingsModal: state.settingsModal,
132
140
  openModelPickerWithTarget: state.openModelPickerWithTarget,
141
+ openProviderModelPickerWithTarget: state.openProviderModelPickerWithTarget,
133
142
  requestRender: state.requestRender,
134
143
  handleEscape: state.handleEscape,
135
144
  }, token)) {
@@ -0,0 +1,391 @@
1
+ import {
2
+ CloudflareDaemonRouteError,
3
+ createCloudflareDaemonClient,
4
+ type CloudflareComponentSelection,
5
+ type CloudflareDaemonClient,
6
+ type CloudflareDiscoverResult,
7
+ type CloudflareOperationalTokenResult,
8
+ type CloudflareProvisionRequest,
9
+ type CloudflareProvisionResult,
10
+ type CloudflareTokenRequirementsResult,
11
+ type CloudflareValidateResult,
12
+ type CloudflareVerifyResult,
13
+ } from '../runtime/cloudflare-control-plane.ts';
14
+ import type { OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
15
+ import type { InputHandler } from './handler.ts';
16
+ import type { OnboardingWizardAction, OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
17
+ import {
18
+ buildCloudflareApiTokenRef,
19
+ buildCloudflareProvisionRequest,
20
+ getCloudflareBatchMode,
21
+ getCloudflareComponentSelection,
22
+ getCloudflareSetupSource,
23
+ shouldShowCloudflareStep,
24
+ } from './onboarding/onboarding-wizard-cloudflare.ts';
25
+
26
+ type CloudflareOnboardingAction = Extract<OnboardingWizardAction,
27
+ | 'cloudflare-token-requirements'
28
+ | 'cloudflare-create-operational-token'
29
+ | 'cloudflare-discover'
30
+ | 'cloudflare-validate'
31
+ | 'cloudflare-provision'
32
+ | 'cloudflare-verify'
33
+ | 'cloudflare-disable'
34
+ >;
35
+
36
+ function getCloudflareDaemonClientForHandler(handler: InputHandler): CloudflareDaemonClient {
37
+ return createCloudflareDaemonClient({
38
+ configManager: handler.uiServices.platform.configManager,
39
+ homeDirectory: handler.uiServices.environment.homeDirectory,
40
+ });
41
+ }
42
+
43
+ function normalizeCloudflareActionError(error: unknown): string {
44
+ if (error instanceof CloudflareDaemonRouteError) {
45
+ return `${error.message} (HTTP ${error.status}, ${error.code})`;
46
+ }
47
+ return error instanceof Error ? error.message : String(error);
48
+ }
49
+
50
+ function setCloudflareWizardStatusForHandler(
51
+ handler: InputHandler,
52
+ title: string,
53
+ lines: readonly string[],
54
+ severity: OnboardingWizardApplyFeedback['severity'] = 'info',
55
+ ): void {
56
+ const message = [title, ...lines].filter((line) => line.length > 0).join('\n');
57
+ handler.onboardingWizard.textState.set('cloudflare.action-status', message);
58
+ handler.onboardingWizard.setApplyFeedback({
59
+ severity,
60
+ title,
61
+ summary: lines[0] ?? title,
62
+ messages: lines.length > 0 ? lines : [title],
63
+ });
64
+ const targetIndex = handler.onboardingWizard.steps.findIndex((step) => step.id === 'cloudflare');
65
+ if (targetIndex >= 0) handler.onboardingWizard.setStep(targetIndex);
66
+ handler.commandContext?.print?.(message);
67
+ handler.requestRender();
68
+ }
69
+
70
+ function formatCloudflareComponents(components: CloudflareComponentSelection): string {
71
+ const enabled = Object.entries(components)
72
+ .filter(([, selected]) => selected === true)
73
+ .map(([component]) => component);
74
+ return enabled.length > 0 ? enabled.join(', ') : 'none';
75
+ }
76
+
77
+ function formatCloudflareRequirements(result: CloudflareTokenRequirementsResult): string[] {
78
+ const permissionLines = result.permissions.length > 0
79
+ ? result.permissions.map((permission) => ` ${permission.scope}: ${permission.permission} (${permission.component}) - ${permission.reason}`)
80
+ : [' No permissions returned for the selected components.'];
81
+ return [
82
+ `components: ${formatCloudflareComponents(result.components)}`,
83
+ 'required permissions:',
84
+ ...permissionLines,
85
+ ...(result.bootstrapToken.instructions.length > 0
86
+ ? ['', 'bootstrap token instructions:', ...result.bootstrapToken.instructions.map((line) => ` ${line}`)]
87
+ : []),
88
+ ];
89
+ }
90
+
91
+ function formatCloudflareValidation(result: CloudflareValidateResult): string[] {
92
+ return [
93
+ `token: ${result.ok ? 'valid' : 'not valid'}`,
94
+ `source: ${result.tokenSource}`,
95
+ result.account
96
+ ? `account: ${result.account.name} (${result.account.id})`
97
+ : 'account: not resolved',
98
+ ];
99
+ }
100
+
101
+ function formatCloudflareDiscovery(result: CloudflareDiscoverResult): string[] {
102
+ return [
103
+ `token source: ${result.tokenSource}`,
104
+ `accounts: ${result.accounts.length}`,
105
+ `zones: ${result.zones.length}`,
106
+ `worker subdomain: ${result.workerSubdomain || 'not detected'}`,
107
+ `queues: ${result.queues?.length ?? 0}`,
108
+ `KV namespaces: ${result.kvNamespaces?.length ?? 0}`,
109
+ `R2 buckets: ${result.r2Buckets?.length ?? 0}`,
110
+ ...(result.selectedAccount ? [`selected account: ${result.selectedAccount.name} (${result.selectedAccount.id})`] : []),
111
+ ...(result.selectedZone ? [`selected zone: ${result.selectedZone.name} (${result.selectedZone.id})`] : []),
112
+ ...result.warnings.map((warning) => `warning: ${warning}`),
113
+ ];
114
+ }
115
+
116
+ function formatCloudflareTokenCreate(result: CloudflareOperationalTokenResult): string[] {
117
+ return [
118
+ `token: ${result.tokenName}${result.tokenId ? ` (${result.tokenId})` : ''}`,
119
+ `account: ${result.accountId}`,
120
+ `stored ref: ${result.apiTokenRef ?? 'not stored'}`,
121
+ `permissions: ${result.permissions.length}`,
122
+ 'Delete or expire the temporary bootstrap token in Cloudflare after confirming the operational token works.',
123
+ ];
124
+ }
125
+
126
+ function formatCloudflareProvision(result: CloudflareProvisionResult): string[] {
127
+ return [
128
+ `result: ${result.ok ? 'ok' : 'needs attention'}`,
129
+ ...(result.worker ? [`worker: ${result.worker.name}${result.worker.baseUrl ? ` at ${result.worker.baseUrl}` : ''}`] : []),
130
+ ...(result.queues ? [`queue: ${result.queues.queueName}; DLQ: ${result.queues.deadLetterQueueName}`] : []),
131
+ ...result.steps.map((step) => `${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`),
132
+ ...(result.verification ? formatCloudflareVerify(result.verification).map((line) => `verify ${line}`) : []),
133
+ ];
134
+ }
135
+
136
+ function formatCloudflareVerify(result: CloudflareVerifyResult): string[] {
137
+ return [
138
+ `worker health: ${result.workerHealth.ok ? 'ok' : 'failed'} (HTTP ${result.workerHealth.status})${result.workerHealth.error ? ` - ${result.workerHealth.error}` : ''}`,
139
+ ...(result.daemonBatchProxy
140
+ ? [`daemon batch proxy: ${result.daemonBatchProxy.ok ? 'ok' : 'failed'} (HTTP ${result.daemonBatchProxy.status})${result.daemonBatchProxy.error ? ` - ${result.daemonBatchProxy.error}` : ''}`]
141
+ : []),
142
+ ];
143
+ }
144
+
145
+ function getCloudflareBootstrapTokenFromWizard(handler: InputHandler): string {
146
+ const wizard = handler.onboardingWizard;
147
+ const setupSource = getCloudflareSetupSource(wizard);
148
+ if (setupSource === 'bootstrap-token') {
149
+ return wizard.getStringFieldValue('cloudflare.bootstrap-token', '');
150
+ }
151
+ if (setupSource === 'bootstrap-env') {
152
+ const envName = wizard.getStringFieldValue('cloudflare.bootstrap-env-name', 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN');
153
+ return process.env[envName] ?? '';
154
+ }
155
+ return '';
156
+ }
157
+
158
+ function getCloudflareOperationalTokenFromWizard(handler: InputHandler): string {
159
+ const wizard = handler.onboardingWizard;
160
+ return getCloudflareSetupSource(wizard) === 'operational-token'
161
+ ? wizard.getStringFieldValue('cloudflare.operational-token', '')
162
+ : '';
163
+ }
164
+
165
+ function getCloudflareApiTokenRefFromWizard(handler: InputHandler): string {
166
+ const wizard = handler.onboardingWizard;
167
+ const setupSource = getCloudflareSetupSource(wizard);
168
+ if (setupSource === 'operational-env') {
169
+ return buildCloudflareApiTokenRef(wizard.getStringFieldValue('cloudflare.operational-env-name', 'CLOUDFLARE_API_TOKEN'));
170
+ }
171
+ return wizard.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
172
+ }
173
+
174
+ async function createCloudflareOperationalTokenForHandler(handler: InputHandler): Promise<CloudflareOperationalTokenResult> {
175
+ const wizard = handler.onboardingWizard;
176
+ const bootstrapToken = getCloudflareBootstrapTokenFromWizard(handler);
177
+ if (!bootstrapToken) {
178
+ throw new Error('A bootstrap token is required. Paste it in the wizard or select an environment variable that is set in this TUI process.');
179
+ }
180
+ const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
181
+ const zoneId = wizard.getStringFieldValue('cloudflare.zone-id', wizard.runtimeSnapshot?.config.cloudflare.zoneId ?? '');
182
+ const zoneName = wizard.getStringFieldValue('cloudflare.zone-name', wizard.runtimeSnapshot?.config.cloudflare.zoneName ?? '');
183
+ return await getCloudflareDaemonClientForHandler(handler).createOperationalToken({
184
+ components: getCloudflareComponentSelection(wizard),
185
+ bootstrapToken,
186
+ ...(accountId ? { accountId } : {}),
187
+ ...(zoneId ? { zoneId } : {}),
188
+ ...(zoneName ? { zoneName } : {}),
189
+ storeApiToken: true,
190
+ persistConfig: true,
191
+ });
192
+ }
193
+
194
+ async function buildCloudflareProvisionInputForHandler(handler: InputHandler): Promise<CloudflareProvisionRequest> {
195
+ const input = buildCloudflareProvisionRequest(handler.onboardingWizard, { includeTransientSecrets: true });
196
+ const setupSource = getCloudflareSetupSource(handler.onboardingWizard);
197
+ if (setupSource === 'bootstrap-token' || setupSource === 'bootstrap-env') {
198
+ const tokenResult = await createCloudflareOperationalTokenForHandler(handler);
199
+ if (tokenResult.apiTokenRef) {
200
+ const withoutInlineToken = { ...input };
201
+ delete withoutInlineToken.apiToken;
202
+ return { ...withoutInlineToken, apiTokenRef: tokenResult.apiTokenRef };
203
+ }
204
+ }
205
+ return input;
206
+ }
207
+
208
+ function buildCloudflareDiscoveryInputForHandler(handler: InputHandler): Parameters<CloudflareDaemonClient['discover']>[0] {
209
+ const wizard = handler.onboardingWizard;
210
+ const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
211
+ const zoneId = wizard.getStringFieldValue('cloudflare.zone-id', wizard.runtimeSnapshot?.config.cloudflare.zoneId ?? '');
212
+ const zoneName = wizard.getStringFieldValue('cloudflare.zone-name', wizard.runtimeSnapshot?.config.cloudflare.zoneName ?? '');
213
+ const bootstrapToken = getCloudflareBootstrapTokenFromWizard(handler);
214
+ const apiToken = getCloudflareOperationalTokenFromWizard(handler) || bootstrapToken;
215
+ const apiTokenRef = getCloudflareApiTokenRefFromWizard(handler);
216
+ return {
217
+ components: getCloudflareComponentSelection(wizard),
218
+ includeResources: true,
219
+ ...(accountId ? { accountId } : {}),
220
+ ...(zoneId ? { zoneId } : {}),
221
+ ...(zoneName ? { zoneName } : {}),
222
+ ...(apiToken ? { apiToken } : apiTokenRef ? { apiTokenRef } : {}),
223
+ };
224
+ }
225
+
226
+ function buildCloudflareValidateInputForHandler(handler: InputHandler): Parameters<CloudflareDaemonClient['validate']>[0] {
227
+ const wizard = handler.onboardingWizard;
228
+ const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
229
+ const bootstrapToken = getCloudflareBootstrapTokenFromWizard(handler);
230
+ const apiToken = getCloudflareOperationalTokenFromWizard(handler) || bootstrapToken;
231
+ const apiTokenRef = getCloudflareApiTokenRefFromWizard(handler);
232
+ return {
233
+ ...(accountId ? { accountId } : {}),
234
+ ...(apiToken ? { apiToken } : apiTokenRef ? { apiTokenRef } : {}),
235
+ };
236
+ }
237
+
238
+ export async function handleCloudflareOnboardingActionForHandler(
239
+ handler: InputHandler,
240
+ action: CloudflareOnboardingAction,
241
+ ): Promise<void> {
242
+ if (handler.onboardingApplyPending) return;
243
+ handler.onboardingApplyPending = true;
244
+ handler.onboardingWizard.clearApplyFeedback();
245
+ handler.requestRender();
246
+ try {
247
+ const client = getCloudflareDaemonClientForHandler(handler);
248
+ if (action === 'cloudflare-token-requirements') {
249
+ const result = await client.tokenRequirements({
250
+ components: getCloudflareComponentSelection(handler.onboardingWizard),
251
+ includeBootstrap: true,
252
+ });
253
+ setCloudflareWizardStatusForHandler(handler, 'Cloudflare token requirements', formatCloudflareRequirements(result));
254
+ return;
255
+ }
256
+
257
+ if (action === 'cloudflare-create-operational-token') {
258
+ const result = await createCloudflareOperationalTokenForHandler(handler);
259
+ setCloudflareWizardStatusForHandler(handler, 'Cloudflare operational token created', formatCloudflareTokenCreate(result));
260
+ await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
261
+ return;
262
+ }
263
+
264
+ if (action === 'cloudflare-discover') {
265
+ const result = await client.discover(buildCloudflareDiscoveryInputForHandler(handler));
266
+ if (result.selectedAccount && !handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', '')) {
267
+ handler.onboardingWizard.setFieldValue('cloudflare.account-id', result.selectedAccount.id);
268
+ } else if (result.accounts.length === 1 && !handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', '')) {
269
+ handler.onboardingWizard.setFieldValue('cloudflare.account-id', result.accounts[0]!.id);
270
+ }
271
+ if (result.selectedZone && !handler.onboardingWizard.getStringFieldValue('cloudflare.zone-id', '')) {
272
+ handler.onboardingWizard.setFieldValue('cloudflare.zone-id', result.selectedZone.id);
273
+ handler.onboardingWizard.setFieldValue('cloudflare.zone-name', result.selectedZone.name);
274
+ } else if (result.zones.length === 1 && !handler.onboardingWizard.getStringFieldValue('cloudflare.zone-id', '')) {
275
+ handler.onboardingWizard.setFieldValue('cloudflare.zone-id', result.zones[0]!.id);
276
+ handler.onboardingWizard.setFieldValue('cloudflare.zone-name', result.zones[0]!.name);
277
+ }
278
+ if (result.workerSubdomain && !handler.onboardingWizard.getStringFieldValue('cloudflare.worker-subdomain', '')) {
279
+ handler.onboardingWizard.setFieldValue('cloudflare.worker-subdomain', result.workerSubdomain);
280
+ }
281
+ setCloudflareWizardStatusForHandler(handler, 'Cloudflare discovery completed', formatCloudflareDiscovery(result));
282
+ return;
283
+ }
284
+
285
+ if (action === 'cloudflare-validate') {
286
+ const result = await client.validate(buildCloudflareValidateInputForHandler(handler));
287
+ setCloudflareWizardStatusForHandler(
288
+ handler,
289
+ result.ok ? 'Cloudflare token validated' : 'Cloudflare token validation needs attention',
290
+ formatCloudflareValidation(result),
291
+ result.ok ? 'info' : 'warning',
292
+ );
293
+ return;
294
+ }
295
+
296
+ if (action === 'cloudflare-provision') {
297
+ const input = await buildCloudflareProvisionInputForHandler(handler);
298
+ const result = await client.provision(input);
299
+ setCloudflareWizardStatusForHandler(
300
+ handler,
301
+ result.ok ? 'Cloudflare provisioning completed' : 'Cloudflare provisioning needs attention',
302
+ formatCloudflareProvision(result),
303
+ result.ok ? 'info' : 'warning',
304
+ );
305
+ await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
306
+ return;
307
+ }
308
+
309
+ if (action === 'cloudflare-verify') {
310
+ const result = await client.verify({
311
+ workerBaseUrl: handler.onboardingWizard.getStringFieldValue('cloudflare.worker-base-url', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerBaseUrl ?? ''),
312
+ workerClientTokenRef: handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerClientTokenRef ?? '',
313
+ });
314
+ setCloudflareWizardStatusForHandler(
315
+ handler,
316
+ result.ok ? 'Cloudflare Worker verified' : 'Cloudflare Worker verification needs attention',
317
+ formatCloudflareVerify(result),
318
+ result.ok ? 'info' : 'warning',
319
+ );
320
+ return;
321
+ }
322
+
323
+ if (action === 'cloudflare-disable') {
324
+ const result = await client.disable({
325
+ accountId: handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.accountId ?? ''),
326
+ apiTokenRef: getCloudflareApiTokenRefFromWizard(handler),
327
+ workerName: handler.onboardingWizard.getStringFieldValue('cloudflare.worker-name', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerName ?? 'goodvibes-batch-worker'),
328
+ persistConfig: true,
329
+ });
330
+ setCloudflareWizardStatusForHandler(
331
+ handler,
332
+ result.ok ? 'Cloudflare integration disabled' : 'Cloudflare disable needs attention',
333
+ result.steps.map((step) => `${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`),
334
+ result.ok ? 'info' : 'warning',
335
+ );
336
+ await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
337
+ }
338
+ } catch (error) {
339
+ setCloudflareWizardStatusForHandler(handler, 'Cloudflare action failed', [normalizeCloudflareActionError(error)], 'error');
340
+ } finally {
341
+ handler.onboardingApplyPending = false;
342
+ handler.requestRender();
343
+ }
344
+ }
345
+
346
+ export async function maybeProvisionCloudflareOnFinalApplyForHandler(handler: InputHandler): Promise<readonly OnboardingVerificationItem[]> {
347
+ const wizard = handler.onboardingWizard;
348
+ if (!shouldShowCloudflareStep(wizard)) return [];
349
+ const cloudflareEnabled = wizard.getBooleanFieldValue('cloudflare.enabled', wizard.isCapabilitySelected('cloudflare-batch') || wizard.runtimeSnapshot?.config.cloudflare.enabled === true);
350
+ if (!cloudflareEnabled) {
351
+ return [{
352
+ id: 'cloudflare:disabled',
353
+ status: 'pass',
354
+ message: 'Cloudflare integration is disabled; local daemon behavior remains active.',
355
+ target: 'cloudflare',
356
+ }];
357
+ }
358
+ const provisionOnApply = wizard.getStringFieldValue('cloudflare.provision-on-apply', 'no') === 'yes';
359
+ if (!provisionOnApply) {
360
+ return [{
361
+ id: 'cloudflare:configuration-saved',
362
+ status: 'pass',
363
+ message: `Cloudflare settings were saved. Batch mode is ${getCloudflareBatchMode(wizard)}; provisioning was not requested on final apply.`,
364
+ target: 'cloudflare',
365
+ }];
366
+ }
367
+
368
+ try {
369
+ const client = getCloudflareDaemonClientForHandler(handler);
370
+ const result = await client.provision(await buildCloudflareProvisionInputForHandler(handler));
371
+ handler.onboardingWizard.textState.set('cloudflare.action-status', [
372
+ result.ok ? 'Cloudflare provisioning completed during final apply.' : 'Cloudflare provisioning needs attention after final apply.',
373
+ ...formatCloudflareProvision(result),
374
+ ].join('\n'));
375
+ return [{
376
+ id: 'cloudflare:provision',
377
+ status: result.ok ? 'pass' : 'warn',
378
+ message: result.ok
379
+ ? 'Cloudflare resources were provisioned and verified through the daemon SDK route.'
380
+ : 'Cloudflare provisioning returned warnings or failed verification. Settings were saved; rerun the Cloudflare wizard action after correcting token/resource issues.',
381
+ target: 'cloudflare',
382
+ }];
383
+ } catch (error) {
384
+ return [{
385
+ id: 'cloudflare:provision',
386
+ status: 'warn',
387
+ message: `Cloudflare provisioning did not complete: ${normalizeCloudflareActionError(error)} Settings were saved; retry from the Cloudflare wizard or /cloudflare command.`,
388
+ target: 'cloudflare',
389
+ }];
390
+ }
391
+ }
@@ -3,6 +3,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
3
3
  import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils/open-external';
4
4
  import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
5
5
  import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
6
+ import { handleCloudflareOnboardingActionForHandler, maybeProvisionCloudflareOnFinalApplyForHandler } from './handler-onboarding-cloudflare.ts';
6
7
  import { applyOnboardingRequest, collectOnboardingSnapshot, verifyOnboardingRequest } from '../runtime/onboarding/index.ts';
7
8
  import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
8
9
  import type { ModelPickerTarget } from './model-picker.ts';
@@ -186,6 +187,24 @@ export function openModelPickerWithTargetForHandler(
186
187
  return true;
187
188
  }
188
189
 
190
+ export function openProviderModelPickerWithTargetForHandler(
191
+ handler: InputHandler,
192
+ target: ModelPickerTarget,
193
+ source: 'settings' | 'onboarding' = 'settings',
194
+ ): boolean {
195
+ const openProviderPicker = handler.commandContext?.openProviderPicker;
196
+ if (!openProviderPicker) return false;
197
+ if (source === 'onboarding' && handler.onboardingWizard.active) {
198
+ handler.onboardingModelPickerCancelSnapshot = captureOnboardingWizardSnapshot(handler.onboardingWizard);
199
+ } else {
200
+ handler.clearOnboardingModelPickerCancelState();
201
+ }
202
+ handler.clearOnboardingPendingModelPickerTarget();
203
+ handler.modelPicker.target = target;
204
+ openProviderPicker();
205
+ return true;
206
+ }
207
+
189
208
  export function handleModelPickerCommitForHandler(handler: InputHandler): boolean {
190
209
  if (handler.onboardingModelPickerCancelSnapshot && handler.onboardingWizard.active) {
191
210
  const selected = handler.modelPicker.mode === 'effort'
@@ -227,6 +246,18 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
227
246
  continueOnboardingSection(handler);
228
247
  return;
229
248
  }
249
+ if (action.startsWith('cloudflare-')) {
250
+ await handleCloudflareOnboardingActionForHandler(handler, action as Extract<OnboardingWizardAction,
251
+ | 'cloudflare-token-requirements'
252
+ | 'cloudflare-create-operational-token'
253
+ | 'cloudflare-discover'
254
+ | 'cloudflare-validate'
255
+ | 'cloudflare-provision'
256
+ | 'cloudflare-verify'
257
+ | 'cloudflare-disable'
258
+ >);
259
+ return;
260
+ }
230
261
  if (action !== 'apply') return;
231
262
  if (handler.onboardingApplyPending) return;
232
263
  const blockers = handler.onboardingWizard.getBlockingFieldLabels();
@@ -272,6 +303,8 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
272
303
  ? { ...item, status: 'warn' }
273
304
  : item));
274
305
  verificationItems = dedupeOnboardingVerificationItems([...verificationItems, ...runtimeWarnings]);
306
+ const cloudflareItems = await maybeProvisionCloudflareOnFinalApplyForHandler(handler);
307
+ verificationItems = dedupeOnboardingVerificationItems([...verificationItems, ...cloudflareItems]);
275
308
  }
276
309
  } catch (error) {
277
310
  showOnboardingApplyFeedbackForHandler(handler, {
@@ -55,7 +55,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
55
55
  const selected = state.modelPicker.getSelected();
56
56
  if (selected) {
57
57
  const currentEffort = state.commandContext?.session.runtime.reasoningEffort ?? 'medium';
58
- if (selected.reasoningEffort && selected.reasoningEffort.length > 0) {
58
+ if (state.modelPicker.target === 'main' && selected.reasoningEffort && selected.reasoningEffort.length > 0) {
59
59
  state.modelPicker.showEffortPicker(selected, currentEffort);
60
60
  } else {
61
61
  const target = state.modelPicker.target;
@@ -3,7 +3,7 @@ import { dirname } from 'node:path';
3
3
  import { InputTokenizer } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
4
4
  import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config/oauth-local-listener';
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, 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, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler, type OnboardingRuntimePosture } from './handler-onboarding.ts';
7
7
  import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
8
8
  import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils/open-external';
9
9
  import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
@@ -325,6 +325,8 @@ export class InputHandler {
325
325
  expandPrompt: (text: string) => this.expandPrompt(text),
326
326
  openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') =>
327
327
  this.openModelPickerWithTarget(target, source),
328
+ openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') =>
329
+ this.openProviderModelPickerWithTarget(target, source),
328
330
  onModelPickerCommit: () => this.handleModelPickerCommit(),
329
331
  onOnboardingAction: (action: OnboardingWizardAction) => { void this.handleOnboardingAction(action); },
330
332
  },
@@ -404,6 +406,7 @@ export class InputHandler {
404
406
  public clearOnboardingModelPickerCancelState(): void { clearOnboardingModelPickerCancelStateForHandler(this); }
405
407
  public restoreOnboardingModelPickerCancelState(): void { restoreOnboardingModelPickerCancelStateForHandler(this); }
406
408
  public openModelPickerWithTarget(target: ModelPickerTarget, source: 'settings' | 'onboarding' = 'settings'): boolean { return openModelPickerWithTargetForHandler(this, target, source); }
409
+ public openProviderModelPickerWithTarget(target: ModelPickerTarget, source: 'settings' | 'onboarding' = 'settings'): boolean { return openProviderModelPickerWithTargetForHandler(this, target, source); }
407
410
  public handleModelPickerCommit(): boolean { return handleModelPickerCommitForHandler(this); }
408
411
  public async handleOnboardingAction(action: OnboardingWizardAction): Promise<void> { await handleOnboardingActionForHandler(this, action); }
409
412
  public async refreshOnboardingHydration(options: { readonly preserveValues?: boolean; readonly targetStepId?: string } = {}): Promise<void> { await refreshOnboardingHydrationForHandler(this, options); }