@pellux/goodvibes-tui 0.19.53 → 0.19.55

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 (48) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +10 -13
  3. package/docs/foundation-artifacts/knowledge-store.sql +27 -0
  4. package/docs/foundation-artifacts/operator-contract.json +15736 -7265
  5. package/package.json +2 -2
  6. package/src/audio/spoken-turn-controller.ts +4 -1
  7. package/src/input/command-args-hint.ts +36 -0
  8. package/src/input/command-registry.ts +3 -1
  9. package/src/input/commands/config.ts +7 -521
  10. package/src/input/commands/knowledge.ts +111 -1
  11. package/src/input/commands/local-runtime.ts +0 -80
  12. package/src/input/commands/operator-runtime.ts +3 -3
  13. package/src/input/commands/planning-runtime.ts +83 -34
  14. package/src/input/commands/shell-core.ts +2 -34
  15. package/src/input/commands/tts-runtime.ts +1 -389
  16. package/src/input/commands.ts +0 -2
  17. package/src/input/handler-modal-routes.ts +61 -7
  18. package/src/input/handler-modal-token-routes.ts +1 -0
  19. package/src/input/handler-picker-routes.ts +50 -4
  20. package/src/input/model-picker-provider-filter.ts +28 -0
  21. package/src/input/model-picker-types.ts +12 -0
  22. package/src/input/model-picker.ts +65 -23
  23. package/src/input/selection-modal.ts +1 -1
  24. package/src/input/settings-modal-behavior.ts +2 -0
  25. package/src/input/settings-modal-subscriptions.ts +95 -0
  26. package/src/input/settings-modal-types.ts +50 -3
  27. package/src/input/settings-modal.ts +106 -134
  28. package/src/input/tts-settings-actions.ts +100 -0
  29. package/src/main.ts +50 -45
  30. package/src/panels/builtin/agent.ts +15 -0
  31. package/src/panels/builtin/shared.ts +17 -0
  32. package/src/panels/project-planning-panel.ts +370 -0
  33. package/src/planning/project-planning-coordinator.ts +249 -0
  34. package/src/renderer/compositor.ts +2 -1
  35. package/src/renderer/conversation-overlays.ts +4 -5
  36. package/src/renderer/model-workspace.ts +488 -0
  37. package/src/renderer/settings-modal-helpers.ts +16 -1
  38. package/src/renderer/settings-modal.ts +616 -716
  39. package/src/runtime/bootstrap-command-context.ts +6 -0
  40. package/src/runtime/bootstrap-command-parts.ts +5 -0
  41. package/src/runtime/bootstrap-shell.ts +2 -0
  42. package/src/runtime/services.ts +33 -2
  43. package/src/runtime/terminal-output-guard.ts +228 -0
  44. package/src/runtime/ui-services.ts +4 -0
  45. package/src/shell/ui-openers.ts +59 -3
  46. package/src/utils/clipboard.ts +2 -1
  47. package/src/version.ts +1 -1
  48. package/src/input/commands/permissions-runtime.ts +0 -104
@@ -1,9 +1,4 @@
1
- import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config/schema';
2
- import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers/registry';
3
- import type { CommandContext, CommandRegistry } from '../command-registry.ts';
4
- import type { SelectionItem } from '../selection-modal.ts';
5
-
6
- const TTS_CONFIG_KEYS = new Set(['provider', 'voice', 'llm-provider', 'llm-model']);
1
+ import type { CommandRegistry } from '../command-registry.ts';
7
2
 
8
3
  export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
9
4
  registry.register({
@@ -31,387 +26,4 @@ export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
31
26
  },
32
27
  });
33
28
 
