@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.
- package/CHANGELOG.md +35 -0
- package/README.md +10 -13
- package/docs/foundation-artifacts/knowledge-store.sql +27 -0
- package/docs/foundation-artifacts/operator-contract.json +15736 -7265
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +4 -1
- package/src/input/command-args-hint.ts +36 -0
- package/src/input/command-registry.ts +3 -1
- package/src/input/commands/config.ts +7 -521
- package/src/input/commands/knowledge.ts +111 -1
- package/src/input/commands/local-runtime.ts +0 -80
- package/src/input/commands/operator-runtime.ts +3 -3
- package/src/input/commands/planning-runtime.ts +83 -34
- package/src/input/commands/shell-core.ts +2 -34
- package/src/input/commands/tts-runtime.ts +1 -389
- package/src/input/commands.ts +0 -2
- package/src/input/handler-modal-routes.ts +61 -7
- package/src/input/handler-modal-token-routes.ts +1 -0
- package/src/input/handler-picker-routes.ts +50 -4
- package/src/input/model-picker-provider-filter.ts +28 -0
- package/src/input/model-picker-types.ts +12 -0
- package/src/input/model-picker.ts +65 -23
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-behavior.ts +2 -0
- package/src/input/settings-modal-subscriptions.ts +95 -0
- package/src/input/settings-modal-types.ts +50 -3
- package/src/input/settings-modal.ts +106 -134
- package/src/input/tts-settings-actions.ts +100 -0
- package/src/main.ts +50 -45
- package/src/panels/builtin/agent.ts +15 -0
- package/src/panels/builtin/shared.ts +17 -0
- package/src/panels/project-planning-panel.ts +370 -0
- package/src/planning/project-planning-coordinator.ts +249 -0
- package/src/renderer/compositor.ts +2 -1
- package/src/renderer/conversation-overlays.ts +4 -5
- package/src/renderer/model-workspace.ts +488 -0
- package/src/renderer/settings-modal-helpers.ts +16 -1
- package/src/renderer/settings-modal.ts +616 -716
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/services.ts +33 -2
- package/src/runtime/terminal-output-guard.ts +228 -0
- package/src/runtime/ui-services.ts +4 -0
- package/src/shell/ui-openers.ts +59 -3
- package/src/utils/clipboard.ts +2 -1
- package/src/version.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +0 -104
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
}
|
package/src/input/commands.ts
CHANGED
|
@@ -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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
else if (token.logicalName === '
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|