@pellux/goodvibes-tui 0.19.31 → 0.19.33

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.
@@ -21,6 +21,7 @@ import { inspectProviderAuth } from '@pellux/goodvibes-sdk/platform/runtime/auth
21
21
  import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing/index';
22
22
  import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
23
23
  import type { GoodVibesCliParseResult } from './types.ts';
24
+ import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
24
25
  import { classifyProviderSetup } from './provider-classification.ts';
25
26
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
26
27
  import { applyRuntimeEndpointFlagOverrides } from './config-overrides.ts';
@@ -378,11 +379,23 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
378
379
  const snapshots = await listProviderRuntimeSnapshots(services.providerRegistry);
379
380
  const current = services.providerRegistry.getCurrentModel();
380
381
  if (sub === 'current') {
381
- const value = { provider: current.provider, model: current.registryKey };
382
+ const snapshot = snapshots.find((candidate) => candidate.providerId === current.provider);
383
+ const authRoutes = snapshot?.runtime.auth?.routes ?? [];
384
+ const value = {
385
+ provider: current.provider,
386
+ model: current.registryKey,
387
+ configured: snapshot?.runtime.auth?.configured ?? true,
388
+ configuredVia: snapshot?.runtime.auth?.mode ?? 'unknown',
389
+ authRoutes,
390
+ authRouteSummary: summarizeProviderAuthRoutes(authRoutes),
391
+ };
382
392
  return formatJsonOrText(runtime.cli)(value, [
383
393
  'GoodVibes current provider',
384
394
  ` provider: ${current.provider}`,
385
395
  ` model: ${current.registryKey}`,
396
+ ` configured: ${yesNo(value.configured)}`,
397
+ ` via: ${value.configuredVia}`,
398
+ ` auth routes: ${value.authRouteSummary}`,
386
399
  ].join('\n'));
387
400
  }
388
401
  if (sub === 'use' || sub === 'set') {
@@ -417,13 +430,21 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
417
430
  configured: snapshot.runtime.auth?.configured ?? true,
418
431
  modelCount: snapshot.modelCount,
419
432
  });
420
- return formatJsonOrText(runtime.cli)({ ...snapshot, setup }, [
433
+ const authRoutes = snapshot.runtime.auth?.routes ?? [];
434
+ return formatJsonOrText(runtime.cli)({
435
+ ...snapshot,
436
+ setup,
437
+ authRoutes,
438
+ authRouteSummary: summarizeProviderAuthRoutes(authRoutes),
439
+ }, [
421
440
  `Provider ${snapshot.providerId}`,
422
441
  ` active: ${yesNo(snapshot.active)}`,
423
442
  ` setup: ${setup.setupLabel}`,
424
443
  ` configured: ${yesNo(snapshot.runtime.auth?.configured ?? true)}`,
425
444
  ` via: ${snapshot.runtime.auth?.mode ?? 'unknown'}`,
426
445
  ` models: ${snapshot.modelCount}`,
446
+ ` auth routes: ${summarizeProviderAuthRoutes(authRoutes)}`,
447
+ ...authRoutes.map((route) => ` ${formatProviderAuthRoute(route)}`),
427
448
  ` detail: ${snapshot.runtime.auth?.detail ?? snapshot.runtime.notes?.join('; ') ?? ''}`,
428
449
  ].join('\n'));
429
450
  }
@@ -442,11 +463,13 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
442
463
  models: snapshot.modelCount,
443
464
  current: current.provider === snapshot.providerId,
444
465
  detail: snapshot.runtime.auth?.detail ?? snapshot.runtime.notes?.join('; ') ?? '',
466
+ authRoutes: snapshot.runtime.auth?.routes ?? [],
467
+ authRouteSummary: summarizeProviderAuthRoutes(snapshot.runtime.auth?.routes),
445
468
  }));
