@pellux/goodvibes-tui 0.19.32 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.33] — 2026-04-26
8
+
9
+ ### Changed
10
+ - `/config-tts` now opens an interactive TTS configuration modal instead of only printing provider and voice lists.
11
+ - `/config-tts providers` and `/config-tts voices [provider]` open selectable provider/voice pickers in the TUI and persist the selected SDK config keys.
12
+ - `/config-tts llm` opens the model picker for a separate `/tts` response-model override; `/tts` still defaults to the current chat provider/model when no override is configured.
13
+
14
+ ### Fixed
15
+ - `/tts` now honors configured `tts.llmProvider`/`tts.llmModel` for that spoken turn without changing the main chat model or global provider settings.
16
+ - Changing the TTS provider clears the provider-specific voice selection so a stale voice id is not reused against a different provider.
17
+
7
18
  ## [0.19.32] — 2026-04-25
8
19
 
9
20
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.32-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.33-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -1269,7 +1269,7 @@ Those pieces cover conversation-noise routing, panel-health/performance budgets,
1269
1269
  | `/notify [action]` | `/ntf` | Manage webhook notifications (ntfy.sh): add, remove, list, clear, test |
1270
1270
  | `/voice [action]` | — | Review optional voice posture and export/inspect voice bundles |
1271
1271
  | `/tts <prompt>` | — | Submit a normal prompt and play the assistant response through live TTS |
1272
- | `/config-tts [action]` | `/tts-config` | Configure TTS provider, voice, and optional spoken-turn LLM overrides |
1272
+ | `/config-tts [action]` | `/tts-config` | Open TTS configuration for provider, voice, and optional `/tts` response-model override |
1273
1273
  | `/diff [target]` | `/d` | Show unified diff: session, head, working, staged, or a git ref |
1274
1274
  | `/mcp [tools]` | — | List connected MCP servers and their tools |
1275
1275
  | `/help [command]` | `/h`, `/?` | Show available commands and keyboard shortcuts |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.32",
