@victor-software-house/pi-multicodex 1.0.8 → 1.0.9
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/README.md +3 -4
- package/account-manager.ts +23 -1
- package/extension.ts +1 -1
- package/package.json +1 -1
- package/status.ts +140 -36
package/README.md
CHANGED
|
@@ -69,11 +69,10 @@ Current direction:
|
|
|
69
69
|
|
|
70
70
|
Current next step:
|
|
71
71
|
|
|
72
|
-
- make MultiCodex own the normal `openai-codex` path directly
|
|
73
|
-
- auto-import pi's existing `openai-codex` auth when it is new or changed
|
|
74
72
|
- mirror the existing codex usage footer style, including support for displaying both reset countdowns
|
|
75
|
-
-
|
|
76
|
-
-
|
|
73
|
+
- debounce expensive refresh work during rapid model cycling
|
|
74
|
+
- move each reset countdown next to its matching usage period
|
|
75
|
+
- add live preview to the `/multicodex-footer` panel before locking the final style
|
|
77
76
|
- tighten footer updates so account switches and quota rotation are reflected immediately
|
|
78
77
|
|
|
79
78
|
## Release validation
|
package/account-manager.ts
CHANGED
|
@@ -18,6 +18,7 @@ const USAGE_REQUEST_TIMEOUT_MS = 10 * 1000;
|
|
|
18
18
|
const QUOTA_COOLDOWN_MS = 60 * 60 * 1000;
|
|
19
19
|
|
|
20
20
|
type WarningHandler = (message: string) => void;
|
|
21
|
+
type StateChangeHandler = () => void;
|
|
21
22
|
|
|
22
23
|
function getErrorMessage(error: unknown): string {
|
|
23
24
|
if (error instanceof Error) return error.message;
|
|
@@ -29,6 +30,7 @@ export class AccountManager {
|
|
|
29
30
|
private usageCache = new Map<string, CodexUsageSnapshot>();
|
|
30
31
|
private warningHandler?: WarningHandler;
|
|
31
32
|
private manualEmail?: string;
|
|
33
|
+
private stateChangeHandlers = new Set<StateChangeHandler>();
|
|
32
34
|
|
|
33
35
|
constructor() {
|
|
34
36
|
this.data = loadStorage();
|
|
@@ -38,6 +40,19 @@ export class AccountManager {
|
|
|
38
40
|
saveStorage(this.data);
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
private notifyStateChanged(): void {
|
|
44
|
+
for (const handler of this.stateChangeHandlers) {
|
|
45
|
+
handler();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onStateChange(handler: StateChangeHandler): () => void {
|
|
50
|
+
this.stateChangeHandlers.add(handler);
|
|
51
|
+
return () => {
|
|
52
|
+
this.stateChangeHandlers.delete(handler);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
getAccounts(): Account[] {
|
|
42
57
|
return this.data.accounts;
|
|
43
58
|
}
|
|
@@ -82,7 +97,6 @@ export class AccountManager {
|
|
|
82
97
|
});
|
|
83
98
|
}
|
|
84
99
|
this.setActiveAccount(email);
|
|
85
|
-
this.save();
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
getActiveAccount(): Account | undefined {
|
|
@@ -111,6 +125,7 @@ export class AccountManager {
|
|
|
111
125
|
setActiveAccount(email: string): void {
|
|
112
126
|
this.data.activeEmail = email;
|
|
113
127
|
this.save();
|
|
128
|
+
this.notifyStateChanged();
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
setManualAccount(email: string): void {
|
|
@@ -118,10 +133,13 @@ export class AccountManager {
|
|
|
118
133
|
if (!account) return;
|
|
119
134
|
this.manualEmail = email;
|
|
120
135
|
account.lastUsed = Date.now();
|
|
136
|
+
this.notifyStateChanged();
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
clearManualAccount(): void {
|
|
140
|
+
if (!this.manualEmail) return;
|
|
124
141
|
this.manualEmail = undefined;
|
|
142
|
+
this.notifyStateChanged();
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
getImportedAccount(): Account | undefined {
|
|
@@ -173,6 +191,7 @@ export class AccountManager {
|
|
|
173
191
|
if (account) {
|
|
174
192
|
account.quotaExhaustedUntil = until;
|
|
175
193
|
this.save();
|
|
194
|
+
this.notifyStateChanged();
|
|
176
195
|
}
|
|
177
196
|
}
|
|
178
197
|
|
|
@@ -201,6 +220,7 @@ export class AccountManager {
|
|
|
201
220
|
timeoutMs: USAGE_REQUEST_TIMEOUT_MS,
|
|
202
221
|
});
|
|
203
222
|
this.usageCache.set(account.email, usage);
|
|
223
|
+
this.notifyStateChanged();
|
|
204
224
|
return usage;
|
|
205
225
|
} catch (error) {
|
|
206
226
|
this.warningHandler?.(
|
|
@@ -283,6 +303,7 @@ export class AccountManager {
|
|
|
283
303
|
}
|
|
284
304
|
if (changed) {
|
|
285
305
|
this.save();
|
|
306
|
+
this.notifyStateChanged();
|
|
286
307
|
}
|
|
287
308
|
}
|
|
288
309
|
|
|
@@ -301,6 +322,7 @@ export class AccountManager {
|
|
|
301
322
|
account.accountId = accountId;
|
|
302
323
|
}
|
|
303
324
|
this.save();
|
|
325
|
+
this.notifyStateChanged();
|
|
304
326
|
return account.accessToken;
|
|
305
327
|
}
|
|
306
328
|
}
|
package/extension.ts
CHANGED
|
@@ -54,7 +54,7 @@ export default function multicodexExtension(pi: ExtensionAPI) {
|
|
|
54
54
|
|
|
55
55
|
pi.on("model_select", (_event: unknown, ctx: ExtensionContext) => {
|
|
56
56
|
lastContext = ctx;
|
|
57
|
-
|
|
57
|
+
statusController.scheduleModelSelectRefresh(ctx);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
pi.on("session_shutdown", (_event: unknown, ctx: ExtensionContext) => {
|
package/package.json
CHANGED
package/status.ts
CHANGED
|
@@ -21,6 +21,7 @@ const STATUS_KEY = "multicodex-usage";
|
|
|
21
21
|
const SETTINGS_KEY = "pi-multicodex";
|
|
22
22
|
const SETTINGS_FILE = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
23
23
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
24
|
+
const MODEL_SELECT_REFRESH_DEBOUNCE_MS = 250;
|
|
24
25
|
const UNKNOWN_PERCENT = "--";
|
|
25
26
|
const FIVE_HOUR_LABEL = "5h:";
|
|
26
27
|
const SEVEN_DAY_LABEL = "7d:";
|
|
@@ -172,6 +173,40 @@ function formatResetCountdown(resetAt: number | undefined): string | undefined {
|
|
|
172
173
|
return `${seconds}s`;
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
function shouldShowReset(
|
|
177
|
+
preferences: FooterPreferences,
|
|
178
|
+
window: Exclude<ResetWindowMode, "both">,
|
|
179
|
+
): boolean {
|
|
180
|
+
if (!preferences.showReset) return false;
|
|
181
|
+
return (
|
|
182
|
+
preferences.resetWindow === "both" || preferences.resetWindow === window
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatUsageSegment(
|
|
187
|
+
ctx: ExtensionContext,
|
|
188
|
+
label: string,
|
|
189
|
+
usedPercent: number | undefined,
|
|
190
|
+
resetAt: number | undefined,
|
|
191
|
+
showReset: boolean,
|
|
192
|
+
preferences: FooterPreferences,
|
|
193
|
+
): string {
|
|
194
|
+
const parts = [
|
|
195
|
+
`${ctx.ui.theme.fg("dim", label)}${formatPercent(
|
|
196
|
+
ctx,
|
|
197
|
+
usedToDisplayPercent(usedPercent, preferences.usageMode),
|
|
198
|
+
preferences.usageMode,
|
|
199
|
+
)}`,
|
|
200
|
+
];
|
|
201
|
+
if (showReset) {
|
|
202
|
+
const countdown = formatResetCountdown(resetAt);
|
|
203
|
+
if (countdown) {
|
|
204
|
+
parts.push(ctx.ui.theme.fg("dim", `(↺${countdown})`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return parts.join(" ");
|
|
208
|
+
}
|
|
209
|
+
|
|
175
210
|
export function isManagedModel(model: MaybeModel): boolean {
|
|
176
211
|
return model?.provider === PROVIDER_ID;
|
|
177
212
|
}
|
|
@@ -195,41 +230,22 @@ export function formatActiveAccountStatus(
|
|
|
195
230
|
.join(" ");
|
|
196
231
|
}
|
|
197
232
|
|
|
198
|
-
const fiveHour =
|
|
233
|
+
const fiveHour = formatUsageSegment(
|
|
199
234
|
ctx,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
235
|
+
FIVE_HOUR_LABEL,
|
|
236
|
+
usage.primary?.usedPercent,
|
|
237
|
+
usage.primary?.resetAt,
|
|
238
|
+
shouldShowReset(preferences, "5h"),
|
|
239
|
+
preferences,
|
|
240
|
+
);
|
|
241
|
+
const sevenDay = formatUsageSegment(
|
|
204
242
|
ctx,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const sevenDayReset = preferences.showReset
|
|
212
|
-
? formatResetCountdown(usage.secondary?.resetAt)
|
|
213
|
-
: undefined;
|
|
214
|
-
const resetText =
|
|
215
|
-
preferences.resetWindow === "5h"
|
|
216
|
-
? fiveHourReset
|
|
217
|
-
? ctx.ui.theme.fg("dim", `(${FIVE_HOUR_LABEL}↺${fiveHourReset})`)
|
|
218
|
-
: undefined
|
|
219
|
-
: preferences.resetWindow === "7d"
|
|
220
|
-
? sevenDayReset
|
|
221
|
-
? ctx.ui.theme.fg("dim", `(${SEVEN_DAY_LABEL}↺${sevenDayReset})`)
|
|
222
|
-
: undefined
|
|
223
|
-
: [
|
|
224
|
-
fiveHourReset
|
|
225
|
-
? ctx.ui.theme.fg("dim", `(${FIVE_HOUR_LABEL}↺${fiveHourReset})`)
|
|
226
|
-
: undefined,
|
|
227
|
-
sevenDayReset
|
|
228
|
-
? ctx.ui.theme.fg("dim", `(${SEVEN_DAY_LABEL}↺${sevenDayReset})`)
|
|
229
|
-
: undefined,
|
|
230
|
-
]
|
|
231
|
-
.filter(Boolean)
|
|
232
|
-
.join(" ") || undefined;
|
|
243
|
+
SEVEN_DAY_LABEL,
|
|
244
|
+
usage.secondary?.usedPercent,
|
|
245
|
+
usage.secondary?.resetAt,
|
|
246
|
+
shouldShowReset(preferences, "7d"),
|
|
247
|
+
preferences,
|
|
248
|
+
);
|
|
233
249
|
|
|
234
250
|
const leading =
|
|
235
251
|
preferences.order === "account-first"
|
|
@@ -238,7 +254,7 @@ export function formatActiveAccountStatus(
|
|
|
238
254
|
const trailing =
|
|
239
255
|
preferences.order === "account-first" ? [] : [accountText].filter(Boolean);
|
|
240
256
|
|
|
241
|
-
return [...leading, fiveHour, sevenDay,
|
|
257
|
+
return [...leading, fiveHour, sevenDay, ...trailing]
|
|
242
258
|
.filter(Boolean)
|
|
243
259
|
.join(" ");
|
|
244
260
|
}
|
|
@@ -315,10 +331,17 @@ function applyPreferenceChange(
|
|
|
315
331
|
|
|
316
332
|
export function createUsageStatusController(accountManager: AccountManager) {
|
|
317
333
|
let refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
334
|
+
let modelSelectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
318
335
|
let activeContext: ExtensionContext | undefined;
|
|
319
336
|
let refreshInFlight = false;
|
|
320
337
|
let queuedRefresh = false;
|
|
321
338
|
let preferences: FooterPreferences = DEFAULT_PREFERENCES;
|
|
339
|
+
let livePreviewPreferences: FooterPreferences | undefined;
|
|
340
|
+
|
|
341
|
+
accountManager.onStateChange(() => {
|
|
342
|
+
if (!activeContext) return;
|
|
343
|
+
renderCachedStatus(activeContext, livePreviewPreferences ?? preferences);
|
|
344
|
+
});
|
|
322
345
|
|
|
323
346
|
function clearStatus(ctx?: ExtensionContext): void {
|
|
324
347
|
ctx?.ui.setStatus(STATUS_KEY, undefined);
|
|
@@ -328,6 +351,42 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
328
351
|
preferences = await loadFooterPreferences();
|
|
329
352
|
}
|
|
330
353
|
|
|
354
|
+
function getStatusText(
|
|
355
|
+
ctx: ExtensionContext,
|
|
356
|
+
preferencesOverride?: FooterPreferences,
|
|
357
|
+
): string | undefined {
|
|
358
|
+
if (!ctx.hasUI) return undefined;
|
|
359
|
+
if (!isManagedModel(ctx.model)) return undefined;
|
|
360
|
+
|
|
361
|
+
const activeAccount = accountManager.getActiveAccount();
|
|
362
|
+
if (!activeAccount) {
|
|
363
|
+
return ctx.ui.theme.fg("warning", "Multicodex no active account");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return formatActiveAccountStatus(
|
|
367
|
+
ctx,
|
|
368
|
+
activeAccount.email,
|
|
369
|
+
accountManager.getCachedUsage(activeAccount.email),
|
|
370
|
+
preferencesOverride ?? preferences,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function renderCachedStatus(
|
|
375
|
+
ctx: ExtensionContext,
|
|
376
|
+
preferencesOverride?: FooterPreferences,
|
|
377
|
+
): void {
|
|
378
|
+
if (!ctx.hasUI) return;
|
|
379
|
+
if (!isManagedModel(ctx.model)) {
|
|
380
|
+
clearStatus(ctx);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const text = getStatusText(ctx, preferencesOverride);
|
|
385
|
+
if (text) {
|
|
386
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
331
390
|
async function updateStatus(ctx: ExtensionContext): Promise<void> {
|
|
332
391
|
if (!ctx.hasUI) return;
|
|
333
392
|
if (!isManagedModel(ctx.model)) {
|
|
@@ -335,6 +394,8 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
335
394
|
return;
|
|
336
395
|
}
|
|
337
396
|
|
|
397
|
+
renderCachedStatus(ctx, livePreviewPreferences ?? preferences);
|
|
398
|
+
|
|
338
399
|
let activeAccount = accountManager.getActiveAccount();
|
|
339
400
|
if (!activeAccount) {
|
|
340
401
|
await accountManager.syncImportedOpenAICodexAuth();
|
|
@@ -354,7 +415,12 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
354
415
|
cachedUsage;
|
|
355
416
|
ctx.ui.setStatus(
|
|
356
417
|
STATUS_KEY,
|
|
357
|
-
formatActiveAccountStatus(
|
|
418
|
+
formatActiveAccountStatus(
|
|
419
|
+
ctx,
|
|
420
|
+
activeAccount.email,
|
|
421
|
+
usage,
|
|
422
|
+
livePreviewPreferences ?? preferences,
|
|
423
|
+
),
|
|
358
424
|
);
|
|
359
425
|
}
|
|
360
426
|
|
|
@@ -377,6 +443,19 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
377
443
|
}
|
|
378
444
|
}
|
|
379
445
|
|
|
446
|
+
function scheduleModelSelectRefresh(ctx: ExtensionContext): void {
|
|
447
|
+
activeContext = ctx;
|
|
448
|
+
renderCachedStatus(ctx, livePreviewPreferences ?? preferences);
|
|
449
|
+
if (modelSelectTimer) {
|
|
450
|
+
clearTimeout(modelSelectTimer);
|
|
451
|
+
}
|
|
452
|
+
modelSelectTimer = setTimeout(() => {
|
|
453
|
+
modelSelectTimer = undefined;
|
|
454
|
+
void refreshFor(ctx);
|
|
455
|
+
}, MODEL_SELECT_REFRESH_DEBOUNCE_MS);
|
|
456
|
+
modelSelectTimer.unref?.();
|
|
457
|
+
}
|
|
458
|
+
|
|
380
459
|
function startAutoRefresh(): void {
|
|
381
460
|
if (refreshTimer) clearInterval(refreshTimer);
|
|
382
461
|
refreshTimer = setInterval(() => {
|
|
@@ -391,6 +470,11 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
391
470
|
clearInterval(refreshTimer);
|
|
392
471
|
refreshTimer = undefined;
|
|
393
472
|
}
|
|
473
|
+
if (modelSelectTimer) {
|
|
474
|
+
clearTimeout(modelSelectTimer);
|
|
475
|
+
modelSelectTimer = undefined;
|
|
476
|
+
}
|
|
477
|
+
livePreviewPreferences = undefined;
|
|
394
478
|
clearStatus(ctx ?? activeContext);
|
|
395
479
|
activeContext = undefined;
|
|
396
480
|
queuedRefresh = false;
|
|
@@ -408,11 +492,23 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
408
492
|
}
|
|
409
493
|
}
|
|
410
494
|
|
|
495
|
+
function renderPreviewLabel(
|
|
496
|
+
ctx: ExtensionContext,
|
|
497
|
+
theme: ExtensionCommandContext["ui"]["theme"],
|
|
498
|
+
draft: FooterPreferences,
|
|
499
|
+
): string {
|
|
500
|
+
const previewText =
|
|
501
|
+
getStatusText(ctx, draft) ?? ctx.ui.theme.fg("dim", "Codex loading...");
|
|
502
|
+
return `${theme.fg("dim", "Preview")}: ${previewText}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
411
505
|
async function openPreferencesPanel(
|
|
412
506
|
ctx: ExtensionCommandContext,
|
|
413
507
|
): Promise<void> {
|
|
414
508
|
await loadPreferences(ctx);
|
|
415
509
|
let draft = preferences;
|
|
510
|
+
livePreviewPreferences = draft;
|
|
511
|
+
renderCachedStatus(ctx, livePreviewPreferences);
|
|
416
512
|
|
|
417
513
|
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
418
514
|
const container = new Container();
|
|
@@ -429,14 +525,20 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
429
525
|
0,
|
|
430
526
|
),
|
|
431
527
|
);
|
|
528
|
+
const previewText = new Text(renderPreviewLabel(ctx, theme, draft), 1, 0);
|
|
529
|
+
container.addChild(previewText);
|
|
432
530
|
|
|
433
531
|
const settingsList = new SettingsList(
|
|
434
532
|
createSettingsItems(draft),
|
|
435
|
-
|
|
533
|
+
9,
|
|
436
534
|
getSettingsListTheme(),
|
|
437
535
|
(id: string, newValue: string) => {
|
|
438
536
|
draft = applyPreferenceChange(draft, id, newValue);
|
|
537
|
+
livePreviewPreferences = draft;
|
|
439
538
|
settingsList.updateValue(id, newValue);
|
|
539
|
+
previewText.setText(renderPreviewLabel(ctx, theme, draft));
|
|
540
|
+
container.invalidate();
|
|
541
|
+
renderCachedStatus(ctx, draft);
|
|
440
542
|
},
|
|
441
543
|
() => done(undefined),
|
|
442
544
|
{ enableSearch: true },
|
|
@@ -451,6 +553,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
451
553
|
});
|
|
452
554
|
|
|
453
555
|
preferences = draft;
|
|
556
|
+
livePreviewPreferences = undefined;
|
|
454
557
|
await persistFooterPreferences(preferences);
|
|
455
558
|
await refreshFor(ctx);
|
|
456
559
|
}
|
|
@@ -459,6 +562,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
459
562
|
loadPreferences,
|
|
460
563
|
openPreferencesPanel,
|
|
461
564
|
refreshFor,
|
|
565
|
+
scheduleModelSelectRefresh,
|
|
462
566
|
startAutoRefresh,
|
|
463
567
|
stopAutoRefresh,
|
|
464
568
|
getPreferences: () => preferences,
|