446
469
  return formatJsonOrText(runtime.cli)(value, [
447
470
  'GoodVibes providers',
448
471
  ...value.map((provider) =>
449
- ` ${provider.current ? '*' : ' '} ${provider.provider.padEnd(18)} setup=${provider.setupClass} configured=${yesNo(provider.configured)} via=${provider.configuredVia ?? 'n/a'} models=${provider.models} ${provider.detail ?? ''}`.trimEnd(),
472
+ ` ${provider.current ? '*' : ' '} ${provider.provider.padEnd(18)} setup=${provider.setupClass} configured=${yesNo(provider.configured)} via=${provider.configuredVia ?? 'n/a'} models=${provider.models} routes=${provider.authRouteSummary} ${provider.detail ?? ''}`.trimEnd(),
450
473
  ),
451
474
  ].join('\n'));
452
475
  });
@@ -0,0 +1,22 @@
1
+ import type { ProviderAuthRouteDescriptor } from '@pellux/goodvibes-sdk/platform/providers/interface';
2
+
3
+ function routeUsable(route: ProviderAuthRouteDescriptor): boolean {
4
+ return route.usable ?? route.configured;
5
+ }
6
+
7
+ export function summarizeProviderAuthRoutes(routes: readonly ProviderAuthRouteDescriptor[] | undefined): string {
8
+ if (!routes?.length) return 'n/a';
9
+ const configured = routes.filter((route) => route.configured).length;
10
+ const usable = routes.filter(routeUsable).length;
11
+ return `${configured}/${routes.length} configured, ${usable}/${routes.length} usable`;
12
+ }
13
+
14
+ export function formatProviderAuthRoute(route: ProviderAuthRouteDescriptor): string {
15
+ const status = [
16
+ route.configured ? 'configured' : 'not configured',
17
+ routeUsable(route) ? 'usable' : 'not usable',
18
+ route.freshness,
19
+ ].filter((part): part is string => Boolean(part));
20
+ const detail = route.detail?.trim();
21
+ return `${route.label} [${route.route}; ${status.join(', ')}]${detail ? ` - ${detail}` : ''}`;
22
+ }
@@ -19,6 +19,7 @@ import type { OpsApi } from '@pellux/goodvibes-sdk/platform/runtime/ops-api';
19
19
  import type { OperatorClient } from '@pellux/goodvibes-sdk/platform/runtime/operator-client';
20
20
  import type { PeerClient } from '@pellux/goodvibes-sdk/platform/runtime/peer-client';
21
21
  import type { DirectTransport } from '@pellux/goodvibes-sdk/platform/runtime/transports/direct';
22
+ import type { VoiceProviderRegistry, VoiceService } from '@pellux/goodvibes-sdk/platform/voice/index';
22
23
  import type {
23
24
  CommandWorkspaceShellServices,
24
25
  } from '@pellux/goodvibes-sdk/platform/runtime/shell-command-workspace';
@@ -58,6 +59,8 @@ export interface CommandUiActions {
58
59
  print: (text: string) => void;
59
60
  exit: () => void;
60
61
  submitInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[]) => void;
62
+ submitSpokenInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[]) => void;
63
+ stopSpokenOutput?: () => void;
61
64
  executeCommand?: (name: string, args: string[]) => Promise<boolean>;
62
65
  cancelGeneration?: () => void;
