@pellux/goodvibes-tui 0.19.32 → 0.19.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +4 -2
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-model-routing.ts +117 -0
  6. package/src/input/command-registry.ts +2 -0
  7. package/src/input/commands/cloudflare-runtime.ts +343 -0
  8. package/src/input/commands/tts-runtime.ts +288 -7
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/feed-context-factory.ts +1 -0
  11. package/src/input/handler-feed.ts +6 -0
  12. package/src/input/handler-modal-routes.ts +23 -10
  13. package/src/input/handler-modal-token-routes.ts +9 -0
  14. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  15. package/src/input/handler-onboarding.ts +33 -0
  16. package/src/input/handler-picker-routes.ts +1 -1
  17. package/src/input/handler.ts +4 -1
  18. package/src/input/model-picker-types.ts +125 -0
  19. package/src/input/model-picker.ts +144 -134
  20. package/src/input/onboarding/onboarding-wizard-apply.ts +81 -0
  21. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +449 -0
  22. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +199 -0
  23. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -0
  24. package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
  25. package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
  26. package/src/input/settings-modal-types.ts +2 -1
  27. package/src/input/settings-modal.ts +30 -8
  28. package/src/main.ts +12 -1
  29. package/src/renderer/buffer.ts +40 -2
  30. package/src/renderer/compositor.ts +25 -17
  31. package/src/renderer/model-picker-overlay.ts +70 -0
  32. package/src/renderer/settings-modal-helpers.ts +1 -0
  33. package/src/runtime/bootstrap-command-parts.ts +4 -0
  34. package/src/runtime/cloudflare-control-plane.ts +328 -0
  35. package/src/runtime/onboarding/derivation.ts +25 -0
  36. package/src/runtime/onboarding/snapshot.ts +2 -0
  37. package/src/runtime/onboarding/types.ts +5 -1
  38. package/src/shell/ui-openers.ts +21 -2
  39. package/src/version.ts +1 -1
