@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
@@ -1,4 +1,6 @@
1
1
  import { NETWORK_MODE_OPTIONS, REASONING_OPTIONS, HITL_MODE_OPTIONS, GUIDANCE_MODE_OPTIONS, PERMISSION_MODE_OPTIONS, SECRET_POLICY_OPTIONS } from './onboarding-wizard-constants.ts';
2
+ import { shouldShowCloudflareStep } from './onboarding-wizard-cloudflare.ts';
3
+ import { buildCloudflareStep } from './onboarding-wizard-cloudflare-step.ts';
2
4
  import {
3
5
  EXTERNAL_SURFACE_SPECS,
4
6
  getExternalSurfaceAutoStartDefaultValue,
@@ -19,27 +21,25 @@ export function buildOnboardingWizardSteps(controller: OnboardingWizardControlle
19
21
  const steps: OnboardingWizardStepDefinition[] = [
20
22
  buildCapabilitiesStep(controller),
21
23
  ];
22
-
23
24
  if (hasServers) {
24
25
  steps.push(buildNetworkStep(controller));
25
26
  }
26
-
27
27
  if (hasServers || controller.hasExistingAccessState()) {
28
28
  steps.push(buildAccessStep(controller));
29
29
  }
30
-
31
30
  if (wantsExternalServices) {
32
31
  steps.push(buildExternalServicesStep(controller));
33
32
  for (const surface of getSelectedExternalSurfaceSpecs(controller)) {
34
33
  steps.push(buildExternalSurfaceStep(controller, surface));
35
34
  }
36
35
  }
37
-
36
+ if (shouldShowCloudflareStep(controller)) {
37
+ steps.push(buildCloudflareStep(controller));
38
+ }
38
39
  steps.push(buildProviderAccessStep(controller));
39
40
  steps.push(buildDefaultModelStep(controller));
40
41
  steps.push(buildExperienceStep(controller));
41
42
  steps.push(buildReviewStep(controller));
42
-
43
43
  return steps.map(addApplyAndContinueAction);
44
44
  }
45
45
 
@@ -49,7 +49,7 @@ function buildApplyAndContinueAction(step: OnboardingWizardStepDefinition): Onbo
49
49
  id: `${step.id}.apply-and-continue`,
50
50
  action: 'apply-and-continue',
51
51
  label: 'Apply & Continue To Next Section',
52
- hint: 'Persist the current wizard settings, verify them, and move to the next onboarding section.',
52
+ hint: 'Save the current wizard selections in this onboarding session and move to the next section. Settings are persisted on the final Review apply.',
53
53
  defaultValue: 'Apply & next',
54
54
  spacerBeforeRows: 2,
55
55
  };
@@ -25,6 +25,7 @@ export type OnboardingWizardStepId =
25
25
  | 'access'
26
26
  | 'external-services'
27
27
  | OnboardingWizardExternalSurfaceStepId
28
+ | 'cloudflare'
28
29
  | 'provider-access'
29
30
  | 'default-model'
30
31
  | 'experience'
@@ -47,6 +48,13 @@ export type OnboardingWizardAction =
47
48
  | 'clear-capabilities'
48
49
  | 'select-all-external-surfaces'
49
50
  | 'clear-external-surfaces'
51
+ | 'cloudflare-token-requirements'
52
+ | 'cloudflare-create-operational-token'
53
+ | 'cloudflare-discover'
54
+ | 'cloudflare-validate'
55
+ | 'cloudflare-provision'
56
+ | 'cloudflare-verify'
57
+ | 'cloudflare-disable'
50
58
  | 'start-openai-subscription'
51
59
  | 'finish-openai-subscription';
52
60
 
@@ -2,7 +2,7 @@ import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config/schema
2
2
  import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
3
3
  import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
4
4
 
5
- export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'danger' | 'tools' | 'flags' | 'network';
5
+ export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'cloudflare' | 'danger' | 'tools' | 'flags' | 'network';
6
6
 
