@hieplp/pi-account-switcher 0.2.2 → 0.2.4
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/package.json +3 -2
- package/src/commands/accounts/edit.ts +1 -1
- package/src/commands/accounts/shared/prompts.ts +35 -11
- package/src/commands/accounts/verify.ts +2 -1
- package/src/commands/base.ts +8 -2
- package/src/commands/models/list.ts +15 -5
- package/src/commands/models/remove.ts +1 -1
- package/src/commands/models/shared/base.ts +6 -2
- package/src/commands/providers/shared/prompts.ts +23 -24
- package/src/extension.ts +37 -5
- package/src/runtime/account-switcher-runtime.ts +17 -11
- package/src/utils/accounts.ts +14 -7
- package/src/utils/filterable-selector.ts +17 -2
- package/src/utils/models.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hieplp/pi-account-switcher",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension for quickly switching between multiple accounts/API keys per provider.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,5 +52,6 @@
|
|
|
52
52
|
"extensions": [
|
|
53
53
|
"./src/extension.ts"
|
|
54
54
|
]
|
|
55
|
-
}
|
|
55
|
+
},
|
|
56
|
+
"packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1"
|
|
56
57
|
}
|
|
@@ -19,7 +19,7 @@ class EditAccountCommand extends AccountCommand {
|
|
|
19
19
|
const original = await this.loadAndSelectAccount(ctx, "Select account to edit");
|
|
20
20
|
if (!original) return;
|
|
21
21
|
|
|
22
|
-
const updated = await new AccountConfigBuilder(ctx.ui, this.runtime.getProviders(), original).collect();
|
|
22
|
+
const updated = await new AccountConfigBuilder(ctx.ui, this.runtime.getProviders(), original).collect(true);
|
|
23
23
|
if (!updated) return;
|
|
24
24
|
|
|
25
25
|
await this.runtime.editAccount(original, updated);
|
|
@@ -37,12 +37,14 @@ export class AccountConfigBuilder {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async withProvider(): Promise<this> {
|
|
40
|
-
const
|
|
40
|
+
const choices = providerUtil.providerChoices(this.customProviders);
|
|
41
|
+
const choice = await uiUtil.filteredSelect(this.ui, "Provider", choices);
|
|
41
42
|
if (!choice) return this;
|
|
42
43
|
|
|
43
44
|
const raw = choice === "custom" ? await this.prompt("Custom provider", "provider-id").asText() : choice;
|
|
44
45
|
const provider = providerUtil.normalizeProvider(raw ?? "");
|
|
45
|
-
|
|
46
|
+
// If provider is empty (user cancelled), return early. collect() will detect missing provider and return undefined.
|
|
47
|
+
if (!provider) return this;
|
|
46
48
|
|
|
47
49
|
this.config.provider = provider;
|
|
48
50
|
this.customProvider = providerUtil.findProvider(provider, this.customProviders);
|
|
@@ -101,21 +103,32 @@ export class AccountConfigBuilder {
|
|
|
101
103
|
const { provider } = this.config;
|
|
102
104
|
if (!provider) return this;
|
|
103
105
|
|
|
106
|
+
const hasExistingCredentials =
|
|
107
|
+
!!this.config.env || !!this.config.providerApiKey || !!this.config.usesProviderApiKey || !!this.config.piAuth;
|
|
108
|
+
|
|
104
109
|
if (this.customProvider) {
|
|
105
110
|
const apiKey = await this.promptForCustomProviderApiKey(this.customProvider);
|
|
106
|
-
if (apiKey
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
if (apiKey) {
|
|
112
|
+
this.config.providerApiKey = apiKey;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
if (!hasExistingCredentials) {
|
|
116
|
+
if (this.customProvider.apiKey) {
|
|
110
117
|
this.config.usesProviderApiKey = true;
|
|
118
|
+
return this;
|
|
111
119
|
}
|
|
112
|
-
|
|
120
|
+
} else {
|
|
121
|
+
// Has existing credentials — fall through to let user choose "keep current" or update
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
const envKeys = providerUtil.requiredEnvKeysForProvider(provider, this.customProviders);
|
|
117
|
-
const envChoice = await this.ui.select("Credential env var", [
|
|
118
|
-
|
|
126
|
+
const envChoice = await this.ui.select("Credential env var", [
|
|
127
|
+
...envKeys,
|
|
128
|
+
"custom",
|
|
129
|
+
...(hasExistingCredentials ? ["keep current"] : []),
|
|
130
|
+
]);
|
|
131
|
+
if (!envChoice || envChoice === "keep current") return this;
|
|
119
132
|
|
|
120
133
|
const envName = envChoice === "custom" ? await this.prompt("Env var name", "PROVIDER_API_KEY").asText() : envChoice;
|
|
121
134
|
if (!envName) throw new Error("Env var name is required");
|
|
@@ -150,7 +163,12 @@ export class AccountConfigBuilder {
|
|
|
150
163
|
};
|
|
151
164
|
}
|
|
152
165
|
|
|
153
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Collect account configuration interactively.
|
|
168
|
+
* @param isEdit - When true, allows keeping existing credentials without re-entering them.
|
|
169
|
+
* Empty input for most fields will preserve the current value.
|
|
170
|
+
*/
|
|
171
|
+
async collect(isEdit = false): Promise<AccountConfig | undefined> {
|
|
154
172
|
await this.withProvider();
|
|
155
173
|
if (!this.config.provider) return undefined;
|
|
156
174
|
|
|
@@ -159,7 +177,13 @@ export class AccountConfigBuilder {
|
|
|
159
177
|
await this.withModel();
|
|
160
178
|
await this.withCredentials();
|
|
161
179
|
|
|
162
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
!isEdit &&
|
|
182
|
+
!this.config.env &&
|
|
183
|
+
!this.config.providerApiKey &&
|
|
184
|
+
!this.config.usesProviderApiKey &&
|
|
185
|
+
!this.config.piAuth
|
|
186
|
+
) {
|
|
163
187
|
this.ui.notify("No credentials configured. Account not saved.", "info");
|
|
164
188
|
return undefined;
|
|
165
189
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { AuthCredential, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { AccountSwitcher } from "@/runtime";
|
|
4
4
|
import type { AccountConfig, AccountSwitcherContext, ProviderConfig, SecretSource } from "@/types";
|
|
@@ -209,6 +209,7 @@ class VerifyAccountsCommand extends AccountCommand {
|
|
|
209
209
|
try {
|
|
210
210
|
ctx.ui.notify(`${prefix} ping: sending request via ${model.provider}/${model.id}...`, "info");
|
|
211
211
|
|
|
212
|
+
const { completeSimple } = await import("@earendil-works/pi-ai");
|
|
212
213
|
const response = await completeSimple(
|
|
213
214
|
model,
|
|
214
215
|
{
|
package/src/commands/base.ts
CHANGED
|
@@ -73,8 +73,14 @@ export abstract class BaseCommand implements Command {
|
|
|
73
73
|
return providerUtil.normalizeProvider(active?.provider ?? "") === provider.id;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
protected isActiveModel(ctx: AccountSwitcherContext, modelId: string): boolean {
|
|
77
|
-
|
|
76
|
+
protected isActiveModel(ctx: AccountSwitcherContext, modelId: string, provider?: string): boolean {
|
|
77
|
+
if (ctx.model?.id !== modelId) return false;
|
|
78
|
+
if (!provider) return true;
|
|
79
|
+
const providers = this.runtime.getProviders();
|
|
80
|
+
return (
|
|
81
|
+
providerUtil.normalizeProviderWithCustom(ctx.model.provider, providers) ===
|
|
82
|
+
providerUtil.normalizeProviderWithCustom(provider, providers)
|
|
83
|
+
);
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
abstract handler(ctx: AccountSwitcherContext, args?: string): Promise<void>;
|
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { AccountSwitcher } from "@/runtime";
|
|
3
3
|
import type { AccountSwitcherContext } from "@/types";
|
|
4
4
|
import { COMMANDS } from "@/constants";
|
|
5
|
-
import { errorUtil } from "@/utils";
|
|
5
|
+
import { errorUtil, providerUtil } from "@/utils";
|
|
6
6
|
import { ModelCommand } from "./shared";
|
|
7
7
|
|
|
8
8
|
export const useListModelsCommand = (pi: ExtensionAPI, runtime: AccountSwitcher) => {
|
|
@@ -18,9 +18,16 @@ class ListModelsCommand extends ModelCommand {
|
|
|
18
18
|
try {
|
|
19
19
|
await this.runtime.load();
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const providers = this.runtime.getProviders();
|
|
22
|
+
const activeAccount = this.runtime.getActiveAccount();
|
|
23
|
+
const activeProvider =
|
|
24
|
+
activeAccount?.piAuth?.provider ??
|
|
25
|
+
(activeAccount
|
|
26
|
+
? (providerUtil.findProvider(activeAccount.provider, providers)?.piAuthProvider ?? activeAccount.provider)
|
|
27
|
+
: undefined);
|
|
28
|
+
const provider = activeProvider ?? ctx.model?.provider;
|
|
22
29
|
if (!provider) {
|
|
23
|
-
ctx.ui.notify("No active model.", "info");
|
|
30
|
+
ctx.ui.notify("No active account or model.", "info");
|
|
24
31
|
return;
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -30,8 +37,11 @@ class ListModelsCommand extends ModelCommand {
|
|
|
30
37
|
return;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
40
|
+
const normalizedProvider = providerUtil.normalizeProviderWithCustom(provider, providers);
|
|
41
|
+
const currentBelongsToProvider =
|
|
42
|
+
ctx.model && providerUtil.normalizeProviderWithCustom(ctx.model.provider, providers) === normalizedProvider;
|
|
43
|
+
const currentId = currentBelongsToProvider ? ctx.model?.id : undefined;
|
|
44
|
+
const model = await this.pick(ctx, `Models (${normalizedProvider})`, models, (m) =>
|
|
35
45
|
m.id === currentId ? `${m.id} ✓` : m.id,
|
|
36
46
|
);
|
|
37
47
|
if (!model) return;
|
|
@@ -19,7 +19,7 @@ class RemoveModelCommand extends ModelCommand {
|
|
|
19
19
|
const providerConfig = await this.loadProvider(ctx);
|
|
20
20
|
if (!providerConfig) return;
|
|
21
21
|
|
|
22
|
-
const removable = (providerConfig.models ?? []).filter((m) => !this.isActiveModel(ctx, m.id));
|
|
22
|
+
const removable = (providerConfig.models ?? []).filter((m) => !this.isActiveModel(ctx, m.id, providerConfig.id));
|
|
23
23
|
if (removable.length === 0) {
|
|
24
24
|
ctx.ui.notify(`Provider "${providerConfig.id}" has no removable model configs.`, "info");
|
|
25
25
|
return;
|
|
@@ -13,13 +13,17 @@ export abstract class ModelCommand extends BaseCommand {
|
|
|
13
13
|
protected async loadProvider(ctx: AccountSwitcherContext): Promise<ProviderConfig | undefined> {
|
|
14
14
|
await this.runtime.load();
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Prefer the active account's provider so that switching to an account whose
|
|
17
|
+
// provider has no models yet still targets the correct (new) provider.
|
|
18
|
+
const activeAccount = this.runtime.getActiveAccount();
|
|
19
|
+
const providers = this.runtime.getProviders();
|
|
20
|
+
const provider = activeAccount?.provider ?? ctx.model?.provider;
|
|
17
21
|
if (!provider) {
|
|
18
22
|
ctx.ui.notify("No active model. Use models:list to select one first.", "info");
|
|
19
23
|
return undefined;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
const config = providerUtil.findProvider(provider,
|
|
26
|
+
const config = providerUtil.findProvider(provider, providers);
|
|
23
27
|
if (!config) {
|
|
24
28
|
ctx.ui.notify(`"${provider}" is a built-in provider. This command only works with custom providers.`, "info");
|
|
25
29
|
return undefined;
|
|
@@ -48,22 +48,22 @@ export class ProviderConfigBuilder {
|
|
|
48
48
|
|
|
49
49
|
async withId(): Promise<this> {
|
|
50
50
|
const raw = await this.prompt("Provider id", this.defaults.id).asText();
|
|
51
|
-
|
|
52
|
-
this.config.id
|
|
51
|
+
this.config.id = providerUtil.normalizeProvider(raw || this.defaults.id);
|
|
52
|
+
if (!this.config.id) throw new Error("Provider id is required");
|
|
53
53
|
return this;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async withLabel(): Promise<this> {
|
|
57
57
|
const id = this.config.id ?? "";
|
|
58
|
-
|
|
58
|
+
const hint = this.defaults.label ?? id;
|
|
59
|
+
this.config.label = (await this.prompt("Provider label", hint).asText()) ?? hint;
|
|
59
60
|
return this;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
async withBaseUrl(): Promise<this> {
|
|
63
|
-
this.config.baseUrl =
|
|
64
|
-
"Base URL (blank for account-only provider)",
|
|
65
|
-
this.defaults.baseUrl
|
|
66
|
-
).asText();
|
|
64
|
+
this.config.baseUrl =
|
|
65
|
+
(await this.prompt("Base URL (blank for account-only provider)", this.defaults.baseUrl).asText()) ??
|
|
66
|
+
this.defaults.baseUrl;
|
|
67
67
|
return this;
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -78,26 +78,23 @@ export class ProviderConfigBuilder {
|
|
|
78
78
|
|
|
79
79
|
async withApiKey(): Promise<this> {
|
|
80
80
|
const id = this.config.id ?? "";
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
this.
|
|
84
|
-
).asText();
|
|
81
|
+
const hint = this.defaults.apiKey ?? `${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
82
|
+
this.config.apiKey =
|
|
83
|
+
(await this.prompt("Pi apiKey env var/name or raw key", hint).asText()) ?? this.defaults.apiKey;
|
|
85
84
|
return this;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
async withEnvKeys(): Promise<this> {
|
|
89
88
|
const { apiKey } = this.config;
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
).asCsv();
|
|
89
|
+
const defaultKeys = this.defaults.envKeys ?? (apiKey ? [apiKey] : [DEFAULTS.envKey]);
|
|
90
|
+
const csv = await this.prompt("Env key suggestions (comma-separated)", defaultKeys.join(", ")).asCsv();
|
|
91
|
+
this.config.envKeys = csv.length > 0 ? csv : defaultKeys;
|
|
94
92
|
return this;
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
async withAliases(): Promise<this> {
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
).map(providerUtil.normalizeProvider);
|
|
96
|
+
const csv = await this.prompt("Aliases (comma-separated, optional)", this.defaults.aliases.join(", ")).asCsv();
|
|
97
|
+
this.config.aliases = csv.length > 0 ? csv.map(providerUtil.normalizeProvider) : this.defaults.aliases;
|
|
101
98
|
return this;
|
|
102
99
|
}
|
|
103
100
|
|
|
@@ -109,15 +106,16 @@ export class ProviderConfigBuilder {
|
|
|
109
106
|
if (rawModels?.some((item) => typeof item !== "object" || item === null || Array.isArray(item))) {
|
|
110
107
|
throw new Error("models must be an array of objects");
|
|
111
108
|
}
|
|
112
|
-
this.config.models = rawModels as ProviderConfig["models"];
|
|
109
|
+
this.config.models = (rawModels as ProviderConfig["models"]) ?? this.defaults.models;
|
|
113
110
|
return this;
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
async withCompat(): Promise<this> {
|
|
117
|
-
this.config.compat =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
this.config.compat =
|
|
115
|
+
(await this.prompt(
|
|
116
|
+
"Compat JSON object (optional)",
|
|
117
|
+
this.defaults.compat ? JSON.stringify(this.defaults.compat) : "",
|
|
118
|
+
).asJsonRecord("compat")) ?? this.defaults.compat;
|
|
121
119
|
return this;
|
|
122
120
|
}
|
|
123
121
|
|
|
@@ -127,7 +125,8 @@ export class ProviderConfigBuilder {
|
|
|
127
125
|
"Configure Pi OAuth provider id?",
|
|
128
126
|
"Only choose yes if this provider maps to a Pi /login auth entry.",
|
|
129
127
|
))
|
|
130
|
-
? await this.prompt("Pi auth provider id", this.defaults.piAuthProvider ?? id).asText()
|
|
128
|
+
? ((await this.prompt("Pi auth provider id", this.defaults.piAuthProvider ?? id).asText()) ??
|
|
129
|
+
this.defaults.piAuthProvider)
|
|
131
130
|
: this.defaults.piAuthProvider;
|
|
132
131
|
return this;
|
|
133
132
|
}
|
package/src/extension.ts
CHANGED
|
@@ -1,15 +1,47 @@
|
|
|
1
1
|
import { createJiti } from "@mariozechner/jiti";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { dirname } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
6
7
|
export default async function accountSwitcherBootstrap(pi: ExtensionAPI) {
|
|
7
8
|
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
|
|
10
|
+
// Resolve @earendil-works/* from the pi loader's context so the same
|
|
11
|
+
// module instances are used (avoids duplicate singletons / instanceof mismatches).
|
|
12
|
+
const loaderRequire = createRequire(import.meta.url);
|
|
13
|
+
const resolveOrUndefined = (id: string): string | undefined => {
|
|
14
|
+
try {
|
|
15
|
+
return loaderRequire.resolve(id);
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const piAiEntry = resolveOrUndefined("@earendil-works/pi-ai");
|
|
22
|
+
const piCodingAgentEntry = resolveOrUndefined("@earendil-works/pi-coding-agent");
|
|
23
|
+
const piAgentCoreEntry = resolveOrUndefined("@earendil-works/pi-agent-core");
|
|
24
|
+
const piTuiEntry = resolveOrUndefined("@earendil-works/pi-tui");
|
|
25
|
+
|
|
26
|
+
const alias: Record<string, string> = { "@": srcDir };
|
|
27
|
+
if (piAiEntry) {
|
|
28
|
+
alias["@earendil-works/pi-ai"] = piAiEntry;
|
|
29
|
+
alias["@mariozechner/pi-ai"] = piAiEntry;
|
|
30
|
+
}
|
|
31
|
+
if (piCodingAgentEntry) {
|
|
32
|
+
alias["@earendil-works/pi-coding-agent"] = piCodingAgentEntry;
|
|
33
|
+
alias["@mariozechner/pi-coding-agent"] = piCodingAgentEntry;
|
|
34
|
+
}
|
|
35
|
+
if (piAgentCoreEntry) {
|
|
36
|
+
alias["@earendil-works/pi-agent-core"] = piAgentCoreEntry;
|
|
37
|
+
alias["@mariozechner/pi-agent-core"] = piAgentCoreEntry;
|
|
38
|
+
}
|
|
39
|
+
if (piTuiEntry) {
|
|
40
|
+
alias["@earendil-works/pi-tui"] = piTuiEntry;
|
|
41
|
+
alias["@mariozechner/pi-tui"] = piTuiEntry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const jiti = createJiti(import.meta.url, { alias });
|
|
13
45
|
|
|
14
46
|
const extension = await jiti.import<(pi: ExtensionAPI) => void | Promise<void>>("./index", {
|
|
15
47
|
default: true,
|
|
@@ -13,6 +13,10 @@ function resolveAuthProvider(account: AccountConfig, providers: ProviderConfig[]
|
|
|
13
13
|
return provider?.piAuthProvider ?? providerUtil.normalizeProvider(account.provider);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function resolveAccountProvider(account: AccountConfig, providers: ProviderConfig[]): string {
|
|
17
|
+
return providerUtil.normalizeProviderWithCustom(resolveAuthProvider(account, providers), providers);
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
export default class AccountSwitcherRuntime implements AccountSwitcher {
|
|
17
21
|
private accountService: AccountService;
|
|
18
22
|
private modelService: ModelService;
|
|
@@ -49,8 +53,16 @@ export default class AccountSwitcherRuntime implements AccountSwitcher {
|
|
|
49
53
|
// leave Pi's default model selection untouched.
|
|
50
54
|
const modelState = this.accountService.getActiveModelState();
|
|
51
55
|
if (modelState) {
|
|
52
|
-
const
|
|
53
|
-
|
|
56
|
+
const providers = this.providerService.getProviders();
|
|
57
|
+
const activeProvider = active ? resolveAccountProvider(active, providers) : undefined;
|
|
58
|
+
const savedProvider = providerUtil.normalizeProviderWithCustom(modelState.provider, providers);
|
|
59
|
+
|
|
60
|
+
// Only restore a saved model when it belongs to the active account's provider.
|
|
61
|
+
// Otherwise Pi can start with credentials for one provider and a model from another.
|
|
62
|
+
if (!activeProvider || savedProvider === activeProvider) {
|
|
63
|
+
const model = ctx.modelRegistry.find(modelState.provider, modelState.id);
|
|
64
|
+
if (model) await this.modelService.applyModel(model, ctx);
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
|
|
@@ -94,10 +106,7 @@ export default class AccountSwitcherRuntime implements AccountSwitcher {
|
|
|
94
106
|
findAccountsByProvider(provider: string): AccountConfig[] {
|
|
95
107
|
const providers = this.providerService.getProviders();
|
|
96
108
|
const normalized = providerUtil.normalizeProviderWithCustom(provider, providers);
|
|
97
|
-
return this.accountService.getAccounts().filter((a) =>
|
|
98
|
-
const accountProvider = providerUtil.normalizeProviderWithCustom(resolveAuthProvider(a, providers), providers);
|
|
99
|
-
return accountProvider === normalized;
|
|
100
|
-
});
|
|
109
|
+
return this.accountService.getAccounts().filter((a) => resolveAccountProvider(a, providers) === normalized);
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
getActiveAccount(): AccountConfig | undefined {
|
|
@@ -123,17 +132,14 @@ export default class AccountSwitcherRuntime implements AccountSwitcher {
|
|
|
123
132
|
|
|
124
133
|
// piAuth accounts authenticate via a separate provider (e.g. github-copilot),
|
|
125
134
|
// so use that for model lookup rather than the account's own provider field.
|
|
126
|
-
const accountProvider =
|
|
127
|
-
resolveAuthProvider(account, providers),
|
|
128
|
-
providers,
|
|
129
|
-
);
|
|
135
|
+
const accountProvider = resolveAccountProvider(account, providers);
|
|
130
136
|
const currentProvider = ctx.model
|
|
131
137
|
? providerUtil.normalizeProviderWithCustom(ctx.model.provider, providers)
|
|
132
138
|
: undefined;
|
|
133
139
|
|
|
134
140
|
// Skip model selection if the active model already belongs to the same provider.
|
|
135
141
|
if (accountProvider !== currentProvider) {
|
|
136
|
-
const model = await modelUtil.pickModel(ctx, account, providers);
|
|
142
|
+
const model = await modelUtil.pickModel(ctx, account, providers, accountProvider);
|
|
137
143
|
if (model) await this.applyModel(model, ctx);
|
|
138
144
|
}
|
|
139
145
|
|
package/src/utils/accounts.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as piAi from "@earendil-works/pi-ai";
|
|
2
1
|
import { readFile } from "node:fs/promises";
|
|
3
2
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import type { AccountConfig, SecretSource } from "@/types";
|
|
@@ -90,10 +89,18 @@ export const accountUtil = {
|
|
|
90
89
|
};
|
|
91
90
|
|
|
92
91
|
function closeCachedSessions(): void {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
// Dynamic import so the module is not required at load time — @earendil-works/pi-ai
|
|
93
|
+
// is a peerDependency provided by the pi agent host, not bundled with this package.
|
|
94
|
+
import("@earendil-works/pi-ai")
|
|
95
|
+
.then((piAi) => {
|
|
96
|
+
const helpers = piAi as {
|
|
97
|
+
cleanupSessionResources?: () => void;
|
|
98
|
+
closeOpenAICodexWebSocketSessions?: () => void;
|
|
99
|
+
};
|
|
100
|
+
helpers.cleanupSessionResources?.();
|
|
101
|
+
helpers.closeOpenAICodexWebSocketSessions?.();
|
|
102
|
+
})
|
|
103
|
+
.catch(() => {
|
|
104
|
+
// pi-ai not available in this environment — skip session cleanup
|
|
105
|
+
});
|
|
99
106
|
}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, type Focusable } from "@earendil-works/pi-tui";
|
|
1
|
+
import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, type Focusable, type Component } from "@earendil-works/pi-tui";
|
|
3
2
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Local implementation of DynamicBorder to avoid a hard runtime dependency
|
|
6
|
+
* on @earendil-works/pi-coding-agent (which is a peer dep not available in
|
|
7
|
+
* the pi agent's npm node_modules at extension load time).
|
|
8
|
+
*/
|
|
9
|
+
class DynamicBorder implements Component {
|
|
10
|
+
private color: (str: string) => string;
|
|
11
|
+
constructor(color: (str: string) => string = (str) => str) {
|
|
12
|
+
this.color = color;
|
|
13
|
+
}
|
|
14
|
+
invalidate() {}
|
|
15
|
+
render(width: number): string[] {
|
|
16
|
+
return [this.color("─".repeat(Math.max(1, width)))];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
type Theme = { fg: (color: ThemeColor, text: string) => string; bold: (text: string) => string };
|
|
6
21
|
|
|
7
22
|
export class FilterableMultiSelectComponent extends Container implements Focusable {
|
package/src/utils/models.ts
CHANGED
|
@@ -10,8 +10,10 @@ export const modelUtil = {
|
|
|
10
10
|
ctx: AccountSwitcherContext,
|
|
11
11
|
account: AccountConfig,
|
|
12
12
|
providers: ProviderConfig[],
|
|
13
|
+
resolvedProvider?: string,
|
|
13
14
|
): Promise<ProviderModel | undefined> => {
|
|
14
|
-
const accountProvider =
|
|
15
|
+
const accountProvider =
|
|
16
|
+
resolvedProvider ?? normalizeProvider(account.piAuth?.provider ?? account.provider, providers);
|
|
15
17
|
const candidates = getProviderModels(ctx, providers, accountProvider);
|
|
16
18
|
|
|
17
19
|
if (candidates.length === 0) {
|