@@ -0,0 +1,328 @@
1
+ import { join } from 'node:path';
2
+ import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing/companion-token';
3
+ import type { ConfigManager } from '../config/index.ts';
4
+
5
+ export const CLOUDFLARE_COMPONENT_IDS = [
6
+ 'workers',
7
+ 'queues',
8
+ 'zeroTrustTunnel',
9
+ 'zeroTrustAccess',
10
+ 'dns',
11
+ 'kv',
12
+ 'durableObjects',
13
+ 'secretsStore',
14
+ 'r2',
15
+ ] as const;
16
+
17
+ export type CloudflareComponent = typeof CLOUDFLARE_COMPONENT_IDS[number];
18
+ export type CloudflareComponentSelection = Partial<Record<CloudflareComponent, boolean>>;
19
+ export type CloudflareBatchMode = 'off' | 'explicit' | 'eligible-by-default';
20
+
21
+ export const DEFAULT_CLOUDFLARE_COMPONENT_SELECTION: Readonly<Record<CloudflareComponent, boolean>> = {
22
+ workers: true,
23
+ queues: true,
24
+ zeroTrustTunnel: false,
25
+ zeroTrustAccess: false,
26
+ dns: false,
27
+ kv: false,
28
+ durableObjects: false,
29
+ secretsStore: false,
30
+ r2: false,
31
+ };
32
+
33
+ export const CLOUDFLARE_COMPONENT_LABELS: Readonly<Record<CloudflareComponent, string>> = {
34
+ workers: 'Workers',
35
+ queues: 'Queues',
36
+ zeroTrustTunnel: 'Zero Trust Tunnel',
37
+ zeroTrustAccess: 'Zero Trust Access',
38
+ dns: 'DNS hostname',
39
+ kv: 'KV',
40
+ durableObjects: 'Durable Objects',
41
+ secretsStore: 'Secrets Store',
42
+ r2: 'R2 artifacts',
43
+ };
44
+
45
+ export interface CloudflareProvisionStep {
46
+ readonly name: string;
47
+ readonly status: 'ok' | 'skipped' | 'warning';
48
+ readonly message?: string;
49
+ readonly resourceId?: string;
50
+ }
51
+
52
+ export interface CloudflareControlPlaneStatus {
53
+ readonly enabled: boolean;
54
+ readonly ready: boolean;
55
+ readonly configured: Record<string, boolean>;
56
+ readonly config: Record<string, unknown>;
57
+ readonly warnings: readonly string[];
58
+ }
59
+
60
+ export interface CloudflareTokenRequirement {
61
+ readonly component: CloudflareComponent | 'bootstrap';
62
+ readonly scope: 'account' | 'zone' | 'user' | 'r2';
63
+ readonly permission: string;
64
+ readonly alternatives?: readonly string[];
65
+ readonly reason: string;
66
+ }
67
+
68
+ export interface CloudflareTokenRequirementsResult {
69
+ readonly ok: true;
70
+ readonly components: Readonly<Record<CloudflareComponent, boolean>>;
71
+ readonly permissions: readonly CloudflareTokenRequirement[];
72
+ readonly bootstrapToken: {
73
+ readonly requiredForSdkCreation: boolean;
74
+ readonly storeInGoodVibes: false;
75
+ readonly instructions: readonly string[];
76
+ };
77
+ }
78
+
79
+ export interface CloudflareValidateResult {
80
+ readonly ok: boolean;
81
+ readonly account?: {
82
+ readonly id: string;
83
+ readonly name: string;
84
+ readonly type?: string;
85
+ };
86
+ readonly tokenSource: string;
87
+ }
88
+
89
+ export interface CloudflareOperationalTokenResult {
90
+ readonly ok: true;
91
+ readonly tokenId?: string;
92
+ readonly tokenName: string;
93
+ readonly tokenSource: 'bootstrap';
94
+ readonly apiTokenRef?: string;
95
+ readonly generatedToken?: string;
96
+ readonly accountId: string;
97
+ readonly zoneId?: string;
98
+ readonly permissions: readonly CloudflareTokenRequirement[];
99
+ }
100
+
101
+ export interface CloudflareDiscoverResult {
102
+ readonly ok: true;
103
+ readonly tokenSource: string;
104
+ readonly accounts: ReadonlyArray<{ readonly id: string; readonly name: string; readonly type?: string }>;
105
+ readonly selectedAccount?: { readonly id: string; readonly name: string; readonly type?: string };
106
+ readonly zones: ReadonlyArray<{ readonly id: string; readonly name: string; readonly status?: string; readonly type?: string }>;
107
+ readonly selectedZone?: { readonly id: string; readonly name: string; readonly status?: string; readonly type?: string };
108
+ readonly workerSubdomain?: string;
109
+ readonly queues?: ReadonlyArray<{ readonly queue_id?: string; readonly queue_name?: string }>;
110
+ readonly kvNamespaces?: ReadonlyArray<{ readonly id?: string; readonly title?: string }>;
111
+ readonly durableObjectNamespaces?: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly class?: string }>;
112
+ readonly r2Buckets?: ReadonlyArray<{ readonly name?: string; readonly storage_class?: string }>;
113
+ readonly secretsStores?: ReadonlyArray<{ readonly id: string; readonly name: string }>;
114
+ readonly tunnels?: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly status?: string }>;
115
+ readonly accessApplications?: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly domain?: string; readonly type?: string }>;
116
+ readonly warnings: readonly string[];
117
+ }
118
+
119
+ export interface CloudflareProvisionResult {
120
+ readonly ok: boolean;
121
+ readonly dryRun: false;
122
+ readonly steps: readonly CloudflareProvisionStep[];
123
+ readonly account?: { readonly id: string; readonly name: string };
124
+ readonly worker?: { readonly name: string; readonly baseUrl?: string; readonly subdomain?: string; readonly hostname?: string; readonly cron?: string };
125
+ readonly queues?: { readonly queueName: string; readonly queueId: string; readonly deadLetterQueueName: string; readonly deadLetterQueueId: string; readonly consumerId?: string };
126
+ readonly verification?: CloudflareVerifyResult;
127
+ }
128
+
129
+ export interface CloudflareVerifyResult {
130
+ readonly ok: boolean;
131
+ readonly workerHealth: {
132
+ readonly ok: boolean;
133
+ readonly status: number;
134
+ readonly error?: string;
135
+ };
136
+ readonly daemonBatchProxy?: {
137
+ readonly ok: boolean;
138
+ readonly status: number;
139
+ readonly error?: string;
140
+ };
141
+ }
142
+
143
+ export interface CloudflareDisableResult {
144
+ readonly ok: boolean;
145
+ readonly steps: readonly CloudflareProvisionStep[];
146
+ }
147
+
148
+ export interface CloudflareTokenRequirementsRequest {
149
+ readonly components?: CloudflareComponentSelection;
150
+ readonly includeBootstrap?: boolean;
151
+ }
152
+
153
+ export interface CloudflareOperationalTokenRequest extends CloudflareTokenRequirementsRequest {
154
+ readonly accountId?: string;
155
+ readonly zoneId?: string;
156
+ readonly zoneName?: string;
157
+ readonly bootstrapToken?: string;
158
+ readonly tokenName?: string;
159
+ readonly expiresOn?: string;
160
+ readonly persistConfig?: boolean;
161
+ readonly storeApiToken?: boolean;
162
+ readonly returnGeneratedToken?: boolean;
163
+ }
164
+
165
+ export interface CloudflareValidateRequest {
166
+ readonly accountId?: string;
167
+ readonly apiToken?: string;
168
+ readonly apiTokenRef?: string;
169
+ }
170
+
171
+ export interface CloudflareDiscoverRequest extends CloudflareValidateRequest {
172
+ readonly components?: CloudflareComponentSelection;
173
+ readonly zoneId?: string;
174
+ readonly zoneName?: string;
175
+ readonly includeResources?: boolean;
176
+ }
177
+
178
+ export interface CloudflareProvisionRequest extends CloudflareDiscoverRequest {
179
+ readonly workerName?: string;
180
+ readonly workerSubdomain?: string;
181
+ readonly workerHostname?: string;
182
+ readonly workerBaseUrl?: string;
183
+ readonly daemonBaseUrl?: string;
184
+ readonly daemonHostname?: string;
185
+ readonly queueName?: string;
186
+ readonly deadLetterQueueName?: string;
187
+ readonly tunnelName?: string;
188
+ readonly tunnelId?: string;
189
+ readonly tunnelServiceUrl?: string;
190
+ readonly kvNamespaceName?: string;
191
+ readonly kvNamespaceId?: string;
192
+ readonly durableObjectNamespaceName?: string;
193
+ readonly durableObjectNamespaceId?: string;
194
+ readonly r2BucketName?: string;
195
+ readonly secretsStoreName?: string;
196
+ readonly secretsStoreId?: string;
197
+ readonly workerCron?: string;
198
+ readonly operatorToken?: string;
199
+ readonly operatorTokenRef?: string;
200
+ readonly workerClientToken?: string;
201
+ readonly workerClientTokenRef?: string;
202
+ readonly storeApiToken?: boolean;
203
+ readonly storeOperatorToken?: boolean;
204
+ readonly storeWorkerClientToken?: boolean;
205
+ readonly returnGeneratedSecrets?: boolean;
206
+ readonly enableWorkersDev?: boolean;
207
+ readonly queueJobPayloads?: boolean;
208
+ readonly verify?: boolean;
209
+ readonly persistConfig?: boolean;
210
+ readonly batchMode?: CloudflareBatchMode;
211
+ }
212
+
213
+ export interface CloudflareVerifyRequest {
214
+ readonly workerBaseUrl?: string;
215
+ readonly workerClientToken?: string;
216
+ readonly workerClientTokenRef?: string;
217
+ }
218
+
219
+ export interface CloudflareDisableRequest extends CloudflareValidateRequest {
220
+ readonly workerName?: string;
221
+ readonly disableWorkerSubdomain?: boolean;
222
+ readonly disableCron?: boolean;
223
+ readonly persistConfig?: boolean;
224
+ }
225
+
226
+ export class CloudflareDaemonRouteError extends Error {
227
+ readonly status: number;
228
+ readonly code: string;
229
+
230
+ constructor(message: string, status: number, code: string) {
231
+ super(message);
232
+ this.name = 'CloudflareDaemonRouteError';
233
+ this.status = status;
234
+ this.code = code;
235
+ }
236
+ }
237
+
238
+ export interface CloudflareDaemonClient {
239
+ status(): Promise<CloudflareControlPlaneStatus>;
240
+ tokenRequirements(input?: CloudflareTokenRequirementsRequest): Promise<CloudflareTokenRequirementsResult>;
241
+ createOperationalToken(input: CloudflareOperationalTokenRequest): Promise<CloudflareOperationalTokenResult>;
242
+ discover(input?: CloudflareDiscoverRequest): Promise<CloudflareDiscoverResult>;
243
+ validate(input?: CloudflareValidateRequest): Promise<CloudflareValidateResult>;
244
+ provision(input: CloudflareProvisionRequest): Promise<CloudflareProvisionResult>;
245
+ verify(input?: CloudflareVerifyRequest): Promise<CloudflareVerifyResult>;
246
+ disable(input?: CloudflareDisableRequest): Promise<CloudflareDisableResult>;
247
+ }
248
+
249
+ export interface CloudflareDaemonClientOptions {
250
+ readonly configManager: Pick<ConfigManager, 'get'>;
251
+ readonly homeDirectory: string;
252
+ }
253
+
254
+ function connectHostForBindHost(host: string): string {
255
+ if (host === '0.0.0.0' || host === '::' || host.trim().length === 0) return '127.0.0.1';
256
+ return host;
257
+ }
258
+
259
+ function hostForUrl(host: string): string {
260
+ return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
261
+ }
262
+
263
+ export function resolveCloudflareDaemonBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
264
+ const configuredBaseUrl = String(configManager.get('controlPlane.baseUrl' as never) ?? '').trim();
265
+ if (configuredBaseUrl) return configuredBaseUrl.replace(/\/+$/, '');
266
+ const host = hostForUrl(connectHostForBindHost(String(configManager.get('controlPlane.host' as never) ?? '127.0.0.1')));
267
+ const portValue = Number(configManager.get('controlPlane.port' as never) ?? 3421);
268
+ const port = Number.isFinite(portValue) && portValue > 0 ? portValue : 3421;
269
+ return `http://${host}:${port}`;
270
+ }
271
+
272
+ export function buildDefaultCloudflareDaemonBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
273
+ return resolveCloudflareDaemonBaseUrl(configManager);
274
+ }
275
+
276
+ function readDaemonToken(homeDirectory: string): string {
277
+ const daemonHomeDir = join(homeDirectory, '.goodvibes', 'daemon');
278
+ return getOrCreateCompanionToken('tui', { daemonHomeDir }).token;
279
+ }
280
+
281
+ async function readJsonResponse<T>(response: Response): Promise<T> {
282
+ const text = await response.text();
283
+ const body = text.trim().length > 0 ? JSON.parse(text) as unknown : {};
284
+ if (!response.ok) {
285
+ const record = body && typeof body === 'object' ? body as Record<string, unknown> : {};
286
+ const message = typeof record.error === 'string' ? record.error : `Cloudflare daemon route failed with HTTP ${response.status}`;
287
+ const code = typeof record.code === 'string' ? record.code : 'CLOUDFLARE_DAEMON_ROUTE_ERROR';
288
+ throw new CloudflareDaemonRouteError(message, response.status, code);
289
+ }
290
+ return body as T;
291
+ }
292
+
293
+ export function createCloudflareDaemonClient(options: CloudflareDaemonClientOptions): CloudflareDaemonClient {
294
+ const baseUrl = resolveCloudflareDaemonBaseUrl(options.configManager);
295
+ const token = readDaemonToken(options.homeDirectory);
296
+
297
+ const requestJson = async <T>(path: string, init: RequestInit = {}): Promise<T> => {
298
+ const headers = new Headers(init.headers);
299
+ headers.set('Authorization', `Bearer ${token}`);
300
+ if (init.body !== undefined) headers.set('Content-Type', 'application/json');
301
+ const response = await fetch(`${baseUrl}${path}`, { ...init, headers });
302
+ return await readJsonResponse<T>(response);
303
+ };
304
+
305
+ const postJson = <T>(path: string, body: unknown): Promise<T> => requestJson<T>(path, {
306
+ method: 'POST',
307
+ body: JSON.stringify(body ?? {}),
308
+ });
309
+
310
+ return {
311
+ status: () => requestJson<CloudflareControlPlaneStatus>('/api/cloudflare/status'),
312
+ tokenRequirements: (input = {}) => postJson<CloudflareTokenRequirementsResult>('/api/cloudflare/token/requirements', input),
313
+ createOperationalToken: (input) => postJson<CloudflareOperationalTokenResult>('/api/cloudflare/token/create', input),
314
+ discover: (input = {}) => postJson<CloudflareDiscoverResult>('/api/cloudflare/discover', input),
315
+ validate: (input = {}) => postJson<CloudflareValidateResult>('/api/cloudflare/validate', input),
316
+ provision: (input) => postJson<CloudflareProvisionResult>('/api/cloudflare/provision', input),
317
+ verify: (input = {}) => postJson<CloudflareVerifyResult>('/api/cloudflare/verify', input),
318
+ disable: (input = {}) => postJson<CloudflareDisableResult>('/api/cloudflare/disable', input),
319
+ };
320
+ }
321
+
322
+ export function normalizeCloudflareComponents(selection: CloudflareComponentSelection | undefined): Record<CloudflareComponent, boolean> {
323
+ const result: Record<CloudflareComponent, boolean> = { ...DEFAULT_CLOUDFLARE_COMPONENT_SELECTION };
324
+ for (const component of CLOUDFLARE_COMPONENT_IDS) {
325
+ if (typeof selection?.[component] === 'boolean') result[component] = selection[component] === true;
326
+ }
327
+ return result;
328
+ }
@@ -238,6 +238,15 @@ function hasExternalIntegrations(snapshot: OnboardingSnapshotState): boolean {
238
238
  || countConfiguredSurfaceKinds(snapshot) > 0;
239
239
  }
