@pellux/goodvibes-tui 0.19.33 → 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 (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/input/command-registry.ts +1 -0
  6. package/src/input/commands/cloudflare-runtime.ts +343 -0
  7. package/src/input/commands/tts-runtime.ts +93 -10
  8. package/src/input/commands.ts +2 -0
  9. package/src/input/feed-context-factory.ts +1 -0
  10. package/src/input/handler-feed.ts +6 -0
  11. package/src/input/handler-modal-routes.ts +23 -10
  12. package/src/input/handler-modal-token-routes.ts +9 -0
  13. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  14. package/src/input/handler-onboarding.ts +33 -0
  15. package/src/input/handler-picker-routes.ts +1 -1
  16. package/src/input/handler.ts +4 -1
  17. package/src/input/model-picker-types.ts +125 -0
  18. package/src/input/model-picker.ts +144 -135
  19. package/src/input/onboarding/onboarding-wizard-apply.ts +81 -0
  20. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +449 -0
  21. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +199 -0
  22. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -0
  23. package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
  24. package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
  25. package/src/input/settings-modal-types.ts +2 -1
  26. package/src/input/settings-modal.ts +30 -8
  27. package/src/renderer/buffer.ts +40 -2
  28. package/src/renderer/compositor.ts +25 -17
  29. package/src/renderer/model-picker-overlay.ts +70 -0
  30. package/src/renderer/settings-modal-helpers.ts +1 -0
  31. package/src/runtime/cloudflare-control-plane.ts +328 -0
  32. package/src/runtime/onboarding/derivation.ts +25 -0
  33. package/src/runtime/onboarding/snapshot.ts +2 -0
  34. package/src/runtime/onboarding/types.ts +5 -1
  35. package/src/shell/ui-openers.ts +10 -1
  36. package/src/version.ts +1 -1
@@ -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
+ }
@@ -4,118 +4,11 @@ import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers/ef
4
4
  import { getQualityTier, getQualityTierFromScore, compositeScore, A_TIER_THRESHOLD } from '@pellux/goodvibes-sdk/platform/providers/model-benchmarks';
5
5
  import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers/model-benchmarks';
