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