63
66
  completeModelSelection?: (selection: {
@@ -76,6 +79,7 @@ export interface CommandShellUiOpeners {
76
79
  reloadSystemPrompt?: () => string;
77
80
  openOnboardingWizard?: (modeOrOptions?: OnboardingWizardMode | OpenOnboardingWizardOptions) => void;
78
81
  openModelPicker?: () => void;
82
+ openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => boolean;
79
83
  openProviderPicker?: () => void;
80
84
  openContextInspector?: () => void;
81
85
  openBookmarkModal?: () => void;
@@ -146,6 +150,8 @@ export interface CommandWorkspaceServices
146
150
  export interface CommandPlatformConfigServices {
147
151
  readonly config: DeepReadonly<GoodVibesConfig>;
148
152
  readonly configManager: ConfigManager;
153
+ readonly voiceProviderRegistry?: VoiceProviderRegistry;
154
+ readonly voiceService?: VoiceService;
149
155
  }
150
156
 
151
157
  export interface CommandPlatformServices
@@ -0,0 +1,334 @@
1
+ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config/schema';
2
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
+ import type { SelectionItem } from '../selection-modal.ts';
4
+
5
+ const TTS_CONFIG_KEYS = new Set(['provider', 'voice', 'llm-provider', 'llm-model']);
6
+
7
+ export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
8
+ registry.register({
9
+ name: 'tts',
10
+ description: 'Submit a normal prompt and play the assistant response through live TTS',
11
+ usage: '<prompt>|stop',
12
+ handler(args, ctx) {
13
+ const first = (args[0] ?? '').toLowerCase();
14
+ if (first === 'stop' || first === 'cancel') {
15
+ ctx.stopSpokenOutput?.();
16
+ ctx.print('Live TTS playback stopped.');
17
+ return;
18
+ }
19
+
20
+ const prompt = args.join(' ').trim();
21
+ if (!prompt) {
22
+ ctx.print('Usage: /tts <prompt> or /tts stop');
23
+ return;
24
+ }
25
+ if (!ctx.submitSpokenInput) {
26
+ ctx.print('Live TTS is not available in this runtime.');
27
+ return;
28
+ }
29
+ ctx.submitSpokenInput(prompt);
30
+ },
31
+ });
32
+
33
+ registry.register({
34
+ name: 'config-tts',
35
+ aliases: ['tts-config'],
36
+ description: 'Configure live TTS provider, voice, and optional spoken-turn LLM overrides',
37
+ usage: '[show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm|llm clear|llm-provider <id|clear>|llm-model <id|clear>]',
38
+ async handler(args, ctx) {
39
+ const sub = (args[0] ?? 'show').toLowerCase();
40
+ if (sub === 'show') {
41
+ if (args.length === 0 && openTtsConfigModal(ctx)) return;
42
+ ctx.print(formatTtsConfig(ctx));
43
+ return;
44
+ }
45
+ if (sub === 'providers') {
46
+ if (openTtsProviderPicker(ctx)) return;
47
+ printTtsProviders(ctx);
48
+ return;
49
+ }
50
+ if (sub === 'voices') {
51
+ if (await openTtsVoicePicker(ctx, args[1])) return;
52
+ await printTtsVoices(ctx, args[1]);
53
+ return;
54
+ }
55
+ if (sub === 'llm' || sub === 'model') {
56
+ const action = (args[1] ?? '').toLowerCase();
57
+ if (action === 'clear' || action === 'default') {
58
+ setTtsConfigValue(ctx, 'tts.llmProvider', '');
59
+ setTtsConfigValue(ctx, 'tts.llmModel', '');
60
+ ctx.print('TTS LLM override cleared. /tts will use the current chat model.');
61
+ return;
62
+ }
63
+ if (ctx.openModelPickerWithTarget?.('tts')) return;
64
+ ctx.print('TTS LLM picker is not available in this runtime. Use /config-tts llm-provider <id> and /config-tts llm-model <model>.');
65
+ return;
66
+ }
67
+ if (TTS_CONFIG_KEYS.has(sub)) {
68
+ const value = args.slice(1).join(' ').trim();
69
+ if (!value) {
70
+ ctx.print(`Usage: /config-tts ${sub} <value|clear>`);
71
+ return;
72
+ }
73
+ const key = ttsConfigKeyForSubcommand(sub);
74
+ const nextValue = value.toLowerCase() === 'clear' ? '' : value;
75
+ const previousProvider = key === 'tts.provider'
76
+ ? String(ctx.platform.configManager.get('tts.provider') ?? '').trim()
77
+ : '';
78
+ setTtsConfigValue(ctx, key, nextValue);
79
+ if (key === 'tts.provider' && previousProvider && previousProvider !== nextValue) {
80
+ setTtsConfigValue(ctx, 'tts.voice', '');
81
+ }
82
+ ctx.print(`${key} ${nextValue ? `set to ${nextValue}` : 'cleared'}.`);
83
+ return;
84
+ }
85
+ 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>]');
86
+ },
87
+ });
88
+ }
89
+
90
+ function ttsConfigKeyForSubcommand(subcommand: string): ConfigKey {
91
+ switch (subcommand) {
92
+ case 'provider': return 'tts.provider';
93
+ case 'voice': return 'tts.voice';
94
+ case 'llm-provider': return 'tts.llmProvider';
95
+ case 'llm-model': return 'tts.llmModel';
96
+ default: throw new Error(`Unknown TTS config key: ${subcommand}`);
97
+ }
98
+ }
99
+
100
+ function formatTtsConfig(ctx: CommandContext): string {
101
+ const cm = ctx.platform.configManager;
102
+ const llmProvider = String(cm.get('tts.llmProvider') ?? '').trim();
103
+ const llmModel = String(cm.get('tts.llmModel') ?? '').trim();
104
+ return [
105
+ 'TTS Configuration',
106
+ ` provider: ${formatValue(cm.get('tts.provider'))}`,
107
+ ` voice: ${formatValue(cm.get('tts.voice'))}`,
108
+ ` spoken-turn llm provider override: ${llmProvider || '(current chat provider)'}`,
109
+ ` spoken-turn llm model override: ${llmModel || '(current chat model)'}`,
110
+ ' playback: live streaming through local mpv or ffplay',
111
+ ' commands: /tts <prompt>, /tts stop, /config-tts, /config-tts providers, /config-tts voices [provider], /config-tts llm',
112
+ ].join('\n');
113
+ }
114
+
115
+ function openTtsConfigModal(ctx: CommandContext): boolean {
116
+ if (!ctx.openSelection) return false;
117
+ const cm = ctx.platform.configManager;
118
+ const provider = String(cm.get('tts.provider') ?? '').trim() || '(default)';
119
+ const voice = String(cm.get('tts.voice') ?? '').trim() || '(provider default)';
120
+ const llmProvider = String(cm.get('tts.llmProvider') ?? '').trim();
121
+ const llmModel = String(cm.get('tts.llmModel') ?? '').trim();
122
+ const llm = llmProvider || llmModel ? `${llmProvider || 'provider'} / ${llmModel || 'model'}` : '(current chat model)';
123
+ const items: SelectionItem[] = [
124
+ {
125
+ id: 'provider',
126
+ label: 'TTS provider',
127
+ detail: provider,
128
+ category: 'speech output',
129
+ primaryAction: 'select',
130
+ actions: '[Enter] choose',
131
+ },
132
+ {
133
+ id: 'voice',
134
+ label: 'TTS voice',
135
+ detail: voice,
136
+ category: 'speech output',
137
+ primaryAction: 'select',
138
+ actions: '[Enter] choose',
139
+ },
140
+ {
141
+ id: 'llm',
142
+ label: 'TTS response model override',
143
+ detail: llm,
144
+ category: 'response generation',
145
+ primaryAction: 'select',
146
+ actions: '[Enter] choose model',
147
+ },
148
+ {
149
+ id: 'clear-voice',
150
+ label: 'Use provider default voice',
151
+ detail: 'clears tts.voice',
152
+ category: 'clear values',
153
+ primaryAction: 'select',
154
+ },
155
+ {
156
+ id: 'clear-llm',
157
+ label: 'Use current chat model for /tts',
158
+ detail: 'clears tts.llmProvider and tts.llmModel',
159
+ category: 'clear values',
160
+ primaryAction: 'select',
161
+ },
162
+ ];
163
+
164
+ ctx.openSelection('TTS Configuration', items, { allowSearch: true }, (result) => {
165
+ if (!result) return;
166
+ if (result.item.id === 'provider') {
167
+ openTtsProviderPicker(ctx);
168
+ return;
169
+ }
170
+ if (result.item.id === 'voice') {
171
+ void openTtsVoicePicker(ctx);
172
+ return;
173
+ }
174
+ if (result.item.id === 'llm') {
175
+ if (!ctx.openModelPickerWithTarget?.('tts')) {
176
+ ctx.print('TTS LLM picker is not available in this runtime.');
177
+ }
178
+ return;
179
+ }
180
+ if (result.item.id === 'clear-voice') {
181
+ setTtsConfigValue(ctx, 'tts.voice', '');
182
+ ctx.print('TTS voice cleared. The provider default voice will be used.');
183
+ return;
184
+ }
185
+ if (result.item.id === 'clear-llm') {
186
+ setTtsConfigValue(ctx, 'tts.llmProvider', '');
187
+ setTtsConfigValue(ctx, 'tts.llmModel', '');
188
+ ctx.print('TTS LLM override cleared. /tts will use the current chat model.');
189
+ }
190
+ });
191
+ return true;
192
+ }
193
+
194
+ function getStreamingTtsProviders(ctx: CommandContext): Array<{ id: string; label: string; capabilities: readonly string[] }> {
195
+ const registry = ctx.platform.voiceProviderRegistry;
196
+ if (!registry) {
197
+ return [];
198
+ }
199
+ return registry.list().filter((provider) => provider.capabilities.includes('tts-stream'));
200
+ }
201
+
202
+ function openTtsProviderPicker(ctx: CommandContext): boolean {
203
+ if (!ctx.openSelection) return false;
204
+ const registry = ctx.platform.voiceProviderRegistry;
205
+ if (!registry) {
206
+ ctx.print('Voice provider registry is not available in this runtime.');
207
+ return true;
208
+ }
209
+ const providers = getStreamingTtsProviders(ctx);
210
+ if (providers.length === 0) {
211
+ ctx.print('No streaming TTS providers are registered.');
212
+ return true;
213
+ }
214
+ const current = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
215
+ const items: SelectionItem[] = providers.map((provider) => ({
216
+ id: provider.id,
217
+ label: provider.label,
218
+ detail: provider.id === current ? `${provider.id} (current)` : provider.id,
219
+ category: 'streaming TTS providers',
220
+ primaryAction: 'select',
221
+ actions: '[Enter] set provider',
222
+ }));
223
+ ctx.openSelection('Choose TTS Provider', items, { preSelectId: current, allowSearch: true }, (result) => {
224
+ if (!result) return;
225
+ const previous = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
226
+ setTtsConfigValue(ctx, 'tts.provider', result.item.id);
227
+ if (previous && previous !== result.item.id) {
228
+ setTtsConfigValue(ctx, 'tts.voice', '');
229
+ ctx.print(`TTS provider set to ${result.item.id}. TTS voice was cleared because voices are provider-specific.`);
230
+ } else {
231
+ ctx.print(`TTS provider set to ${result.item.id}.`);
232
+ }
233
+ });
234
+ return true;
235
+ }
236
+
237
+ function printTtsProviders(ctx: CommandContext): void {
238
+ const registry = ctx.platform.voiceProviderRegistry;
239
+ if (!registry) {
240
+ ctx.print('Voice provider registry is not available in this runtime.');
241
+ return;
242
+ }
243
+ const providers = getStreamingTtsProviders(ctx);
244
+ if (providers.length === 0) {
245
+ ctx.print('No streaming TTS providers are registered.');
246
+ return;
247
+ }
248
+ ctx.print([
249
+ 'Streaming TTS Providers',
250
+ ...providers.map((provider) => ` ${provider.id}: ${provider.label}`),
251
+ '',
252
+ 'Set provider: /config-tts provider <provider-id>',
253
+ ].join('\n'));
254
+ }
255
+
256
+ async function openTtsVoicePicker(ctx: CommandContext, providerArg?: string): Promise<boolean> {
257
+ if (!ctx.openSelection) return false;
258
+ const service = ctx.platform.voiceService;
259
+ if (!service) {
260
+ ctx.print('Voice service is not available in this runtime.');
261
+ return true;
262
+ }
263
+ const providerId = (providerArg ?? String(ctx.platform.configManager.get('tts.provider') ?? '')).trim() || undefined;
264
+ try {
265
+ const voices = await service.listVoices(providerId);
266
+ if (voices.length === 0) {
267
+ ctx.print(providerId ? `No voices returned for ${providerId}.` : 'No TTS voices returned.');
268
+ return true;
269
+ }
270
+ const current = String(ctx.platform.configManager.get('tts.voice') ?? '').trim();
271
+ const items: SelectionItem[] = [
272
+ {
273
+ id: '__default__',
274
+ label: 'Use provider default voice',
275
+ detail: current ? 'clears tts.voice' : '(current)',
276
+ category: 'voice',
277
+ primaryAction: 'select',
278
+ },
279
+ ...voices.map((voice) => ({
280
+ id: voice.id,
281
+ label: voice.label || voice.id,
282
+ detail: voice.id === current ? `${voice.id} (current)` : voice.id,
283
+ category: providerId ?? 'voices',
284
+ primaryAction: 'select' as const,
285
+ actions: '[Enter] set voice',
286
+ })),
287
+ ];
288
+ ctx.openSelection(`Choose TTS Voice${providerId ? ` (${providerId})` : ''}`, items, { preSelectId: current || '__default__', allowSearch: true }, (result) => {
289
+ if (!result) return;
290
+ const nextVoice = result.item.id === '__default__' ? '' : result.item.id;
291
+ setTtsConfigValue(ctx, 'tts.voice', nextVoice);
292
+ ctx.print(nextVoice ? `TTS voice set to ${nextVoice}.` : 'TTS voice cleared. The provider default voice will be used.');
293
+ });
294
+ return true;
295
+ } catch (error) {
296
+ ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
297
+ return true;
298
+ }
299
+ }
300
+
301
+ async function printTtsVoices(ctx: CommandContext, providerArg?: string): Promise<void> {
302
+ const service = ctx.platform.voiceService;
303
+ if (!service) {
304
+ ctx.print('Voice service is not available in this runtime.');
305
+ return;
306
+ }
307
+ const providerId = (providerArg ?? String(ctx.platform.configManager.get('tts.provider') ?? '')).trim() || undefined;
308
+ try {
309
+ const voices = await service.listVoices(providerId);
310
+ if (voices.length === 0) {
311
+ ctx.print(providerId ? `No voices returned for ${providerId}.` : 'No TTS voices returned.');
312
+ return;
313
+ }
314
+ ctx.print([
315
+ `TTS Voices${providerId ? ` (${providerId})` : ''}`,
316
+ ...voices.slice(0, 60).map((voice) => ` ${voice.id}: ${voice.label}`),
317
+ ...(voices.length > 60 ? [` ... ${voices.length - 60} more`] : []),
318
+ '',
319
+ 'Set voice: /config-tts voice <voice-id>',
320
+ 'Use provider default voice: /config-tts voice clear',
321
+ ].join('\n'));
322
+ } catch (error) {
323
+ ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
324
+ }
325
+ }
326
+
327
+ function setTtsConfigValue(ctx: CommandContext, key: ConfigKey, value: string): void {
328
+ ctx.platform.configManager.setDynamic(key, value);
329
+ }
330
+
331
+ function formatValue(value: unknown): string {
332
+ const text = String(value ?? '').trim();
333
+ return text || '(default)';
334
+ }
@@ -54,6 +54,7 @@ import { registerIntelligenceRuntimeCommands } from './commands/intelligence-run
54
54
  import { registerConversationRuntimeCommands } from './commands/conversation-runtime.ts';
55
55
  import { registerQrcodeRuntimeCommands } from './commands/qrcode-runtime.ts';
56
56
  import { registerOnboardingRuntimeCommands } from './commands/onboarding-runtime.ts';
57
+ import { registerTtsRuntimeCommands } from './commands/tts-runtime.ts';
57
58
 
58
59
  /**
59
60
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -102,6 +103,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
102
103
  registerConversationRuntimeCommands(registry);
103
104
  registerQrcodeRuntimeCommands(registry);
104
105
  registerOnboardingRuntimeCommands(registry);
106
+ registerTtsRuntimeCommands(registry);
105
107
  registerLocalRuntimeCommands(registry);
106
108
  registerSessionWorkflowCommands(registry);
107
109
  registerDiscoveryRuntimeCommands(registry);
@@ -145,6 +145,13 @@ function showOnboardingApplyFeedbackForHandler(handler: InputHandler, feedback:
145
145
  handler.requestRender();
146
146
  }
147
147
 
148
+ function continueOnboardingSection(handler: InputHandler): void {
149
+ handler.onboardingWizard.commitEdit();
150
+ handler.onboardingWizard.clearApplyFeedback();
151
+ handler.onboardingWizard.nextStep();
152
+ handler.requestRender();
153
+ }
154
+
148
155
  export function clearOnboardingPendingModelPickerTargetForHandler(handler: InputHandler): void {
149
156
  handler.onboardingWizard.clearPendingModelPickerTarget();
150
157
  }
@@ -215,6 +222,11 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
215
222
  await handler.handleOpenAiSubscriptionFinish();
216
223
  return;
217
224
  }
225
+ if (action === 'apply-and-continue') {
226
+ if (handler.onboardingApplyPending) return;
227
+ continueOnboardingSection(handler);
228
+ return;
229
+ }
218
230
  if (action !== 'apply') return;
219
231
  if (handler.onboardingApplyPending) return;
220
232
  const blockers = handler.onboardingWizard.getBlockingFieldLabels();
@@ -12,8 +12,9 @@ export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap';
12
12
  * 'main' → provider.provider + provider.model (default)
13
13
  * 'helper' → helper.globalProvider + helper.globalModel (+ helper.enabled: true)
14
14
  * 'tool' → tools.llmProvider + tools.llmModel (+ tools.llmEnabled: true)
15
+ * 'tts' → tts.llmProvider + tts.llmModel
15
16
  */
16
- export type ModelPickerTarget = 'main' | 'helper' | 'tool';
17
+ export type ModelPickerTarget = 'main' | 'helper' | 'tool' | 'tts';
17
18
 
18
19
  /**
19
20
  * Pricing tier filter.
@@ -8,7 +8,7 @@ import {
8
8
  } from './onboarding-wizard-external-surfaces.ts';
9
9
  import { countSelected, modelSelectionLabel, normalizeText } from './onboarding-wizard-helpers.ts';
10
10
  import type { OnboardingWizardController } from './onboarding-wizard.ts';
11
- import type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardRadioFieldDefinition, OnboardingWizardRadioOption, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
11
+ import type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardActionFieldDefinition, OnboardingWizardChecklistFieldDefinition, OnboardingWizardExternalSurfaceStepId, OnboardingWizardFieldDefinition, OnboardingWizardModelPickerFieldDefinition, OnboardingWizardRadioFieldDefinition, OnboardingWizardRadioOption, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
12
12
 
13
13
  export function buildOnboardingWizardSteps(controller: OnboardingWizardController): readonly OnboardingWizardStepDefinition[] {
14
14
  if (controller.hydrationPending || controller.hydrationError !== null) return [buildLoadingStep(controller)];
@@ -40,9 +40,32 @@ export function buildOnboardingWizardSteps(controller: OnboardingWizardControlle
40
40
  steps.push(buildExperienceStep(controller));
41
41
  steps.push(buildReviewStep(controller));
42
42
 
43
- return steps;
43
+ return steps.map(addApplyAndContinueAction);
44
44
  }
45
45
 
46
+ function buildApplyAndContinueAction(step: OnboardingWizardStepDefinition): OnboardingWizardActionFieldDefinition {
47
+ return {
48
+ kind: 'action',
49
+ id: `${step.id}.apply-and-continue`,
50
+ action: 'apply-and-continue',
51
+ label: 'Apply & Continue To Next Section',
52
+ hint: 'Persist the current wizard settings, verify them, and move to the next onboarding section.',
53
+ defaultValue: 'Apply & next',
54
+ spacerBeforeRows: 2,
55
+ };
56
+ }
57
+
58
+ function addApplyAndContinueAction(step: OnboardingWizardStepDefinition): OnboardingWizardStepDefinition {
59
+ if (step.id === 'loading' || step.id === 'review') return step;
60
+ return {
61
+ ...step,
62
+ fields: [
63
+ ...step.fields,
64
+ buildApplyAndContinueAction(step),
65
+ ],
66
+ };
67
+ }
68
+
46
69
  export function buildLoadingStep(controller: OnboardingWizardController): OnboardingWizardStepDefinition {
47
70
  const failed = controller.hydrationError !== null;
48
71
  return {
@@ -42,6 +42,7 @@ export type OnboardingWizardFieldKind =
42
42
 
43
43
  export type OnboardingWizardAction =
44
44
  | 'apply'
45
+ | 'apply-and-continue'
45
46
  | 'select-all-capabilities'
46
47
  | 'clear-capabilities'
47
48
  | 'select-all-external-surfaces'
@@ -75,6 +76,7 @@ interface OnboardingWizardFieldBase {
75
76
  readonly kind: OnboardingWizardFieldKind;
76
77
  readonly label: string;
77
78
  readonly hint: string;
79
+ readonly spacerBeforeRows?: number;
78
80
  }
79
81
 
80
82
  export interface OnboardingWizardChecklistFieldDefinition extends OnboardingWizardFieldBase {