3
+ "version": "0.19.33",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -0,0 +1,117 @@
1
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
2
+ import type { ModelDefinition, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers/registry';
3
+ import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers/interface';
4
+ import type { Orchestrator, OrchestratorUserInputOptions } from '../core/orchestrator.ts';
5
+
6
+ const SPOKEN_TURN_SOURCE = 'tts';
7
+
8
+ type RunTurn = (
9
+ text: string,
10
+ content?: ContentPart[],
11
+ options?: OrchestratorUserInputOptions,
12
+ ) => Promise<void>;
13
+
14
+ type PatchableOrchestrator = {
15
+ runTurn?: RunTurn;
16
+ setCoreServices: (services: { providerRegistry?: ProviderRegistry }) => void;
17
+ };
18
+
19
+ export interface SpokenTurnModelRoutingOptions {
20
+ readonly orchestrator: Orchestrator;
21
+ readonly providerRegistry: ProviderRegistry;
22
+ readonly configManager: Pick<ConfigManager, 'get'>;
23
+ readonly notify?: (message: string) => void;
24
+ }
25
+
26
+ export function createSpokenTurnInputOptions(): OrchestratorUserInputOptions {
27
+ return {
28
+ origin: {
29
+ source: SPOKEN_TURN_SOURCE,
30
+ surface: 'tui',
31
+ metadata: { spokenOutput: true },
32
+ },
33
+ };
34
+ }
35
+
36
+ export function attachSpokenTurnModelRouting(options: SpokenTurnModelRoutingOptions): () => void {
37
+ const target = options.orchestrator as unknown as PatchableOrchestrator;
38
+ const originalRunTurn = target.runTurn?.bind(options.orchestrator);
39
+ if (!originalRunTurn) return () => {};
40
+
41
+ target.runTurn = async (text, content, inputOptions) => {
42
+ if (!isSpokenTurn(inputOptions)) {
43
+ await originalRunTurn(text, content, inputOptions);
44
+ return;
45
+ }
46
+
47
+ const override = resolveSpokenTurnModelOverride({
48
+ providerRegistry: options.providerRegistry,
49
+ configManager: options.configManager,
50
+ notify: options.notify,
51
+ });
52
+ if (!override) {
53
+ await originalRunTurn(text, content, inputOptions);
54
+ return;
55
+ }
56
+
57
+ const routedRegistry = createRoutedProviderRegistry(options.providerRegistry, override);
58
+ target.setCoreServices({ providerRegistry: routedRegistry });
59
+ try {
60
+ await originalRunTurn(text, content, inputOptions);
61
+ } finally {
62
+ target.setCoreServices({ providerRegistry: options.providerRegistry });
63
+ }
64
+ };
65
+
66
+ return () => {
67
+ target.runTurn = originalRunTurn;
68
+ target.setCoreServices({ providerRegistry: options.providerRegistry });
69
+ };
70
+ }
71
+
72
+ export function resolveSpokenTurnModelOverride(options: {
73
+ readonly providerRegistry: Pick<ProviderRegistry, 'listModels' | 'getCurrentModel'>;
74
+ readonly configManager: Pick<ConfigManager, 'get'>;
75
+ readonly notify?: (message: string) => void;
76
+ }): ModelDefinition | null {
77
+ const modelRef = readConfigString(options.configManager, 'tts.llmModel');
78
+ if (!modelRef) return null;
79
+
80
+ const providerId = readConfigString(options.configManager, 'tts.llmProvider');
81
+ const current = options.providerRegistry.getCurrentModel();
82
+ const model = options.providerRegistry.listModels().find((candidate) => {
83
+ const refMatches = candidate.registryKey === modelRef || candidate.id === modelRef;
84
+ const providerMatches = !providerId || candidate.provider === providerId;
85
+ return refMatches && providerMatches;
86
+ });
87
+
88
+ if (!model) {
89
+ options.notify?.(`[TTS] Configured TTS LLM '${modelRef}' was not found; using current chat model.`);
90
+ return null;
91
+ }
92
+ if (model.selectable === false) {
93
+ options.notify?.(`[TTS] Configured TTS LLM '${modelRef}' is not selectable; using current chat model.`);
94
+ return null;
95
+ }
96
+ if ((model.registryKey ?? model.id) === (current.registryKey ?? current.id)) return null;
97
+ return model;
98
+ }
99
+
100
+ function isSpokenTurn(options: OrchestratorUserInputOptions | undefined): boolean {
101
+ return options?.origin?.source === SPOKEN_TURN_SOURCE
102
+ || options?.origin?.metadata?.['spokenOutput'] === true;
103
+ }
104
+
105
+ function createRoutedProviderRegistry(providerRegistry: ProviderRegistry, model: ModelDefinition): ProviderRegistry {
106
+ return new Proxy(providerRegistry, {
107
+ get(target, prop, receiver) {
108
+ if (prop === 'getCurrentModel') return () => model;
109
+ const value = Reflect.get(target, prop, receiver);
110
+ return typeof value === 'function' ? value.bind(target) : value;
111
+ },
112
+ });
113
+ }
114
+
115
+ function readConfigString(configManager: Pick<ConfigManager, 'get'>, key: 'tts.llmProvider' | 'tts.llmModel'): string {
116
+ return String(configManager.get(key) ?? '').trim();
117
+ }
@@ -79,6 +79,7 @@ export interface CommandShellUiOpeners {
79
79
  reloadSystemPrompt?: () => string;
80
80
  openOnboardingWizard?: (modeOrOptions?: OnboardingWizardMode | OpenOnboardingWizardOptions) => void;
81
81
  openModelPicker?: () => void;
82
+ openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => boolean;
82
83
  openProviderPicker?: () => void;
83
84
  openContextInspector?: () => void;
84
85
  openBookmarkModal?: () => void;
@@ -1,5 +1,6 @@
1
1
  import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config/schema';
2
2
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
+ import type { SelectionItem } from '../selection-modal.ts';
3
4
 
4
5
  const TTS_CONFIG_KEYS = new Set(['provider', 'voice', 'llm-provider', 'llm-model']);
5
6
 
@@ -33,21 +34,36 @@ export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
33
34
  name: 'config-tts',
34
35
  aliases: ['tts-config'],
35
36
  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>]',
37
+ usage: '[show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm|llm clear|llm-provider <id|clear>|llm-model <id|clear>]',
37
38
  async handler(args, ctx) {
38
39
  const sub = (args[0] ?? 'show').toLowerCase();
39
40
  if (sub === 'show') {
41
+ if (args.length === 0 && openTtsConfigModal(ctx)) return;
40
42
  ctx.print(formatTtsConfig(ctx));
41
43
  return;
42
44
  }
43
45
  if (sub === 'providers') {
44
- await printTtsProviders(ctx);
46
+ if (openTtsProviderPicker(ctx)) return;
47
+ printTtsProviders(ctx);
45
48
  return;
46
49
  }
47
50
  if (sub === 'voices') {
51
+ if (await openTtsVoicePicker(ctx, args[1])) return;
48
52
  await printTtsVoices(ctx, args[1]);
49
53
  return;
50
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
+ }
51
67
  if (TTS_CONFIG_KEYS.has(sub)) {
52
68
  const value = args.slice(1).join(' ').trim();
53
69
  if (!value) {
@@ -56,11 +72,17 @@ export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
56
72
  }
57
73
  const key = ttsConfigKeyForSubcommand(sub);
58
74
  const nextValue = value.toLowerCase() === 'clear' ? '' : value;
59
- ctx.platform.configManager.setDynamic(key, nextValue);
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
+ }
60
82
  ctx.print(`${key} ${nextValue ? `set to ${nextValue}` : 'cleared'}.`);
61
83
  return;
62
84
  }
63
- ctx.print('Usage: /config-tts [show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm-provider <id|clear>|llm-model <id|clear>]');
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>]');
64
86
  },