240
240
 
241
+ function hasCloudflareBatch(snapshot: OnboardingSnapshotState): boolean {
242
+ return snapshot.config.cloudflare.enabled
243
+ || snapshot.config.batch.queueBackend === 'cloudflare'
244
+ || snapshot.config.batch.mode !== 'off'
245
+ || snapshot.config.cloudflare.accountId.trim().length > 0
246
+ || snapshot.config.cloudflare.apiTokenRef.trim().length > 0
247
+ || snapshot.config.cloudflare.workerBaseUrl.trim().length > 0;
248
+ }
249
+
241
250
  function describeLocalTuiOnly(snapshot: OnboardingSnapshotState): string {
242
251
  if (!hasAnyServerEnabled(snapshot)) {
243
252
  return 'Use GoodVibes only in this terminal. No browser access, background service, HTTP listener, external app surface, or network setup.';
@@ -278,6 +287,14 @@ function describeExternalIntegrations(snapshot: OnboardingSnapshotState): string
278
287
  return `Review and configure ${integrationCount} detected external app, service, or surface integration signal(s).`;
279
288
  }
280
289
 
290
+ function describeCloudflareBatch(snapshot: OnboardingSnapshotState): string {
291
+ if (hasCloudflareBatch(snapshot)) {
292
+ return 'Review Cloudflare Workers/Queues batch processing, token storage, and optional remote daemon provisioning settings.';
293
+ }
294
+
295
+ return 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. Immediate local daemon behavior stays the default unless enabled.';
296
+ }
297
+
281
298
  function getAcknowledgementAccepted(
282
299
  snapshot: OnboardingSnapshotState,
283
300
  target: OnboardingAcknowledgementTarget,
@@ -346,6 +363,12 @@ export function deriveStep1Capabilities(
346
363
  selected: hasExternalIntegrations(snapshot),
347
364
  detail: describeExternalIntegrations(snapshot),
348
365
  },
366
+ {
367
+ id: 'cloudflare-batch',
368
+ label: 'Use Cloudflare for batch or remote daemon work',
369
+ selected: hasCloudflareBatch(snapshot),
370
+ detail: describeCloudflareBatch(snapshot),
371
+ },
349
372
  ];
350
373
  }
