@victor-software-house/pi-multicodex 1.0.8 → 1.0.10
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 +53 -6
- package/account-manager.ts +23 -1
- package/extension.ts +1 -1
- package/package.json +1 -1
- package/status.ts +153 -44
package/README.md
CHANGED
|
@@ -69,12 +69,59 @@ Current direction:
|
|
|
69
69
|
|
|
70
70
|
Current next step:
|
|
71
71
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
- refine the footer color palette with small visual adjustments only
|
|
73
|
+
- document the account-rotation behavior contract explicitly
|
|
74
|
+
- improve the `/multicodex-use` and `/multicodex-status` everyday UX
|
|
75
|
+
|
|
76
|
+
## Behavior contract
|
|
77
|
+
|
|
78
|
+
The current runtime behavior is:
|
|
79
|
+
|
|
80
|
+
### Account selection priority
|
|
81
|
+
|
|
82
|
+
1. Use the manual account selected with `/multicodex-use` when it is still available.
|
|
83
|
+
2. Otherwise clear the stale manual override and select the best available managed account.
|
|
84
|
+
3. Best-account selection prefers:
|
|
85
|
+
- untouched accounts with usage data
|
|
86
|
+
- then the account whose weekly reset window ends first
|
|
87
|
+
- then a random available account as fallback
|
|
88
|
+
|
|
89
|
+
### Quota exhaustion semantics
|
|
90
|
+
|
|
91
|
+
- Quota and rate-limit style failures are detected from provider error text.
|
|
92
|
+
- When a request fails before any output is streamed, MultiCodex marks that account exhausted and retries on another account.
|
|
93
|
+
- Exhaustion lasts until the next known reset time.
|
|
94
|
+
- If usage data does not provide a reset time, exhaustion falls back to a 1 hour cooldown.
|
|
95
|
+
|
|
96
|
+
### Retry policy
|
|
97
|
+
|
|
98
|
+
- MultiCodex retries account rotation up to 5 times for a single request.
|
|
99
|
+
- Retries only happen for quota/rate-limit style failures that occur before output is forwarded.
|
|
100
|
+
- Once output has started streaming, the original error is surfaced instead of rotating.
|
|
101
|
+
|
|
102
|
+
### Manual override behavior
|
|
103
|
+
|
|
104
|
+
- `/multicodex-use <identifier>` sets the manual account override immediately.
|
|
105
|
+
- `/multicodex-use` with no argument opens the account picker and sets the selected manual override.
|
|
106
|
+
- Manual override is session-local state.
|
|
107
|
+
- Manual override clears automatically when the selected account is no longer available or when it hits quota during rotation.
|
|
108
|
+
|
|
109
|
+
### Usage cache and refresh rules
|
|
110
|
+
|
|
111
|
+
- Usage is cached in memory for 5 minutes per account.
|
|
112
|
+
- Footer updates render cached usage immediately and refresh in the background when needed.
|
|
113
|
+
- Rapid `model_select` changes debounce background refresh work so non-Codex model switching clears the footer immediately.
|
|
114
|
+
|
|
115
|
+
### Error classification
|
|
116
|
+
|
|
117
|
+
Quota rotation currently treats these error classes as interchangeable:
|
|
118
|
+
|
|
119
|
+
- HTTP `429`
|
|
120
|
+
- `quota`
|
|
121
|
+
- `usage limit`
|
|
122
|
+
- `rate limit`
|
|
123
|
+
- `too many requests`
|
|
124
|
+
- `limit reached`
|
|
78
125
|
|
|
79
126
|
## Release validation
|
|
80
127
|
|
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,7 +21,9 @@ 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 = "--";
|
|
26
|
+
const BRAND_LABEL = "Codex";
|
|
25
27
|
const FIVE_HOUR_LABEL = "5h:";
|
|
26
28
|
const SEVEN_DAY_LABEL = "7d:";
|
|
27
29
|
|
|
@@ -138,13 +140,21 @@ function usedToDisplayPercent(
|
|
|
138
140
|
return mode === "left" ? left : clampPercent(100 - left);
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
function formatBrand(ctx: ExtensionContext): string {
|
|
144
|
+
return ctx.ui.theme.fg("muted", BRAND_LABEL);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatLoading(ctx: ExtensionContext): string {
|
|
148
|
+
return ctx.ui.theme.fg("muted", "loading...");
|
|
149
|
+
}
|
|
150
|
+
|
|
141
151
|
function formatPercent(
|
|
142
152
|
ctx: ExtensionContext,
|
|
143
153
|
displayPercent: number | undefined,
|
|
144
154
|
mode: PercentDisplayMode,
|
|
145
155
|
): string {
|
|
146
156
|
if (typeof displayPercent !== "number" || Number.isNaN(displayPercent)) {
|
|
147
|
-
return ctx.ui.theme.fg("
|
|
157
|
+
return ctx.ui.theme.fg("dim", UNKNOWN_PERCENT);
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
const text = `${Math.round(clampPercent(displayPercent))}% ${mode}`;
|
|
@@ -172,6 +182,40 @@ function formatResetCountdown(resetAt: number | undefined): string | undefined {
|
|
|
172
182
|
return `${seconds}s`;
|
|
173
183
|
}
|
|
174
184
|
|
|
185
|
+
function shouldShowReset(
|
|
186
|
+
preferences: FooterPreferences,
|
|
187
|
+
window: Exclude<ResetWindowMode, "both">,
|
|
188
|
+
): boolean {
|
|
189
|
+
if (!preferences.showReset) return false;
|
|
190
|
+
return (
|
|
191
|
+
preferences.resetWindow === "both" || preferences.resetWindow === window
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatUsageSegment(
|
|
196
|
+
ctx: ExtensionContext,
|
|
197
|
+
label: string,
|
|
198
|
+
usedPercent: number | undefined,
|
|
199
|
+
resetAt: number | undefined,
|
|
200
|
+
showReset: boolean,
|
|
201
|
+
preferences: FooterPreferences,
|
|
202
|
+
): string {
|
|
203
|
+
const parts = [
|
|
204
|
+
`${ctx.ui.theme.fg("dim", label)}${formatPercent(
|
|
205
|
+
ctx,
|
|
206
|
+
usedToDisplayPercent(usedPercent, preferences.usageMode),
|
|
207
|
+
preferences.usageMode,
|
|
208
|
+
)}`,
|
|
209
|
+
];
|
|
210
|
+
if (showReset) {
|
|
211
|
+
const countdown = formatResetCountdown(resetAt);
|
|
212
|
+
if (countdown) {
|
|
213
|
+
parts.push(ctx.ui.theme.fg("muted", `(↺${countdown})`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return parts.join(" ");
|
|
217
|
+
}
|
|
218
|
+
|
|
175
219
|
export function isManagedModel(model: MaybeModel): boolean {
|
|
176
220
|
return model?.provider === PROVIDER_ID;
|
|
177
221
|
}
|
|
@@ -186,59 +230,36 @@ export function formatActiveAccountStatus(
|
|
|
186
230
|
? ctx.ui.theme.fg("muted", accountEmail)
|
|
187
231
|
: undefined;
|
|
188
232
|
if (!usage) {
|
|
189
|
-
return [
|
|
190
|
-
ctx.ui.theme.fg("dim", "Codex"),
|
|
191
|
-
accountText,
|
|
192
|
-
ctx.ui.theme.fg("dim", "loading..."),
|
|
193
|
-
]
|
|
233
|
+
return [formatBrand(ctx), accountText, formatLoading(ctx)]
|
|
194
234
|
.filter(Boolean)
|
|
195
235
|
.join(" ");
|
|
196
236
|
}
|
|
197
237
|
|
|
198
|
-
const fiveHour =
|
|
238
|
+
const fiveHour = formatUsageSegment(
|
|
199
239
|
ctx,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
FIVE_HOUR_LABEL,
|
|
241
|
+
usage.primary?.usedPercent,
|
|
242
|
+
usage.primary?.resetAt,
|
|
243
|
+
shouldShowReset(preferences, "5h"),
|
|
244
|
+
preferences,
|
|
245
|
+
);
|
|
246
|
+
const sevenDay = formatUsageSegment(
|
|
204
247
|
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;
|
|
248
|
+
SEVEN_DAY_LABEL,
|
|
249
|
+
usage.secondary?.usedPercent,
|
|
250
|
+
usage.secondary?.resetAt,
|
|
251
|
+
shouldShowReset(preferences, "7d"),
|
|
252
|
+
preferences,
|
|
253
|
+
);
|
|
233
254
|
|
|
234
255
|
const leading =
|
|
235
256
|
preferences.order === "account-first"
|
|
236
|
-
? [ctx
|
|
237
|
-
: [ctx
|
|
257
|
+
? [formatBrand(ctx), accountText]
|
|
258
|
+
: [formatBrand(ctx)];
|
|
238
259
|
const trailing =
|
|
239
260
|
preferences.order === "account-first" ? [] : [accountText].filter(Boolean);
|
|
240
261
|
|
|
241
|
-
return [...leading, fiveHour, sevenDay,
|
|
262
|
+
return [...leading, fiveHour, sevenDay, ...trailing]
|
|
242
263
|
.filter(Boolean)
|
|
243
264
|
.join(" ");
|
|
244
265
|
}
|
|
@@ -315,10 +336,17 @@ function applyPreferenceChange(
|
|
|
315
336
|
|
|
316
337
|
export function createUsageStatusController(accountManager: AccountManager) {
|
|
317
338
|
let refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
339
|
+
let modelSelectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
318
340
|
let activeContext: ExtensionContext | undefined;
|
|
319
341
|
let refreshInFlight = false;
|
|
320
342
|
let queuedRefresh = false;
|
|
321
343
|
let preferences: FooterPreferences = DEFAULT_PREFERENCES;
|
|
344
|
+
let livePreviewPreferences: FooterPreferences | undefined;
|
|
345
|
+
|
|
346
|
+
accountManager.onStateChange(() => {
|
|
347
|
+
if (!activeContext) return;
|
|
348
|
+
renderCachedStatus(activeContext, livePreviewPreferences ?? preferences);
|
|
349
|
+
});
|
|
322
350
|
|
|
323
351
|
function clearStatus(ctx?: ExtensionContext): void {
|
|
324
352
|
ctx?.ui.setStatus(STATUS_KEY, undefined);
|
|
@@ -328,6 +356,42 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
328
356
|
preferences = await loadFooterPreferences();
|
|
329
357
|
}
|
|
330
358
|
|
|
359
|
+
function getStatusText(
|
|
360
|
+
ctx: ExtensionContext,
|
|
361
|
+
preferencesOverride?: FooterPreferences,
|
|
362
|
+
): string | undefined {
|
|
363
|
+
if (!ctx.hasUI) return undefined;
|
|
364
|
+
if (!isManagedModel(ctx.model)) return undefined;
|
|
365
|
+
|
|
366
|
+
const activeAccount = accountManager.getActiveAccount();
|
|
367
|
+
if (!activeAccount) {
|
|
368
|
+
return ctx.ui.theme.fg("warning", "Multicodex no active account");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return formatActiveAccountStatus(
|
|
372
|
+
ctx,
|
|
373
|
+
activeAccount.email,
|
|
374
|
+
accountManager.getCachedUsage(activeAccount.email),
|
|
375
|
+
preferencesOverride ?? preferences,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function renderCachedStatus(
|
|
380
|
+
ctx: ExtensionContext,
|
|
381
|
+
preferencesOverride?: FooterPreferences,
|
|
382
|
+
): void {
|
|
383
|
+
if (!ctx.hasUI) return;
|
|
384
|
+
if (!isManagedModel(ctx.model)) {
|
|
385
|
+
clearStatus(ctx);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const text = getStatusText(ctx, preferencesOverride);
|
|
390
|
+
if (text) {
|
|
391
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
331
395
|
async function updateStatus(ctx: ExtensionContext): Promise<void> {
|
|
332
396
|
if (!ctx.hasUI) return;
|
|
333
397
|
if (!isManagedModel(ctx.model)) {
|
|
@@ -335,6 +399,8 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
335
399
|
return;
|
|
336
400
|
}
|
|
337
401
|
|
|
402
|
+
renderCachedStatus(ctx, livePreviewPreferences ?? preferences);
|
|
403
|
+
|
|
338
404
|
let activeAccount = accountManager.getActiveAccount();
|
|
339
405
|
if (!activeAccount) {
|
|
340
406
|
await accountManager.syncImportedOpenAICodexAuth();
|
|
@@ -354,7 +420,12 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
354
420
|
cachedUsage;
|
|
355
421
|
ctx.ui.setStatus(
|
|
356
422
|
STATUS_KEY,
|
|
357
|
-
formatActiveAccountStatus(
|
|
423
|
+
formatActiveAccountStatus(
|
|
424
|
+
ctx,
|
|
425
|
+
activeAccount.email,
|
|
426
|
+
usage,
|
|
427
|
+
livePreviewPreferences ?? preferences,
|
|
428
|
+
),
|
|
358
429
|
);
|
|
359
430
|
}
|
|
360
431
|
|
|
@@ -377,6 +448,19 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
377
448
|
}
|
|
378
449
|
}
|
|
379
450
|
|
|
451
|
+
function scheduleModelSelectRefresh(ctx: ExtensionContext): void {
|
|
452
|
+
activeContext = ctx;
|
|
453
|
+
renderCachedStatus(ctx, livePreviewPreferences ?? preferences);
|
|
454
|
+
if (modelSelectTimer) {
|
|
455
|
+
clearTimeout(modelSelectTimer);
|
|
456
|
+
}
|
|
457
|
+
modelSelectTimer = setTimeout(() => {
|
|
458
|
+
modelSelectTimer = undefined;
|
|
459
|
+
void refreshFor(ctx);
|
|
460
|
+
}, MODEL_SELECT_REFRESH_DEBOUNCE_MS);
|
|
461
|
+
modelSelectTimer.unref?.();
|
|
462
|
+
}
|
|
463
|
+
|
|
380
464
|
function startAutoRefresh(): void {
|
|
381
465
|
if (refreshTimer) clearInterval(refreshTimer);
|
|
382
466
|
refreshTimer = setInterval(() => {
|
|
@@ -391,6 +475,11 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
391
475
|
clearInterval(refreshTimer);
|
|
392
476
|
refreshTimer = undefined;
|
|
393
477
|
}
|
|
478
|
+
if (modelSelectTimer) {
|
|
479
|
+
clearTimeout(modelSelectTimer);
|
|
480
|
+
modelSelectTimer = undefined;
|
|
481
|
+
}
|
|
482
|
+
livePreviewPreferences = undefined;
|
|
394
483
|
clearStatus(ctx ?? activeContext);
|
|
395
484
|
activeContext = undefined;
|
|
396
485
|
queuedRefresh = false;
|
|
@@ -408,11 +497,23 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
408
497
|
}
|
|
409
498
|
}
|
|
410
499
|
|
|
500
|
+
function renderPreviewLabel(
|
|
501
|
+
ctx: ExtensionContext,
|
|
502
|
+
theme: ExtensionCommandContext["ui"]["theme"],
|
|
503
|
+
draft: FooterPreferences,
|
|
504
|
+
): string {
|
|
505
|
+
const previewText =
|
|
506
|
+
getStatusText(ctx, draft) ?? `${formatBrand(ctx)} ${formatLoading(ctx)}`;
|
|
507
|
+
return `${theme.fg("dim", "Preview")}: ${previewText}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
411
510
|
async function openPreferencesPanel(
|
|
412
511
|
ctx: ExtensionCommandContext,
|
|
413
512
|
): Promise<void> {
|
|
414
513
|
await loadPreferences(ctx);
|
|
415
514
|
let draft = preferences;
|
|
515
|
+
livePreviewPreferences = draft;
|
|
516
|
+
renderCachedStatus(ctx, livePreviewPreferences);
|
|
416
517
|
|
|
417
518
|
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
418
519
|
const container = new Container();
|
|
@@ -429,14 +530,20 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
429
530
|
0,
|
|
430
531
|
),
|
|
431
532
|
);
|
|
533
|
+
const previewText = new Text(renderPreviewLabel(ctx, theme, draft), 1, 0);
|
|
534
|
+
container.addChild(previewText);
|
|
432
535
|
|
|
433
536
|
const settingsList = new SettingsList(
|
|
434
537
|
createSettingsItems(draft),
|
|
435
|
-
|
|
538
|
+
9,
|
|
436
539
|
getSettingsListTheme(),
|
|
437
540
|
(id: string, newValue: string) => {
|
|
438
541
|
draft = applyPreferenceChange(draft, id, newValue);
|
|
542
|
+
livePreviewPreferences = draft;
|
|
439
543
|
settingsList.updateValue(id, newValue);
|
|
544
|
+
previewText.setText(renderPreviewLabel(ctx, theme, draft));
|
|
545
|
+
container.invalidate();
|
|
546
|
+
renderCachedStatus(ctx, draft);
|
|
440
547
|
},
|
|
441
548
|
() => done(undefined),
|
|
442
549
|
{ enableSearch: true },
|
|
@@ -451,6 +558,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
451
558
|
});
|
|
452
559
|
|
|
453
560
|
preferences = draft;
|
|
561
|
+
livePreviewPreferences = undefined;
|
|
454
562
|
await persistFooterPreferences(preferences);
|
|
455
563
|
await refreshFor(ctx);
|
|
456
564
|
}
|
|
@@ -459,6 +567,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
459
567
|
loadPreferences,
|
|
460
568
|
openPreferencesPanel,
|
|
461
569
|
refreshFor,
|
|
570
|
+
scheduleModelSelectRefresh,
|
|
462
571
|
startAutoRefresh,
|
|
463
572
|
stopAutoRefresh,
|
|
464
573
|
getPreferences: () => preferences,
|