@pellux/goodvibes-tui 0.19.0 → 0.19.3

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 CHANGED
@@ -4,14 +4,78 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.3] - 2026-04-18
8
+
9
+ ### Fixed
10
+ - Extracted CLI flag parsing (`--provider`, `--model`) from `src/main.ts` and `src/daemon/cli.ts` into a shared `src/cli-flags.ts` module. `src/main.ts` now passes the 800-line architecture-check gate that was tripped in 0.19.1 and 0.19.2 (CI failures on both prior releases).
11
+
12
+ ### Changed
13
+ - No user-visible behavior change. Pure structural refactor.
14
+
15
+ ### Note
16
+ - `v0.19.1` and `v0.19.2` GitHub releases have 0 binary assets due to the architecture-check CI failure. Consumers should upgrade to `0.19.3` for access to the binaries + consistent release state.
17
+
18
+ ---
19
+
20
+ ## [0.19.2] — 2026-04-18
21
+
22
+ ### Fixed
23
+ - Daemon test: `channel account APIs expose surface auth and secret posture` — corrected
24
+ `GET /api/providers` assertions from legacy `providerId` field to current `id` field
25
+ (SDK 0.21.x provider-routes.ts renamed the field). The subscription-oauth auth routes
26
+ assertion was dropped from the list endpoint (it only exists on per-provider snapshots).
27
+ - Model picker now correctly surfaces `configuredVia='secrets'` tier as `[key]` badge
28
+ (previously all secrets-manager-keyed providers were collapsed to `[env]`). Secrets are
29
+ pre-resolved async before the picker renders; `secretsManager` is now threaded from
30
+ `RuntimeServices` through `wireShellUiOpeners`.
31
+
32
+ ---
33
+
34
+ ## [0.19.1] — 2026-04-17
35
+
36
+ ### Changed
37
+ - Upgraded `@pellux/goodvibes-sdk` from 0.21.1 to 0.21.6 — includes secrets-tier
38
+ `configuredVia` discriminator, HTTP `/api/providers` + `/api/providers/current` (GET/PATCH),
39
+ reactive `model.changed` SSE event, `BUILTIN_LABEL_MAP` brand-accurate provider labels,
40
+ and clean unconfigured-provider errors instead of silent 401 pass-through.
41
+
42
+ ### Added
43
+ - `--provider <id>` and `--model <registryKey>` CLI flags for daemon startup
44
+ (`goodvibes-daemon`) and TUI shell startup (`goodvibes`). Both override
45
+ `~/.goodvibes/tui/settings.json` provider/model values for the session.
46
+ If `--model provider:modelId` format is used, `--provider` is inferred automatically.
47
+ - TUI model picker now populates `configuredVia` per provider and renders it as
48
+ a badge (`[env]`, `[sub]`, `[anon]`) in the provider-browse view, derived from
49
+ env-var presence and active OAuth subscription sessions.
50
+
51
+ ### Fixed
52
+ - **Venice-picking diagnosis**: the silent 401 Venice fallback was caused by the
53
+ pre-0.21.2 SDK passing unconfigured-provider requests through to the upstream API
54
+ unchanged. As of 0.21.2 (included in 0.21.6), `createCompanionProviderAdapter` now
55
+ checks `provider.isConfigured()` before `provider.chat()` and yields a structured
56
+ error immediately. The in-registry fallback to first-selectable-model (which may
57
+ resolve to venice on some installs) only triggers when `getCurrentModel()` cannot
58
+ find the stored `currentModelId` in the model registry — typically an old daemon
59
+ instance with stale config. Restarting the daemon after a settings.json change
60
+ resolves this; the `--model` CLI flag eliminates the need to edit settings.json
61
+ manually. ConfigManager reactivity (live disk-change → registry update) is out of
62
+ scope: the PATCH `/api/providers/current` route is the correct live-switch path
63
+ (calls `setCurrentModel()` + `configManager.set()` atomically).
64
+
65
+
7
66
  ## [0.19.0] — 2026-04-18
8
67
 
9
68
  ### Changed
10
69
  - Upgraded `@pellux/goodvibes-sdk` from 0.19.6 to 0.21.1 (soak-period release).