351
374
 
@@ -360,6 +383,7 @@ export function deriveStep1CapabilityFlags(
360
383
  readonly httpListener: boolean;
361
384
  readonly web: boolean;
362
385
  readonly surfaces: boolean;
386
+ readonly cloudflare: boolean;
363
387
  } {
364
388
  return {
365
389
  providers: hasConfiguredProviderState(snapshot) || hasCustomizedProviderRouting(snapshot),
@@ -372,6 +396,7 @@ export function deriveStep1CapabilityFlags(
372
396
  httpListener: snapshot.bindSettings.httpListenerEnabled,
373
397
  web: snapshot.bindSettings.web.enabled,
374
398
  surfaces: countConfiguredSurfaceKinds(snapshot) > 0,
399
+ cloudflare: hasCloudflareBatch(snapshot),
375
400
  };
376
401
  }
377
402
 
@@ -41,6 +41,8 @@ function buildConfigSnapshot(
41
41
  surfaces: config.getCategory('surfaces'),
42
42
  service: config.getCategory('service'),
43
43
  featureFlags: config.getCategory('featureFlags'),
44
+ batch: config.getCategory('batch'),
45
+ cloudflare: config.getCategory('cloudflare'),
44
46
  };
45
47
  }
46
48
 
@@ -47,6 +47,8 @@ export interface OnboardingConfigSnapshot {
47
47
  readonly surfaces: GoodVibesConfig['surfaces'];
48
48
  readonly service: GoodVibesConfig['service'];
49
49
  readonly featureFlags: GoodVibesConfig['featureFlags'];
50
+ readonly batch: GoodVibesConfig['batch'];
51
+ readonly cloudflare: GoodVibesConfig['cloudflare'];
50
52
  }