34
- registry.register({
35
- name: 'config-tts',
36
- aliases: ['tts-config'],
37
- description: 'Configure live TTS provider, voice, and optional spoken-turn LLM overrides',
38
- usage: '[show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm|llm clear|llm-provider <id|clear>|llm-model <id|clear>]',
39
- async handler(args, ctx) {
40
- const sub = (args[0] ?? 'show').toLowerCase();
41
- if (sub === 'show') {
42
- if (args.length === 0 && openTtsConfigModal(ctx)) return;
43
- ctx.print(formatTtsConfig(ctx));
44
- return;
45
- }
46
- if (sub === 'providers') {
47
- if (openTtsProviderPicker(ctx)) return;
48
- printTtsProviders(ctx);
49
- return;
50
- }
51
- if (sub === 'voices') {
52
- if (await openTtsVoicePicker(ctx, args[1])) return;
53
- await printTtsVoices(ctx, args[1]);
54
- return;
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
- }
68
- if (TTS_CONFIG_KEYS.has(sub)) {
69
- const value = args.slice(1).join(' ').trim();
70
- if (!value) {
71
- if ((sub === 'llm-provider' || sub === 'llm-model') && openTtsLlmPicker(ctx)) return;
72
- ctx.print(`Usage: /config-tts ${sub} <value|clear>`);
73
- return;
74
- }
75
- const key = ttsConfigKeyForSubcommand(sub);
76
- const nextValue = value.toLowerCase() === 'clear' ? '' : value;
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
- }
92
- ctx.print(`${key} ${nextValue ? `set to ${nextValue}` : 'cleared'}.`);
93
- return;
94
- }
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>]');
96
- },
97
- });
98
- }
99
-
100
- function ttsConfigKeyForSubcommand(subcommand: string): ConfigKey {
101
- switch (subcommand) {
102
- case 'provider': return 'tts.provider';
103
- case 'voice': return 'tts.voice';
104
- case 'llm-provider': return 'tts.llmProvider';
105
- case 'llm-model': return 'tts.llmModel';
106
- default: throw new Error(`Unknown TTS config key: ${subcommand}`);
107
- }
108
- }
109
-
110
- function formatTtsConfig(ctx: CommandContext): string {
111
- const cm = ctx.platform.configManager;
112
- const llmProvider = String(cm.get('tts.llmProvider') ?? '').trim();
113
- const llmModel = String(cm.get('tts.llmModel') ?? '').trim();
114
- return [
115
- 'TTS Configuration',
116
- ` provider: ${formatValue(cm.get('tts.provider'))}`,
117
- ` voice: ${formatValue(cm.get('tts.voice'))}`,
118
- ` spoken-turn llm provider override: ${llmProvider || '(current chat provider)'}`,
119
- ` spoken-turn llm model override: ${llmModel || '(current chat model)'}`,
120
- ' playback: live streaming through local mpv or ffplay',
121
- ' commands: /tts <prompt>, /tts stop, /config-tts, /config-tts providers, /config-tts voices [provider], /config-tts llm',
122
- ].join('\n');
123
- }
124
-
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 {
264
- const registry = ctx.platform.voiceProviderRegistry;
265
- if (!registry) {
266
- ctx.print('Voice provider registry is not available in this runtime.');
267
- return;
268
- }
269
- const providers = getStreamingTtsProviders(ctx);
270
- if (providers.length === 0) {
271
- ctx.print('No streaming TTS providers are registered.');
272
- return;
273
- }
274
- ctx.print([
275
- 'Streaming TTS Providers',
276
- ...providers.map((provider) => ` ${provider.id}: ${provider.label}`),
277
- '',
278
- 'Set provider: /config-tts provider <provider-id>',
279
- ].join('\n'));
280
- }
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
-
384
- async function printTtsVoices(ctx: CommandContext, providerArg?: string): Promise<void> {
385
- const service = ctx.platform.voiceService;
386
- if (!service) {
387
- ctx.print('Voice service is not available in this runtime.');
388
- return;
389
- }
390
- const providerId = (providerArg ?? String(ctx.platform.configManager.get('tts.provider') ?? '')).trim() || undefined;
391
- try {
392
- const voices = await service.listVoices(providerId);
393
- if (voices.length === 0) {
394
- ctx.print(providerId ? `No voices returned for ${providerId}.` : 'No TTS voices returned.');
395
- return;
396
- }
397
- ctx.print([
398
- `TTS Voices${providerId ? ` (${providerId})` : ''}`,
399
- ...voices.slice(0, 60).map((voice) => ` ${voice.id}: ${voice.label}`),
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',
404
- ].join('\n'));
405
- } catch (error) {
406
- ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
407
- }
408
- }
409
-
410
- function setTtsConfigValue(ctx: CommandContext, key: ConfigKey, value: string): void {
411
- ctx.platform.configManager.setDynamic(key, value);
412
- }
413
-
414
- function formatValue(value: unknown): string {
415
- const text = String(value ?? '').trim();
416
- return text || '(default)';
417
29
  }
