@pellux/goodvibes-tui 0.19.30 → 0.19.32
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 +24 -0
- package/README.md +12 -3
- 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-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 +5 -0
- package/src/input/commands/services-runtime.ts +1 -0
- package/src/input/commands/tts-runtime.ts +136 -0
- package/src/input/commands.ts +2 -0
- package/src/input/handler-onboarding.ts +12 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +25 -2
- package/src/input/onboarding/onboarding-wizard-types.ts +2 -0
- package/src/main.ts +19 -29
- package/src/panels/services-panel.ts +2 -0
- 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 +6 -1
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/onboarding/derivation.ts +1 -1
- package/src/runtime/onboarding/snapshot.ts +2 -0
- package/src/runtime/onboarding/types.ts +1 -0
- package/src/runtime/services.ts +1 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config/schema';
|
|
2
|
+
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
3
|
+
|
|
4
|
+
const TTS_CONFIG_KEYS = new Set(['provider', 'voice', 'llm-provider', 'llm-model']);
|
|
5
|
+
|
|
6
|
+
export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
|
|
7
|
+
registry.register({
|
|
8
|
+
name: 'tts',
|
|
9
|
+
description: 'Submit a normal prompt and play the assistant response through live TTS',
|
|
10
|
+
usage: '<prompt>|stop',
|
|
11
|
+
handler(args, ctx) {
|
|
12
|
+
const first = (args[0] ?? '').toLowerCase();
|
|
13
|
+
if (first === 'stop' || first === 'cancel') {
|
|
14
|
+
ctx.stopSpokenOutput?.();
|
|
15
|
+
ctx.print('Live TTS playback stopped.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const prompt = args.join(' ').trim();
|
|
20
|
+
if (!prompt) {
|
|
21
|
+
ctx.print('Usage: /tts <prompt> or /tts stop');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!ctx.submitSpokenInput) {
|
|
25
|
+
ctx.print('Live TTS is not available in this runtime.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
ctx.submitSpokenInput(prompt);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
registry.register({
|
|
33
|
+
name: 'config-tts',
|
|
34
|
+
aliases: ['tts-config'],
|
|
35
|
+
description: 'Configure live TTS provider, voice, and optional spoken-turn LLM overrides',
|
|
36
|
+
usage: '[show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm-provider <id|clear>|llm-model <id|clear>]',
|
|
37
|
+
async handler(args, ctx) {
|
|
38
|
+
const sub = (args[0] ?? 'show').toLowerCase();
|
|
39
|
+
if (sub === 'show') {
|
|
40
|
+
ctx.print(formatTtsConfig(ctx));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (sub === 'providers') {
|
|
44
|
+
await printTtsProviders(ctx);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (sub === 'voices') {
|
|
48
|
+
await printTtsVoices(ctx, args[1]);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (TTS_CONFIG_KEYS.has(sub)) {
|
|
52
|
+
const value = args.slice(1).join(' ').trim();
|
|
53
|
+
if (!value) {
|
|
54
|
+
ctx.print(`Usage: /config-tts ${sub} <value|clear>`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const key = ttsConfigKeyForSubcommand(sub);
|
|
58
|
+
const nextValue = value.toLowerCase() === 'clear' ? '' : value;
|
|
59
|
+
ctx.platform.configManager.setDynamic(key, nextValue);
|
|
60
|
+
ctx.print(`${key} ${nextValue ? `set to ${nextValue}` : 'cleared'}.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.print('Usage: /config-tts [show|providers|voices [provider]|provider <id|clear>|voice <id|clear>|llm-provider <id|clear>|llm-model <id|clear>]');
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ttsConfigKeyForSubcommand(subcommand: string): ConfigKey {
|
|
69
|
+
switch (subcommand) {
|
|
70
|
+
case 'provider': return 'tts.provider';
|
|
71
|
+
case 'voice': return 'tts.voice';
|
|
72
|
+
case 'llm-provider': return 'tts.llmProvider';
|
|
73
|
+
case 'llm-model': return 'tts.llmModel';
|
|
74
|
+
default: throw new Error(`Unknown TTS config key: ${subcommand}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatTtsConfig(ctx: CommandContext): string {
|
|
79
|
+
const cm = ctx.platform.configManager;
|
|
80
|
+
const llmProvider = String(cm.get('tts.llmProvider') ?? '').trim();
|
|
81
|
+
const llmModel = String(cm.get('tts.llmModel') ?? '').trim();
|
|
82
|
+
return [
|
|
83
|
+
'TTS Configuration',
|
|
84
|
+
` provider: ${formatValue(cm.get('tts.provider'))}`,
|
|
85
|
+
` voice: ${formatValue(cm.get('tts.voice'))}`,
|
|
86
|
+
` spoken-turn llm provider override: ${llmProvider || '(current chat provider)'}`,
|
|
87
|
+
` spoken-turn llm model override: ${llmModel || '(current chat model)'}`,
|
|
88
|
+
' playback: live streaming through local mpv or ffplay',
|
|
89
|
+
' commands: /tts <prompt>, /tts stop, /config-tts providers, /config-tts voices [provider]',
|
|
90
|
+
].join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function printTtsProviders(ctx: CommandContext): Promise<void> {
|
|
94
|
+
const registry = ctx.platform.voiceProviderRegistry;
|
|
95
|
+
if (!registry) {
|
|
96
|
+
ctx.print('Voice provider registry is not available in this runtime.');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const providers = registry.list().filter((provider) => provider.capabilities.includes('tts-stream'));
|
|
100
|
+
if (providers.length === 0) {
|
|
101
|
+
ctx.print('No streaming TTS providers are registered.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
ctx.print([
|
|
105
|
+
'Streaming TTS Providers',
|
|
106
|
+
...providers.map((provider) => ` ${provider.id}: ${provider.label}`),
|
|
107
|
+
].join('\n'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function printTtsVoices(ctx: CommandContext, providerArg?: string): Promise<void> {
|
|
111
|
+
const service = ctx.platform.voiceService;
|
|
112
|
+
if (!service) {
|
|
113
|
+
ctx.print('Voice service is not available in this runtime.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const providerId = (providerArg ?? String(ctx.platform.configManager.get('tts.provider') ?? '')).trim() || undefined;
|
|
117
|
+
try {
|
|
118
|
+
const voices = await service.listVoices(providerId);
|
|
119
|
+
if (voices.length === 0) {
|
|
120
|
+
ctx.print(providerId ? `No voices returned for ${providerId}.` : 'No TTS voices returned.');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
ctx.print([
|
|
124
|
+
`TTS Voices${providerId ? ` (${providerId})` : ''}`,
|
|
125
|
+
...voices.slice(0, 60).map((voice) => ` ${voice.id}: ${voice.label}`),
|
|
126
|
+
...(voices.length > 60 ? [` ... ${voices.length - 60} more`] : []),
|
|
127
|
+
].join('\n'));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatValue(value: unknown): string {
|
|
134
|
+
const text = String(value ?? '').trim();
|
|
135
|
+
return text || '(default)';
|
|
136
|
+
}
|
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();
|
|
@@ -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 {
|
package/src/main.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { buildPersistedSessionContext, formatReturnContextForDisplay, getReturnC
|
|
|
51
51
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
|
|
52
52
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
53
53
|
import { applyInitialTuiCliState } from './cli/tui-startup.ts';
|
|
54
|
+
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
54
55
|
|
|
55
56
|
const ALT_SCREEN_ENTER = '\x1b[?1049h';
|
|
56
57
|
const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
@@ -72,7 +73,6 @@ async function main() {
|
|
|
72
73
|
homeDirectory: homedir(),
|
|
73
74
|
}, 'goodvibes');
|
|
74
75
|
|
|
75
|
-
// ── Bootstrap runtime subsystems via bootstrapRuntime.
|
|
76
76
|
const ctx: BootstrapContext = await bootstrapRuntime(stdout, {
|
|
77
77
|
configManager,
|
|
78
78
|
workingDir: bootstrapWorkingDir,
|
|
@@ -118,7 +118,6 @@ async function main() {
|
|
|
118
118
|
ctx.services.wrfcController.setPlanManager(ctx.services.planManager);
|
|
119
119
|
let activeConversationWidth = stdout.columns || 80;
|
|
120
120
|
conversation.setWidthProvider(() => activeConversationWidth);
|
|
121
|
-
// ── HITL UX mode — read from config and apply at startup ─────────────────
|
|
122
121
|
{
|
|
123
122
|
const hitlMode = configManager.get('behavior.hitlMode') as HITLMode | undefined;
|
|
124
123
|
if (hitlMode && (hitlMode === 'quiet' || hitlMode === 'balanced' || hitlMode === 'operator')) {
|
|
@@ -126,7 +125,6 @@ async function main() {
|
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
// Use the panel manager owned by the runtime service graph.
|
|
130
128
|
const panelManager = ctx.services.panelManager;
|
|
131
129
|
const buildSessionContinuityHints = () => {
|
|
132
130
|
const sessionSnapshot = uiServices.readModels.session.getSnapshot();
|
|
@@ -145,7 +143,6 @@ async function main() {
|
|
|
145
143
|
};
|
|
146
144
|
};
|
|
147
145
|
|
|
148
|
-
// Permission state — set while a permission prompt is blocking the orchestrator
|
|
149
146
|
let pendingPermission: PendingPermissionState | null = null;
|
|
150
147
|
approvalBroker.subscribe((approval) => {
|
|
151
148
|
if (!pendingPermission) return;
|
|
@@ -155,20 +152,13 @@ async function main() {
|
|
|
155
152
|
render();
|
|
156
153
|
});
|
|
157
154
|
|
|
158
|
-
// --- Streaming speed tracking (B2) ---
|
|
159
155
|
let streamStartTime = 0;
|
|
160
156
|
let streamDeltaCount = 0;
|
|
161
157
|
let streamTokenSpeed = 0;
|
|
162
158
|
|
|
163
159
|
let scrollTop = 0;
|
|
164
|
-
/** When true, view auto-scrolls to bottom on every render.
|
|
165
|
-
* False when user manually scrolls up. Reset on user input. */
|
|
166
160
|
let scrollLocked = true;
|
|
167
161
|
|
|
168
|
-
// lastGitInfo is a mutable ref provided by bootstrap (updated asynchronously)
|
|
169
|
-
// Use lastGitInfoRef.value inside render to get the current value.
|
|
170
|
-
|
|
171
|
-
/** Content width inside the prompt box (box width minus padding). */
|
|
172
162
|
const getPromptContentWidth = () => {
|
|
173
163
|
const w = stdout.columns || 80;
|
|
174
164
|
const boxMargin = 2;
|
|
@@ -195,23 +185,12 @@ async function main() {
|
|
|
195
185
|
scrollTop = Math.max(0, conversation.history.getLineCount() - vHeight);
|
|
196
186
|
};
|
|
197
187
|
|
|
198
|
-
// main.ts-owned unsub functions for shell-owned typed runtime subscriptions
|
|
199
|
-
// Bootstrap-owned unsubs are in ctx.bootstrapUnsubs and cleared by ctx.shutdown().
|
|
200
188
|
const unsubs: Array<() => void> = [];
|
|
201
|
-
|
|
202
|
-
// Crash recovery interval handle — cleared on exit
|
|
203
189
|
let recoveryInterval: ReturnType<typeof setInterval> | null = null;
|
|
204
|
-
|
|
205
|
-
// Recovery flow state
|
|
190
|
+
let stopSpokenOutputForExit: (() => void) | null = null;
|
|
206
191
|
let recoveryPending = false;
|
|
207
192
|
|
|
208
|
-
/**
|
|
209
|
-
* Full application teardown.
|
|
210
|
-
* Clears main.ts-owned listeners, calls ctx.shutdown() for logical teardown,
|
|
211
|
-
* then tears down the terminal and exits the process.
|
|
212
|
-
*/
|
|
213
193
|
const sigintHandler = (): void => input.feed('\x03');
|
|
214
|
-
// Track unhandled rejections to detect cascading failures
|
|
215
194
|
let _unhandledRejectionCount = 0;
|
|
216
195
|
let _unhandledRejectionWindowStart = Date.now();
|
|
217
196
|
const unhandledRejectionHandler = (reason: unknown): void => {
|
|
@@ -244,17 +223,14 @@ async function main() {
|
|
|
244
223
|
};
|
|
245
224
|
|
|
246
225
|
const exitApp = (): void => {
|
|
247
|
-
|
|
226
|
+
stopSpokenOutputForExit?.();
|
|
248
227
|
unsubs.forEach(fn => fn());
|
|
249
|
-
// Clear bootstrap-owned unsubs + interval via ctx.shutdown()
|
|
250
228
|
const snapshot = conversation.toJSON() as { messages: Array<import('./core/conversation.ts').ConversationMessageSnapshot>; timestamp?: number };
|
|
251
229
|
ctx.shutdown({ ...snapshot, ...buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints()) }).catch((err) => {
|
|
252
230
|
logger.debug('ctx.shutdown error during exitApp (non-fatal)', { error: summarizeError(err) });
|
|
253
231
|
});
|
|
254
|
-
// Clear recovery interval
|
|
255
232
|
if (recoveryInterval !== null) { clearInterval(recoveryInterval); recoveryInterval = null; }
|
|
256
233
|
deleteRecoveryFile({ homeDirectory });
|
|
257
|
-
// Terminal teardown — main.ts exclusively owns these
|
|
258
234
|
stdin.removeAllListeners('data');
|
|
259
235
|
stdout.removeListener('resize', resizeHandler);
|
|
260
236
|
process.removeListener('SIGINT', sigintHandler);
|
|
@@ -265,10 +241,18 @@ async function main() {
|
|
|
265
241
|
process.exit(0);
|
|
266
242
|
};
|
|
267
243
|
|
|
268
|
-
// main.ts owns terminal teardown, so it binds the shell exit bridge here.
|
|
269
244
|
commandContext.exit = exitApp;
|
|
270
245
|
|
|
271
|
-
const
|
|
246
|
+
const spokenTurns = wireSpokenTurnRuntime({
|
|
247
|
+
voiceService: ctx.services.voiceService,
|
|
248
|
+
configManager,
|
|
249
|
+
events: uiServices.events,
|
|
250
|
+
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
251
|
+
});
|
|
252
|
+
stopSpokenOutputForExit = () => spokenTurns.stop();
|
|
253
|
+
unsubs.push(...spokenTurns.unsubs);
|
|
254
|
+
|
|
255
|
+
const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
|
|
272
256
|
input.clearModalStack();
|
|
273
257
|
scrollLocked = true; // Re-lock on any user input
|
|
274
258
|
const AT_MODEL_RE = /@model:([^\s]+)/g;
|
|
@@ -302,6 +286,9 @@ async function main() {
|
|
|
302
286
|
}
|
|
303
287
|
}
|
|
304
288
|
if (processedText || content) {
|
|
289
|
+
if (options.spokenOutput && processedText) {
|
|
290
|
+
spokenTurns.submitNextTurn(processedText);
|
|
291
|
+
}
|
|
305
292
|
orchestrator.handleUserInput(processedText, content).catch((err: unknown) => {
|
|
306
293
|
logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
|
|
307
294
|
});
|
|
@@ -311,6 +298,7 @@ async function main() {
|
|
|
311
298
|
};
|
|
312
299
|
|
|
313
300
|
const cancelGeneration = () => {
|
|
301
|
+
spokenTurns.stop('Spoken output stopped.');
|
|
314
302
|
if (orchestrator.isThinking) {
|
|
315
303
|
orchestrator.abort();
|
|
316
304
|
}
|
|
@@ -338,6 +326,8 @@ async function main() {
|
|
|
338
326
|
};
|
|
339
327
|
|
|
340
328
|
commandContext.submitInput = submitInput;
|
|
329
|
+
commandContext.submitSpokenInput = (text, content) => submitInput(text, content, { spokenOutput: true });
|
|
330
|
+
commandContext.stopSpokenOutput = () => spokenTurns.stop();
|
|
341
331
|
commandContext.executeCommand = (name, args) => commandRegistry.execute(name, args, commandContext);
|
|
342
332
|
commandContext.cancelGeneration = cancelGeneration;
|
|
343
333
|
commandContext.jumpToBookmark = jumpToBookmark;
|
|
@@ -195,6 +195,8 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
195
195
|
...buildStatusPill(inspect.hasWebhookUrl ? 'good' : 'info', inspect.hasWebhookUrl ? 'present' : 'missing'),
|
|
196
196
|
[' Signing secret: ', C.label],
|
|
197
197
|
...buildStatusPill(inspect.hasSigningSecret ? 'good' : 'info', inspect.hasSigningSecret ? 'present' : 'missing'),
|
|
198
|
+
[' App token: ', C.label],
|
|
199
|
+
...buildStatusPill(inspect.hasAppToken ? 'good' : 'info', inspect.hasAppToken ? 'present' : 'missing'),
|
|
198
200
|
]));
|
|
199
201
|
if (selected.lastTest) {
|
|
200
202
|
detailLines.push(buildPanelLine(width, [
|
|
@@ -113,26 +113,50 @@ function buildFieldRows(
|
|
|
113
113
|
visibleFields: number,
|
|
114
114
|
capacity: number,
|
|
115
115
|
): readonly RenderedFieldRow[] {
|
|
116
|
-
|
|
116
|
+
wizard.ensureSelectionVisible(visibleFields);
|
|
117
|
+
const fields = wizard.currentStep.fields;
|
|
117
118
|
const rows: RenderedFieldRow[] = [];
|
|
119
|
+
if (fields.length === 0 || capacity <= 0) return rows;
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const allRows: RenderedFieldRow[] = [];
|
|
122
|
+
fields.forEach((field, absoluteIndex) => {
|
|
123
|
+
const spacerRows = Math.max(0, field.spacerBeforeRows ?? 0);
|
|
124
|
+
for (let index = 0; index < spacerRows; index += 1) {
|
|
125
|
+
allRows.push({ kind: 'empty' });
|
|
126
|
+
}
|
|
127
|
+
allRows.push({ kind: 'field', field, absoluteIndex });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const selectedFieldIndex = wizard.getSelectedFieldIndex();
|
|
131
|
+
const selectedRowIndex = Math.max(0, allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === selectedFieldIndex));
|
|
132
|
+
const scrollFieldIndex = wizard.scrollOffsets[wizard.stepIndex] ?? 0;
|
|
133
|
+
const scrollRowIndex = allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === scrollFieldIndex);
|
|
134
|
+
const maxStart = Math.max(0, allRows.length - capacity);
|
|
135
|
+
let start = clamp(scrollRowIndex >= 0 ? scrollRowIndex : 0, 0, maxStart);
|
|
136
|
+
|
|
137
|
+
if (selectedRowIndex < start) start = selectedRowIndex;
|
|
138
|
+
if (selectedRowIndex >= start + capacity) start = selectedRowIndex - capacity + 1;
|
|
139
|
+
start = clamp(start, 0, maxStart);
|
|
140
|
+
|
|
141
|
+
if (start > 0 && selectedRowIndex === start) start = Math.max(0, start - 1);
|
|
142
|
+
if (start + capacity < allRows.length && selectedRowIndex === start + capacity - 1) {
|
|
143
|
+
start = Math.min(maxStart, start + 1);
|
|
124
144
|
}
|
|
125
145
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
rows
|
|
129
|
-
|
|
146
|
+
rows.push(...allRows.slice(start, start + capacity));
|
|
147
|
+
if (start > 0 && rows.length > 0) {
|
|
148
|
+
rows[0] = {
|
|
149
|
+
kind: 'moreAbove',
|
|
150
|
+
text: `${OVERLAY_GLYPHS.moreAbove} ${start} more above`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
130
153
|
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
const hiddenBelow = Math.max(0, allRows.length - (start + capacity));
|
|
155
|
+
if (hiddenBelow > 0 && rows.length > 0) {
|
|
156
|
+
rows[rows.length - 1] = {
|
|
133
157
|
kind: 'moreBelow',
|
|
134
|
-
text: `${OVERLAY_GLYPHS.moreBelow} ${
|
|
135
|
-
}
|
|
158
|
+
text: `${OVERLAY_GLYPHS.moreBelow} ${hiddenBelow} more below`,
|
|
159
|
+
};
|
|
136
160
|
}
|
|
137
161
|
|
|
138
162
|
while (rows.length < capacity) rows.push({ kind: 'empty' });
|
|
@@ -154,7 +154,7 @@ export class UIFactory {
|
|
|
154
154
|
const promptLines = prompt.split('\n');
|
|
155
155
|
const TEXT_COLOR = promptFocused ? '252' : '246';
|
|
156
156
|
const BG_COLOR = promptFocused ? '#2a2a2a' : '#1f2430';
|
|
157
|
-
const BORDER_COLOR =
|
|
157
|
+
const BORDER_COLOR = BG_COLOR;
|
|
158
158
|
const boxMargin = 2; const boxWidth = width - (boxMargin * 2); const boxStartX = boxMargin;
|
|
159
159
|
const createBaseLine = () => {
|
|
160
160
|
const l = createEmptyLine(width);
|
|
@@ -37,6 +37,7 @@ import { createBootstrapCommandShellServices, type PlanRuntimeService, type Remo
|
|
|
37
37
|
import type { OperatorClient } from '@pellux/goodvibes-sdk/platform/runtime/operator-client';
|
|
38
38
|
import type { PeerClient } from '@pellux/goodvibes-sdk/platform/runtime/peer-client';
|
|
39
39
|
import type { DirectTransport } from '@pellux/goodvibes-sdk/platform/runtime/transports/direct';
|
|
40
|
+
import type { VoiceProviderRegistry, VoiceService } from '@pellux/goodvibes-sdk/platform/voice/index';
|
|
40
41
|
import {
|
|
41
42
|
createBootstrapCommandActions,
|
|
42
43
|
createBootstrapCommandClientsSection,
|
|
@@ -58,6 +59,8 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
58
59
|
requestPermission: PermissionRequestHandler;
|
|
59
60
|
toolRegistry: ToolRegistry;
|
|
60
61
|
mcpRegistry: McpRegistry;
|
|
62
|
+
voiceProviderRegistry?: VoiceProviderRegistry;
|
|
63
|
+
voiceService?: VoiceService;
|
|
61
64
|
forensicsRegistry: ForensicsRegistry;
|
|
62
65
|
policyRuntimeState: PolicyRuntimeState;
|
|
63
66
|
readModels: UiReadModels;
|
|
@@ -122,6 +125,8 @@ export function createBootstrapCommandContext(
|
|
|
122
125
|
requestPermission,
|
|
123
126
|
toolRegistry,
|
|
124
127
|
mcpRegistry,
|
|
128
|
+
voiceProviderRegistry,
|
|
129
|
+
voiceService,
|
|
125
130
|
forensicsRegistry,
|
|
126
131
|
policyRuntimeState,
|
|
127
132
|
readModels,
|
|
@@ -223,7 +228,7 @@ export function createBootstrapCommandContext(
|
|
|
223
228
|
profileManager,
|
|
224
229
|
bookmarkManager,
|
|
225
230
|
}, shellServices);
|
|
226
|
-
const platform = createBootstrapCommandPlatformSection({ configManager }, shellServices);
|
|
231
|
+
const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService }, shellServices);
|
|
227
232
|
const extensions = createBootstrapCommandExtensionsSection({
|
|
228
233
|
toolRegistry,
|
|
229
234
|
mcpRegistry,
|
|
@@ -40,6 +40,7 @@ import type { BootstrapCommandShellServices } from '@pellux/goodvibes-sdk/platfo
|
|
|
40
40
|
import type { OperatorClient } from '@pellux/goodvibes-sdk/platform/runtime/operator-client';
|
|
41
41
|
import type { PeerClient } from '@pellux/goodvibes-sdk/platform/runtime/peer-client';
|
|
42
42
|
import type { DirectTransport } from '@pellux/goodvibes-sdk/platform/runtime/transports/direct';
|
|
43
|
+
import type { VoiceProviderRegistry, VoiceService } from '@pellux/goodvibes-sdk/platform/voice/index';
|
|
43
44
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
|
|
44
45
|
|
|
45
46
|
export type BootstrapCommandSessionSection = CommandContext['session'];
|
|
@@ -74,6 +75,8 @@ export interface BootstrapCommandSectionOptions {
|
|
|
74
75
|
readonly requestPermission: PermissionRequestHandler;
|
|
75
76
|
readonly toolRegistry: ToolRegistry;
|
|
76
77
|
readonly mcpRegistry: McpRegistry;
|
|
78
|
+
readonly voiceProviderRegistry?: VoiceProviderRegistry;
|
|
79
|
+
readonly voiceService?: VoiceService;
|
|
77
80
|
readonly forensicsRegistry: ForensicsRegistry;
|
|
78
81
|
readonly policyRuntimeState: PolicyRuntimeState;
|
|
79
82
|
readonly readModels: UiReadModels;
|
|
@@ -321,13 +324,15 @@ export function createBootstrapCommandWorkspaceSection(
|
|
|
321
324
|
export function createBootstrapCommandPlatformSection(
|
|
322
325
|
options: Pick<
|
|
323
326
|
BootstrapCommandSectionOptions,
|
|
324
|
-
'configManager'
|
|
327
|
+
'configManager' | 'voiceProviderRegistry' | 'voiceService'
|
|
325
328
|
>,
|
|
326
329
|
shellServices: BootstrapCommandShellServices,
|
|
327
330
|
): BootstrapCommandPlatformSection {
|
|
328
331
|
return {
|
|
329
332
|
config: getConfigSnapshot(options.configManager),
|
|
330
333
|
configManager: options.configManager,
|
|
334
|
+
voiceProviderRegistry: options.voiceProviderRegistry,
|
|
335
|
+
voiceService: options.voiceService,
|
|
331
336
|
...shellServices.platform,
|
|
332
337
|
};
|
|
333
338
|
}
|
|
@@ -183,6 +183,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
183
183
|
requestPermission: (request) => permissionPromptRef.requestPermission(request),
|
|
184
184
|
toolRegistry,
|
|
185
185
|
mcpRegistry: services.mcpRegistry,
|
|
186
|
+
voiceProviderRegistry: services.voiceProviders,
|
|
187
|
+
voiceService: services.voiceService,
|
|
186
188
|
forensicsRegistry,
|
|
187
189
|
policyRuntimeState,
|
|
188
190
|
readModels: uiServices.readModels,
|
|
@@ -211,7 +211,7 @@ function hasRemoteDeviceAccess(snapshot: OnboardingSnapshotState): boolean {
|
|
|
211
211
|
function hasWebhookOrEventIngress(snapshot: OnboardingSnapshotState): boolean {
|
|
212
212
|
return snapshot.bindSettings.httpListenerEnabled
|
|
213
213
|
|| hasInboundEventSurface(snapshot)
|
|
214
|
-
|| snapshot.services.services.some((service) => service.hasWebhookUrl || service.hasSigningSecret || service.hasPublicKey);
|
|
214
|
+
|| snapshot.services.services.some((service) => service.hasWebhookUrl || service.hasSigningSecret || service.hasPublicKey || service.hasAppToken);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
function getProviderIdentityIds(snapshot: OnboardingSnapshotState): Set<string> {
|
|
@@ -122,6 +122,7 @@ async function buildServicesSnapshot(
|
|
|
122
122
|
hasWebhookUrl: inspection?.hasWebhookUrl ?? false,
|
|
123
123
|
hasSigningSecret: inspection?.hasSigningSecret ?? false,
|
|
124
124
|
hasPublicKey: inspection?.hasPublicKey ?? false,
|
|
125
|
+
hasAppToken: inspection?.hasAppToken ?? false,
|
|
125
126
|
} satisfies OnboardingServiceState,
|
|
126
127
|
issue: null,
|
|
127
128
|
};
|
|
@@ -139,6 +140,7 @@ async function buildServicesSnapshot(
|
|
|
139
140
|
hasWebhookUrl: false,
|
|
140
141
|
hasSigningSecret: false,
|
|
141
142
|
hasPublicKey: false,
|
|
143
|
+
hasAppToken: false,
|
|
142
144
|
} satisfies OnboardingServiceState,
|
|
143
145
|
issue: {
|
|
144
146
|
area: 'services',
|
package/src/runtime/services.ts
CHANGED
|
@@ -417,6 +417,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
417
417
|
const projectIndex = new ProjectIndex(workingDirectory);
|
|
418
418
|
const channelDeliveryRouter = new ChannelDeliveryRouter({
|
|
419
419
|
configManager,
|
|
420
|
+
secretsManager,
|
|
420
421
|
serviceRegistry,
|
|
421
422
|
artifactStore,
|
|
422
423
|
});
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.19.
|
|
9
|
+
let _version = '0.19.32';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|