51
53
 
52
54
  export interface OnboardingProviderRoutingSnapshot {
@@ -196,7 +198,8 @@ export type OnboardingStep1CapabilityId =
196
198
  | 'browser-access'
197
199
  | 'network-access'
198
200
  | 'webhook-events'
199
- | 'external-integrations';
201
+ | 'external-integrations'
202
+ | 'cloudflare-batch';
200
203
 
201
204
  export interface OnboardingStep1CapabilityItem {
202
205
  readonly id: OnboardingStep1CapabilityId;
@@ -214,6 +217,7 @@ export interface OnboardingStep1CapabilityFlags {
214
217
  readonly httpListener: boolean;
215
218
  readonly web: boolean;
216
219
  readonly surfaces: boolean;
220
+ readonly cloudflare: boolean;
217
221
  }
218
222
 
219
223
  export interface OnboardingAcknowledgementState {
@@ -111,6 +111,22 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
111
111
  return new Set(results.filter((v): v is string => v !== null));
112
112
  }
113
113
 
114
+ const getCurrentModelForPickerTarget = (): string => {
115
+ const target = input.modelPicker.target;
116
+ if (target === 'helper') return String(configManager.get('helper.globalModel') || runtime.model);
117
+ if (target === 'tool') return String(configManager.get('tools.llmModel') || runtime.model);
118
+ if (target === 'tts') return String(configManager.get('tts.llmModel') || runtime.model);
119
+ return runtime.model;
120
+ };
121
+
122
+ const getCurrentProviderForPickerTarget = (): string => {
123
+ const target = input.modelPicker.target;
124
+ if (target === 'helper') return String(configManager.get('helper.globalProvider') || runtime.provider);
125
+ if (target === 'tool') return String(configManager.get('tools.llmProvider') || runtime.provider);
126
+ if (target === 'tts') return String(configManager.get('tts.llmProvider') || runtime.provider);
127
+ return runtime.provider;
128
+ };
129
+
114
130
  commandContext.openModelPicker = () => {
115
131
  void (async () => {
116
132
  const models = providerRegistry.getSelectableModels();
@@ -124,7 +140,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
124
140
  });
125
141
  void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
126
142
  input.modalOpened('modelPicker');
127
- input.modelPicker.openAllModels(models, runtime.model);
143
+ input.modelPicker.openAllModels(models, getCurrentModelForPickerTarget());
128
144
  render();
129
145
  })().catch((error: unknown) => {
130
146
  commandContext.print?.(`Model picker failed to open: ${error instanceof Error ? error.message : String(error)}`);
@@ -132,6 +148,9 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
132
148
  });
133
149
  };
134
150
 
151
+ commandContext.openModelPickerWithTarget = (target) => input.openModelPickerWithTarget(target);
152
+ commandContext.openProviderModelPickerWithTarget = (target) => input.openProviderModelPickerWithTarget(target);
153
+
135
154
  commandContext.openProviderPicker = () => {
136
155
  void (async () => {
137
156
  const providers = [...new Set(providerRegistry.listModels().map((model) => model.provider))];
@@ -140,7 +159,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
140
159
  const secretProviderIds = await resolveSecretProviderIds();
141
160
  input.modelPicker.configuredViaMap = buildConfiguredViaMap(providers, configuredIds, subscriptionManager, secretProviderIds);
142
161
  input.modalOpened('modelPicker');
143
- input.modelPicker.openProviders(providers, runtime.provider);
162
+ input.modelPicker.openProviders(providers, getCurrentProviderForPickerTarget());
144
163
  render();
145
164
  })().catch((error: unknown) => {
146
165
  commandContext.print?.(`Provider picker failed to open: ${error instanceof Error ? error.message : String(error)}`);
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.32';
9
+ let _version = '0.19.34';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;