@@ -43,7 +43,6 @@ import { registerMemoryProductRuntimeCommands } from './commands/memory-product-
43
43
  import { registerSkillsRuntimeCommands } from './commands/skills-runtime.ts';
44
44
  import { registerServicesRuntimeCommands } from './commands/services-runtime.ts';
45
45
  import { registerTasksRuntimeCommands } from './commands/tasks-runtime.ts';
46
- import { registerPermissionsRuntimeCommands } from './commands/permissions-runtime.ts';
47
46
  import { registerLocalProviderRuntimeCommands } from './commands/local-provider-runtime.ts';
48
47
  import { registerHealthRuntimeCommands } from './commands/health-runtime.ts';
49
48
  import { registerSettingsSyncRuntimeCommands } from './commands/settings-sync-runtime.ts';
@@ -93,7 +92,6 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
93
92
  registerExperienceRuntimeCommands(registry);
94
93
  registerServicesRuntimeCommands(registry);
95
94
  registerTasksRuntimeCommands(registry);
96
- registerPermissionsRuntimeCommands(registry);
97
95
  registerLocalProviderRuntimeCommands(registry);
98
96
  registerHealthRuntimeCommands(registry);
99
97
  registerSettingsSyncRuntimeCommands(registry);
@@ -1,6 +1,7 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
2
2
  import type { SelectionResult, SelectionAction } from './selection-modal.ts';
3
3
  import type { CommandContext } from './command-registry.ts';
4
+ import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
4
5
 