6
6
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers/registry';
7
+ import { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
8
+ import type { BenchmarkSort, CapabilityFilter, CategoryFilter, FilteredModelsCache, FilteredProvidersCache, GroupByMode, ModelItemsCache, ModelPickerTarget, PickerItem, PickerMode, ProviderItemsCache } from './model-picker-types.ts';
7
9
 
8
- export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap';
9
-
10
- /**
11
- * Which config keys the model picker writes to on commit.
12
- * 'main' → provider.provider + provider.model (default)
13
- * 'helper' → helper.globalProvider + helper.globalModel (+ helper.enabled: true)
14
- * 'tool' → tools.llmProvider + tools.llmModel (+ tools.llmEnabled: true)
15
- * 'tts' → tts.llmProvider + tts.llmModel
16
- */
17
- export type ModelPickerTarget = 'main' | 'helper' | 'tool' | 'tts';
18
-
19
- /**
20
- * Pricing tier filter.
21
- * 'paid' matches ModelDefinition tiers 'standard' and 'premium' for forward-compat
22
- * with future CatalogModel tiers ('free' | 'paid' | 'subscription').
23
- */
24
- export type CategoryFilter = 'all' | 'free' | 'paid' | 'subscription';
25
-
26
- /** Model family grouping names. */
27
- export type ModelFamily =
28
- | 'GPT'
29
- | 'Claude'
30
- | 'Gemini'
31
- | 'Llama'
32
- | 'Qwen'
33
- | 'GLM'
34
- | 'MiniMax'
35
- | 'DeepSeek'
36
- | 'Mistral'
37
- | 'Command'
38
- | 'Grok'
39
- | 'Kimi'
40
- | 'Other';
41
-
42
- /** Capability filter — subset of ModelDefinition capabilities. */
43
- export type CapabilityFilter = 'reasoning' | 'toolUse' | 'multimodal' | 'none';
44
-
45
- /** Benchmark score sort order. */
46
- export type BenchmarkSort = 'none' | 'composite' | 'swe' | 'gpqa';
47
-
48
- /** Group-by cycling order. */
49
- export type GroupByMode = 'provider' | 'family' | 'pricingTier' | 'qualityTier';
50
-
51
- // ── Family detection helpers ──────────────────────────────────────────────────
52
-
53
- /** Patterns for detecting model family from id/displayName. */
54
- const FAMILY_PATTERNS: Array<{ pattern: RegExp; family: ModelFamily }> = [
55
- { pattern: /claude/i, family: 'Claude' },
56
- { pattern: /gpt|\bo1\b|\bo3\b|\bo4\b/i, family: 'GPT' },
57
- { pattern: /gemini/i, family: 'Gemini' },
58
- { pattern: /llama/i, family: 'Llama' },
59
- { pattern: /qwen/i, family: 'Qwen' },
60
- { pattern: /glm|chatglm/i, family: 'GLM' },
61
- { pattern: /minimax|abab/i, family: 'MiniMax' },
62
- { pattern: /deepseek/i, family: 'DeepSeek' },
63
- { pattern: /mistral|mixtral/i, family: 'Mistral' },
64
- { pattern: /command|cohere/i, family: 'Command' },
65
- { pattern: /grok/i, family: 'Grok' },
66
- { pattern: /kimi|moonshot/i, family: 'Kimi' },
67
- ];
68
-
69
- /** Detect the model family from id and displayName. */
70
- export function detectFamily(model: ModelDefinition): ModelFamily {
71
- const haystack = `${model.id} ${model.displayName}`;
72
- for (const { pattern, family } of FAMILY_PATTERNS) {
73
- if (pattern.test(haystack)) return family;
74
- }
75
- return 'Other';
76
- }
77
-
78
- /**
79
- * Map ModelDefinition tier to CategoryFilter bucket.
80
- * 'standard' and 'premium' both map to 'paid' for forward-compat.
81
- */
82
- export function tierToCategoryFilter(tier: string | undefined): CategoryFilter {
83
- if (tier === 'free') return 'free';
84
- if (tier === 'subscription') return 'subscription';
85
- return 'paid';
86
- }
87
-
88
- /** A generic selectable item for non-model modes. */
89
- export interface PickerItem {
90
- id: string;
91
- label: string;
92
- detail?: string;
93
- /** If true, this item is a group header (not selectable). */
94
- isGroupHeader?: boolean;
95
- /** Quality tier badge for model items: S/A/B/C. */
96
- qualityTier?: string;
97
- /** Whether this model is pinned/favorited. */
98
- isPinned?: boolean;
99
- /** True when model tier is free. */
100
- isFree?: boolean;
101
- /** True when this provider item has a configured API key. */
102
- isConfigured?: boolean;
103
- /** How the provider is configured — shown as a badge in provider mode. */
104
- configuredVia?: 'env' | 'secrets' | 'subscription' | 'anonymous';
105
- }
106
-
107
- /** Provider IDs treated as "Popular" in the provider picker. */
108
- export const POPULAR_PROVIDERS: ReadonlySet<string> = new Set([
109
- 'anthropic',
110
- 'google',
111
- 'groq',
112
- 'mistral',
113
- 'nvidia',
114
- 'ollama',
115
- 'openai',
116
- 'openrouter',
117
- 'synthetic',
118
- ]);
10
+ export { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
11
+ export type { BenchmarkSort, CapabilityFilter, CategoryFilter, GroupByMode, ModelFamily, ModelPickerTarget, PickerItem, PickerMode } from './model-picker-types.ts';
119
12
 
120
13
  /**
121
14
  * ModelPickerModal - Multi-step interactive picker for model, provider, and effort.
@@ -180,12 +73,18 @@ export class ModelPickerModal {
180
73
  /** Current group-by mode. */
181
74
  public groupBy: GroupByMode = 'provider';
182
75
 
76
+ private filteredModelsCache: FilteredModelsCache | null = null;
77
+ private filteredProvidersCache: FilteredProvidersCache | null = null;
78
+ private modelItemsCache: ModelItemsCache | null = null;
79
+ private providerItemsCache: ProviderItemsCache | null = null;
80
+
183
81
  // ── Category filter cycling ───────────────────────────────────────────────
184
82
  private static readonly CATEGORY_CYCLE: CategoryFilter[] = ['all', 'free', 'paid', 'subscription'];
185
83
  /** Cycle to next pricing tier filter. */
186
84
  cycleCategory(): void {
187
85
  const idx = ModelPickerModal.CATEGORY_CYCLE.indexOf(this.categoryFilter);
188
86
  this.categoryFilter = ModelPickerModal.CATEGORY_CYCLE[(idx + 1) % ModelPickerModal.CATEGORY_CYCLE.length];
87
+ this.clearFilteredCaches();
189
88
  this._clampSelection();
190
89
  }
191
90
 
@@ -195,6 +94,7 @@ export class ModelPickerModal {
195
94
  cycleGroupBy(): void {
196
95
  const idx = ModelPickerModal.GROUP_BY_CYCLE.indexOf(this.groupBy);
197
96
  this.groupBy = ModelPickerModal.GROUP_BY_CYCLE[(idx + 1) % ModelPickerModal.GROUP_BY_CYCLE.length];
97
+ this.clearFilteredCaches();
198
98
  this._clampSelection();
199
99
  }
200
100
 
@@ -204,6 +104,7 @@ export class ModelPickerModal {
204
104
  cycleBenchmarkSort(): void {
205
105
  const idx = ModelPickerModal.BENCHMARK_SORT_CYCLE.indexOf(this.benchmarkSort);
206
106
  this.benchmarkSort = ModelPickerModal.BENCHMARK_SORT_CYCLE[(idx + 1) % ModelPickerModal.BENCHMARK_SORT_CYCLE.length];
107
+ this.clearFilteredCaches();
207
108
  this._clampSelection();
208
109
  }
209
110
 
@@ -266,8 +167,9 @@ export class ModelPickerModal {
266
167
  this.query = '';
267
168
  this.categoryFilter = 'all';
268
169
  this.capabilityFilter = 'none';
269
- // Start selection at the top of the Popular providers group.
270
- this.selectedIndex = 0;
170
+ const filtered = this.getFilteredProviders();
171
+ const currentIndex = filtered.findIndex((provider) => provider === currentProvider);
172
+ this.selectedIndex = currentIndex >= 0 ? currentIndex : 0;
271
173
  this.scrollOffset = 0;
272
174
  }
273
175
 
@@ -315,6 +217,7 @@ export class ModelPickerModal {
315
217
  this.query = '';
316
218
  this.categoryFilter = 'all';
317
219
  this.capabilityFilter = 'none';
220
+ this.clearCaches();
318
221
  }
319
222
 
320
223
  // ── Search helpers ─────────────────────────────────────────────────────────
@@ -322,6 +225,7 @@ export class ModelPickerModal {
322
225
  /** Append a character to the search query and clamp selectedIndex. */
323
226
  appendChar(ch: string): void {
324
227
  this.query += ch;
228
+ this.clearFilteredCaches();
325
229
  this._clampSelection();
326
230
  }
327
231
 
@@ -329,6 +233,7 @@ export class ModelPickerModal {
329
233
  deleteChar(): void {
330
234
  if (this.query.length > 0) {
331
235
  this.query = this.query.slice(0, -1);
236
+ this.clearFilteredCaches();
332
237
  this._clampSelection();
333
238
  }
334
239
  }
@@ -336,6 +241,7 @@ export class ModelPickerModal {
336
241
  /** Clear the search query and clamp selectedIndex. */
337
242
  clearQuery(): void {
338
243
  this.query = '';
244
+ this.clearFilteredCaches();
339
245
  this._clampSelection();
340
246
  }
341
247
 
@@ -354,18 +260,21 @@ export class ModelPickerModal {
354
260
  /** Set category filter and clamp selectedIndex. */
355
261
  setCategoryFilter(filter: CategoryFilter): void {
356
262
  this.categoryFilter = filter;
263
+ this.clearFilteredCaches();
357
264
  this._clampSelection();
358
265
  }
359
266
 
360
267
  /** Set capability filter and clamp selectedIndex. */
361
268
  setCapabilityFilter(filter: CapabilityFilter): void {
362
269
  this.capabilityFilter = filter;
270
+ this.clearFilteredCaches();
363
271
  this._clampSelection();
364
272
  }
365
273
 
366
274
  /** Toggle the available-only filter. */
367
275
  toggleAvailableOnly(): void {
368
276
  this.availableOnly = !this.availableOnly;
277
+ this.clearFilteredCaches();
369
278
  this._clampSelection();
370
279
  }
371
280
 
@@ -396,15 +305,50 @@ export class ModelPickerModal {
396
305
 
397
306
  /** Return providers matching the current query (case-insensitive substring), in grouped order. */
398
307
  getFilteredProviders(): string[] {
308
+ const cached = this.filteredProvidersCache;
309
+ if (
310
+ cached !== null
311
+ && cached.providersRef === this.providers
312
+ && cached.query === this.query
313
+ ) {
314
+ return cached.result;
315
+ }
316
+
399
317
  const { popular, all } = this.getGroupedProviders();
400
318
  const ordered = [...popular, ...all];
401
- if (this.query.trim().length === 0) return ordered;
402
- const q = this.query.toLowerCase();
403
- return ordered.filter(p => p.toLowerCase().includes(q));
319
+ const result = this.query.trim().length === 0
320
+ ? ordered
321
+ : ordered.filter(p => p.toLowerCase().includes(this.query.toLowerCase()));
322
+ this.filteredProvidersCache = {
323
+ providersRef: this.providers,
324
+ query: this.query,
325
+ result,
326
+ };
327
+ return result;
404
328
  }
405
329
 
406
330
  /** Return models matching all current filters, sorted per benchmarkSort. */
407
331
  getFilteredModels(): ModelDefinition[] {
332
+ const configuredProvidersKey = setKey(this.configuredProviders);
333
+ const pinnedIdsKey = setKey(this.pinnedIds);
334
+ const recentIdsKey = orderedListKey(this.recentIds);
335
+ const cached = this.filteredModelsCache;
336
+ if (
337
+ cached !== null
338
+ && cached.modelsRef === this.models
339
+ && cached.configuredProvidersKey === configuredProvidersKey
340
+ && cached.pinnedIdsKey === pinnedIdsKey
341
+ && cached.recentIdsKey === recentIdsKey
342
+ && cached.query === this.query
343
+ && cached.categoryFilter === this.categoryFilter
344
+ && cached.capabilityFilter === this.capabilityFilter
345
+ && cached.availableOnly === this.availableOnly
346
+ && cached.benchmarkSort === this.benchmarkSort
347
+ && cached.groupBy === this.groupBy
348
+ ) {
349
+ return cached.result;
350
+ }
351
+
408
352
  let result = this.models;
409
353
 
410
354
  // Available-only filter
@@ -520,6 +464,20 @@ export class ModelPickerModal {
520
464
  result = [...recent, ...rest];
521
465
  }
522
466
 
467
+ this.filteredModelsCache = {
468
+ modelsRef: this.models,
469
+ configuredProvidersKey,
470
+ pinnedIdsKey,
471
+ recentIdsKey,
472
+ query: this.query,
473
+ categoryFilter: this.categoryFilter,
474
+ capabilityFilter: this.capabilityFilter,
475
+ availableOnly: this.availableOnly,
476
+ benchmarkSort: this.benchmarkSort,
477
+ groupBy: this.groupBy,
478
+ result,
479
+ };
480
+ this.modelItemsCache = null;
523
481
  return result;
524
482
  }
525
483
 
@@ -529,6 +487,7 @@ export class ModelPickerModal {
529
487
  */
530
488
  async loadRecentModels(n = 10): Promise<void> {
531
489
  this.recentIds = await this.favoritesStore.getRecentModels(n);
490
+ this.clearFilteredCaches();
532
491
  }
533
492
 
534
493
  /**
@@ -574,6 +533,16 @@ export class ModelPickerModal {
574
533
  getItems(): PickerItem[] {
575
534
  if (this.mode === 'model') {
576
535
  const filtered = this.getFilteredModels();
536
+ const pinnedIdsKey = setKey(this.pinnedIds);
537
+ const cached = this.modelItemsCache;
538
+ if (
539
+ cached !== null
540
+ && cached.filteredModelsRef === filtered
541
+ && cached.pinnedIdsKey === pinnedIdsKey
542
+ && cached.groupBy === this.groupBy
543
+ ) {
544
+ return cached.result;
545
+ }
577
546
 
578
547
  // Separate pinned and unpinned
579
548
  const pinned = filtered.filter(m => this.pinnedIds.has(m.id));
@@ -600,33 +569,45 @@ export class ModelPickerModal {
600
569
  items.push(this._modelToItem(m, false));
601
570
  }
602
571
 
572
+ this.modelItemsCache = {
573
+ filteredModelsRef: filtered,
574
+ pinnedIdsKey,
575
+ groupBy: this.groupBy,
576
+ result: items,
577
+ };
603
578
  return items;
604
579
  }
605
580
  if (this.mode === 'provider') {
606
- const q = this.query.trim().toLowerCase();
607
- const { popular, all } = this.getGroupedProviders();
608
-
609
- const filterGroup = (group: string[]) =>
610
- q.length === 0 ? group : group.filter(p => p.toLowerCase().includes(q));
611
-
612
- const filteredPopular = filterGroup(popular);
613
- const filteredAll = filterGroup(all);
581
+ const filteredProviders = this.getFilteredProviders();
582
+ const configuredProvidersKey = setKey(this.configuredProviders);
583
+ const configuredViaKey = mapKey(this.configuredViaMap);
584
+ const cached = this.providerItemsCache;
585
+ if (
586
+ cached !== null
587
+ && cached.filteredProvidersRef === filteredProviders
588
+ && cached.configuredProvidersKey === configuredProvidersKey
589
+ && cached.configuredViaKey === configuredViaKey
590
+ ) {
591
+ return cached.result;
592
+ }
614
593
 
615
594
  const providerItems: PickerItem[] = [];
616
-
617
- if (filteredPopular.length > 0) {
618
- providerItems.push({ id: '__header__popular', label: 'Popular', isGroupHeader: true });
619
- for (const p of filteredPopular) {
620
- providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
621
- }
622
- }
623
- if (filteredAll.length > 0) {
624
- providerItems.push({ id: '__header__all', label: 'All Providers', isGroupHeader: true });
625
- for (const p of filteredAll) {
626
- providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
595
+ let currentGroup: 'Popular' | 'All Providers' | null = null;
596
+ for (const p of filteredProviders) {
597
+ const group: 'Popular' | 'All Providers' = POPULAR_PROVIDERS.has(p.toLowerCase()) ? 'Popular' : 'All Providers';
598
+ if (group !== currentGroup) {
599
+ providerItems.push({ id: `__header__${group}`, label: group, isGroupHeader: true });
600
+ currentGroup = group;
627
601
  }
602
+ providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
628
603
  }
629
604
 
605
+ this.providerItemsCache = {
606
+ filteredProvidersRef: filteredProviders,
607
+ configuredProvidersKey,
608
+ configuredViaKey,
609
+ result: providerItems,
610
+ };
630
611
  return providerItems;
631
612
  }
632
613
  // effort mode
@@ -743,4 +724,32 @@ export class ModelPickerModal {
743
724
  getBenchmarkEntry(model: ModelDefinition) {
744
725
  return this.benchmarkStore.getBenchmarks(model.id) ?? this.benchmarkStore.getBenchmarks(model.displayName);
745
726
  }
727
+
728
+ private clearFilteredCaches(): void {
729
+ this.filteredModelsCache = null;
730
+ this.filteredProvidersCache = null;
731
+ this.modelItemsCache = null;
732
+ this.providerItemsCache = null;
733
+ }
734
+
735
+ private clearCaches(): void {
736
+ this.clearFilteredCaches();
737
+ }
738
+ }
739
+
740
+ function setKey(values: ReadonlySet<string>): string {
741
+ if (values.size === 0) return '';
742
+ return [...values].sort().join('\u001f');
743
+ }
744
+
745
+ function orderedListKey(values: readonly string[]): string {
746
+ return values.length === 0 ? '' : values.join('\u001f');
747
+ }
748
+
749
+ function mapKey(values: ReadonlyMap<string, string | undefined>): string {
750
+ if (values.size === 0) return '';
751
+ return [...values.entries()]
752
+ .sort(([left], [right]) => left.localeCompare(right))
753
+ .map(([key, value]) => `${key}\u001e${value ?? ''}`)
754
+ .join('\u001f');
746
755
  }
@@ -1,5 +1,13 @@
1
1
  import type { OnboardingAcknowledgementTarget, OnboardingApplyOperation, OnboardingApplyRequest } from '../../runtime/onboarding/index.ts';
2
2
  import { getServerSurfaceFeatureFlags } from '../../runtime/surface-feature-flags.ts';
3
+ import {
4
+ buildCloudflareApiTokenRef,
5
+ buildCloudflareOperationalTokenRef,
6
+ getCloudflareBatchMode,
7
+ getCloudflareComponentSelection,
8
+ getCloudflareSetupSource,
9
+ shouldShowCloudflareStep,
10
+ } from './onboarding-wizard-cloudflare.ts';
3
11
  import {
4
12
  EXTERNAL_SURFACE_SPECS,
5
13
  getExternalSurfaceAutoStartDefaultValue,
@@ -111,6 +119,10 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
111
119
 
112
120
  setSecret('OPENAI_API_KEY', controller.getStringFieldValue('providers.openai-api-key', ''));
113
121
 
122
+ if (shouldShowCloudflareStep(controller)) {
123
+ addCloudflareOperations(controller, operations, setSecret);
124
+ }
125
+
114
126
  const externalIntegrations = controller.isCapabilitySelected('external-integrations');
115
127
  const enabledExternalSurfaceIds: string[] = [];
116
128
  for (const surface of EXTERNAL_SURFACE_SPECS) {
@@ -165,6 +177,75 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
165
177
  };
166
178
  }
167
179
 
180
+ function addCloudflareOperations(
181
+ controller: OnboardingWizardController,
182
+ operations: OnboardingApplyOperation[],
183
+ setSecret: (key: string, value: string) => void,
184
+ ): void {
185
+ const setConfig = (
186
+ key: Extract<OnboardingApplyOperation, { kind: 'set-config' }>['key'],
187
+ value: unknown,
188
+ ): void => {
189
+ operations.push({ kind: 'set-config', key, value });
190
+ };
191
+
192
+ const config = controller.runtimeSnapshot?.config.cloudflare;
193
+ const enabledDefault = controller.isCapabilitySelected('cloudflare-batch') || config?.enabled === true;
194
+ const enabled = controller.getBooleanFieldValue('cloudflare.enabled', enabledDefault);
195
+ const components = getCloudflareComponentSelection(controller);
196
+ const batchMode = enabled ? getCloudflareBatchMode(controller) : 'off';
197
+ const setupSource = getCloudflareSetupSource(controller);
198
+ const existingApiTokenRef = config?.apiTokenRef ?? '';
199
+ let apiTokenRef = existingApiTokenRef;
200
+
201
+ if (!enabled) {
202
+ setConfig('cloudflare.enabled', false);
203
+ setConfig('batch.mode', 'off');
204
+ setConfig('batch.queueBackend', 'local');
205
+ return;
206
+ }
207
+
208
+ if (setupSource === 'operational-env') {
209
+ apiTokenRef = buildCloudflareApiTokenRef(
210
+ controller.getStringFieldValue('cloudflare.operational-env-name', 'CLOUDFLARE_API_TOKEN'),
211
+ );
212
+ } else if (setupSource === 'operational-token') {
213
+ const token = controller.getStringFieldValue('cloudflare.operational-token', '');
214
+ if (token.length > 0) {
215
+ setSecret('CLOUDFLARE_API_TOKEN', token);
216
+ apiTokenRef = buildCloudflareOperationalTokenRef();
217
+ }
218
+ }
219
+
220
+ setConfig('cloudflare.enabled', true);
221
+ setConfig('cloudflare.freeTierMode', controller.getStringFieldValue('cloudflare.free-tier-mode', config?.freeTierMode === false ? 'no' : 'yes') === 'yes');
222
+ setConfig('cloudflare.accountId', controller.getStringFieldValue('cloudflare.account-id', config?.accountId ?? ''));
223
+ setConfig('cloudflare.apiTokenRef', apiTokenRef);
224
+ setConfig('cloudflare.zoneId', controller.getStringFieldValue('cloudflare.zone-id', config?.zoneId ?? ''));
225
+ setConfig('cloudflare.zoneName', controller.getStringFieldValue('cloudflare.zone-name', config?.zoneName ?? ''));
226
+ setConfig('cloudflare.workerName', controller.getStringFieldValue('cloudflare.worker-name', config?.workerName ?? 'goodvibes-batch-worker'));
227
+ setConfig('cloudflare.workerSubdomain', controller.getStringFieldValue('cloudflare.worker-subdomain', config?.workerSubdomain ?? ''));
228
+ setConfig('cloudflare.workerHostname', controller.getStringFieldValue('cloudflare.worker-hostname', config?.workerHostname ?? ''));
229
+ setConfig('cloudflare.workerBaseUrl', controller.getStringFieldValue('cloudflare.worker-base-url', config?.workerBaseUrl ?? ''));
230
+ setConfig('cloudflare.daemonBaseUrl', controller.getStringFieldValue('cloudflare.daemon-base-url', config?.daemonBaseUrl ?? ''));
231
+ setConfig('cloudflare.daemonHostname', controller.getStringFieldValue('cloudflare.daemon-hostname', config?.daemonHostname ?? ''));
232
+ setConfig('cloudflare.workerCron', controller.getStringFieldValue('cloudflare.worker-cron', config?.workerCron ?? '*/5 * * * *'));
233
+ setConfig('cloudflare.queueName', controller.getStringFieldValue('cloudflare.queue-name', config?.queueName ?? 'goodvibes-batch'));
234
+ setConfig('cloudflare.deadLetterQueueName', controller.getStringFieldValue('cloudflare.dead-letter-queue-name', config?.deadLetterQueueName ?? 'goodvibes-batch-dlq'));
235
+ setConfig('cloudflare.tunnelName', controller.getStringFieldValue('cloudflare.tunnel-name', config?.tunnelName ?? 'goodvibes-daemon'));
236
+ setConfig('cloudflare.tunnelId', controller.getStringFieldValue('cloudflare.tunnel-id', config?.tunnelId ?? ''));
237
+ setConfig('cloudflare.kvNamespaceName', controller.getStringFieldValue('cloudflare.kv-namespace-name', config?.kvNamespaceName ?? 'goodvibes-runtime'));
238
+ setConfig('cloudflare.kvNamespaceId', controller.getStringFieldValue('cloudflare.kv-namespace-id', config?.kvNamespaceId ?? ''));
239
+ setConfig('cloudflare.durableObjectNamespaceName', controller.getStringFieldValue('cloudflare.do-namespace-name', config?.durableObjectNamespaceName ?? 'GoodVibesCoordinator'));
240
+ setConfig('cloudflare.durableObjectNamespaceId', controller.getStringFieldValue('cloudflare.do-namespace-id', config?.durableObjectNamespaceId ?? ''));
241
+ setConfig('cloudflare.r2BucketName', controller.getStringFieldValue('cloudflare.r2-bucket-name', config?.r2BucketName ?? 'goodvibes-artifacts'));
242
+ setConfig('cloudflare.secretsStoreName', controller.getStringFieldValue('cloudflare.secrets-store-name', config?.secretsStoreName ?? 'goodvibes'));
243
+ setConfig('cloudflare.secretsStoreId', controller.getStringFieldValue('cloudflare.secrets-store-id', config?.secretsStoreId ?? ''));
244
+ setConfig('cloudflare.maxQueueOpsPerDay', controller.getNumberFieldValue('cloudflare.max-queue-ops-per-day', config?.maxQueueOpsPerDay ?? 10000, 1));
245
+ setConfig('batch.mode', batchMode);
246
+ setConfig('batch.queueBackend', batchMode !== 'off' && components.queues ? 'cloudflare' : 'local');
247
+ }
248
+
168
249
  export function addNetworkOperations(
169
250
  controller: OnboardingWizardController,
170
251
  operations: OnboardingApplyOperation[],