@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.
@@ -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
+ }
@@ -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
- // Clear main.ts-owned event subscriptions
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 submitInput = (text: string, content?: ContentPart[]) => {
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
- const fieldWindow = wizard.getFieldWindow(visibleFields);
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
- if (fieldWindow.start > 0) {
120
- rows.push({
121
- kind: 'moreAbove',
122
- text: `${OVERLAY_GLYPHS.moreAbove} ${fieldWindow.start} more above`,
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
- fieldWindow.fields.forEach((field, index) => {
127
- const absoluteIndex = fieldWindow.start + index;
128
- rows.push({ kind: 'field', field, absoluteIndex });
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
- if (fieldWindow.end < fieldWindow.total) {
132
- rows.push({
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} ${fieldWindow.total - fieldWindow.end} more below`,
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 = promptFocused ? BG_COLOR : '#334155';
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',
@@ -75,6 +75,7 @@ export interface OnboardingServiceState {
75
75
  readonly hasWebhookUrl: boolean;
76
76
  readonly hasSigningSecret: boolean;
77
77
  readonly hasPublicKey: boolean;
78
+ readonly hasAppToken: boolean;
78
79
  }
79
80
 
80
81
  export interface OnboardingServicesSnapshot {
@@ -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.30';
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;