11
- TUI adaptation required: regenerated `docs/foundation-artifacts/operator-contract.json`
12
- to match the updated `buildOperatorContract()` output in the new SDK version.
13
- `peer-contract.json`, `knowledge-graphql.graphql`, and `knowledge-store.sql` were
14
- unchanged by this SDK bump.
70
+ TUI adaptations required:
71
+ 1. `docs/foundation-artifacts/operator-contract.json` regenerated to match
72
+ updated `buildOperatorContract()` output (`peer-contract.json`, knowledge
73
+ artifacts unchanged).
74
+ 2. `scripts/perf-check.ts` — `platform/runtime/perf/index` barrel removed;
75
+ `createPerfMonitor()` factory removed; migrated to `new PerfMonitor()` with
76
+ imports split to `perf/monitor` and `perf/reporter` sub-paths.
77
+ 3. `scripts/eval-gate.ts` — `platform/runtime/eval/index` barrel removed;
78
+ imports split to `eval/baseline`, `eval/format`, and `eval/scorecard`.
15
79
 
16
80
  ### Added
17
81
  - Wave B panel migration: migrated 5 panels (knowledge, marketplace, memory,
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.0-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.3-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.21.1"
6
+ "version": "0.21.6"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.0",
3
+ "version": "0.19.3",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -89,7 +89,7 @@
89
89
  "@anthropic-ai/vertex-sdk": "^0.16.0",
90
90
  "@ast-grep/napi": "^0.42.0",
91
91
  "@aws/bedrock-token-generator": "^1.1.0",
92
- "@pellux/goodvibes-sdk": "0.21.1",
92
+ "@pellux/goodvibes-sdk": "0.21.6",
93
93
  "bash-language-server": "^5.6.0",
94
94
  "fuse.js": "^7.1.0",
95
95
  "graphql": "^16.13.2",
@@ -0,0 +1,48 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared CLI flag parsing for TUI shell and daemon entrypoints.
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export type CliFlags = {
6
+ readonly provider: string | undefined;
7
+ readonly model: string | undefined;
8
+ };
9
+
10
+ /**
11
+ * Parse `--provider` / `--model` / `--help` flags from an argv slice.
12
+ *
13
+ * @param argv - argv array (pass `process.argv.slice(2)`)
14
+ * @param binary - binary name shown in the --help usage line (e.g. "goodvibes" or "goodvibes-daemon")
15
+ */
16
+ export function parseCliFlags(argv: readonly string[], binary = 'goodvibes'): CliFlags {
17
+ let provider: string | undefined;
18
+ let model: string | undefined;
19
+
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const arg = argv[i];
22
+ if (arg === '--help' || arg === '-h') {
23
+ // eslint-disable-next-line no-console
24
+ console.log([
25
+ `Usage: ${binary} [options]`,
26
+ '',
27
+ 'Options:',
28
+ ' --provider <id> Override the provider from settings.json at startup',
29
+ ' --model <registryKey> Override the model from settings.json at startup',
30
+ ' Format: provider:modelId (e.g. inception:mercury-2)',
31
+ ' If provider:modelId format is used, --provider is inferred',
32
+ ' --help, -h Show this help message',
33
+ ].join('\n'));
34
+ process.exit(0);
35
+ }
36
+ if (arg === '--provider' && argv[i + 1] !== undefined) {
37
+ provider = argv[++i];
38
+ } else if (arg === '--model' && argv[i + 1] !== undefined) {
39
+ model = argv[++i];
40
+ // Infer provider from registryKey format (provider:modelId) if --provider not given
41
+ if (typeof model === 'string' && model.includes(':') && provider === undefined) {
42
+ provider = model.split(':')[0];
43
+ }
44
+ }
45
+ }
46
+
47
+ return { provider, model };
48
+ }
package/src/daemon/cli.ts CHANGED
@@ -17,11 +17,14 @@ import {
17
17
  } from '@pellux/goodvibes-sdk/platform/pairing/index';
18
18
  import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
19
19
 
20
+ import { parseCliFlags } from '../cli-flags.ts';
20
21
  type DaemonCliOwnership = {
21
22
  readonly workingDirectory: string;
22
23
  readonly homeDirectory: string;
23
24
  };
24
25
 