5
6
  type SelectionRouteState = {
6
7
  selectionModal: {
@@ -210,18 +211,28 @@ type SettingsRouteState = {
210
211
  active: boolean;
211
212
  editingMode: boolean;
212
213
  currentCategory: string;
214
+ focusPane?: 'categories' | 'settings';
213
215
  commitEdit: () => void;
214
216
  toggleSelectedFlag: () => void;
215
217
  activateSelected: () => void;
216
218
  adjustSelected: (direction: 'left' | 'right', step?: number) => void;
217
- moveUp: () => void;
218
- moveDown: () => void;
219
+ moveFocusedUp?: () => void;
220
+ moveFocusedDown?: () => void;
221
+ moveUp?: () => void;
222
+ moveDown?: () => void;
223
+ focusCategories?: () => void;
224
+ focusSettings?: () => void;
225
+ toggleFocusPane?: () => void;
219
226
  nextCategory: () => void;
227
+ prevCategory?: () => void;
220
228
  editBackspace: () => void;
221
229
  editChar: (char: string) => void;
222
230
  pendingModelPickerTarget: import('./model-picker.ts').ModelPickerTarget | null;
223
231
  pendingProviderModelPickerTarget?: import('./model-picker.ts').ModelPickerTarget | null;
232
+ pendingSettingsPickerAction?: 'tts-provider' | 'tts-voice' | null;
233
+ resetSelected?: () => { key: string; value: unknown } | null;
224
234
  };
235
+ commandContext?: CommandContext;
225
236
  /** Called when the settings modal requests the model picker for a non-main target. */
226
237
  openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => void;
227
238
  /** Called when the settings modal requests provider selection before model selection. */
@@ -230,7 +241,29 @@ type SettingsRouteState = {
230
241
  handleEscape: () => void;
231
242
  };
232
243
 
244
+ function syncRuntimeAfterSettingReset(ctx: CommandContext | undefined, key: string, value: unknown): void {
245
+ if (!ctx) return;
246
+ if (key === 'provider.model') ctx.session.runtime.model = String(value);
247
+ if (key === 'provider.provider') ctx.session.runtime.provider = String(value);
248
+ if (key === 'provider.reasoningEffort') ctx.session.runtime.reasoningEffort = String(value);
249
+ }
250
+
233
251
  function consumeSettingsPickerRequest(state: SettingsRouteState): void {
252
+ const settingsAction = state.settingsModal.pendingSettingsPickerAction ?? null;
253
+ if (settingsAction !== null) {
254
+ state.settingsModal.pendingSettingsPickerAction = null;
255
+ if (!state.commandContext) return;
256
+ if (settingsAction === 'tts-provider') {
257
+ openTtsProviderPicker(state.commandContext);
258
+ return;
259
+ }
260
+ void openTtsVoicePicker(state.commandContext).catch((error: unknown) => {
261
+ state.commandContext?.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
262
+ state.requestRender();
263
+ });
264
+ return;
265
+ }
266
+
234
267
  const providerModelTarget = state.settingsModal.pendingProviderModelPickerTarget ?? null;
235
268
  if (providerModelTarget !== null) {
236
269
  state.settingsModal.pendingProviderModelPickerTarget = null;
@@ -248,32 +281,53 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
248
281
  if (!state.settingsModal.active) return false;
249
282
 
250
283
  if (token.type === 'key') {
284
+ const focusPane = state.settingsModal.focusPane ?? 'settings';
251
285
  if (token.logicalName === 'escape') {
252
286
  state.handleEscape();
253
287
  return true;
254
288
  }
255
289
  if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode)) {
256
290
  if (state.settingsModal.editingMode) state.settingsModal.commitEdit();
291
+ else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
257
292
  else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
258
293
  else {
259
294
  state.settingsModal.activateSelected();
260
295
  consumeSettingsPickerRequest(state);
261
296
  }
262
297
  } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode) {
263
- state.settingsModal.adjustSelected(token.logicalName, token.shift ? 10 : 1);
264
- } else if (token.logicalName === 'up') state.settingsModal.moveUp();
265
- else if (token.logicalName === 'down') state.settingsModal.moveDown();
266
- else if (token.logicalName === 'tab') state.settingsModal.nextCategory();
298
+ if (token.logicalName === 'left') state.settingsModal.focusCategories?.();
299
+ else state.settingsModal.focusSettings?.();
300
+ } else if (token.logicalName === 'up') {
301
+ if (state.settingsModal.moveFocusedUp) state.settingsModal.moveFocusedUp();
302
+ else state.settingsModal.moveUp?.();
303
+ } else if (token.logicalName === 'down') {
304
+ if (state.settingsModal.moveFocusedDown) state.settingsModal.moveFocusedDown();
305
+ else state.settingsModal.moveDown?.();
306
+ }
307
+ else if (token.logicalName === 'r' && !state.settingsModal.editingMode) {
308
+ const reset = state.settingsModal.resetSelected?.();
309
+ if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
310
+ }
311
+ else if (token.logicalName === 'tab') {
312
+ if (state.settingsModal.toggleFocusPane) state.settingsModal.toggleFocusPane();
313
+ else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
314
+ else state.settingsModal.focusCategories?.();
315
+ }
267
316
  else if (token.logicalName === 'backspace' && state.settingsModal.editingMode) state.settingsModal.editBackspace();
268
317
  } else if (token.type === 'text') {
269
318
  if (token.value === ' ' && !state.settingsModal.editingMode) {
270
- if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
319
+ const focusPane = state.settingsModal.focusPane ?? 'settings';
320
+ if (focusPane === 'categories') state.settingsModal.focusSettings?.();
321
+ else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
271
322
  else {
272
323
  state.settingsModal.activateSelected();
273
324
  consumeSettingsPickerRequest(state);
274
325
  }
275
326
  } else if (state.settingsModal.editingMode) {
276
327
  state.settingsModal.editChar(token.value);
328
+ } else if (token.value === 'r') {
329
+ const reset = state.settingsModal.resetSelected?.();
330
+ if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
277
331
  }
278
332
  }