65
87
  });
66
88
  }
@@ -86,17 +108,139 @@ function formatTtsConfig(ctx: CommandContext): string {
86
108
  ` spoken-turn llm provider override: ${llmProvider || '(current chat provider)'}`,
87
109
  ` spoken-turn llm model override: ${llmModel || '(current chat model)'}`,
88
110
  ' playback: live streaming through local mpv or ffplay',
89
- ' commands: /tts <prompt>, /tts stop, /config-tts providers, /config-tts voices [provider]',
111
+ ' commands: /tts <prompt>, /tts stop, /config-tts, /config-tts providers, /config-tts voices [provider], /config-tts llm',
90
112
  ].join('\n');
91
113
  }
92
114
 
93
- async function printTtsProviders(ctx: CommandContext): Promise<void> {
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 {
94
238
  const registry = ctx.platform.voiceProviderRegistry;
95
239
  if (!registry) {
96
240
  ctx.print('Voice provider registry is not available in this runtime.');
97
241
  return;
98
242
  }
99
- const providers = registry.list().filter((provider) => provider.capabilities.includes('tts-stream'));
243
+ const providers = getStreamingTtsProviders(ctx);
100
244
  if (providers.length === 0) {
101
245
  ctx.print('No streaming TTS providers are registered.');
102
246
  return;
@@ -104,9 +248,56 @@ async function printTtsProviders(ctx: CommandContext): Promise<void> {
104
248
  ctx.print([
105
249
  'Streaming TTS Providers',
106
250
  ...providers.map((provider) => ` ${provider.id}: ${provider.label}`),
251
+ '',
252
+ 'Set provider: /config-tts provider <provider-id>',
107
253
  ].join('\n'));
108
254
  }
109
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
+
110
301
  async function printTtsVoices(ctx: CommandContext, providerArg?: string): Promise<void> {
111
302
  const service = ctx.platform.voiceService;
112
303
  if (!service) {
@@ -124,12 +315,19 @@ async function printTtsVoices(ctx: CommandContext, providerArg?: string): Promis
124
315
  `TTS Voices${providerId ? ` (${providerId})` : ''}`,
125
316
  ...voices.slice(0, 60).map((voice) => ` ${voice.id}: ${voice.label}`),
126
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',
127
321
  ].join('\n'));
128
322
  } catch (error) {
129
323
  ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
130
324
  }
131
325
  }
132
326
 
327
+ function setTtsConfigValue(ctx: CommandContext, key: ConfigKey, value: string): void {
328
+ ctx.platform.configManager.setDynamic(key, value);
329
+ }
330
+
133
331
  function formatValue(value: unknown): string {
134
332
  const text = String(value ?? '').trim();
135
333
  return text || '(default)';
@@ -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.
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 {
@@ -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) {
@@ -111,6 +111,14 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
111
111
  return new Set(results.filter((v): v is string => v !== null));
112
112
  }
113
113
 
114
+ const getCurrentModelForPickerTarget = (): string => {
115
+ const target = input.modelPicker.target;
116
+ if (target === 'helper') return String(configManager.get('helper.globalModel') || runtime.model);
117
+ if (target === 'tool') return String(configManager.get('tools.llmModel') || runtime.model);
118
+ if (target === 'tts') return String(configManager.get('tts.llmModel') || runtime.model);
119
+ return runtime.model;
120
+ };
121
+
114
122
  commandContext.openModelPicker = () => {
115
123
  void (async () => {
116
124
  const models = providerRegistry.getSelectableModels();
@@ -124,7 +132,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
124
132
  });
125
133
  void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
126
134
  input.modalOpened('modelPicker');
127
- input.modelPicker.openAllModels(models, runtime.model);
135
+ input.modelPicker.openAllModels(models, getCurrentModelForPickerTarget());
128
136
  render();
129
137
  })().catch((error: unknown) => {
130
138
  commandContext.print?.(`Model picker failed to open: ${error instanceof Error ? error.message : String(error)}`);
@@ -132,6 +140,8 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
132
140
  });
133
141
  };
134
142
 
143
+ commandContext.openModelPickerWithTarget = (target) => input.openModelPickerWithTarget(target);
144
+
135
145
  commandContext.openProviderPicker = () => {
136
146
  void (async () => {
137
147
  const providers = [...new Set(providerRegistry.listModels().map((model) => model.provider))];
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.32';
9
+ let _version = '0.19.33';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;