@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,5 +1,7 @@
1
1
  import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config/schema';
2
+ import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers/registry';
2
3
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
4
+ import type { SelectionItem } from '../selection-modal.ts';
3
5
 
4
6
  const TTS_CONFIG_KEYS = new Set(['provider', 'voice', 'llm-provider', 'llm-model']);
5
7
 
@@ -33,34 +35,64 @@ export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
33
35
  name: 'config-tts',
34
36
  aliases: ['tts-config'],
35
37
  description: 'Configure live TTS provider, voice, and optional spoken-turn LLM overrides',
36
- usage: '[show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm-provider <id|clear>|llm-model <id|clear>]',
38
+ usage: '[show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm|llm clear|llm-provider <id|clear>|llm-model <id|clear>]',
37
39
  async handler(args, ctx) {
38
40
  const sub = (args[0] ?? 'show').toLowerCase();
39
41
  if (sub === 'show') {
42
+ if (args.length === 0 && openTtsConfigModal(ctx)) return;
40
43
  ctx.print(formatTtsConfig(ctx));
41
44
  return;
42
45
  }
43
46
  if (sub === 'providers') {
44
- await printTtsProviders(ctx);
47
+ if (openTtsProviderPicker(ctx)) return;
48
+ printTtsProviders(ctx);
45
49
  return;
46
50
  }
47
51
  if (sub === 'voices') {
52
+ if (await openTtsVoicePicker(ctx, args[1])) return;
48
53
  await printTtsVoices(ctx, args[1]);
49
54
  return;
50
55
  }
56
+ if (sub === 'llm' || sub === 'model') {
57
+ const action = (args[1] ?? '').toLowerCase();
58
+ if (action === 'clear' || action === 'default') {
59
+ setTtsConfigValue(ctx, 'tts.llmProvider', '');
60
+ setTtsConfigValue(ctx, 'tts.llmModel', '');
61
+ ctx.print('TTS LLM override cleared. /tts will use the current chat model.');
62
+ return;
63
+ }
64
+ if (openTtsLlmPicker(ctx)) return;
65
+ ctx.print('TTS LLM picker is not available in this runtime. Use /config-tts llm-provider <id> and /config-tts llm-model <model>.');
66
+ return;
67
+ }
51
68
  if (TTS_CONFIG_KEYS.has(sub)) {
52
69
  const value = args.slice(1).join(' ').trim();
53
70
  if (!value) {
71
+ if ((sub === 'llm-provider' || sub === 'llm-model') && openTtsLlmPicker(ctx)) return;
54
72
  ctx.print(`Usage: /config-tts ${sub} <value|clear>`);
55
73
  return;
56
74
  }
57
75
  const key = ttsConfigKeyForSubcommand(sub);
58
76
  const nextValue = value.toLowerCase() === 'clear' ? '' : value;
59
- ctx.platform.configManager.setDynamic(key, nextValue);
77
+ const previousProvider = key === 'tts.provider'
78
+ ? String(ctx.platform.configManager.get('tts.provider') ?? '').trim()
79
+ : '';
80
+ if (key === 'tts.llmProvider') {
81
+ setTtsLlmProvider(ctx, nextValue);
82
+ return;
83
+ }
84
+ if (key === 'tts.llmModel') {
85
+ setTtsLlmModel(ctx, nextValue);
86
+ return;
87
+ }
88
+ setTtsConfigValue(ctx, key, nextValue);
89
+ if (key === 'tts.provider' && previousProvider && previousProvider !== nextValue) {
90
+ setTtsConfigValue(ctx, 'tts.voice', '');
91
+ }
60
92
  ctx.print(`${key} ${nextValue ? `set to ${nextValue}` : 'cleared'}.`);
61
93
  return;
62
94
  }
63
- ctx.print('Usage: /config-tts [show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm-provider <id|clear>|llm-model <id|clear>]');
95
+ ctx.print('Usage: /config-tts [show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm|llm clear|llm-provider <id|clear>|llm-model <id|clear>]');
64
96
  },
65
97
  });
66
98
  }