279
333
 
@@ -137,6 +137,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
137
137
 
138
138
  if (handleSettingsModalToken({
139
139
  settingsModal: state.settingsModal,
140
+ commandContext: state.commandContext,
140
141
  openModelPickerWithTarget: state.openModelPickerWithTarget,
141
142
  openProviderModelPickerWithTarget: state.openProviderModelPickerWithTarget,
142
143
  requestRender: state.requestRender,
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
3
3
  import type { CommandContext } from './command-registry.ts';
4
- import type { CategoryFilter, ModelPickerModal } from './model-picker.ts';
4
+ import type { CapabilityFilter, CategoryFilter, ModelPickerModal } from './model-picker.ts';
5
5
  import { MODEL_PICKER_CHROME_LINES } from '../renderer/model-picker-overlay.ts';
6
6
  import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils/path-safety';
7
7
  import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
@@ -49,6 +49,11 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
49
49
  if (state.modelPicker.mode === 'contextCap') state.modelPicker.deleteContextCapChar();
50
50
  else if (state.modelPicker.searchFocused && (state.modelPicker.mode === 'model' || state.modelPicker.mode === 'provider')) state.modelPicker.deleteChar();
51
51
  } else if (token.logicalName === 'enter') {
52
+ if (state.modelPicker.focusPane === 'targets') {
53
+ state.modelPicker.focusItems();
54
+ state.requestRender();
55
+ return true;
56
+ }
52
57
  const mode = state.modelPicker.mode;
53
58
  const idx = state.modelPicker.selectedIndex;
54
59
  if (mode === 'model') {
@@ -102,6 +107,11 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
102
107
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
103
108
  }
104
109
  } else if (token.logicalName === 'up') {
110
+ if (state.modelPicker.focusPane === 'targets') {
111
+ state.modelPicker.moveTarget(-1);
112
+ state.requestRender();
113
+ return true;
114
+ }
105
115
  if (state.modelPicker.canFocusSearch() && !state.modelPicker.searchFocused && state.modelPicker.selectedIndex === 0) {
106
116
  state.modelPicker.focusSearch();
107
117
  } else if (!state.modelPicker.searchFocused) {
@@ -109,19 +119,39 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
109
119
  state.modelPicker.moveUp(maxVis);
110
120
  }
111
121
  } else if (token.logicalName === 'down') {
122
+ if (state.modelPicker.focusPane === 'targets') {
123
+ state.modelPicker.moveTarget(1);
124
+ state.requestRender();
125
+ return true;
126
+ }
112
127
  if (state.modelPicker.searchFocused) {
113
128
  state.modelPicker.blurSearch();
114
129
  } else {
115
130
  const maxVis = Math.max(5, state.getViewportHeight() - MODEL_PICKER_CHROME_LINES - 4);
116
131
  state.modelPicker.moveDown(maxVis);
117
132
  }
133
+ } else if (token.logicalName === 'left' && !state.modelPicker.searchFocused && state.modelPicker.mode !== 'contextCap') {
134
+ state.modelPicker.focusTargets();
135
+ } else if (token.logicalName === 'right' && !state.modelPicker.searchFocused && state.modelPicker.mode !== 'contextCap') {
136
+ state.modelPicker.focusItems();
118
137
  } else if (token.logicalName === 'tab' && state.modelPicker.mode === 'model') {
119
- const cycle: CategoryFilter[] = ['all', 'free', 'paid', 'subscription'];
120
- const cur = cycle.indexOf(state.modelPicker.categoryFilter);
121
- state.modelPicker.setCategoryFilter(cycle[(cur + 1) % cycle.length]!);
138
+ if (state.modelPicker.focusPane === 'targets') {
139
+ state.modelPicker.focusItems();
140
+ } else {
141
+ const cycle: CategoryFilter[] = ['all', 'free', 'paid', 'subscription'];
142
+ const cur = cycle.indexOf(state.modelPicker.categoryFilter);
143
+ state.modelPicker.setCategoryFilter(cycle[(cur + 1) % cycle.length]!);
144
+ }
122
145
  } else if (!state.modelPicker.searchFocused && token.logicalName === 'g' && state.modelPicker.mode === 'model') {
123
146
  state.modelPicker.cycleGroupBy();
147
+ } else if (!state.modelPicker.searchFocused && token.logicalName === 'c' && state.modelPicker.mode === 'model') {
148
+ cycleCapabilityFilter(state.modelPicker);
149
+ } else if (!state.modelPicker.searchFocused && token.logicalName === 'a' && state.modelPicker.mode === 'model') {
150
+ state.modelPicker.toggleAvailableOnly();
151
+ } else if (!state.modelPicker.searchFocused && token.logicalName === 'b' && state.modelPicker.mode === 'model') {
152
+ state.modelPicker.cycleBenchmarkSort();
124
153
  } else if (!state.modelPicker.searchFocused && token.logicalName === '/' && state.modelPicker.canFocusSearch()) {
154
+ state.modelPicker.focusItems();
125
155
  state.modelPicker.focusSearch();
126
156
  }
127
157
  } else if (token.type === 'text') {
@@ -139,9 +169,19 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
139
169
  } else if (token.value === ' ' && state.modelPicker.mode === 'model') {
140
170
  const selected = state.modelPicker.getSelected();
141
171
  if (selected && state.modelPicker.isLocalModel(selected)) state.modelPicker.enterContextCapMode(selected);
172
+ } else if (token.value === '\t') {
173
+ if (state.modelPicker.focusPane === 'targets') state.modelPicker.focusItems();
174
+ else state.modelPicker.focusTargets();
142
175
  } else if (token.value === 'g' && state.modelPicker.mode === 'model') {
143
176
  state.modelPicker.cycleGroupBy();
177
+ } else if (token.value === 'c' && state.modelPicker.mode === 'model') {
178
+ cycleCapabilityFilter(state.modelPicker);
179
+ } else if (token.value === 'a' && state.modelPicker.mode === 'model') {
180
+ state.modelPicker.toggleAvailableOnly();
181
+ } else if (token.value === 'b' && state.modelPicker.mode === 'model') {
182
+ state.modelPicker.cycleBenchmarkSort();
144
183
  } else if (token.value === '/' && state.modelPicker.canFocusSearch()) {
184
+ state.modelPicker.focusItems();
145
185
  state.modelPicker.focusSearch();
146
186
  }
147
187
  }
@@ -150,6 +190,12 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
150
190
  return true;
151
191
  }
152
192
 
193
+ function cycleCapabilityFilter(modelPicker: ModelPickerModal): void {
194
+ const cycle: CapabilityFilter[] = ['none', 'reasoning', 'toolUse', 'multimodal'];
195
+ const cur = cycle.indexOf(modelPicker.capabilityFilter);
196
+ modelPicker.setCapabilityFilter(cycle[(cur + 1) % cycle.length]!);
197
+ }
198
+
153
199
  type ProcessRouteState = {
154
200
  processModal: {
155
201
  active: boolean;