@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 +68 -4
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli-flags.ts +48 -0
- package/src/daemon/cli.ts +14 -0
- package/src/input/model-picker.ts +6 -2
- package/src/main.ts +18 -26
- package/src/renderer/model-picker-overlay.ts +9 -2
- package/src/shell/ui-openers.ts +91 -14
- package/src/version.ts +1 -1
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
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.
|
|
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",
|
package/src/cli-flags.ts
ADDED
|
@@ -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
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/shell/ui-openers.ts
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
input.modelPicker.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
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;
|