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