@@ -86,17 +118,155 @@ function formatTtsConfig(ctx: CommandContext): string {
86
118
  ` spoken-turn llm provider override: ${llmProvider || '(current chat provider)'}`,
87
119
  ` spoken-turn llm model override: ${llmModel || '(current chat model)'}`,
88
120
  ' playback: live streaming through local mpv or ffplay',
89
- ' commands: /tts <prompt>, /tts stop, /config-tts providers, /config-tts voices [provider]',
121
+ ' commands: /tts <prompt>, /tts stop, /config-tts, /config-tts providers, /config-tts voices [provider], /config-tts llm',
90
122
  ].join('\n');
91
123
  }
92
124
 
93
- async function printTtsProviders(ctx: CommandContext): Promise<void> {
125
+ function openTtsConfigModal(ctx: CommandContext): boolean {
126
+ if (!ctx.openSelection) return false;
127
+ const cm = ctx.platform.configManager;
128
+ const provider = String(cm.get('tts.provider') ?? '').trim() || '(default)';
129
+ const voice = String(cm.get('tts.voice') ?? '').trim() || '(provider default)';
130
+ const llmProvider = String(cm.get('tts.llmProvider') ?? '').trim();
131
+ const llmModel = String(cm.get('tts.llmModel') ?? '').trim();
132
+ const llmProviderLabel = llmProvider || '(current chat provider)';
133
+ const llmModelLabel = llmModel || '(current chat model)';
134
+ const items: SelectionItem[] = [
135
+ {
136
+ id: 'provider',
137
+ label: 'TTS provider',
138
+ detail: provider,
139
+ category: 'speech output',
140
+ primaryAction: 'select',
141
+ actions: '[Enter] choose',
142
+ },
143
+ {
144
+ id: 'voice',
145
+ label: 'TTS voice',
146
+ detail: voice,
147
+ category: 'speech output',
148
+ primaryAction: 'select',
149
+ actions: '[Enter] choose',
150
+ },
151
+ {
152
+ id: 'llm-provider',
153
+ label: 'TTS LLM provider',
154
+ detail: llmProviderLabel,
155
+ category: 'response generation',
156
+ primaryAction: 'select',
157
+ actions: '[Enter] choose provider and model',
158
+ },
159
+ {
160
+ id: 'llm-model',
161
+ label: 'TTS LLM model',
162
+ detail: llmModelLabel,
163
+ category: 'response generation',
164
+ primaryAction: 'select',
165
+ actions: '[Enter] choose provider and model',
166
+ },
167
+ {
168
+ id: 'clear-voice',
169
+ label: 'Use provider default voice',
170
+ detail: 'clears tts.voice',
171
+ category: 'clear values',
172
+ primaryAction: 'select',
173
+ },
174
+ {
175
+ id: 'clear-llm',
176
+ label: 'Use current chat model for /tts',
177
+ detail: 'clears tts.llmProvider and tts.llmModel',
178
+ category: 'clear values',
179
+ primaryAction: 'select',
180
+ },
181
+ ];
182
+
183
+ ctx.openSelection('TTS Configuration', items, { allowSearch: true }, (result) => {
184
+ if (!result) return;
185
+ if (result.item.id === 'provider') {
186
+ openTtsProviderPicker(ctx);
187
+ return;
188
+ }
189
+ if (result.item.id === 'voice') {
190
+ void openTtsVoicePicker(ctx);
191
+ return;
192
+ }
193
+ if (result.item.id === 'llm-provider') {
194
+ openTtsLlmPicker(ctx);
195
+ return;
196
+ }
197
+ if (result.item.id === 'llm-model') {
198
+ openTtsLlmPicker(ctx);
199
+ return;
200
+ }
201
+ if (result.item.id === 'clear-voice') {
202
+ setTtsConfigValue(ctx, 'tts.voice', '');
203
+ ctx.print('TTS voice cleared. The provider default voice will be used.');
204
+ return;
205
+ }
206
+ if (result.item.id === 'clear-llm') {
207
+ setTtsConfigValue(ctx, 'tts.llmProvider', '');
208
+ setTtsConfigValue(ctx, 'tts.llmModel', '');
209
+ ctx.print('TTS LLM override cleared. /tts will use the current chat model.');
210
+ }
211
+ });
212
+ return true;
213
+ }
214
+
215
+ function openTtsLlmPicker(ctx: CommandContext): boolean {
216
+ if (ctx.openProviderModelPickerWithTarget?.('tts')) return true;
217
+ return ctx.openModelPickerWithTarget?.('tts') ?? false;
218
+ }
219
+
220
+ function getStreamingTtsProviders(ctx: CommandContext): Array<{ id: string; label: string; capabilities: readonly string[] }> {
221
+ const registry = ctx.platform.voiceProviderRegistry;
222
+ if (!registry) {
223
+ return [];
224
+ }
225
+ return registry.list().filter((provider) => provider.capabilities.includes('tts-stream'));
226
+ }
227
+
228
+ function openTtsProviderPicker(ctx: CommandContext): boolean {
229
+ if (!ctx.openSelection) return false;
230
+ const registry = ctx.platform.voiceProviderRegistry;
231
+ if (!registry) {
232
+ ctx.print('Voice provider registry is not available in this runtime.');
233
+ return true;
234
+ }
235
+ const providers = getStreamingTtsProviders(ctx);
236
+ if (providers.length === 0) {
237
+ ctx.print('No streaming TTS providers are registered.');
238
+ return true;
239
+ }
240
+ const current = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
241
+ const items: SelectionItem[] = providers.map((provider) => ({
242
+ id: provider.id,
243
+ label: provider.label,
244
+ detail: provider.id === current ? `${provider.id} (current)` : provider.id,
245
+ category: 'streaming TTS providers',
246
+ primaryAction: 'select',
247
+ actions: '[Enter] set provider',
248
+ }));
249
+ ctx.openSelection('Choose TTS Provider', items, { preSelectId: current, allowSearch: true }, (result) => {
250
+ if (!result) return;
251
+ const previous = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
252
+ setTtsConfigValue(ctx, 'tts.provider', result.item.id);
253
+ if (previous && previous !== result.item.id) {
254
+ setTtsConfigValue(ctx, 'tts.voice', '');
255
+ ctx.print(`TTS provider set to ${result.item.id}. TTS voice was cleared because voices are provider-specific.`);
256
+ } else {
257
+ ctx.print(`TTS provider set to ${result.item.id}.`);
258
+ }
259
+ });
260
+ return true;
261
+ }
262
+
263
+ function printTtsProviders(ctx: CommandContext): void {
94
264
  const registry = ctx.platform.voiceProviderRegistry;
95
265
  if (!registry) {
96
266
  ctx.print('Voice provider registry is not available in this runtime.');
97
267
  return;
98
268
  }
99
- const providers = registry.list().filter((provider) => provider.capabilities.includes('tts-stream'));
269
+ const providers = getStreamingTtsProviders(ctx);
100
270
  if (providers.length === 0) {
101
271
  ctx.print('No streaming TTS providers are registered.');
102
272
  return;
@@ -104,9 +274,113 @@ async function printTtsProviders(ctx: CommandContext): Promise<void> {
104
274
  ctx.print([
105
275
  'Streaming TTS Providers',
106
276
  ...providers.map((provider) => ` ${provider.id}: ${provider.label}`),
277
+ '',
278
+ 'Set provider: /config-tts provider <provider-id>',
107
279
  ].join('\n'));
108
280
  }
109
281
 
282
+ function getSelectableLlmModels(ctx: CommandContext): ModelDefinition[] {
283
+ const registry = ctx.provider.providerRegistry as Partial<Pick<typeof ctx.provider.providerRegistry, 'getSelectableModels' | 'listModels'>>;
284
+ if (typeof registry.getSelectableModels === 'function') return registry.getSelectableModels();
285
+ if (typeof registry.listModels === 'function') return registry.listModels().filter((model) => model.selectable !== false);
286
+ return [];
287
+ }
288
+
289
+ function setTtsLlmProvider(ctx: CommandContext, nextValue: string): void {
290
+ if (!nextValue) {
291
+ setTtsConfigValue(ctx, 'tts.llmProvider', '');
292
+ setTtsConfigValue(ctx, 'tts.llmModel', '');
293
+ ctx.print('TTS LLM override cleared. /tts will use the current chat model.');
294
+ return;
295
+ }
296
+ const previousProvider = String(ctx.platform.configManager.get('tts.llmProvider') ?? '').trim();
297
+ setTtsConfigValue(ctx, 'tts.llmProvider', nextValue);
298
+ if (previousProvider && previousProvider !== nextValue) {
299
+ setTtsConfigValue(ctx, 'tts.llmModel', '');
300
+ ctx.print(`TTS LLM provider set to ${nextValue}. TTS LLM model was cleared because models are provider-specific.`);
301
+ return;
302
+ }
303
+ ctx.print(`TTS LLM provider set to ${nextValue}.`);
304
+ }
305
+
306
+ function setTtsLlmModel(ctx: CommandContext, nextValue: string): void {
307
+ if (!nextValue) {
308
+ setTtsConfigValue(ctx, 'tts.llmModel', '');
309
+ ctx.print('TTS LLM model override cleared. /tts will use the current chat model unless a model is selected.');
310
+ return;
311
+ }
312
+ const preferredProvider = String(ctx.platform.configManager.get('tts.llmProvider') ?? '').trim() || undefined;
313
+ const selected = findSelectableLlmModel(ctx, nextValue, preferredProvider);
314
+ if (selected) {
315
+ setTtsConfigValue(ctx, 'tts.llmProvider', selected.provider);
316
+ setTtsConfigValue(ctx, 'tts.llmModel', getModelRegistryKey(selected));
317
+ ctx.print(`TTS LLM set to ${selected.displayName} (${selected.provider}).`);
318
+ return;
319
+ }
320
+ setTtsConfigValue(ctx, 'tts.llmModel', nextValue);
321
+ ctx.print(`tts.llmModel set to ${nextValue}.`);
322
+ }
323
+
324
+ function findSelectableLlmModel(ctx: CommandContext, ref: string, preferredProvider?: string): ModelDefinition | undefined {
325
+ const matches = getSelectableLlmModels(ctx).filter((model) =>
326
+ model.registryKey === ref || model.id === ref || model.displayName === ref,
327
+ );
328
+ if (preferredProvider) {
329
+ const providerMatch = matches.find((model) => model.provider === preferredProvider);
330
+ if (providerMatch) return providerMatch;
331
+ }
332
+ return matches[0];
333
+ }
334
+
335
+ function getModelRegistryKey(model: ModelDefinition): string {
336
+ return model.registryKey ?? `${model.provider}:${model.id}`;
337
+ }
338
+
339
+ async function openTtsVoicePicker(ctx: CommandContext, providerArg?: string): Promise<boolean> {
340
+ if (!ctx.openSelection) return false;
341
+ const service = ctx.platform.voiceService;
342
+ if (!service) {
343
+ ctx.print('Voice service is not available in this runtime.');
344
+ return true;
345
+ }
346
+ const providerId = (providerArg ?? String(ctx.platform.configManager.get('tts.provider') ?? '')).trim() || undefined;
347
+ try {
348
+ const voices = await service.listVoices(providerId);
349
+ if (voices.length === 0) {
350
+ ctx.print(providerId ? `No voices returned for ${providerId}.` : 'No TTS voices returned.');
351
+ return true;
352
+ }
353
+ const current = String(ctx.platform.configManager.get('tts.voice') ?? '').trim();
354
+ const items: SelectionItem[] = [
355
+ {
356
+ id: '__default__',
357
+ label: 'Use provider default voice',
358
+ detail: current ? 'clears tts.voice' : '(current)',
359
+ category: 'voice',
360
+ primaryAction: 'select',
361
+ },
362
+ ...voices.map((voice) => ({
363
+ id: voice.id,
364
+ label: voice.label || voice.id,
365
+ detail: voice.id === current ? `${voice.id} (current)` : voice.id,
366
+ category: providerId ?? 'voices',
367
+ primaryAction: 'select' as const,
368
+ actions: '[Enter] set voice',
369
+ })),
370
+ ];
371
+ ctx.openSelection(`Choose TTS Voice${providerId ? ` (${providerId})` : ''}`, items, { preSelectId: current || '__default__', allowSearch: true }, (result) => {
372
+ if (!result) return;
373
+ const nextVoice = result.item.id === '__default__' ? '' : result.item.id;
374
+ setTtsConfigValue(ctx, 'tts.voice', nextVoice);
375
+ ctx.print(nextVoice ? `TTS voice set to ${nextVoice}.` : 'TTS voice cleared. The provider default voice will be used.');
376
+ });
377
+ return true;
378
+ } catch (error) {
379
+ ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
380
+ return true;
381
+ }
382
+ }
383
+
110
384
  async function printTtsVoices(ctx: CommandContext, providerArg?: string): Promise<void> {
111
385
  const service = ctx.platform.voiceService;
112
386
  if (!service) {
@@ -124,12 +398,19 @@ async function printTtsVoices(ctx: CommandContext, providerArg?: string): Promis
124
398
  `TTS Voices${providerId ? ` (${providerId})` : ''}`,
125
399
  ...voices.slice(0, 60).map((voice) => ` ${voice.id}: ${voice.label}`),
126
400
  ...(voices.length > 60 ? [` ... ${voices.length - 60} more`] : []),
401
+ '',
402
+ 'Set voice: /config-tts voice <voice-id>',
403
+ 'Use provider default voice: /config-tts voice clear',
127
404
  ].join('\n'));
128
405
  } catch (error) {
129
406
  ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
130
407
  }
131
408
  }
132
409
 
410
+ function setTtsConfigValue(ctx: CommandContext, key: ConfigKey, value: string): void {
411
+ ctx.platform.configManager.setDynamic(key, value);
412
+ }
413
+
133
414
  function formatValue(value: unknown): string {
134
415
  const text = String(value ?? '').trim();
135
416
  return text || '(default)';
@@ -55,6 +55,7 @@ import { registerConversationRuntimeCommands } from './commands/conversation-run
55
55
  import { registerQrcodeRuntimeCommands } from './commands/qrcode-runtime.ts';
56
56
  import { registerOnboardingRuntimeCommands } from './commands/onboarding-runtime.ts';
57
57
  import { registerTtsRuntimeCommands } from './commands/tts-runtime.ts';
58
+ import { registerCloudflareRuntimeCommands } from './commands/cloudflare-runtime.ts';
58
59
 
59
60
  /**
60
61
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -104,6 +105,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
104
105
  registerQrcodeRuntimeCommands(registry);
105
106
  registerOnboardingRuntimeCommands(registry);
106
107
  registerTtsRuntimeCommands(registry);
108
+ registerCloudflareRuntimeCommands(registry);
107
109
  registerLocalRuntimeCommands(registry);
108
110
  registerSessionWorkflowCommands(registry);
109
111
  registerDiscoveryRuntimeCommands(registry);
@@ -152,6 +152,7 @@ export interface FeedContextClosures {
152
152
  cleanupMarkerRegistry: (text: string) => void;
153
153
  expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
154
154
  openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
155
+ openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
155
156
  onModelPickerCommit: () => boolean;
156
157
  onOnboardingAction: (action: import('./onboarding/onboarding-wizard.ts').OnboardingWizardAction) => void;
157
158
  }
@@ -153,6 +153,7 @@ export interface InputFeedContext {
153
153
  readonly cleanupMarkerRegistry: (text: string) => void;
154
154
  readonly expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
155
155
  readonly openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
156
+ readonly openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
156
157
  readonly onModelPickerCommit: () => boolean;
157
158
  readonly onOnboardingAction: (action: OnboardingWizardAction) => void;
158
159
  readonly exitApp: () => void;
@@ -176,6 +177,10 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
176
177
  searchShortcutMatch: token.type === 'key' && keybindings.matches('search', token),
177
178
  selectionModal: context.selectionModal,
178
179
  selectionCallback: context.selectionCallback,
180
+ getSelectionCallback: () => context.selectionCallback,
181
+ setSelectionCallback: (callback) => {
182
+ context.selectionCallback = callback;
183
+ },
179
184
  bookmarkModal: context.bookmarkModal,
180
185
  settingsModal: context.settingsModal,
181
186
  sessionPickerModal: context.sessionPickerModal,
@@ -213,6 +218,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
213
218
  scroll: context.scroll,
214
219
  getScrollTop: context.getScrollTop,
215
220
  openModelPickerWithTarget: context.openModelPickerWithTarget,
221
+ openProviderModelPickerWithTarget: context.openProviderModelPickerWithTarget,
216
222
  onModelPickerCommit: context.onModelPickerCommit,
217
223
  onOnboardingAction: context.onOnboardingAction,
218
224
  }, token);
@@ -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)) {