7
7
  export const SETTINGS_CATEGORIES: SettingsCategory[] = [
8
8
  'display',
@@ -15,6 +15,7 @@ export const SETTINGS_CATEGORIES: SettingsCategory[] = [
15
15
  'mcp',
16
16
  'sandbox',
17
17
  'surfaces',
18
+ 'cloudflare',
18
19
  'danger',
19
20
  'tools',
20
21
  'flags',
@@ -41,13 +41,21 @@ export {
41
41
  type SubscriptionEntry,
42
42
  } from './settings-modal-types.ts';
43
43
 
44
+ type ModelPickerLaunch =
45
+ | { readonly flow: 'providerModel'; readonly target: ModelPickerTarget }
46
+ | { readonly flow: 'model'; readonly target: ModelPickerTarget };
47
+
44
48
  /**
45
- * Map a config key to the model picker target it should open, or null if the
46
- * setting should use the normal inline text-edit flow.
49
+ * Map config keys to the shared provider/model picker flows. Provider rows open
50
+ * provider first; model rows open directly to models for the same target.
47
51
  */
48
- function _modelPickerTargetForKey(key: string): ModelPickerTarget | null {
49
- if (key === 'helper.globalProvider' || key === 'helper.globalModel') return 'helper';
50
- if (key === 'tools.llmProvider' || key === 'tools.llmModel') return 'tool';
52
+ function _modelPickerLaunchForKey(key: string): ModelPickerLaunch | null {
53
+ if (key === 'helper.globalProvider') return { flow: 'providerModel', target: 'helper' };
54
+ if (key === 'helper.globalModel') return { flow: 'model', target: 'helper' };
55
+ if (key === 'tools.llmProvider') return { flow: 'providerModel', target: 'tool' };
56
+ if (key === 'tools.llmModel') return { flow: 'model', target: 'tool' };
57
+ if (key === 'tts.llmProvider') return { flow: 'providerModel', target: 'tts' };
58
+ if (key === 'tts.llmModel') return { flow: 'model', target: 'tts' };
51
59
  return null;
52
60
  }
53
61
 
@@ -94,6 +102,8 @@ export class SettingsModal {
94
102
  * Consumed and cleared by the route handler after each Enter/Space action.
95
103
  */
96
104
  public pendingModelPickerTarget: ModelPickerTarget | null = null;
105
+ /** Set when the highlighted setting should open provider selection before model selection. */
106
+ public pendingProviderModelPickerTarget: ModelPickerTarget | null = null;
97
107
  /** Provider awaiting explicit logout confirmation, if any. */
98
108
  public subscriptionLogoutConfirmationTarget: string | null = null;
99
109
 
@@ -146,6 +156,8 @@ export class SettingsModal {
146
156
  this.selectedIndex = 0;
147
157
  this.editingMode = false;
148
158
  this.editBuffer = '';
159
+ this.pendingModelPickerTarget = null;
160
+ this.pendingProviderModelPickerTarget = null;
149
161
  this.mcpAllowAllConfirmationTarget = null;
150
162
  this.subscriptionLogoutConfirmationTarget = null;
151
163
  this.lastSaveTriggeredRestart = null;
@@ -156,6 +168,8 @@ export class SettingsModal {
156
168
  this.active = false;
157
169
  this.editingMode = false;
158
170
  this.editBuffer = '';
171
+ this.pendingModelPickerTarget = null;
172
+ this.pendingProviderModelPickerTarget = null;
159
173
  this.mcpAllowAllConfirmationTarget = null;
160
174
  this.subscriptionLogoutConfirmationTarget = null;
161
175
  this.lastSaveTriggeredRestart = null;
@@ -290,9 +304,13 @@ export class SettingsModal {
290
304
  const { setting } = entry;
291
305
 
292
306
  // Delegate provider/model picker settings to the model picker UI
293
- const pickerTarget = _modelPickerTargetForKey(setting.key);
294
- if (pickerTarget !== null) {
295
- this.pendingModelPickerTarget = pickerTarget;
307
+ const pickerLaunch = _modelPickerLaunchForKey(setting.key);
308
+ if (pickerLaunch !== null) {
309
+ if (pickerLaunch.flow === 'providerModel') {
310
+ this.pendingProviderModelPickerTarget = pickerLaunch.target;
311
+ } else {
312
+ this.pendingModelPickerTarget = pickerLaunch.target;
313
+ }
296
314
  return;
297
315
  }
298
316
 
@@ -521,6 +539,8 @@ export class SettingsModal {
521
539
  cat = 'network';
522
540
  } else if (rawCat === 'surfaces') {
523
541
  cat = 'surfaces';
542
+ } else if (rawCat === 'cloudflare' || rawCat === 'batch') {
543
+ cat = 'cloudflare';
524
544
  } else {
525
545
  cat = rawCat as SettingsCategory;
526
546
  }
@@ -750,6 +770,8 @@ export class SettingsModal {
750
770
  }
751
771
  } else if (rawCat === 'surfaces') {
752
772
  cat = 'surfaces';
773
+ } else if (rawCat === 'cloudflare' || rawCat === 'batch') {
774
+ cat = 'cloudflare';
753
775
  } else {
754
776
  cat = rawCat as SettingsCategory;
755
777
  }
package/src/main.ts CHANGED
@@ -52,6 +52,10 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-displ
52
52
  import { prepareShellCliRuntime } from './cli/entrypoint.ts';
53
53
  import { applyInitialTuiCliState } from './cli/tui-startup.ts';
54
54
  import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
55
+ import {
56
+ attachSpokenTurnModelRouting,
57
+ createSpokenTurnInputOptions,
58
+ } from './audio/spoken-turn-model-routing.ts';
55
59
 
56
60
  const ALT_SCREEN_ENTER = '\x1b[?1049h';
57
61
  const ALT_SCREEN_EXIT = '\x1b[?1049l';
@@ -251,6 +255,12 @@ async function main() {
251
255
  });
252
256
  stopSpokenOutputForExit = () => spokenTurns.stop();
253
257
  unsubs.push(...spokenTurns.unsubs);
258
+ unsubs.push(attachSpokenTurnModelRouting({
259
+ orchestrator,
260
+ providerRegistry,
261
+ configManager,
262
+ notify: (message) => { systemMessageRouter.high(message); render(); },
263
+ }));
254
264
 
255
265
  const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
256
266
  input.clearModalStack();
@@ -289,7 +299,8 @@ async function main() {
289
299
  if (options.spokenOutput && processedText) {
290
300
  spokenTurns.submitNextTurn(processedText);
291
301
  }
292
- orchestrator.handleUserInput(processedText, content).catch((err: unknown) => {
302
+ const inputOptions = options.spokenOutput ? createSpokenTurnInputOptions() : undefined;
303
+ orchestrator.handleUserInput(processedText, content, inputOptions).catch((err: unknown) => {
293
304
  logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
294
305
  });
295
306
  } else {
@@ -39,7 +39,13 @@ export class TerminalBuffer {
39
39
 
40
40
  public blitLine(row: number, line: Line): void {
41
41
  if (row >= 0 && row < this.height) {
42
- this.cells[row] = [...line];
42
+ const current = this.cells[row]!;
43
+ const next = createEmptyLine(this.width);
44
+ for (let x = 0; x < Math.min(line.length, this.width); x++) {
45
+ next[x] = { ...line[x]! };
46
+ }
47
+ if (linesEqual(current, next, this.width)) return;
48
+ this.cells[row] = next;
43
49
  this.dirtyRows[row] = true;
44
50
  }
45
51
  }
@@ -56,12 +62,17 @@ export class TerminalBuffer {
56
62
  * If dimensions changed, reallocates cells array.
57
63
  * Always clears the dirty bitmap.
58
64
  */
59
- public reset(width: number, height: number): void {
65
+ public reset(width: number, height: number, source?: TerminalBuffer | null): void {
60
66
  if (width !== this.width || height !== this.height) {
61
67
  this.width = width;
62
68
  this.height = height;
63
69
  this.cells = Array.from({ length: height }, () => createEmptyLine(width));
64
70
  this.dirtyRows = new Array(height).fill(false);
71
+ } else if (source && source.width === width && source.height === height) {
72
+ for (let y = 0; y < this.height; y++) {
73
+ this.cells[y] = source.cells[y]!.map(cell => ({ ...cell }));
74
+ this.dirtyRows[y] = false;
75
+ }
65
76
  } else {
66
77
  for (let y = 0; y < this.height; y++) {
67
78
  const row = this.cells[y]!;
@@ -72,4 +83,31 @@ export class TerminalBuffer {
72
83
  }
73
84
  }
74
85
  }
86
+
87
+ public clearDirty(): void {
88
+ this.dirtyRows.fill(false);
89
+ }
90
+ }
91
+
92
+ function linesEqual(left: Line, right: Line, width: number): boolean {
93
+ for (let x = 0; x < width; x++) {
94
+ const a = left[x];
95
+ const b = right[x];
96
+ if (!a && !b) continue;
97
+ if (!a || !b) return false;
98
+ if (
99
+ a.char !== b.char
100
+ || a.fg !== b.fg
101
+ || a.bg !== b.bg
102
+ || a.bold !== b.bold
103
+ || a.dim !== b.dim
104
+ || a.underline !== b.underline
105
+ || a.italic !== b.italic
106
+ || a.strikethrough !== b.strikethrough
107
+ || (a.link ?? '') !== (b.link ?? '')
108
+ ) {
109
+ return false;
110
+ }
111
+ }
112
+ return true;
75
113
  }
@@ -1,6 +1,6 @@
1
1
  import { TerminalBuffer } from './buffer.ts';
2
2
  import { DiffEngine } from './diff.ts';
3
- import { type Line, createStyledCell } from '../types/grid.ts';
3
+ import { type Line, createEmptyCell, createStyledCell } from '../types/grid.ts';
4
4
  import { getDisplayWidth } from '../utils/terminal-width.ts';
5
5
  import type { SearchManager } from '../input/search.ts';
6
6
 
@@ -78,7 +78,7 @@ export class Compositor {
78
78
  if (!this.backBuffer) {
79
79
  this.backBuffer = new TerminalBuffer(width, height);
80
80
  } else {
81
- this.backBuffer.reset(width, height);
81
+ this.backBuffer.reset(width, height, this.frontBuffer);
82
82
  }
83
83
  const newBuffer = this.backBuffer;
84
84
 
@@ -154,19 +154,31 @@ export class Compositor {
154
154
  }
155
155
 
156
156
  const panelStartX = sepX + 1;
157
+ const clearPanelRemainder = (fromX = 0) => {
158
+ for (let x = Math.max(0, fromX); x < panelWidth; x++) {
159
+ newBuffer.setCell(panelStartX + x, screenY, createEmptyCell());
160
+ }
161
+ };
162
+ const drawPanelLine = (panelLine: Line | undefined) => {
163
+ if (panelLine === undefined) {
164
+ clearPanelRemainder();
165
+ return;
166
+ }
167
+ const limit = Math.min(panelLine.length, panelWidth);
168
+ for (let x = 0; x < limit; x++) {
169
+ const cell = panelLine[x];
170
+ if (cell !== undefined) {
171
+ newBuffer.setCell(panelStartX + x, screenY, cell);
172
+ }
173
+ }
174
+ clearPanelRemainder(limit);
175
+ };
157
176
 
158
177
  if (!hasBottomPane) {
159
178
  // --- Single pane mode ---
160
179
  // viewport row 0 → workspace bar, viewport rows 1+ → panel content
161
180
  const panelLine = i === 0 ? p.workspaceBar : p.topContent[i - 1];
162
- if (panelLine !== undefined) {
163
- for (let x = 0; x < panelWidth; x++) {
164
- const cell = panelLine[x];
165
- if (cell !== undefined) {
166
- newBuffer.setCell(panelStartX + x, screenY, cell);
167
- }
168
- }
169
- }
181
+ drawPanelLine(panelLine);
170
182
  } else {
171
183
  // --- Two pane mode ---
172
184
  // Row layout (by viewport row i):
@@ -203,13 +215,8 @@ export class Compositor {
203
215
  panelLine = p.bottomContent?.[i - (hSepRow + 2)];
204
216
  }
205
217
 
206
- if (panelLine !== undefined) {
207
- for (let x = 0; x < panelWidth; x++) {
208
- const cell = panelLine[x];
209
- if (cell !== undefined) {
210
- newBuffer.setCell(panelStartX + x, screenY, cell);
211
- }
212
- }
218
+ if (i !== hSepRow) {
219
+ drawPanelLine(panelLine);
213
220
  }
214
221
  }
215
222
  }
@@ -269,6 +276,7 @@ export class Compositor {
269
276
  // Swap: back (just written) becomes the new front reference; old front becomes the next back
270
277
  const swap = this.frontBuffer;
271
278
  this.frontBuffer = this.backBuffer;
279
+ this.frontBuffer.clearDirty();
272
280
  this.backBuffer = swap;
273
281
  }
274
282
  }
@@ -34,6 +34,10 @@ const MODE_TITLES: Record<string, string> = {
34
34
  */
35
35
  export const MODEL_PICKER_CHROME_LINES = 7;
36
36
 
37
+ const renderCache = new WeakMap<ModelPickerModal, { key: string; lines: Line[] }>();
38
+ const objectIds = new WeakMap<object, number>();
39
+ let nextObjectId = 1;
40
+
37
41
  function putRowText(line: Line, startX: number, maxWidth: number, text: string, fg: string, bg = '', bold = false, dim = false): void {
38
42
  putOverlayText(line, startX, maxWidth, text, { fg, bg, bold, dim });
39
43
  }
@@ -51,6 +55,10 @@ export function renderModelPickerOverlay(
51
55
  maxVisible = 20,
52
56
  viewportHeight?: number,
53
57
  ): Line[] {
58
+ const cacheKey = getRenderCacheKey(picker, width, maxVisible, viewportHeight);
59
+ const cached = renderCache.get(picker);
60
+ if (cached?.key === cacheKey) return cached.lines;
61
+
54
62
  const lines: Line[] = [];
55
63
  const metrics = getOverlaySurfaceMetrics(width, viewportHeight ?? 24, {
56
64
  chromeRows: MODEL_PICKER_CHROME_LINES,
@@ -399,5 +407,67 @@ export function renderModelPickerOverlay(
399
407
  putRowText(footerLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(hints, contentW), contentW), mutedFg, '', false, true);
400
408
  lines.push(footerLine);
401
409
 
410
+ renderCache.set(picker, { key: cacheKey, lines });
402
411
  return lines;
403
412
  }
413
+
414
+ function getRenderCacheKey(
415
+ picker: ModelPickerModal,
416
+ width: number,
417
+ maxVisible: number,
418
+ viewportHeight: number | undefined,
419
+ ): string {
420
+ const base = [
421
+ width,
422
+ maxVisible,
423
+ viewportHeight ?? '',
424
+ picker.mode,
425
+ picker.target,
426
+ picker.query,
427
+ picker.searchFocused ? 1 : 0,
428
+ picker.selectedIndex,
429
+ picker.scrollOffset,
430
+ picker.categoryFilter,
431
+ picker.capabilityFilter,
432
+ picker.availableOnly ? 1 : 0,
433
+ picker.benchmarkSort,
434
+ picker.groupBy,
435
+ keyForSet(picker.pinnedIds),
436
+ keyForSet(picker.configuredProviders),
437
+ ];
438
+
439
+ if (picker.mode === 'model') {
440
+ const filtered = picker.getFilteredModels();
441
+ const selected = filtered[picker.selectedIndex];
442
+ base.push(objectId(picker.models), objectId(filtered), filtered.length, selected?.registryKey ?? selected?.id ?? '');
443
+ } else if (picker.mode === 'provider') {
444
+ const filteredProviders = picker.getFilteredProviders();
445
+ base.push(objectId(picker.providers), objectId(filteredProviders), filteredProviders.length, keyForMap(picker.configuredViaMap));
446
+ } else if (picker.mode === 'effort') {
447
+ base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
448
+ } else if (picker.mode === 'contextCap') {
449
+ base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
450
+ }
451
+
452
+ return base.join('\u001e');
453
+ }
454
+
455
+ function objectId(value: object): number {
456
+ const existing = objectIds.get(value);
457
+ if (existing !== undefined) return existing;
458
+ const next = nextObjectId++;
459
+ objectIds.set(value, next);
460
+ return next;
461
+ }
462
+
463
+ function keyForSet(values: ReadonlySet<string>): string {
464
+ return values.size === 0 ? '' : [...values].sort().join('\u001f');
465
+ }
466
+
467
+ function keyForMap(values: ReadonlyMap<string, string | undefined>): string {
468
+ if (values.size === 0) return '';
469
+ return [...values.entries()]
470
+ .sort(([left], [right]) => left.localeCompare(right))
471
+ .map(([key, value]) => `${key}\u001d${value ?? ''}`)
472
+ .join('\u001f');
473
+ }
@@ -76,6 +76,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
76
76
  mcp: 'MCP',
77
77
  sandbox: 'Sandbox',
78
78
  surfaces: 'Surfaces',
79
+ cloudflare: 'Cloudflare',
79
80
  danger: 'Danger',
80
81
  tools: 'Tools',
81
82
  flags: 'Flags',
@@ -203,6 +203,10 @@ export function createBootstrapCommandActions(
203
203
  configManager.set('tools.llmModel', key);
204
204
  configManager.setDynamic('tools.llmEnabled' as never, true);
205
205
  conversation.log(`Tool LLM set to: ${def.displayName} (${def.provider})`, { fg: '135' });
206
+ } else if (resolvedTarget === 'tts') {
207
+ configManager.set('tts.llmProvider', def.provider);
208
+ configManager.set('tts.llmModel', key);
209
+ conversation.log(`TTS LLM set to: ${def.displayName} (${def.provider})`, { fg: '135' });
206
210
  } else {
207
211
  // Default: main provider/model
208
212
  if (contextCap != null && contextCap > 0) {