@victor-software-house/pi-multicodex 2.1.5 → 2.2.1
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/account-manager.ts +21 -67
- package/auth.ts +1 -47
- package/commands.ts +254 -67
- package/package.json +1 -1
- package/provider.ts +1 -2
package/account-manager.ts
CHANGED
|
@@ -2,12 +2,8 @@ import {
|
|
|
2
2
|
type OAuthCredentials,
|
|
3
3
|
refreshOpenAICodexToken,
|
|
4
4
|
} from "@mariozechner/pi-ai/oauth";
|
|
5
|
-
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
6
5
|
import { normalizeUnknownError } from "pi-provider-utils/streams";
|
|
7
|
-
import {
|
|
8
|
-
loadImportedOpenAICodexAuth,
|
|
9
|
-
writeActiveTokenToAuthJson,
|
|
10
|
-
} from "./auth";
|
|
6
|
+
import { loadImportedOpenAICodexAuth } from "./auth";
|
|
11
7
|
import { isAccountAvailable, pickBestAccount } from "./selection";
|
|
12
8
|
import {
|
|
13
9
|
type Account,
|
|
@@ -48,23 +44,6 @@ export class AccountManager {
|
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
/**
|
|
52
|
-
* Write the active account's tokens to auth.json so pi's background features
|
|
53
|
-
* (rename, compaction) can resolve a valid API key via AuthStorage.
|
|
54
|
-
*/
|
|
55
|
-
private syncActiveTokenToAuthJson(account: Account): void {
|
|
56
|
-
try {
|
|
57
|
-
writeActiveTokenToAuthJson({
|
|
58
|
-
access: account.accessToken,
|
|
59
|
-
refresh: account.refreshToken,
|
|
60
|
-
expires: account.expiresAt,
|
|
61
|
-
accountId: account.accountId,
|
|
62
|
-
});
|
|
63
|
-
} catch {
|
|
64
|
-
// Best-effort sync — do not block token resolution.
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
47
|
onStateChange(handler: StateChangeHandler): () => void {
|
|
69
48
|
this.stateChangeHandlers.add(handler);
|
|
70
49
|
return () => {
|
|
@@ -366,6 +345,17 @@ export class AccountManager {
|
|
|
366
345
|
return changed;
|
|
367
346
|
}
|
|
368
347
|
|
|
348
|
+
detachImportedAuth(email: string): boolean {
|
|
349
|
+
const account = this.getAccount(email);
|
|
350
|
+
if (!account) return false;
|
|
351
|
+
const changed = this.clearImportedLink(account);
|
|
352
|
+
if (changed) {
|
|
353
|
+
this.save();
|
|
354
|
+
this.notifyStateChanged();
|
|
355
|
+
}
|
|
356
|
+
return changed;
|
|
357
|
+
}
|
|
358
|
+
|
|
369
359
|
async syncImportedOpenAICodexAuth(): Promise<boolean> {
|
|
370
360
|
const imported = await loadImportedOpenAICodexAuth();
|
|
371
361
|
if (!imported) return false;
|
|
@@ -624,12 +614,11 @@ export class AccountManager {
|
|
|
624
614
|
}
|
|
625
615
|
|
|
626
616
|
if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
|
|
627
|
-
this.syncActiveTokenToAuthJson(account);
|
|
628
617
|
return account.accessToken;
|
|
629
618
|
}
|
|
630
619
|
|
|
631
|
-
//
|
|
632
|
-
//
|
|
620
|
+
// Imported auth is read-only. MultiCodex never refreshes or writes
|
|
621
|
+
// auth.json and instead requires the user to repair pi auth explicitly.
|
|
633
622
|
if (account.importSource === "pi-openai-codex") {
|
|
634
623
|
return this.ensureValidTokenForImportedAccount(account);
|
|
635
624
|
}
|
|
@@ -652,7 +641,6 @@ export class AccountManager {
|
|
|
652
641
|
}
|
|
653
642
|
this.save();
|
|
654
643
|
this.notifyStateChanged();
|
|
655
|
-
this.syncActiveTokenToAuthJson(account);
|
|
656
644
|
return account.accessToken;
|
|
657
645
|
} catch (error) {
|
|
658
646
|
this.markNeedsReauth(account);
|
|
@@ -667,17 +655,14 @@ export class AccountManager {
|
|
|
667
655
|
}
|
|
668
656
|
|
|
669
657
|
/**
|
|
670
|
-
*
|
|
658
|
+
* Read-only path for imported pi auth.
|
|
671
659
|
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* errors caused by pi and multicodex both refreshing the same token
|
|
675
|
-
* simultaneously.
|
|
660
|
+
* MultiCodex may read auth.json to mirror pi's currently active Codex auth,
|
|
661
|
+
* but it must never refresh or write auth.json itself.
|
|
676
662
|
*/
|
|
677
663
|
private async ensureValidTokenForImportedAccount(
|
|
678
664
|
account: Account,
|
|
679
665
|
): Promise<string> {
|
|
680
|
-
// Check if pi already refreshed since our last sync.
|
|
681
666
|
const latest = await loadImportedOpenAICodexAuth();
|
|
682
667
|
if (latest && Date.now() < latest.credentials.expires - 5 * 60 * 1000) {
|
|
683
668
|
account.accessToken = latest.credentials.access;
|
|
@@ -696,40 +681,9 @@ export class AccountManager {
|
|
|
696
681
|
return account.accessToken;
|
|
697
682
|
}
|
|
698
683
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const authStorage = AuthStorage.create();
|
|
704
|
-
apiKey = await authStorage.getApiKey("openai-codex");
|
|
705
|
-
} catch {
|
|
706
|
-
// AuthStorage refresh failed; mark for re-auth below.
|
|
707
|
-
}
|
|
708
|
-
if (!apiKey) {
|
|
709
|
-
this.markNeedsReauth(account);
|
|
710
|
-
throw new Error(
|
|
711
|
-
`${account.email}: token refresh failed — run /login openai-codex to re-authenticate`,
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Read the refreshed tokens back from auth.json.
|
|
716
|
-
const refreshed = await loadImportedOpenAICodexAuth();
|
|
717
|
-
if (refreshed) {
|
|
718
|
-
account.accessToken = refreshed.credentials.access;
|
|
719
|
-
account.refreshToken = refreshed.credentials.refresh;
|
|
720
|
-
account.expiresAt = refreshed.credentials.expires;
|
|
721
|
-
account.importFingerprint = refreshed.fingerprint;
|
|
722
|
-
const accountId =
|
|
723
|
-
typeof refreshed.credentials.accountId === "string"
|
|
724
|
-
? refreshed.credentials.accountId
|
|
725
|
-
: undefined;
|
|
726
|
-
if (accountId) {
|
|
727
|
-
account.accountId = accountId;
|
|
728
|
-
}
|
|
729
|
-
this.save();
|
|
730
|
-
this.notifyStateChanged();
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
return apiKey;
|
|
684
|
+
this.markNeedsReauth(account);
|
|
685
|
+
throw new Error(
|
|
686
|
+
`${account.email}: imported pi auth is expired — run /login openai-codex to re-authenticate`,
|
|
687
|
+
);
|
|
734
688
|
}
|
|
735
689
|
}
|
package/auth.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
promises as fs,
|
|
4
|
-
readFileSync,
|
|
5
|
-
writeFileSync,
|
|
6
|
-
} from "node:fs";
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
7
2
|
import type { OAuthCredentials } from "@mariozechner/pi-ai/oauth";
|
|
8
3
|
import { getAgentAuthPath } from "pi-provider-utils/agent-paths";
|
|
9
4
|
|
|
@@ -127,47 +122,6 @@ export function parseImportedOpenAICodexAuth(
|
|
|
127
122
|
};
|
|
128
123
|
}
|
|
129
124
|
|
|
130
|
-
/**
|
|
131
|
-
* Write the active account's tokens to auth.json so pi's background features
|
|
132
|
-
* (rename, compaction, inline suggestions) can resolve a valid API key through
|
|
133
|
-
* the normal AuthStorage path.
|
|
134
|
-
*/
|
|
135
|
-
/**
|
|
136
|
-
* Synchronously write the active account's tokens to auth.json so pi's
|
|
137
|
-
* background features (rename, compaction) can resolve a valid API key.
|
|
138
|
-
*
|
|
139
|
-
* Uses synchronous I/O to avoid interleaved writes with pi's own code.
|
|
140
|
-
*/
|
|
141
|
-
export function writeActiveTokenToAuthJson(creds: {
|
|
142
|
-
access: string;
|
|
143
|
-
refresh: string;
|
|
144
|
-
expires: number;
|
|
145
|
-
accountId?: string;
|
|
146
|
-
}): void {
|
|
147
|
-
let auth: Record<string, unknown> = {};
|
|
148
|
-
try {
|
|
149
|
-
if (existsSync(AUTH_FILE)) {
|
|
150
|
-
const raw = readFileSync(AUTH_FILE, "utf8");
|
|
151
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
152
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
153
|
-
auth = parsed as Record<string, unknown>;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} catch {
|
|
157
|
-
// File missing or corrupt — start fresh.
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
auth["openai-codex"] = {
|
|
161
|
-
type: "oauth",
|
|
162
|
-
access: creds.access,
|
|
163
|
-
refresh: creds.refresh,
|
|
164
|
-
expires: creds.expires,
|
|
165
|
-
accountId: creds.accountId,
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
125
|
export async function loadImportedOpenAICodexAuth(): Promise<
|
|
172
126
|
ImportedOpenAICodexAuth | undefined
|
|
173
127
|
> {
|
package/commands.ts
CHANGED
|
@@ -5,22 +5,22 @@ import type {
|
|
|
5
5
|
ExtensionAPI,
|
|
6
6
|
ExtensionCommandContext,
|
|
7
7
|
} from "@mariozechner/pi-coding-agent";
|
|
8
|
-
import {
|
|
8
|
+
import { DynamicBorder, rawKeyHint } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import {
|
|
10
10
|
type AutocompleteItem,
|
|
11
11
|
Container,
|
|
12
|
-
|
|
12
|
+
getKeybindings,
|
|
13
13
|
matchesKey,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Spacer,
|
|
15
|
+
truncateToWidth,
|
|
16
|
+
visibleWidth,
|
|
16
17
|
} from "@mariozechner/pi-tui";
|
|
17
18
|
import { getAgentSettingsPath } from "pi-provider-utils/agent-paths";
|
|
18
19
|
import { normalizeUnknownError } from "pi-provider-utils/streams";
|
|
19
20
|
import type { AccountManager } from "./account-manager";
|
|
20
|
-
import { writeActiveTokenToAuthJson } from "./auth";
|
|
21
21
|
import { openLoginInBrowser } from "./browser";
|
|
22
22
|
import type { createUsageStatusController } from "./status";
|
|
23
|
-
import { STORAGE_FILE } from "./storage";
|
|
23
|
+
import { type Account, STORAGE_FILE } from "./storage";
|
|
24
24
|
import { formatResetAt, isUsageUntouched } from "./usage";
|
|
25
25
|
|
|
26
26
|
const SETTINGS_FILE = getAgentSettingsPath();
|
|
@@ -87,35 +87,42 @@ function parseResetTarget(value: string): ResetTarget | undefined {
|
|
|
87
87
|
return undefined;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function
|
|
90
|
+
function isPlaceholderAccount(account: Account): boolean {
|
|
91
|
+
return (
|
|
92
|
+
!account.accessToken || !account.refreshToken || account.expiresAt <= 0
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getAccountTags(
|
|
91
97
|
accountManager: AccountManager,
|
|
92
|
-
|
|
93
|
-
): string {
|
|
94
|
-
const account = accountManager.getAccount(email);
|
|
95
|
-
if (!account) return email;
|
|
98
|
+
account: Account,
|
|
99
|
+
): string[] {
|
|
96
100
|
const usage = accountManager.getCachedUsage(account.email);
|
|
97
101
|
const active = accountManager.getActiveAccount();
|
|
98
102
|
const manual = accountManager.getManualAccount();
|
|
99
103
|
const quotaHit =
|
|
100
104
|
account.quotaExhaustedUntil && account.quotaExhaustedUntil > Date.now();
|
|
101
|
-
const untouched = isUsageUntouched(usage) ? "untouched" : null;
|
|
102
105
|
const imported = account.importSource
|
|
103
106
|
? account.importMode === "synthetic"
|
|
104
107
|
? "pi auth only"
|
|
105
108
|
: "pi auth"
|
|
106
109
|
: null;
|
|
107
|
-
|
|
108
|
-
const tags = [
|
|
110
|
+
return [
|
|
109
111
|
active?.email === account.email ? "active" : null,
|
|
110
112
|
manual?.email === account.email ? "manual" : null,
|
|
111
|
-
reauth,
|
|
113
|
+
account.needsReauth ? "needs reauth" : null,
|
|
114
|
+
isPlaceholderAccount(account) ? "placeholder" : null,
|
|
112
115
|
quotaHit ? "quota" : null,
|
|
113
|
-
untouched,
|
|
116
|
+
isUsageUntouched(usage) ? "untouched" : null,
|
|
114
117
|
imported,
|
|
115
|
-
]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
].filter((value): value is string => Boolean(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatUsageSummary(
|
|
122
|
+
accountManager: AccountManager,
|
|
123
|
+
account: Account,
|
|
124
|
+
): string {
|
|
125
|
+
const usage = accountManager.getCachedUsage(account.email);
|
|
119
126
|
const primaryUsed = usage?.primary?.usedPercent;
|
|
120
127
|
const secondaryUsed = usage?.secondary?.usedPercent;
|
|
121
128
|
const primaryReset = usage?.primary?.resetAt;
|
|
@@ -124,8 +131,18 @@ function formatAccountStatusLine(
|
|
|
124
131
|
primaryUsed === undefined ? "unknown" : `${Math.round(primaryUsed)}%`;
|
|
125
132
|
const secondaryLabel =
|
|
126
133
|
secondaryUsed === undefined ? "unknown" : `${Math.round(secondaryUsed)}%`;
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
return `5h ${primaryLabel} reset:${formatResetAt(primaryReset)} | weekly ${secondaryLabel} reset:${formatResetAt(secondaryReset)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatAccountStatusLine(
|
|
138
|
+
accountManager: AccountManager,
|
|
139
|
+
email: string,
|
|
140
|
+
): string {
|
|
141
|
+
const account = accountManager.getAccount(email);
|
|
142
|
+
if (!account) return email;
|
|
143
|
+
const tags = getAccountTags(accountManager, account).join(", ");
|
|
144
|
+
const suffix = tags ? ` (${tags})` : "";
|
|
145
|
+
return `${account.email}${suffix} - ${formatUsageSummary(accountManager, account)}`;
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
function getSubcommandCompletions(prefix: string): AutocompleteItem[] | null {
|
|
@@ -228,16 +245,10 @@ async function loginAndActivateAccount(
|
|
|
228
245
|
onPrompt: async ({ message }) => (await ctx.ui.input(message)) || "",
|
|
229
246
|
});
|
|
230
247
|
|
|
248
|
+
const existing = accountManager.getAccount(identifier);
|
|
231
249
|
const account = accountManager.addOrUpdateAccount(identifier, creds);
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
access: creds.access,
|
|
235
|
-
refresh: creds.refresh,
|
|
236
|
-
expires: creds.expires,
|
|
237
|
-
accountId:
|
|
238
|
-
typeof creds.accountId === "string" ? creds.accountId : undefined,
|
|
239
|
-
});
|
|
240
|
-
await accountManager.syncImportedOpenAICodexAuth();
|
|
250
|
+
if (existing?.importSource) {
|
|
251
|
+
accountManager.detachImportedAuth(account.email);
|
|
241
252
|
}
|
|
242
253
|
accountManager.setManualAccount(account.email);
|
|
243
254
|
ctx.ui.notify(`Now using ${account.email}`, "info");
|
|
@@ -344,58 +355,234 @@ async function openAccountManagementPanel(
|
|
|
344
355
|
accountManager: AccountManager,
|
|
345
356
|
): Promise<AccountPanelResult> {
|
|
346
357
|
const accounts = accountManager.getAccounts();
|
|
347
|
-
const items = accounts.map((account) => ({
|
|
348
|
-
value: account.email,
|
|
349
|
-
label: formatAccountStatusLine(accountManager, account.email),
|
|
350
|
-
}));
|
|
351
358
|
|
|
352
|
-
return ctx.ui.custom<AccountPanelResult>((
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
359
|
+
return ctx.ui.custom<AccountPanelResult>((tui, theme, _kb, done) => {
|
|
360
|
+
const kb = getKeybindings();
|
|
361
|
+
let selectedIndex = 0;
|
|
362
|
+
const maxVisible = 12;
|
|
363
|
+
|
|
364
|
+
function getSelectedAccount(): Account | undefined {
|
|
365
|
+
return accounts[selectedIndex];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function findNextIndex(from: number, direction: number): number {
|
|
369
|
+
if (accounts.length === 0) return 0;
|
|
370
|
+
return Math.max(0, Math.min(accounts.length - 1, from + direction));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function renderTag(text: string): string {
|
|
374
|
+
if (text === "active") {
|
|
375
|
+
return theme.fg("accent", `[${text}]`);
|
|
376
|
+
}
|
|
377
|
+
if (text === "manual") {
|
|
378
|
+
return theme.fg("warning", `[${text}]`);
|
|
379
|
+
}
|
|
380
|
+
if (text === "needs reauth") {
|
|
381
|
+
return theme.fg("error", `[${text}]`);
|
|
382
|
+
}
|
|
383
|
+
if (text === "placeholder") {
|
|
384
|
+
return theme.fg("warning", `[${text}]`);
|
|
385
|
+
}
|
|
386
|
+
if (text === "quota") {
|
|
387
|
+
return theme.fg("warning", `[${text}]`);
|
|
388
|
+
}
|
|
389
|
+
if (text === "pi auth" || text === "pi auth only") {
|
|
390
|
+
return theme.fg("success", `[${text}]`);
|
|
391
|
+
}
|
|
392
|
+
return theme.fg("muted", `[${text}]`);
|
|
393
|
+
}
|
|
367
394
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
395
|
+
function renderRow(
|
|
396
|
+
account: Account,
|
|
397
|
+
selected: boolean,
|
|
398
|
+
width: number,
|
|
399
|
+
): string[] {
|
|
400
|
+
const cursor = selected ? theme.fg("accent", ">") : theme.fg("dim", " ");
|
|
401
|
+
const name = selected ? theme.bold(account.email) : account.email;
|
|
402
|
+
const tags = getAccountTags(accountManager, account)
|
|
403
|
+
.map((tag) => renderTag(tag))
|
|
404
|
+
.join(" ");
|
|
405
|
+
const primary = truncateToWidth(
|
|
406
|
+
`${cursor} ${name}${tags ? ` ${tags}` : ""}`,
|
|
407
|
+
width,
|
|
408
|
+
"",
|
|
409
|
+
);
|
|
410
|
+
const summaryColor = account.needsReauth
|
|
411
|
+
? "warning"
|
|
412
|
+
: isPlaceholderAccount(account)
|
|
413
|
+
? "muted"
|
|
414
|
+
: "dim";
|
|
415
|
+
const secondary = theme.fg(
|
|
416
|
+
summaryColor,
|
|
417
|
+
formatUsageSummary(accountManager, account),
|
|
418
|
+
);
|
|
419
|
+
return [primary, truncateToWidth(` ${secondary}`, width, "")];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const header = {
|
|
423
|
+
invalidate() {},
|
|
424
|
+
render(width: number): string[] {
|
|
425
|
+
const title = theme.bold("MultiCodex Accounts");
|
|
426
|
+
const sep = theme.fg("muted", " · ");
|
|
427
|
+
const hints = [
|
|
428
|
+
rawKeyHint("enter", "use"),
|
|
429
|
+
rawKeyHint("u", "refresh"),
|
|
430
|
+
rawKeyHint("r", "reauth"),
|
|
431
|
+
rawKeyHint("n", "add"),
|
|
432
|
+
rawKeyHint("backspace", "remove"),
|
|
433
|
+
rawKeyHint("esc", "close"),
|
|
434
|
+
].join(sep);
|
|
435
|
+
const spacing = Math.max(
|
|
436
|
+
1,
|
|
437
|
+
width - visibleWidth(title) - visibleWidth(hints),
|
|
438
|
+
);
|
|
439
|
+
const reauthCount = accountManager.getAccountsNeedingReauth().length;
|
|
440
|
+
const placeholderCount = accounts.filter((account) =>
|
|
441
|
+
isPlaceholderAccount(account),
|
|
442
|
+
).length;
|
|
443
|
+
const status = [
|
|
444
|
+
`${accounts.length} account${accounts.length === 1 ? "" : "s"}`,
|
|
445
|
+
reauthCount > 0 ? `${reauthCount} need reauth` : undefined,
|
|
446
|
+
placeholderCount > 0
|
|
447
|
+
? `${placeholderCount} placeholder${placeholderCount === 1 ? "" : "s"}`
|
|
448
|
+
: undefined,
|
|
449
|
+
]
|
|
450
|
+
.filter(Boolean)
|
|
451
|
+
.join(" · ");
|
|
452
|
+
return [
|
|
453
|
+
truncateToWidth(`${title}${" ".repeat(spacing)}${hints}`, width, ""),
|
|
454
|
+
theme.fg("muted", status),
|
|
455
|
+
];
|
|
456
|
+
},
|
|
371
457
|
};
|
|
372
|
-
|
|
373
|
-
|
|
458
|
+
|
|
459
|
+
const list = {
|
|
460
|
+
invalidate() {},
|
|
461
|
+
render(width: number): string[] {
|
|
462
|
+
const lines: string[] = [];
|
|
463
|
+
if (accounts.length === 0) {
|
|
464
|
+
return [theme.fg("muted", " No managed accounts")];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const visibleRows = Math.max(1, Math.floor(maxVisible / 2));
|
|
468
|
+
const startIndex = Math.max(
|
|
469
|
+
0,
|
|
470
|
+
Math.min(
|
|
471
|
+
selectedIndex - Math.floor(visibleRows / 2),
|
|
472
|
+
Math.max(0, accounts.length - visibleRows),
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
const endIndex = Math.min(accounts.length, startIndex + visibleRows);
|
|
476
|
+
|
|
477
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
478
|
+
const account = accounts[index];
|
|
479
|
+
if (!account) continue;
|
|
480
|
+
lines.push(...renderRow(account, index === selectedIndex, width));
|
|
481
|
+
if (index < endIndex - 1) {
|
|
482
|
+
lines.push("");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const selected = getSelectedAccount();
|
|
487
|
+
if (selected) {
|
|
488
|
+
lines.push("");
|
|
489
|
+
const detail = isPlaceholderAccount(selected)
|
|
490
|
+
? `selected: ${selected.email} · restored placeholder, re-auth required`
|
|
491
|
+
: `selected: ${selected.email}`;
|
|
492
|
+
lines.push(truncateToWidth(theme.fg("dim", detail), width, ""));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const current = selectedIndex + 1;
|
|
496
|
+
lines.push(
|
|
497
|
+
theme.fg(
|
|
498
|
+
"dim",
|
|
499
|
+
` ${current}/${accounts.length} visible account rows`,
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
return lines;
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const container = new Container();
|
|
507
|
+
container.addChild(new Spacer(1));
|
|
508
|
+
container.addChild(new DynamicBorder());
|
|
509
|
+
container.addChild(new Spacer(1));
|
|
510
|
+
container.addChild(header);
|
|
511
|
+
container.addChild(new Spacer(1));
|
|
512
|
+
container.addChild(list);
|
|
513
|
+
container.addChild(new Spacer(1));
|
|
514
|
+
container.addChild(new DynamicBorder());
|
|
374
515
|
|
|
375
516
|
return {
|
|
376
|
-
render
|
|
377
|
-
|
|
378
|
-
|
|
517
|
+
render(width: number) {
|
|
518
|
+
return container.render(width);
|
|
519
|
+
},
|
|
520
|
+
invalidate() {
|
|
521
|
+
container.invalidate();
|
|
522
|
+
},
|
|
523
|
+
handleInput(data: string) {
|
|
524
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
525
|
+
selectedIndex = findNextIndex(selectedIndex, -1);
|
|
526
|
+
tui.requestRender();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
530
|
+
selectedIndex = findNextIndex(selectedIndex, 1);
|
|
531
|
+
tui.requestRender();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (kb.matches(data, "tui.select.pageUp")) {
|
|
535
|
+
selectedIndex = findNextIndex(selectedIndex, -5);
|
|
536
|
+
tui.requestRender();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (kb.matches(data, "tui.select.pageDown")) {
|
|
540
|
+
selectedIndex = findNextIndex(selectedIndex, 5);
|
|
541
|
+
tui.requestRender();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (
|
|
545
|
+
kb.matches(data, "tui.select.cancel") ||
|
|
546
|
+
matchesKey(data, "ctrl+c")
|
|
547
|
+
) {
|
|
548
|
+
done(undefined);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (
|
|
552
|
+
data === "\r" ||
|
|
553
|
+
data === "\n" ||
|
|
554
|
+
kb.matches(data, "tui.select.confirm")
|
|
555
|
+
) {
|
|
556
|
+
const selected = getSelectedAccount();
|
|
557
|
+
if (selected) {
|
|
558
|
+
done({ action: "select", email: selected.email });
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
379
562
|
if (data.toLowerCase() === "n") {
|
|
380
563
|
done({ action: "add" });
|
|
381
564
|
return;
|
|
382
565
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
566
|
+
if (data.toLowerCase() === "u") {
|
|
567
|
+
const selected = getSelectedAccount();
|
|
568
|
+
if (selected) {
|
|
569
|
+
done({ action: "refresh", email: selected.email });
|
|
570
|
+
}
|
|
386
571
|
return;
|
|
387
572
|
}
|
|
388
|
-
if (
|
|
389
|
-
|
|
573
|
+
if (data.toLowerCase() === "r") {
|
|
574
|
+
const selected = getSelectedAccount();
|
|
575
|
+
if (selected) {
|
|
576
|
+
done({ action: "reauth", email: selected.email });
|
|
577
|
+
}
|
|
390
578
|
return;
|
|
391
579
|
}
|
|
392
|
-
if (matchesKey(data,
|
|
580
|
+
if (matchesKey(data, "backspace")) {
|
|
581
|
+
const selected = getSelectedAccount();
|
|
393
582
|
if (selected) {
|
|
394
|
-
done({ action: "remove", email: selected.
|
|
583
|
+
done({ action: "remove", email: selected.email });
|
|
395
584
|
}
|
|
396
|
-
return;
|
|
397
585
|
}
|
|
398
|
-
selectList.handleInput(data);
|
|
399
586
|
},
|
|
400
587
|
};
|
|
401
588
|
});
|
package/package.json
CHANGED
package/provider.ts
CHANGED
|
@@ -53,8 +53,7 @@ function getActiveApiKey(accountManager: AccountManager): string {
|
|
|
53
53
|
return account.accessToken;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
//
|
|
57
|
-
// as long as auth.json has valid tokens.
|
|
56
|
+
// Fallback placeholder until MultiCodex resolves a usable managed account.
|
|
58
57
|
return "pending-login";
|
|
59
58
|
}
|
|
60
59
|
|