@pellux/goodvibes-tui 0.19.32 → 0.19.34

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 (39) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +4 -2
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-model-routing.ts +117 -0
  6. package/src/input/command-registry.ts +2 -0
  7. package/src/input/commands/cloudflare-runtime.ts +343 -0
  8. package/src/input/commands/tts-runtime.ts +288 -7
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/feed-context-factory.ts +1 -0
  11. package/src/input/handler-feed.ts +6 -0
  12. package/src/input/handler-modal-routes.ts +23 -10
  13. package/src/input/handler-modal-token-routes.ts +9 -0
  14. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  15. package/src/input/handler-onboarding.ts +33 -0
  16. package/src/input/handler-picker-routes.ts +1 -1
  17. package/src/input/handler.ts +4 -1
  18. package/src/input/model-picker-types.ts +125 -0
  19. package/src/input/model-picker.ts +144 -134
  20. package/src/input/onboarding/onboarding-wizard-apply.ts +81 -0
  21. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +449 -0
  22. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +199 -0
  23. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -0
  24. package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
  25. package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
  26. package/src/input/settings-modal-types.ts +2 -1
  27. package/src/input/settings-modal.ts +30 -8
  28. package/src/main.ts +12 -1
  29. package/src/renderer/buffer.ts +40 -2
  30. package/src/renderer/compositor.ts +25 -17
  31. package/src/renderer/model-picker-overlay.ts +70 -0
  32. package/src/renderer/settings-modal-helpers.ts +1 -0
  33. package/src/runtime/bootstrap-command-parts.ts +4 -0
  34. package/src/runtime/cloudflare-control-plane.ts +328 -0
  35. package/src/runtime/onboarding/derivation.ts +25 -0
  36. package/src/runtime/onboarding/snapshot.ts +2 -0
  37. package/src/runtime/onboarding/types.ts +5 -1
  38. package/src/shell/ui-openers.ts +21 -2
  39. package/src/version.ts +1 -1
@@ -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); }
@@ -0,0 +1,125 @@
1
+ import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers/registry';
2
+
3
+ export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap';
4
+
5
+ /**
6
+ * Which config keys the model picker writes to on commit.
7
+ * 'main' -> provider.provider + provider.model (default)
8
+ * 'helper' -> helper.globalProvider + helper.globalModel (+ helper.enabled: true)
9
+ * 'tool' -> tools.llmProvider + tools.llmModel (+ tools.llmEnabled: true)
10
+ * 'tts' -> tts.llmProvider + tts.llmModel
11
+ */
12
+ export type ModelPickerTarget = 'main' | 'helper' | 'tool' | 'tts';
13
+
14
+ /**
15
+ * Pricing tier filter.
16
+ * 'paid' matches ModelDefinition tiers 'standard' and 'premium' for forward-compat
17
+ * with future CatalogModel tiers ('free' | 'paid' | 'subscription').
18
+ */
19
+ export type CategoryFilter = 'all' | 'free' | 'paid' | 'subscription';
20
+
21
+ export type ModelFamily =
22
+ | 'GPT'
23
+ | 'Claude'
24
+ | 'Gemini'
25
+ | 'Llama'
26
+ | 'Qwen'
27
+ | 'GLM'
28
+ | 'MiniMax'
29
+ | 'DeepSeek'
30
+ | 'Mistral'
31
+ | 'Command'
32
+ | 'Grok'
33
+ | 'Kimi'
34
+ | 'Other';
35
+
36
+ export type CapabilityFilter = 'reasoning' | 'toolUse' | 'multimodal' | 'none';
37
+ export type BenchmarkSort = 'none' | 'composite' | 'swe' | 'gpqa';
38
+ export type GroupByMode = 'provider' | 'family' | 'pricingTier' | 'qualityTier';
39
+
40
+ const FAMILY_PATTERNS: Array<{ pattern: RegExp; family: ModelFamily }> = [
41
+ { pattern: /claude/i, family: 'Claude' },
42
+ { pattern: /gpt|\bo1\b|\bo3\b|\bo4\b/i, family: 'GPT' },
43
+ { pattern: /gemini/i, family: 'Gemini' },
44
+ { pattern: /llama/i, family: 'Llama' },
45
+ { pattern: /qwen/i, family: 'Qwen' },
46
+ { pattern: /glm|chatglm/i, family: 'GLM' },
47
+ { pattern: /minimax|abab/i, family: 'MiniMax' },
48
+ { pattern: /deepseek/i, family: 'DeepSeek' },
49
+ { pattern: /mistral|mixtral/i, family: 'Mistral' },
50
+ { pattern: /command|cohere/i, family: 'Command' },
51
+ { pattern: /grok/i, family: 'Grok' },
52
+ { pattern: /kimi|moonshot/i, family: 'Kimi' },
53
+ ];
54
+
55
+ export function detectFamily(model: ModelDefinition): ModelFamily {
56
+ const haystack = `${model.id} ${model.displayName}`;
57
+ for (const { pattern, family } of FAMILY_PATTERNS) {
58
+ if (pattern.test(haystack)) return family;
59
+ }
60
+ return 'Other';
61
+ }
62
+
63
+ export function tierToCategoryFilter(tier: string | undefined): CategoryFilter {
64
+ if (tier === 'free') return 'free';
65
+ if (tier === 'subscription') return 'subscription';
66
+ return 'paid';
67
+ }
68
+
69
+ export interface PickerItem {
70
+ id: string;
71
+ label: string;
72
+ detail?: string;
73
+ isGroupHeader?: boolean;
74
+ qualityTier?: string;
75
+ isPinned?: boolean;
76
+ isFree?: boolean;
77
+ isConfigured?: boolean;
78
+ configuredVia?: 'env' | 'secrets' | 'subscription' | 'anonymous';
79
+ }
80
+
81
+ export const POPULAR_PROVIDERS: ReadonlySet<string> = new Set([
82
+ 'anthropic',
83
+ 'google',
84
+ 'groq',
85
+ 'mistral',
86
+ 'nvidia',
87
+ 'ollama',
88
+ 'openai',
89
+ 'openrouter',
90
+ 'synthetic',
91
+ ]);
92
+
93
+ export interface FilteredModelsCache {
94
+ readonly modelsRef: ModelDefinition[];
95
+ readonly configuredProvidersKey: string;
96
+ readonly pinnedIdsKey: string;
97
+ readonly recentIdsKey: string;
98
+ readonly query: string;
99
+ readonly categoryFilter: CategoryFilter;
100
+ readonly capabilityFilter: CapabilityFilter;
101
+ readonly availableOnly: boolean;
102
+ readonly benchmarkSort: BenchmarkSort;
103
+ readonly groupBy: GroupByMode;
104
+ readonly result: ModelDefinition[];
105
+ }
106
+
107
+ export interface FilteredProvidersCache {
108
+ readonly providersRef: string[];
109
+ readonly query: string;
110
+ readonly result: string[];
111
+ }
112
+
113
+ export interface ModelItemsCache {
114
+ readonly filteredModelsRef: ModelDefinition[];
115
+ readonly pinnedIdsKey: string;
116
+ readonly groupBy: GroupByMode;
117
+ readonly result: PickerItem[];
118
+ }
119
+
120
+ export interface ProviderItemsCache {
121
+ readonly filteredProvidersRef: string[];
122
+ readonly configuredProvidersKey: string;
123
+ readonly configuredViaKey: string;
124
+ readonly result: PickerItem[];
125
+ }