26
+ // CLI flag parsing delegated to shared module — see src/cli-flags.ts
27
+
25
28
  type DaemonCliTokens = {
26
29
  readonly daemonToken: string | undefined;
27
30
  readonly httpToken: string | undefined;
@@ -72,6 +75,17 @@ async function main(): Promise<void> {
72
75
  const { workingDirectory: workingDir, homeDirectory } = resolveDaemonCliOwnership();
73
76
  const config = new ConfigManager({ workingDir, homeDir: homeDirectory, surfaceRoot: 'tui' });
74
77
  new GlobalNetworkTransportInstaller().install(config);
78
+
79
+ // Apply CLI flags — override settings.json before the provider registry is constructed
80
+ const cliFlags = parseCliFlags(process.argv.slice(2), 'goodvibes-daemon');
81
+ if (cliFlags.provider !== undefined) {
82
+ config.set('provider.provider', cliFlags.provider);
83
+ logger.info('daemon: --provider flag applied', { provider: cliFlags.provider });
84
+ }
85
+ if (cliFlags.model !== undefined) {
86
+ config.set('provider.model', cliFlags.model);
87
+ logger.info('daemon: --model flag applied', { model: cliFlags.model });
88
+ }
75
89
  const runtimeBus = new RuntimeEventBus();
76
90
  const runtimeStore = createRuntimeStore();
77
91
  const runtimeServices = createRuntimeServices({
@@ -99,6 +99,8 @@ export interface PickerItem {
99
99
  isFree?: boolean;
100
100
  /** True when this provider item has a configured API key. */
101
101
  isConfigured?: boolean;
102
+ /** How the provider is configured — shown as a badge in provider mode. */
103
+ configuredVia?: 'env' | 'secrets' | 'subscription' | 'anonymous';
102
104
  }
103
105
 
104
106
  /** Provider IDs treated as "Popular" in the provider picker. */
@@ -166,6 +168,8 @@ export class ModelPickerModal {
166
168
  public availableOnly = true;
167
169
  /** Set of provider names that have a configured key (used for availableOnly filter). */
168
170
  public configuredProviders: Set<string> = new Set();
171
+ /** How each provider is configured — drives badge display in provider mode. */
172
+ public configuredViaMap: Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'> = new Map();
169
173
  /** IDs of pinned/favorite models — shown at top of list. */
170
174
  public pinnedIds: Set<string> = new Set();
171
175
  /** IDs of recently used models — shown after pinned, before the rest. */
@@ -612,13 +616,13 @@ export class ModelPickerModal {
612
616
  if (filteredPopular.length > 0) {
613
617
  providerItems.push({ id: '__header__popular', label: 'Popular', isGroupHeader: true });
614
618
  for (const p of filteredPopular) {
615
- providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p) });
619
+ providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
616
620
  }
617
621
  }
618
622
  if (filteredAll.length > 0) {
619
623
  providerItems.push({ id: '__header__all', label: 'All Providers', isGroupHeader: true });
620
624
  for (const p of filteredAll) {
621
- providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p) });
625
+ providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
622
626
  }
623
627
  }
624
628
 
package/src/main.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env bun
2
- // Main shell entrypoint. Composition-heavy startup remains here, with
3
- // lower-level session/bootstrap/input helpers extracted into focused modules.
4
2
  import { homedir } from 'node:os';
5
3
  import { join } from 'node:path';
6
4
  import { Compositor } from './renderer/compositor.ts';
@@ -54,7 +52,7 @@ import { deriveComposerState } from './core/composer-state.ts';
54
52
  import { buildPersistedSessionContext, formatReturnContextForDisplay, getReturnContextMode, maybeAssistReturnContextSummary } from '@pellux/goodvibes-sdk/platform/runtime/session-return-context';
55
53
  import { GlobalNetworkTransportInstaller } from '@pellux/goodvibes-sdk/platform/runtime/network/index';
56
54
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
57
-
55
+ import { parseCliFlags } from './cli-flags.ts';
58
56
 
59
57
  const ALT_SCREEN_ENTER = '\x1b[?1049h';
60
58
  const ALT_SCREEN_EXIT = '\x1b[?1049l';
@@ -95,10 +93,16 @@ async function main() {
95
93
  });
96
94
  new GlobalNetworkTransportInstaller().install(configManager);
97
95
 
98
- // ── Bootstrap all runtime subsystems ─────────────────────────────────────
99
- // bootstrapRuntime initializes all subsystems in dependency order and returns
100
- // a fully-wired BootstrapContext. main.ts owns terminal setup, the render loop,
101
- // stdin input, and signal handlers — everything else is in bootstrap.
96
+ // Apply CLI flags override settings.json before the provider registry is constructed
97
+ const cliFlags = parseCliFlags(process.argv.slice(2), 'goodvibes');
98
+ if (cliFlags.provider !== undefined) {
99
+ configManager.set('provider.provider', cliFlags.provider);
100
+ }
101
+ if (cliFlags.model !== undefined) {
102
+ configManager.set('provider.model', cliFlags.model);
103
+ }
104
+
105
+ // ── Bootstrap runtime subsystems via bootstrapRuntime.
102
106
  const ctx: BootstrapContext = await bootstrapRuntime(stdout, {
103
107
  configManager,
104
108
  workingDir: bootstrapWorkingDir,
@@ -128,7 +132,7 @@ async function main() {
128
132
  } = ctx;
129
133
  const workingDir = ctx.services.workingDirectory;
130
134
  const homeDirectory = ctx.services.homeDirectory;
131
- const { approvalBroker, agentManager, modeManager, processManager, providerRegistry } = ctx.services;
135
+ const { approvalBroker, agentManager, modeManager, processManager, providerRegistry, secretsManager, subscriptionManager } = ctx.services;
132
136
  conversation.setSessionMemoryStore(ctx.services.sessionMemoryStore);
133
137
  conversation.setSessionLineageTracker(ctx.services.sessionLineageTracker);
134
138
  orchestrator.setCoreServices({
@@ -381,9 +385,6 @@ async function main() {
381
385
  });
382
386
 
383
387
  // ── InputHandler — created here so getViewportHeight can reference it ──────
384
- // orchestratorRefs.getViewportHeight and .scrollToEnd are patched immediately after.
385
-
386
- // ── InputHandler ────────────────────────────────────────────────────────
387
388
  const input: InputHandler = new InputHandler(
388
389
  () => render(),
389
390
  selection,
@@ -450,7 +451,6 @@ async function main() {
450
451
  toolCount,
451
452
  };
452
453
 
453
- // Sessions start fresh — use /session resume to load a previous session
454
454
 
455
455
  // --- Render function ---
456
456
  const render = () => {
@@ -462,7 +462,6 @@ async function main() {
462
462
  const sessionSnapshot = uiServices.readModels.session.getSnapshot();
463
463
  const agentSnapshot = uiServices.readModels.agents.getSnapshot();
464
464
 
465
-
466
465
  // Build header and footer FIRST so we know the exact viewport height
467
466
  const headerLines = UIFactory.createHeader(width, currentModel.id, currentModel.provider, conversation.title || undefined, lastGitInfoRef.value);
468
467
  const managerAgents = agentManager.list().filter(
@@ -683,7 +682,8 @@ async function main() {
683
682
  runtime,
684
683
  featureFlags: ctx.featureFlags,
685
684
  mcpRegistry: ctx.services.mcpRegistry,
686
- subscriptionManager: ctx.services.subscriptionManager,
685
+ subscriptionManager,
686
+ secretsManager,
687
687
  serviceRegistry: ctx.services.serviceRegistry,
688
688
  getConfiguredProviderIds: ctx._getConfiguredProviderIds,
689
689
  getPinned: ctx._getPinned,
@@ -691,6 +691,7 @@ async function main() {
691
691
  });
692
692
 
693
693
  // --- Streaming speed + tool preview wiring ---
694
+ const refreshGit = () => gitStatusProvider.refresh().then((info) => { lastGitInfoRef.value = info; render(); }).catch(() => { /* non-fatal */ });
694
695
  // Refresh git status after each turn completes or after tool results arrive
695
696
  unsubs.push(uiServices.events.turns.on('TURN_COMPLETED', () => {
696
697
  // Auto-save after every LLM turn so kills don't lose the session
@@ -707,22 +708,13 @@ async function main() {
707
708
  );
708
709
  hookDispatcher.fire({ path: 'Lifecycle:session:save' as HookEventPath, phase: 'Lifecycle' as HookPhase, category: 'session' as HookCategory, specific: 'save', sessionId: runtime.sessionId, timestamp: Date.now(), payload: { sessionId: runtime.sessionId } }).catch((err: unknown) => logger.debug('hook fire error', { error: summarizeError(err) }));
709
710
  } catch (e) { logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) }); }
710
- gitStatusProvider.refresh().then((info) => {
711
- lastGitInfoRef.value = info;
712
- render();
713
- }).catch(() => { /* non-fatal */ });
711
+ refreshGit();
714
712
  }));
715
713
  unsubs.push(uiServices.events.tools.on('TOOL_SUCCEEDED', () => {
716
- gitStatusProvider.refresh().then((info) => {
717
- lastGitInfoRef.value = info;
718
- render();
719
- }).catch(() => { /* non-fatal */ });
714
+ refreshGit();
720
715
  }));
721
716
  unsubs.push(uiServices.events.tools.on('TOOL_FAILED', () => {
722
- gitStatusProvider.refresh().then((info) => {
723
- lastGitInfoRef.value = info;
724
- render();
725
- }).catch(() => { /* non-fatal */ });
717
+ refreshGit();
726
718
  }));
727
719
 
728
720
  unsubs.push(uiServices.events.turns.on('STREAM_START', () => {
@@ -294,12 +294,19 @@ export function renderModelPickerOverlay(
294
294
  const isSelected = selectableIdx === picker.selectedIndex;
295
295
  const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
296
296
  const checkmark = item.isConfigured ? '✓ ' : ' ';
297
- const labelW = contentW - 2 - 2; // indicator(2) + checkmark(2)
297
+ // configuredVia badge: right-aligned short label (env/sub/anon)
298
+ const viaBadge = item.configuredVia === 'env' ? ' [env]'
299
+ : item.configuredVia === 'secrets' ? ' [key]'
300
+ : item.configuredVia === 'subscription' ? ' [sub]'
301
+ : item.configuredVia === 'anonymous' ? ' [anon]'
302
+ : '';
303
+ const badgeW = viaBadge.length;
304
+ const labelW = contentW - 2 - 2 - badgeW; // indicator(2) + checkmark(2) + badge
298
305
  const labelStr = item.label.length > labelW
299
306
  ? item.label.slice(0, labelW - 3) + '...'
300
307
  : item.label.padEnd(labelW);
301
308
  const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
302
- const rowText = indicator + checkmark + labelStr;
309
+ const rowText = indicator + checkmark + labelStr + viaBadge;
303
310
  putRowText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
304
311
  lines.push(row);
305
312
  }
@@ -8,6 +8,7 @@ import type { MutableRuntimeState } from '@pellux/goodvibes-sdk/platform/runtime
8
8
  import type { FeatureFlagManager } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/index';
9
9
  import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp/registry';
10
10
  import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
11
+ import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config/secrets';
11
12
  import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
12
13
 
13
14
  type WireShellUiOpenersOptions = {
@@ -21,12 +22,57 @@ type WireShellUiOpenersOptions = {
21
22
  featureFlags: FeatureFlagManager;
22
23
  mcpRegistry: McpRegistry;
23
24
  subscriptionManager: SubscriptionManager;
25
+ secretsManager?: Pick<SecretsManager, 'get'>;
24
26
  serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
25
27
  getConfiguredProviderIds: () => string[];
26
28
  getPinned: () => Promise<string[]>;
27
29
  render: () => void;
28
30
  };
29
31
 
32
+ /**
33
+ * Derive the configuredVia tier for a provider.
34
+ * Tier order mirrors SDK provider-routes.ts: env → secrets → subscription → undefined.
35
+ * The preResolvedSecretKeys set is pre-fetched async before the sync picker render cycle.
36
+ */
37
+ function deriveConfiguredVia(
38
+ providerId: string,
39
+ configuredIds: Set<string>,
40
+ subscriptionManager: SubscriptionManager,
41
+ preResolvedSecretKeys?: ReadonlySet<string>,
42
+ ): 'env' | 'secrets' | 'subscription' | 'anonymous' | undefined {
43
+ if (!configuredIds.has(providerId)) return undefined;
44
+
45
+ // Tier 1: subscription check (most specific — subscription overrides env for this provider)
46
+ const subs = subscriptionManager.list();
47
+ if (subs.some((s) => s.provider === providerId)) return 'subscription';
48
+
49
+ // Tier 2: env-var present (process.env check; anonymous providers don't appear in configuredIds)
50
+ // We don't have BUILTIN_PROVIDER_ENV_KEYS here; if env was used the configuredIds path covers it.
51
+ // The presence in configuredIds and no subscription → either env or secrets.
52
+ // Tier 3: secrets-manager backed (pre-resolved async batch)
53
+ if (preResolvedSecretKeys && preResolvedSecretKeys.has(providerId)) return 'secrets';
54
+
55
+ return 'env';
56
+ }
57
+
58
+ /**
59
+ * Build a configuredViaMap for the given provider list.
60
+ * Pass preResolvedSecretKeys (from an async SecretsManager batch) to surface the 'secrets' tier.
61
+ */
62
+ function buildConfiguredViaMap(
63
+ providers: string[],
64
+ configuredIds: Set<string>,
65
+ subscriptionManager: SubscriptionManager,
66
+ preResolvedSecretKeys?: ReadonlySet<string>,
67
+ ): Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'> {
68
+ const map = new Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'>();
69
+ for (const p of providers) {
70
+ const via = deriveConfiguredVia(p, configuredIds, subscriptionManager, preResolvedSecretKeys);
71
+ if (via !== undefined) map.set(p, via);
72
+ }
73
+ return map;
74
+ }
75
+
30
76
  export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
31
77
  const {
32
78
  commandContext,
@@ -39,30 +85,61 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
39
85
  featureFlags,
40
86
  mcpRegistry,
41
87
  subscriptionManager,
88
+ secretsManager,
42
89
  serviceRegistry,
43
90
  getConfiguredProviderIds,
44
91
  getPinned,
45
92
  render,
46
93
  } = options;
47
94
 
95
+ /**
96
+ * Pre-resolve which provider IDs have secrets-manager keys (async batch, SDK tier pattern).
97
+ * Returns a set of provider IDs (not env var names) that are secrets-backed.
98
+ * Falls back to empty set if secretsManager is not provided.
99
+ */
100
+ async function resolveSecretProviderIds(): Promise<ReadonlySet<string>> {
101
+ if (!secretsManager) return new Set<string>();
102
+ const configuredIds = new Set(getConfiguredProviderIds());
103
+ // For each configured provider, check if secretsManager has a key for it by provider ID.
104
+ // We use provider ID as the lookup key since we don't have BUILTIN_PROVIDER_ENV_KEYS here.
105
+ const results = await Promise.all(
106
+ [...configuredIds].map(async (providerId) => {
107
+ const val = await secretsManager.get(providerId).catch(() => null);
108
+ return val !== null ? providerId : null;
109
+ }),
110
+ );
111
+ return new Set(results.filter((v): v is string => v !== null));
112
+ }
113
+
48
114
  commandContext.openModelPicker = () => {
49
- const models = providerRegistry.getSelectableModels();
50
- input.modelPicker.configuredProviders = new Set(getConfiguredProviderIds());
51
- void getPinned().then((pinned) => {
52
- input.modelPicker.pinnedIds = new Set(pinned);
53
- });
54
- void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
55
- input.modalOpened('modelPicker');
56
- input.modelPicker.openAllModels(models, runtime.model);
57
- render();
115
+ void (async () => {
116
+ const models = providerRegistry.getSelectableModels();
117
+ const configuredIds = new Set(getConfiguredProviderIds());
118
+ input.modelPicker.configuredProviders = configuredIds;
119
+ const providerIds = [...new Set(models.map((m) => m.provider))];
120
+ const secretProviderIds = await resolveSecretProviderIds();
121
+ input.modelPicker.configuredViaMap = buildConfiguredViaMap(providerIds, configuredIds, subscriptionManager, secretProviderIds);
122
+ void getPinned().then((pinned) => {
123
+ input.modelPicker.pinnedIds = new Set(pinned);
124
+ });
125
+ void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
126
+ input.modalOpened('modelPicker');
127
+ input.modelPicker.openAllModels(models, runtime.model);
128
+ render();
129
+ })();
58
130
  };
59
131
 
60
132
  commandContext.openProviderPicker = () => {
61
- const providers = [...new Set(providerRegistry.listModels().map((model) => model.provider))];
62
- input.modelPicker.configuredProviders = new Set(getConfiguredProviderIds());
63
- input.modalOpened('modelPicker');
64
- input.modelPicker.openProviders(providers, runtime.provider);
65
- render();
133
+ void (async () => {
134
+ const providers = [...new Set(providerRegistry.listModels().map((model) => model.provider))];
135
+ const configuredIds = new Set(getConfiguredProviderIds());
136
+ input.modelPicker.configuredProviders = configuredIds;
137
+ const secretProviderIds = await resolveSecretProviderIds();
138
+ input.modelPicker.configuredViaMap = buildConfiguredViaMap(providers, configuredIds, subscriptionManager, secretProviderIds);
139
+ input.modalOpened('modelPicker');
140
+ input.modelPicker.openProviders(providers, runtime.provider);
141
+ render();
142
+ })();
66
143
  };
67
144
 
68
145
  commandContext.openSelection = (title, items, opts, callback) => {
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.0';
9
+ let _version = '0.19.3';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;