@narumitw/pi-usage 0.60.7 → 0.60.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/src/usage.ts CHANGED
@@ -1,68 +1,45 @@
1
1
  // This orchestrator intentionally remains over 1,000 lines because its menu, query generations,
2
2
  // account cache, cancellation, and session lifecycle share one consistency boundary.
3
3
  import { randomUUID } from "node:crypto";
4
- import type {
5
- ExtensionAPI,
6
- ExtensionCommandContext,
7
- ExtensionContext,
8
- } from "@earendil-works/pi-coding-agent";
4
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
5
  import { FAST_USAGE_WARNING, registerCodexFastMode } from "./codex-fast-runtime.js";
10
6
  import {
11
- type CodexResetAvailability,
12
- type CodexResetOption,
13
- type CodexResetOutcome,
14
- codexResetActionDescription,
15
- codexResetCount,
16
- consumeCodexResetCredit,
17
- formatCodexResetOutcome,
18
- genericCodexResetOption,
19
- listCodexResetCredits,
20
- resetConfirmationLines,
21
- resetLabel,
22
- resetOptionExpiration,
23
- resolveCodexResetAuth,
7
+ type CodexResetAvailability,
8
+ type CodexResetOption,
9
+ type CodexResetOutcome,
10
+ codexResetActionDescription,
11
+ codexResetCount,
12
+ consumeCodexResetCredit,
13
+ formatCodexResetOutcome,
14
+ genericCodexResetOption,
15
+ listCodexResetCredits,
16
+ resetConfirmationLines,
17
+ resetLabel,
18
+ resetOptionExpiration,
19
+ resolveCodexResetAuth,
24
20
  } from "./codex-resets.js";
25
- import {
26
- abortError,
27
- awaitWithDeadline,
28
- errorMessage,
29
- runWithConcurrency,
30
- UsageCache,
31
- } from "./core.js";
21
+ import { abortError, awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
32
22
  import { formatProviderStates, formatUsageStatusline } from "./format.js";
33
23
  import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
34
- import {
35
- adapterForProvider,
36
- isStaleExtensionContextError,
37
- queryProviderUsage,
38
- resolveUsageAuth,
39
- } from "./query.js";
40
- import {
41
- createUsageSettingsRuntime,
42
- type UsageSettingsRuntime,
43
- type UsageSettingsState,
44
- } from "./settings.js";
24
+ import { adapterForProvider, isStaleExtensionContextError, queryProviderUsage, resolveUsageAuth } from "./query.js";
25
+ import { createUsageSettingsRuntime, type UsageSettingsRuntime, type UsageSettingsState } from "./settings.js";
45
26
  import type {
46
- PiModel,
47
- ProviderUsageState,
48
- ResolvedUsageAuth,
49
- UsageDisplayState,
50
- UsageProviderAdapter,
27
+ PiModel,
28
+ ProviderUsageState,
29
+ ResolvedUsageAuth,
30
+ UsageDisplayState,
31
+ UsageProviderAdapter,
51
32
  } from "./types.js";
52
33
  import {
53
- configuredAdapters,
54
- isAbortError,
55
- isTimeoutError,
56
- modelIdentity,
57
- providerDisplayName,
58
- setBoundedMap,
34
+ configuredAdapters,
35
+ isAbortError,
36
+ isTimeoutError,
37
+ modelIdentity,
38
+ providerDisplayName,
39
+ setBoundedMap,
59
40
  } from "./usage-helpers.js";
60
41
  import { showUsageSettings } from "./usage-settings-ui.js";
61
- import {
62
- createUsageTargetSelectOptions,
63
- listUsageTargets,
64
- resolveUsageTarget,
65
- } from "./usage-targets.js";
42
+ import { createUsageTargetSelectOptions, listUsageTargets, resolveUsageTarget } from "./usage-targets.js";
66
43
 
67
44
  const CACHE_TTL_MS = 5 * 60 * 1000;
68
45
  const STATUS_COUNTDOWN_REFRESH_MS = 60 * 1000;
@@ -80,1411 +57,1272 @@ const SETTINGS = "Settings";
80
57
  const REDEEM_CODEX_RESET = "Redeem usage limit reset…";
81
58
 
82
59
  type UsageExtensionDependencies = {
83
- credentialReader?: (providerId: string) => unknown;
84
- createRedemptionId?: () => string;
85
- settingsRuntime?: UsageSettingsRuntime;
60
+ credentialReader?: (providerId: string) => unknown;
61
+ createRedemptionId?: () => string;
62
+ settingsRuntime?: UsageSettingsRuntime;
86
63
  };
87
64
 
88
65
  type QueryOutcome = {
89
- state: ProviderUsageState;
90
- fingerprint?: string;
91
- authState?: "unavailable";
92
- rememberedTargetId?: string;
66
+ state: ProviderUsageState;
67
+ fingerprint?: string;
68
+ authState?: "unavailable";
69
+ rememberedTargetId?: string;
93
70
  };
94
71
 
95
72
  class UsageTargetSelectionChangedError extends Error {
96
- override readonly name = "UsageTargetSelectionChangedError";
73
+ override readonly name = "UsageTargetSelectionChangedError";
97
74
  }
98
75
 
99
76
  type StableCurrent = {
100
- outcome: QueryOutcome;
101
- model: PiModel | undefined;
77
+ outcome: QueryOutcome;
78
+ model: PiModel | undefined;
102
79
  };
103
80
 
104
- export default function usageExtension(
105
- pi: ExtensionAPI,
106
- dependencies: UsageExtensionDependencies = {},
107
- ) {
108
- const credentialReader = dependencies.credentialReader;
109
- const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
110
- const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
111
- const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
112
- const cache = new UsageCache(CACHE_TTL_MS);
113
- const failureBackoff = new Map<string, { until: number; message: string }>();
114
- const latestQueries = new Map<string, number>();
115
- const activeControllers = new Set<AbortController>();
116
- let querySequence = 0;
117
- let activeCurrentIdentity: string | undefined;
118
- let sessionActive = false;
119
- let statusGeneration = 0;
120
- let sessionGeneration = 0;
121
- let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
122
- let statusCountdownTimer: ReturnType<typeof setTimeout> | undefined;
123
- let statusController: AbortController | undefined;
124
- let fastRuntime: ReturnType<typeof registerCodexFastMode>;
81
+ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExtensionDependencies = {}) {
82
+ const credentialReader = dependencies.credentialReader;
83
+ const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
84
+ const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
85
+ const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
86
+ const cache = new UsageCache(CACHE_TTL_MS);
87
+ const failureBackoff = new Map<string, { until: number; message: string }>();
88
+ const latestQueries = new Map<string, number>();
89
+ const activeControllers = new Set<AbortController>();
90
+ let querySequence = 0;
91
+ let activeCurrentIdentity: string | undefined;
92
+ let sessionActive = false;
93
+ let statusGeneration = 0;
94
+ let sessionGeneration = 0;
95
+ let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
96
+ let statusCountdownTimer: ReturnType<typeof setTimeout> | undefined;
97
+ let statusController: AbortController | undefined;
98
+ let fastRuntime: ReturnType<typeof registerCodexFastMode>;
125
99
 
126
- const clearStatusRefreshTimer = () => {
127
- if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
128
- statusRefreshTimer = undefined;
129
- };
100
+ const clearStatusRefreshTimer = () => {
101
+ if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
102
+ statusRefreshTimer = undefined;
103
+ };
130
104
 
131
- const clearStatusCountdownTimer = () => {
132
- if (statusCountdownTimer) clearTimeout(statusCountdownTimer);
133
- statusCountdownTimer = undefined;
134
- };
105
+ const clearStatusCountdownTimer = () => {
106
+ if (statusCountdownTimer) clearTimeout(statusCountdownTimer);
107
+ statusCountdownTimer = undefined;
108
+ };
135
109
 
136
- const clearStatusTimers = () => {
137
- clearStatusRefreshTimer();
138
- clearStatusCountdownTimer();
139
- };
110
+ const clearStatusTimers = () => {
111
+ clearStatusRefreshTimer();
112
+ clearStatusCountdownTimer();
113
+ };
140
114
 
141
- const safeSetStatus = (ctx: ExtensionContext, value: string | undefined): boolean => {
142
- try {
143
- ctx.ui.setStatus(STATUS_KEY, value);
144
- return true;
145
- } catch (error) {
146
- if (isStaleExtensionContextError(error)) return false;
147
- throw error;
148
- }
149
- };
115
+ const safeSetStatus = (ctx: ExtensionContext, value: string | undefined): boolean => {
116
+ try {
117
+ ctx.ui.setStatus(STATUS_KEY, value);
118
+ return true;
119
+ } catch (error) {
120
+ if (isStaleExtensionContextError(error)) return false;
121
+ throw error;
122
+ }
123
+ };
150
124
 
151
- const clearStatus = (ctx: ExtensionContext) => {
152
- statusGeneration += 1;
153
- statusController?.abort();
154
- statusController = undefined;
155
- clearStatusTimers();
156
- safeSetStatus(ctx, undefined);
157
- };
125
+ const clearStatus = (ctx: ExtensionContext) => {
126
+ statusGeneration += 1;
127
+ statusController?.abort();
128
+ statusController = undefined;
129
+ clearStatusTimers();
130
+ safeSetStatus(ctx, undefined);
131
+ };
158
132
 
159
- const scheduleStatusRefresh = (ctx: ExtensionContext, model: PiModel) => {
160
- clearStatusRefreshTimer();
161
- const generation = statusGeneration;
162
- statusRefreshTimer = setTimeout(() => {
163
- statusRefreshTimer = undefined;
164
- if (!sessionActive || generation !== statusGeneration) return;
165
- startStatusRefresh(ctx, model, true);
166
- }, CACHE_TTL_MS);
167
- statusRefreshTimer.unref?.();
168
- };
133
+ const scheduleStatusRefresh = (ctx: ExtensionContext, model: PiModel) => {
134
+ clearStatusRefreshTimer();
135
+ const generation = statusGeneration;
136
+ statusRefreshTimer = setTimeout(() => {
137
+ statusRefreshTimer = undefined;
138
+ if (!sessionActive || generation !== statusGeneration) return;
139
+ startStatusRefresh(ctx, model, true);
140
+ }, CACHE_TTL_MS);
141
+ statusRefreshTimer.unref?.();
142
+ };
169
143
 
170
- const publishStatus = (
171
- ctx: ExtensionContext,
172
- outcome: QueryOutcome,
173
- model: PiModel,
174
- shouldSchedule: boolean,
175
- ) => {
176
- clearStatusCountdownTimer();
177
- if (adapterForProvider(model.provider)?.publishesStatusline === false) {
178
- clearStatusRefreshTimer();
179
- safeSetStatus(ctx, undefined);
180
- return;
181
- }
182
- if (outcome.state.status === "unsupported") {
183
- clearStatusRefreshTimer();
184
- safeSetStatus(ctx, undefined);
185
- return;
186
- }
187
- if (outcome.state.status !== "ready") {
188
- const chip =
189
- outcome.state.status === "auth-unavailable"
190
- ? "auth unavailable"
191
- : outcome.state.status === "selection-required"
192
- ? "selection required"
193
- : `usage err: ${outcome.state.message.slice(0, 50)}`;
144
+ const publishStatus = (ctx: ExtensionContext, outcome: QueryOutcome, model: PiModel, shouldSchedule: boolean) => {
145
+ clearStatusCountdownTimer();
146
+ if (adapterForProvider(model.provider)?.publishesStatusline === false) {
147
+ clearStatusRefreshTimer();
148
+ safeSetStatus(ctx, undefined);
149
+ return;
150
+ }
151
+ if (outcome.state.status === "unsupported") {
152
+ clearStatusRefreshTimer();
153
+ safeSetStatus(ctx, undefined);
154
+ return;
155
+ }
156
+ if (outcome.state.status !== "ready") {
157
+ const chip =
158
+ outcome.state.status === "auth-unavailable"
159
+ ? "auth unavailable"
160
+ : outcome.state.status === "selection-required"
161
+ ? "selection required"
162
+ : `usage err: ${outcome.state.message.slice(0, 50)}`;
194
163
 
195
- if (safeSetStatus(ctx, chip)) {
196
- if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
197
- }
198
- return;
199
- }
200
- const showCodexResetCountdown =
201
- outcome.state.report.providerId === "openai-codex" &&
202
- settingsRuntime.get().settings.codexStatusResetCountdown;
203
- const now = Date.now();
204
- const rawValue = formatUsageStatusline(
205
- outcome.state.report,
206
- model,
207
- now,
208
- showCodexResetCountdown,
209
- );
210
- const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
211
- if (!safeSetStatus(ctx, value)) return;
212
- if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
213
- if (
214
- sessionActive &&
215
- showCodexResetCountdown &&
216
- outcome.state.report.buckets.some(
217
- (bucket) =>
218
- bucket.resetsAt !== undefined &&
219
- Number.isFinite(bucket.resetsAt) &&
220
- bucket.resetsAt * 1_000 > now,
221
- )
222
- ) {
223
- const generation = statusGeneration;
224
- statusCountdownTimer = setTimeout(() => {
225
- statusCountdownTimer = undefined;
226
- if (!sessionActive || generation !== statusGeneration) return;
227
- publishStatus(ctx, outcome, model, false);
228
- }, STATUS_COUNTDOWN_REFRESH_MS);
229
- statusCountdownTimer.unref?.();
230
- }
231
- };
164
+ if (safeSetStatus(ctx, chip)) {
165
+ if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
166
+ }
167
+ return;
168
+ }
169
+ const showCodexResetCountdown =
170
+ outcome.state.report.providerId === "openai-codex" && settingsRuntime.get().settings.codexStatusResetCountdown;
171
+ const now = Date.now();
172
+ const rawValue = formatUsageStatusline(outcome.state.report, model, now, showCodexResetCountdown);
173
+ const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
174
+ if (!safeSetStatus(ctx, value)) return;
175
+ if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
176
+ if (
177
+ sessionActive &&
178
+ showCodexResetCountdown &&
179
+ outcome.state.report.buckets.some(
180
+ (bucket) => bucket.resetsAt !== undefined && Number.isFinite(bucket.resetsAt) && bucket.resetsAt * 1_000 > now,
181
+ )
182
+ ) {
183
+ const generation = statusGeneration;
184
+ statusCountdownTimer = setTimeout(() => {
185
+ statusCountdownTimer = undefined;
186
+ if (!sessionActive || generation !== statusGeneration) return;
187
+ publishStatus(ctx, outcome, model, false);
188
+ }, STATUS_COUNTDOWN_REFRESH_MS);
189
+ statusCountdownTimer.unref?.();
190
+ }
191
+ };
232
192
 
233
- const invalidateProviderState = (providerId: string) => {
234
- cache.clearProvider(providerId);
235
- for (const key of failureBackoff.keys()) {
236
- if (key.startsWith(`${providerId}:`)) failureBackoff.delete(key);
237
- }
238
- for (const key of latestQueries.keys()) {
239
- if (key.startsWith(`${providerId}:`)) latestQueries.delete(key);
240
- }
241
- };
193
+ const invalidateProviderState = (providerId: string) => {
194
+ cache.clearProvider(providerId);
195
+ for (const key of failureBackoff.keys()) {
196
+ if (key.startsWith(`${providerId}:`)) failureBackoff.delete(key);
197
+ }
198
+ for (const key of latestQueries.keys()) {
199
+ if (key.startsWith(`${providerId}:`)) latestQueries.delete(key);
200
+ }
201
+ };
242
202
 
243
- const transitionCurrentIdentity = (nextIdentity: string, providerId: string) => {
244
- if (!activeCurrentIdentity || activeCurrentIdentity === nextIdentity) {
245
- activeCurrentIdentity = nextIdentity;
246
- return;
247
- }
248
- const previousProviderId = activeCurrentIdentity.split(":", 1)[0] ?? "";
249
- for (const id of new Set([previousProviderId, providerId])) {
250
- if (id) invalidateProviderState(id);
251
- }
252
- activeCurrentIdentity = nextIdentity;
253
- };
203
+ const transitionCurrentIdentity = (nextIdentity: string, providerId: string) => {
204
+ if (!activeCurrentIdentity || activeCurrentIdentity === nextIdentity) {
205
+ activeCurrentIdentity = nextIdentity;
206
+ return;
207
+ }
208
+ const previousProviderId = activeCurrentIdentity.split(":", 1)[0] ?? "";
209
+ for (const id of new Set([previousProviderId, providerId])) {
210
+ if (id) invalidateProviderState(id);
211
+ }
212
+ activeCurrentIdentity = nextIdentity;
213
+ };
254
214
 
255
- const queryAdapterState = async (
256
- ctx: ExtensionContext,
257
- adapter: UsageProviderAdapter,
258
- displayState: UsageDisplayState,
259
- force: boolean,
260
- signal: AbortSignal,
261
- authRetry = 0,
262
- deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS,
263
- ): Promise<QueryOutcome> => {
264
- const expectedSessionGeneration = sessionGeneration;
265
- const expectedSessionId = ctx.sessionManager.getSessionId();
266
- const expectedModelIdentity = modelIdentity(ctx.model);
267
- const expectedTargetId = adapter.targets
268
- ? settingsRuntime.get().settings.selectedTargets[adapter.id]
269
- : undefined;
270
- const providerName = providerDisplayName(ctx, adapter.id);
271
- let auth: ResolvedUsageAuth | undefined;
272
- try {
273
- auth = await awaitWithDeadline(
274
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
275
- signal,
276
- Math.max(1, deadlineAt - Date.now()),
277
- `resolving ${adapter.displayName} runtime auth`,
278
- );
279
- } catch (error) {
280
- if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
281
- if (displayState === "current") {
282
- transitionCurrentIdentity(`${adapter.id}:auth-error`, adapter.id);
283
- }
284
- return {
285
- state: {
286
- providerId: adapter.id,
287
- providerName,
288
- displayState,
289
- status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
290
- message: errorMessage(error),
291
- },
292
- };
293
- }
294
- const requiresRequestBoundaryGuard =
295
- adapter.targets !== undefined ||
296
- [
297
- "baseten",
298
- "deepseek",
299
- "minimax",
300
- "minimax-cn",
301
- "moonshotai",
302
- "moonshotai-cn",
303
- "vercel-ai-gateway",
304
- "xai",
305
- "zai",
306
- "zai-coding-cn",
307
- ].includes(adapter.id);
308
- const requestContextChanged = () =>
309
- expectedSessionGeneration !== sessionGeneration ||
310
- ctx.sessionManager.getSessionId() !== expectedSessionId ||
311
- modelIdentity(ctx.model) !== expectedModelIdentity ||
312
- (adapter.targets !== undefined &&
313
- settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
314
- if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
315
- if (!auth) {
316
- if (displayState === "current") {
317
- transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
318
- }
319
- return {
320
- state: {
321
- providerId: adapter.id,
322
- providerName,
323
- displayState,
324
- status: "auth-unavailable",
325
- message: `No runtime credential is configured for ${providerName}.`,
326
- },
327
- authState: "unavailable",
328
- };
329
- }
330
- let retryableAuthChanged = false;
331
- const guard = async () => {
332
- if (signal.aborted || requestContextChanged()) throw abortError();
333
- if (!requiresRequestBoundaryGuard) return;
334
- const revalidated = await awaitWithDeadline(
335
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
336
- signal,
337
- Math.max(1, deadlineAt - Date.now()),
338
- `revalidating ${providerName} runtime auth`,
339
- );
340
- if (signal.aborted || requestContextChanged()) throw abortError();
341
- if (revalidated?.fingerprint !== auth.fingerprint) {
342
- if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
343
- retryableAuthChanged = true;
344
- throw new Error(`${providerName} runtime credential changed during the usage query.`);
345
- }
346
- throw abortError();
347
- }
348
- };
349
- let queryFingerprint = adapter.targets
350
- ? `${auth.fingerprint}:target:${expectedTargetId ?? "unresolved"}`
351
- : auth.fingerprint;
352
- let failureKey = `${adapter.id}:${queryFingerprint}`;
353
- let queryId: number | undefined;
354
- try {
355
- const previousDiscoveryFailure = failureBackoff.get(failureKey);
356
- if (!force && previousDiscoveryFailure && previousDiscoveryFailure.until > Date.now()) {
357
- return {
358
- state: {
359
- providerId: adapter.id,
360
- providerName,
361
- displayState,
362
- status: "query-failed",
363
- message: previousDiscoveryFailure.message,
364
- },
365
- fingerprint: auth.fingerprint,
366
- rememberedTargetId: expectedTargetId,
367
- };
368
- }
369
- const target = await resolveUsageTarget(
370
- adapter,
371
- auth,
372
- expectedTargetId,
373
- signal,
374
- Math.max(1, deadlineAt - Date.now()),
375
- guard,
376
- );
377
- if (target.kind === "selection-required") {
378
- if (displayState === "current") {
379
- transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
380
- }
381
- return {
382
- state: {
383
- providerId: adapter.id,
384
- providerName,
385
- displayState,
386
- status: "selection-required",
387
- singularLabel: adapter.targets?.singularLabel ?? "target",
388
- pluralLabel: adapter.targets?.pluralLabel ?? "targets",
389
- choices: target.choices,
390
- },
391
- fingerprint: auth.fingerprint,
392
- rememberedTargetId: expectedTargetId,
393
- };
394
- }
395
- queryFingerprint = adapter.targets
396
- ? `${auth.fingerprint}:target:${target.targetId ?? "none"}`
397
- : auth.fingerprint;
398
- failureKey = `${adapter.id}:${queryFingerprint}`;
399
- if (displayState === "current") {
400
- transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
401
- }
402
- const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
403
- if (cached) {
404
- return {
405
- state: {
406
- providerId: adapter.id,
407
- providerName,
408
- displayState,
409
- status: "ready",
410
- report: cached,
411
- },
412
- fingerprint: auth.fingerprint,
413
- rememberedTargetId: expectedTargetId,
414
- };
415
- }
416
- const previousFailure = failureBackoff.get(failureKey);
417
- if (!force && previousFailure && previousFailure.until > Date.now()) {
418
- return {
419
- state: {
420
- providerId: adapter.id,
421
- providerName,
422
- displayState,
423
- status: "query-failed",
424
- message: previousFailure.message,
425
- },
426
- fingerprint: auth.fingerprint,
427
- rememberedTargetId: expectedTargetId,
428
- };
429
- }
430
- failureBackoff.delete(failureKey);
431
- querySequence += 1;
432
- queryId = querySequence;
433
- setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
434
- const report = await queryProviderUsage(
435
- adapter,
436
- auth,
437
- signal,
438
- Math.max(1, deadlineAt - Date.now()),
439
- requiresRequestBoundaryGuard ? guard : undefined,
440
- target.targetId,
441
- );
442
- if (requiresRequestBoundaryGuard) await guard();
443
- const effectiveReport = { ...report, providerName };
444
- if (latestQueries.get(failureKey) === queryId) {
445
- cache.set(adapter.id, queryFingerprint, effectiveReport);
446
- failureBackoff.delete(failureKey);
447
- }
448
- return {
449
- state: {
450
- providerId: adapter.id,
451
- providerName,
452
- displayState,
453
- status: "ready",
454
- report: effectiveReport,
455
- },
456
- fingerprint: auth.fingerprint,
457
- rememberedTargetId: expectedTargetId,
458
- };
459
- } catch (error) {
460
- if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
461
- if (
462
- retryableAuthChanged &&
463
- authRetry === 0 &&
464
- !signal.aborted &&
465
- !requestContextChanged() &&
466
- Date.now() < deadlineAt
467
- ) {
468
- if (queryId !== undefined && latestQueries.get(failureKey) === queryId) {
469
- latestQueries.delete(failureKey);
470
- }
471
- return queryAdapterState(
472
- ctx,
473
- adapter,
474
- displayState,
475
- true,
476
- signal,
477
- authRetry + 1,
478
- deadlineAt,
479
- );
480
- }
481
- const message = errorMessage(error);
482
- const now = Date.now();
483
- for (const [key, failure] of failureBackoff) {
484
- if (failure.until <= now) failureBackoff.delete(key);
485
- }
486
- if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
487
- if (adapter.invalidateCacheOnFailure) cache.delete(adapter.id, queryFingerprint);
488
- setBoundedMap(
489
- failureBackoff,
490
- failureKey,
491
- { until: now + FAILURE_BACKOFF_MS, message },
492
- MAX_ACCOUNT_STATES,
493
- );
494
- }
495
- return {
496
- state: {
497
- providerId: adapter.id,
498
- providerName,
499
- displayState,
500
- status: "query-failed",
501
- message,
502
- },
503
- fingerprint: auth.fingerprint,
504
- rememberedTargetId: expectedTargetId,
505
- };
506
- }
507
- };
215
+ const queryAdapterState = async (
216
+ ctx: ExtensionContext,
217
+ adapter: UsageProviderAdapter,
218
+ displayState: UsageDisplayState,
219
+ force: boolean,
220
+ signal: AbortSignal,
221
+ authRetry = 0,
222
+ deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS,
223
+ ): Promise<QueryOutcome> => {
224
+ const expectedSessionGeneration = sessionGeneration;
225
+ const expectedSessionId = ctx.sessionManager.getSessionId();
226
+ const expectedModelIdentity = modelIdentity(ctx.model);
227
+ const expectedTargetId = adapter.targets ? settingsRuntime.get().settings.selectedTargets[adapter.id] : undefined;
228
+ const providerName = providerDisplayName(ctx, adapter.id);
229
+ let auth: ResolvedUsageAuth | undefined;
230
+ try {
231
+ auth = await awaitWithDeadline(
232
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
233
+ signal,
234
+ Math.max(1, deadlineAt - Date.now()),
235
+ `resolving ${adapter.displayName} runtime auth`,
236
+ );
237
+ } catch (error) {
238
+ if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
239
+ if (displayState === "current") {
240
+ transitionCurrentIdentity(`${adapter.id}:auth-error`, adapter.id);
241
+ }
242
+ return {
243
+ state: {
244
+ providerId: adapter.id,
245
+ providerName,
246
+ displayState,
247
+ status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
248
+ message: errorMessage(error),
249
+ },
250
+ };
251
+ }
252
+ const requiresRequestBoundaryGuard =
253
+ adapter.targets !== undefined ||
254
+ [
255
+ "baseten",
256
+ "deepseek",
257
+ "minimax",
258
+ "minimax-cn",
259
+ "moonshotai",
260
+ "moonshotai-cn",
261
+ "vercel-ai-gateway",
262
+ "xai",
263
+ "zai",
264
+ "zai-coding-cn",
265
+ ].includes(adapter.id);
266
+ const requestContextChanged = () =>
267
+ expectedSessionGeneration !== sessionGeneration ||
268
+ ctx.sessionManager.getSessionId() !== expectedSessionId ||
269
+ modelIdentity(ctx.model) !== expectedModelIdentity ||
270
+ (adapter.targets !== undefined &&
271
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
272
+ if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
273
+ if (!auth) {
274
+ if (displayState === "current") {
275
+ transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
276
+ }
277
+ return {
278
+ state: {
279
+ providerId: adapter.id,
280
+ providerName,
281
+ displayState,
282
+ status: "auth-unavailable",
283
+ message: `No runtime credential is configured for ${providerName}.`,
284
+ },
285
+ authState: "unavailable",
286
+ };
287
+ }
288
+ let retryableAuthChanged = false;
289
+ const guard = async () => {
290
+ if (signal.aborted || requestContextChanged()) throw abortError();
291
+ if (!requiresRequestBoundaryGuard) return;
292
+ const revalidated = await awaitWithDeadline(
293
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
294
+ signal,
295
+ Math.max(1, deadlineAt - Date.now()),
296
+ `revalidating ${providerName} runtime auth`,
297
+ );
298
+ if (signal.aborted || requestContextChanged()) throw abortError();
299
+ if (revalidated?.fingerprint !== auth.fingerprint) {
300
+ if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
301
+ retryableAuthChanged = true;
302
+ throw new Error(`${providerName} runtime credential changed during the usage query.`);
303
+ }
304
+ throw abortError();
305
+ }
306
+ };
307
+ let queryFingerprint = adapter.targets
308
+ ? `${auth.fingerprint}:target:${expectedTargetId ?? "unresolved"}`
309
+ : auth.fingerprint;
310
+ let failureKey = `${adapter.id}:${queryFingerprint}`;
311
+ let queryId: number | undefined;
312
+ try {
313
+ const previousDiscoveryFailure = failureBackoff.get(failureKey);
314
+ if (!force && previousDiscoveryFailure && previousDiscoveryFailure.until > Date.now()) {
315
+ return {
316
+ state: {
317
+ providerId: adapter.id,
318
+ providerName,
319
+ displayState,
320
+ status: "query-failed",
321
+ message: previousDiscoveryFailure.message,
322
+ },
323
+ fingerprint: auth.fingerprint,
324
+ rememberedTargetId: expectedTargetId,
325
+ };
326
+ }
327
+ const target = await resolveUsageTarget(
328
+ adapter,
329
+ auth,
330
+ expectedTargetId,
331
+ signal,
332
+ Math.max(1, deadlineAt - Date.now()),
333
+ guard,
334
+ );
335
+ if (target.kind === "selection-required") {
336
+ if (displayState === "current") {
337
+ transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
338
+ }
339
+ return {
340
+ state: {
341
+ providerId: adapter.id,
342
+ providerName,
343
+ displayState,
344
+ status: "selection-required",
345
+ singularLabel: adapter.targets?.singularLabel ?? "target",
346
+ pluralLabel: adapter.targets?.pluralLabel ?? "targets",
347
+ choices: target.choices,
348
+ },
349
+ fingerprint: auth.fingerprint,
350
+ rememberedTargetId: expectedTargetId,
351
+ };
352
+ }
353
+ queryFingerprint = adapter.targets ? `${auth.fingerprint}:target:${target.targetId ?? "none"}` : auth.fingerprint;
354
+ failureKey = `${adapter.id}:${queryFingerprint}`;
355
+ if (displayState === "current") {
356
+ transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
357
+ }
358
+ const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
359
+ if (cached) {
360
+ return {
361
+ state: {
362
+ providerId: adapter.id,
363
+ providerName,
364
+ displayState,
365
+ status: "ready",
366
+ report: cached,
367
+ },
368
+ fingerprint: auth.fingerprint,
369
+ rememberedTargetId: expectedTargetId,
370
+ };
371
+ }
372
+ const previousFailure = failureBackoff.get(failureKey);
373
+ if (!force && previousFailure && previousFailure.until > Date.now()) {
374
+ return {
375
+ state: {
376
+ providerId: adapter.id,
377
+ providerName,
378
+ displayState,
379
+ status: "query-failed",
380
+ message: previousFailure.message,
381
+ },
382
+ fingerprint: auth.fingerprint,
383
+ rememberedTargetId: expectedTargetId,
384
+ };
385
+ }
386
+ failureBackoff.delete(failureKey);
387
+ querySequence += 1;
388
+ queryId = querySequence;
389
+ setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
390
+ const report = await queryProviderUsage(
391
+ adapter,
392
+ auth,
393
+ signal,
394
+ Math.max(1, deadlineAt - Date.now()),
395
+ requiresRequestBoundaryGuard ? guard : undefined,
396
+ target.targetId,
397
+ );
398
+ if (requiresRequestBoundaryGuard) await guard();
399
+ const effectiveReport = { ...report, providerName };
400
+ if (latestQueries.get(failureKey) === queryId) {
401
+ cache.set(adapter.id, queryFingerprint, effectiveReport);
402
+ failureBackoff.delete(failureKey);
403
+ }
404
+ return {
405
+ state: {
406
+ providerId: adapter.id,
407
+ providerName,
408
+ displayState,
409
+ status: "ready",
410
+ report: effectiveReport,
411
+ },
412
+ fingerprint: auth.fingerprint,
413
+ rememberedTargetId: expectedTargetId,
414
+ };
415
+ } catch (error) {
416
+ if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
417
+ if (
418
+ retryableAuthChanged &&
419
+ authRetry === 0 &&
420
+ !signal.aborted &&
421
+ !requestContextChanged() &&
422
+ Date.now() < deadlineAt
423
+ ) {
424
+ if (queryId !== undefined && latestQueries.get(failureKey) === queryId) {
425
+ latestQueries.delete(failureKey);
426
+ }
427
+ return queryAdapterState(ctx, adapter, displayState, true, signal, authRetry + 1, deadlineAt);
428
+ }
429
+ const message = errorMessage(error);
430
+ const now = Date.now();
431
+ for (const [key, failure] of failureBackoff) {
432
+ if (failure.until <= now) failureBackoff.delete(key);
433
+ }
434
+ if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
435
+ if (adapter.invalidateCacheOnFailure) cache.delete(adapter.id, queryFingerprint);
436
+ setBoundedMap(failureBackoff, failureKey, { until: now + FAILURE_BACKOFF_MS, message }, MAX_ACCOUNT_STATES);
437
+ }
438
+ return {
439
+ state: {
440
+ providerId: adapter.id,
441
+ providerName,
442
+ displayState,
443
+ status: "query-failed",
444
+ message,
445
+ },
446
+ fingerprint: auth.fingerprint,
447
+ rememberedTargetId: expectedTargetId,
448
+ };
449
+ }
450
+ };
508
451
 
509
- const loadTargetChoices = async (
510
- ctx: ExtensionContext,
511
- adapter: UsageProviderAdapter,
512
- signal: AbortSignal,
513
- ) => {
514
- if (!adapter.targets) throw new Error("Provider does not support usage targets.");
515
- const expectedSessionGeneration = sessionGeneration;
516
- const expectedSessionId = ctx.sessionManager.getSessionId();
517
- const expectedModel = modelIdentity(ctx.model);
518
- const expectedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
519
- const deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS;
520
- const changed = () =>
521
- expectedSessionGeneration !== sessionGeneration ||
522
- ctx.sessionManager.getSessionId() !== expectedSessionId ||
523
- modelIdentity(ctx.model) !== expectedModel ||
524
- settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
525
- const auth = await awaitWithDeadline(
526
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
527
- signal,
528
- Math.max(1, deadlineAt - Date.now()),
529
- `resolving ${providerDisplayName(ctx, adapter.id)} runtime auth`,
530
- );
531
- if (!auth || signal.aborted || changed()) throw abortError();
532
- const guard = async () => {
533
- if (signal.aborted || changed()) throw abortError();
534
- const revalidated = await awaitWithDeadline(
535
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
536
- signal,
537
- Math.max(1, deadlineAt - Date.now()),
538
- `revalidating ${providerDisplayName(ctx, adapter.id)} runtime auth`,
539
- );
540
- if (signal.aborted || changed() || revalidated?.fingerprint !== auth.fingerprint) {
541
- throw abortError();
542
- }
543
- };
544
- const choices = await listUsageTargets(
545
- adapter,
546
- auth,
547
- signal,
548
- Math.max(1, deadlineAt - Date.now()),
549
- guard,
550
- );
551
- return { choices, fingerprint: auth.fingerprint };
552
- };
452
+ const loadTargetChoices = async (ctx: ExtensionContext, adapter: UsageProviderAdapter, signal: AbortSignal) => {
453
+ if (!adapter.targets) throw new Error("Provider does not support usage targets.");
454
+ const expectedSessionGeneration = sessionGeneration;
455
+ const expectedSessionId = ctx.sessionManager.getSessionId();
456
+ const expectedModel = modelIdentity(ctx.model);
457
+ const expectedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
458
+ const deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS;
459
+ const changed = () =>
460
+ expectedSessionGeneration !== sessionGeneration ||
461
+ ctx.sessionManager.getSessionId() !== expectedSessionId ||
462
+ modelIdentity(ctx.model) !== expectedModel ||
463
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
464
+ const auth = await awaitWithDeadline(
465
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
466
+ signal,
467
+ Math.max(1, deadlineAt - Date.now()),
468
+ `resolving ${providerDisplayName(ctx, adapter.id)} runtime auth`,
469
+ );
470
+ if (!auth || signal.aborted || changed()) throw abortError();
471
+ const guard = async () => {
472
+ if (signal.aborted || changed()) throw abortError();
473
+ const revalidated = await awaitWithDeadline(
474
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
475
+ signal,
476
+ Math.max(1, deadlineAt - Date.now()),
477
+ `revalidating ${providerDisplayName(ctx, adapter.id)} runtime auth`,
478
+ );
479
+ if (signal.aborted || changed() || revalidated?.fingerprint !== auth.fingerprint) {
480
+ throw abortError();
481
+ }
482
+ };
483
+ const choices = await listUsageTargets(adapter, auth, signal, Math.max(1, deadlineAt - Date.now()), guard);
484
+ return { choices, fingerprint: auth.fingerprint };
485
+ };
553
486
 
554
- const queryCurrentState = async (
555
- ctx: ExtensionContext,
556
- model: PiModel | undefined,
557
- force: boolean,
558
- signal: AbortSignal,
559
- ): Promise<QueryOutcome> => {
560
- const adapter = adapterForProvider(model?.provider);
561
- if (!adapter) {
562
- const providerId = model?.provider ?? "none";
563
- transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
564
- return {
565
- state: {
566
- providerId,
567
- providerName: providerDisplayName(ctx, providerId),
568
- displayState: "current",
569
- status: "unsupported",
570
- message: model
571
- ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
572
- : "No model is selected.",
573
- },
574
- };
575
- }
576
- return queryAdapterState(ctx, adapter, "current", force, signal);
577
- };
487
+ const queryCurrentState = async (
488
+ ctx: ExtensionContext,
489
+ model: PiModel | undefined,
490
+ force: boolean,
491
+ signal: AbortSignal,
492
+ ): Promise<QueryOutcome> => {
493
+ const adapter = adapterForProvider(model?.provider);
494
+ if (!adapter) {
495
+ const providerId = model?.provider ?? "none";
496
+ transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
497
+ return {
498
+ state: {
499
+ providerId,
500
+ providerName: providerDisplayName(ctx, providerId),
501
+ displayState: "current",
502
+ status: "unsupported",
503
+ message: model
504
+ ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
505
+ : "No model is selected.",
506
+ },
507
+ };
508
+ }
509
+ return queryAdapterState(ctx, adapter, "current", force, signal);
510
+ };
578
511
 
579
- const refreshCurrentStatus = async (
580
- ctx: ExtensionContext,
581
- model: PiModel | undefined,
582
- force: boolean,
583
- ) => {
584
- const adapter = adapterForProvider(model?.provider);
585
- if (!adapter || !model) {
586
- const providerId = model?.provider ?? "none";
587
- transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
588
- clearStatus(ctx);
589
- return;
590
- }
591
- if (adapter.publishesStatusline === false) {
592
- clearStatus(ctx);
593
- return;
594
- }
595
- statusGeneration += 1;
596
- const generation = statusGeneration;
597
- clearStatusCountdownTimer();
598
- statusController?.abort();
599
- const controller = new AbortController();
600
- statusController = controller;
601
- activeControllers.add(controller);
602
- try {
603
- if (!safeSetStatus(ctx, "checking")) return;
604
- const outcome = await queryCurrentState(ctx, model, force, controller.signal);
605
- if (!sessionActive || generation !== statusGeneration || controller.signal.aborted) return;
606
- if (!(await outcomeStillCurrent(ctx, model, generation, outcome, controller.signal))) {
607
- if (sessionActive && generation === statusGeneration) {
608
- queueMicrotask(() => startStatusRefresh(ctx, ctx.model, false));
609
- }
610
- return;
611
- }
612
- publishStatus(ctx, outcome, model, true);
613
- } finally {
614
- activeControllers.delete(controller);
615
- if (statusController === controller) statusController = undefined;
616
- }
617
- };
512
+ const refreshCurrentStatus = async (ctx: ExtensionContext, model: PiModel | undefined, force: boolean) => {
513
+ const adapter = adapterForProvider(model?.provider);
514
+ if (!adapter || !model) {
515
+ const providerId = model?.provider ?? "none";
516
+ transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
517
+ clearStatus(ctx);
518
+ return;
519
+ }
520
+ if (adapter.publishesStatusline === false) {
521
+ clearStatus(ctx);
522
+ return;
523
+ }
524
+ statusGeneration += 1;
525
+ const generation = statusGeneration;
526
+ clearStatusCountdownTimer();
527
+ statusController?.abort();
528
+ const controller = new AbortController();
529
+ statusController = controller;
530
+ activeControllers.add(controller);
531
+ try {
532
+ if (!safeSetStatus(ctx, "checking")) return;
533
+ const outcome = await queryCurrentState(ctx, model, force, controller.signal);
534
+ if (!sessionActive || generation !== statusGeneration || controller.signal.aborted) return;
535
+ if (!(await outcomeStillCurrent(ctx, model, generation, outcome, controller.signal))) {
536
+ if (sessionActive && generation === statusGeneration) {
537
+ queueMicrotask(() => startStatusRefresh(ctx, ctx.model, false));
538
+ }
539
+ return;
540
+ }
541
+ publishStatus(ctx, outcome, model, true);
542
+ } finally {
543
+ activeControllers.delete(controller);
544
+ if (statusController === controller) statusController = undefined;
545
+ }
546
+ };
618
547
 
619
- const startStatusRefresh = (
620
- ctx: ExtensionContext,
621
- model: PiModel | undefined,
622
- force: boolean,
623
- ) => {
624
- void refreshCurrentStatus(ctx, model, force).catch((error) => {
625
- if (isStaleExtensionContextError(error) || isAbortError(error)) return;
626
- const message = errorMessage(error);
627
- console.error("[pi-usage] refresh failed:", message);
628
- safeSetStatus(ctx, `usage err: ${message.slice(0, 50)}`);
629
- });
630
- };
548
+ const startStatusRefresh = (ctx: ExtensionContext, model: PiModel | undefined, force: boolean) => {
549
+ void refreshCurrentStatus(ctx, model, force).catch((error) => {
550
+ if (isStaleExtensionContextError(error) || isAbortError(error)) return;
551
+ const message = errorMessage(error);
552
+ console.error("[pi-usage] refresh failed:", message);
553
+ safeSetStatus(ctx, `usage err: ${message.slice(0, 50)}`);
554
+ });
555
+ };
631
556
 
632
- const runMenuOperation = async <T>(
633
- ctx: ExtensionCommandContext,
634
- label: string,
635
- parentSignal: AbortSignal,
636
- operation: (signal: AbortSignal) => Promise<T>,
637
- cancellable = true,
638
- ): Promise<T | undefined> => {
639
- const { runTask } = await import("@narumitw/pi-tui-kit");
640
- if (parentSignal.aborted) return undefined;
641
- const result = await runTask(ctx, {
642
- label,
643
- signal: parentSignal,
644
- cancellable,
645
- onError: () => undefined,
646
- task: ({ signal }) => operation(signal),
647
- });
648
- switch (result.kind) {
649
- case "completed":
650
- return result.value;
651
- case "cancelled":
652
- case "stale":
653
- return undefined;
654
- case "error":
655
- throw result.error;
656
- }
657
- };
557
+ const runMenuOperation = async <T>(
558
+ ctx: ExtensionCommandContext,
559
+ label: string,
560
+ parentSignal: AbortSignal,
561
+ operation: (signal: AbortSignal) => Promise<T>,
562
+ cancellable = true,
563
+ ): Promise<T | undefined> => {
564
+ const { runTask } = await import("@narumitw/pi-tui-kit");
565
+ if (parentSignal.aborted) return undefined;
566
+ const result = await runTask(ctx, {
567
+ label,
568
+ signal: parentSignal,
569
+ cancellable,
570
+ onError: () => undefined,
571
+ task: ({ signal }) => operation(signal),
572
+ });
573
+ switch (result.kind) {
574
+ case "completed":
575
+ return result.value;
576
+ case "cancelled":
577
+ case "stale":
578
+ return undefined;
579
+ case "error":
580
+ throw result.error;
581
+ }
582
+ };
658
583
 
659
- const outcomeStillCurrent = async (
660
- ctx: ExtensionContext,
661
- model: PiModel | undefined,
662
- generation: number,
663
- outcome: QueryOutcome,
664
- signal: AbortSignal,
665
- ): Promise<boolean> => {
666
- if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
667
- return false;
668
- }
669
- const adapter = adapterForProvider(model?.provider);
670
- const selectionStillCurrent =
671
- !adapter?.targets ||
672
- settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId;
673
- if (!selectionStillCurrent) return false;
674
- if (outcome.authState === "unavailable") {
675
- if (!adapter) return false;
676
- try {
677
- const auth = await awaitWithDeadline(
678
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
679
- signal,
680
- DEFAULT_TIMEOUT_MS,
681
- `revalidating ${adapter.displayName} runtime auth`,
682
- );
683
- return (
684
- generation === statusGeneration &&
685
- modelIdentity(ctx.model) === modelIdentity(model) &&
686
- (!adapter.targets ||
687
- settingsRuntime.get().settings.selectedTargets[adapter.id] ===
688
- outcome.rememberedTargetId) &&
689
- auth === undefined
690
- );
691
- } catch (error) {
692
- if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
693
- return false;
694
- }
695
- }
696
- if (!outcome.fingerprint) return true;
697
- if (!adapter) return false;
698
- try {
699
- const auth = await awaitWithDeadline(
700
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
701
- signal,
702
- DEFAULT_TIMEOUT_MS,
703
- `revalidating ${adapter.displayName} runtime auth`,
704
- );
705
- return (
706
- generation === statusGeneration &&
707
- modelIdentity(ctx.model) === modelIdentity(model) &&
708
- (!adapter.targets ||
709
- settingsRuntime.get().settings.selectedTargets[adapter.id] ===
710
- outcome.rememberedTargetId) &&
711
- auth?.fingerprint === outcome.fingerprint
712
- );
713
- } catch (error) {
714
- if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
715
- return false;
716
- }
717
- };
584
+ const outcomeStillCurrent = async (
585
+ ctx: ExtensionContext,
586
+ model: PiModel | undefined,
587
+ generation: number,
588
+ outcome: QueryOutcome,
589
+ signal: AbortSignal,
590
+ ): Promise<boolean> => {
591
+ if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
592
+ return false;
593
+ }
594
+ const adapter = adapterForProvider(model?.provider);
595
+ const selectionStillCurrent =
596
+ !adapter?.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId;
597
+ if (!selectionStillCurrent) return false;
598
+ if (outcome.authState === "unavailable") {
599
+ if (!adapter) return false;
600
+ try {
601
+ const auth = await awaitWithDeadline(
602
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
603
+ signal,
604
+ DEFAULT_TIMEOUT_MS,
605
+ `revalidating ${adapter.displayName} runtime auth`,
606
+ );
607
+ return (
608
+ generation === statusGeneration &&
609
+ modelIdentity(ctx.model) === modelIdentity(model) &&
610
+ (!adapter.targets ||
611
+ settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) &&
612
+ auth === undefined
613
+ );
614
+ } catch (error) {
615
+ if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
616
+ return false;
617
+ }
618
+ }
619
+ if (!outcome.fingerprint) return true;
620
+ if (!adapter) return false;
621
+ try {
622
+ const auth = await awaitWithDeadline(
623
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
624
+ signal,
625
+ DEFAULT_TIMEOUT_MS,
626
+ `revalidating ${adapter.displayName} runtime auth`,
627
+ );
628
+ return (
629
+ generation === statusGeneration &&
630
+ modelIdentity(ctx.model) === modelIdentity(model) &&
631
+ (!adapter.targets ||
632
+ settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) &&
633
+ auth?.fingerprint === outcome.fingerprint
634
+ );
635
+ } catch (error) {
636
+ if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
637
+ return false;
638
+ }
639
+ };
718
640
 
719
- const queryStableCurrent = async (
720
- ctx: ExtensionCommandContext,
721
- force: boolean,
722
- controller: AbortController,
723
- label: string,
724
- ): Promise<StableCurrent | undefined> => {
725
- for (let attempt = 0; attempt < 3; attempt += 1) {
726
- const model = ctx.model;
727
- const generation = statusGeneration;
728
- const result = await runMenuOperation(ctx, label, controller.signal, async (signal) => {
729
- const outcome = await queryCurrentState(ctx, model, force, signal);
730
- return {
731
- outcome,
732
- stable: await outcomeStillCurrent(ctx, model, generation, outcome, signal),
733
- };
734
- });
735
- if (!result) return undefined;
736
- if (result.stable) return { outcome: result.outcome, model };
737
- force = false;
738
- }
739
- ctx.ui.notify("The active model or account kept changing; reopen /usage to retry.", "warning");
740
- return undefined;
741
- };
641
+ const queryStableCurrent = async (
642
+ ctx: ExtensionCommandContext,
643
+ force: boolean,
644
+ controller: AbortController,
645
+ label: string,
646
+ ): Promise<StableCurrent | undefined> => {
647
+ for (let attempt = 0; attempt < 3; attempt += 1) {
648
+ const model = ctx.model;
649
+ const generation = statusGeneration;
650
+ const result = await runMenuOperation(ctx, label, controller.signal, async (signal) => {
651
+ const outcome = await queryCurrentState(ctx, model, force, signal);
652
+ return {
653
+ outcome,
654
+ stable: await outcomeStillCurrent(ctx, model, generation, outcome, signal),
655
+ };
656
+ });
657
+ if (!result) return undefined;
658
+ if (result.stable) return { outcome: result.outcome, model };
659
+ force = false;
660
+ }
661
+ ctx.ui.notify("The active model or account kept changing; reopen /usage to retry.", "warning");
662
+ return undefined;
663
+ };
742
664
 
743
- const publishStableCurrent = (ctx: ExtensionCommandContext, current: StableCurrent) => {
744
- if (current.model) publishStatus(ctx, current.outcome, current.model, sessionActive);
745
- else safeSetStatus(ctx, undefined);
746
- };
665
+ const publishStableCurrent = (ctx: ExtensionCommandContext, current: StableCurrent) => {
666
+ if (current.model) publishStatus(ctx, current.outcome, current.model, sessionActive);
667
+ else safeSetStatus(ctx, undefined);
668
+ };
747
669
 
748
- const showMenu = async (ctx: ExtensionCommandContext): Promise<void> => {
749
- if (!ctx.hasUI) throw new Error("/usage requires TUI or RPC mode.");
750
- statusGeneration += 1;
751
- const menuGeneration = statusGeneration;
752
- statusController?.abort();
753
- statusController = undefined;
754
- clearStatusTimers();
755
- const controller = new AbortController();
756
- activeControllers.add(controller);
757
- try {
758
- let stableCurrent = await queryStableCurrent(
759
- ctx,
760
- false,
761
- controller,
762
- "Checking current usage…",
763
- );
764
- if (!stableCurrent) return;
765
- publishStableCurrent(ctx, stableCurrent);
766
- let current = stableCurrent.outcome;
767
- let visibleStates: ProviderUsageState[] = [current.state];
768
- let fastState = settingsRuntime.get();
769
- let resetAvailability: CodexResetAvailability | undefined;
770
- let selectedReset: CodexResetOption | undefined;
771
- let resetAuthFingerprint: string | undefined;
772
- let resetModelIdentity: string | undefined;
773
- let redemptionId: string | undefined;
774
- let resetOutcome: CodexResetOutcome | undefined;
775
- let resetFailure: string | undefined;
776
- const actionableTargetState = (): ProviderUsageState | undefined => {
777
- if (visibleStates.length !== 1) return undefined;
778
- const state = visibleStates[0];
779
- return state &&
780
- (state.status === "ready" || state.status === "selection-required") &&
781
- adapterForProvider(state.providerId)?.targets
782
- ? state
783
- : undefined;
784
- };
785
- const promptForTarget = async (
786
- state: ProviderUsageState,
787
- stateFingerprint?: string,
788
- ): Promise<boolean> => {
789
- const adapter = adapterForProvider(state.providerId);
790
- if (!adapter?.targets) return false;
791
- const expectedRememberedTargetId =
792
- settingsRuntime.get().settings.selectedTargets[adapter.id];
793
- const snapshot =
794
- state.status === "selection-required" && stateFingerprint
795
- ? { choices: state.choices, fingerprint: stateFingerprint }
796
- : await runMenuOperation(
797
- ctx,
798
- `Loading ${adapter.targets.pluralLabel}…`,
799
- controller.signal,
800
- (signal) => loadTargetChoices(ctx, adapter, signal),
801
- );
802
- if (!snapshot || controller.signal.aborted || statusGeneration !== menuGeneration) {
803
- return false;
804
- }
805
- const selectOptions = createUsageTargetSelectOptions(snapshot.choices);
806
- const selected = await ctx.ui.select(
807
- `Select ${adapter.targets.singularLabel} for ${providerDisplayName(ctx, adapter.id)}`,
808
- [...selectOptions.options],
809
- { signal: controller.signal },
810
- );
811
- if (
812
- selected === undefined ||
813
- controller.signal.aborted ||
814
- statusGeneration !== menuGeneration ||
815
- settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
816
- ) {
817
- return false;
818
- }
819
- const targetId = selectOptions.targetIdFor(selected);
820
- if (!targetId) return false;
821
- const revalidated = await runMenuOperation(
822
- ctx,
823
- `Revalidating ${adapter.targets.singularLabel}…`,
824
- controller.signal,
825
- (signal) => loadTargetChoices(ctx, adapter, signal),
826
- );
827
- if (
828
- !revalidated ||
829
- controller.signal.aborted ||
830
- statusGeneration !== menuGeneration ||
831
- settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
832
- ) {
833
- return false;
834
- }
835
- if (
836
- revalidated.fingerprint !== snapshot.fingerprint ||
837
- !revalidated.choices.some((choice) => choice.id === targetId)
838
- ) {
839
- ctx.ui.notify(
840
- `${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
841
- "warning",
842
- );
843
- return false;
844
- }
845
- let saved: Readonly<UsageSettingsState> | undefined;
846
- try {
847
- saved = await runMenuOperation(
848
- ctx,
849
- `Saving ${adapter.targets.singularLabel}…`,
850
- controller.signal,
851
- (signal) =>
852
- settingsRuntime.updateSelectedTarget(adapter.id, targetId, signal, async () => {
853
- let published: Awaited<ReturnType<typeof loadTargetChoices>>;
854
- try {
855
- published = await loadTargetChoices(ctx, adapter, signal);
856
- } catch (error) {
857
- if (
858
- signal.aborted ||
859
- controller.signal.aborted ||
860
- statusGeneration !== menuGeneration ||
861
- isStaleExtensionContextError(error)
862
- ) {
863
- throw error;
864
- }
865
- throw new UsageTargetSelectionChangedError();
866
- }
867
- if (
868
- published.fingerprint !== snapshot.fingerprint ||
869
- !published.choices.some((choice) => choice.id === targetId)
870
- ) {
871
- throw new UsageTargetSelectionChangedError();
872
- }
873
- }),
874
- );
875
- } catch (error) {
876
- if (error instanceof UsageTargetSelectionChangedError) {
877
- ctx.ui.notify(
878
- `${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
879
- "warning",
880
- );
881
- return false;
882
- }
883
- throw error;
884
- }
885
- if (!saved || controller.signal.aborted || statusGeneration !== menuGeneration) {
886
- return false;
887
- }
888
- invalidateProviderState(adapter.id);
889
- return true;
890
- };
891
- const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
892
- if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
893
- type Screen =
894
- | "main"
895
- | "providers"
896
- | "reset-picker"
897
- | "reset-confirm"
898
- | "reset-result"
899
- | "reset-error";
900
- type Action =
901
- | "refresh"
902
- | "settings"
903
- | "target"
904
- | "toggle-fast"
905
- | "another"
906
- | "all"
907
- | "provider"
908
- | "open-resets"
909
- | "select-reset"
910
- | "cancel-reset"
911
- | "consume-reset"
912
- | "back-to-usage"
913
- | "back-to-resets";
914
- const menu = defineMenu<undefined, Screen, Action, ExtensionCommandContext>({
915
- start: "main",
916
- screens: {
917
- main: () => {
918
- const fastAvailability = fastRuntime.availability(ctx.model);
919
- const targetState = actionableTargetState();
920
- const targetAdapter = adapterForProvider(targetState?.providerId);
921
- const fastLines =
922
- fastAvailability.kind === "available"
923
- ? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
924
- : fastAvailability.kind === "unavailable"
925
- ? [`Fast mode: Unavailable · ${fastAvailability.reason}`]
926
- : [];
927
- return {
928
- kind: "actions",
929
- title: "Provider usage",
930
- lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
931
- items: [
932
- { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
933
- { id: "settings", label: SETTINGS, action: "settings" },
934
- ...(targetState && targetAdapter?.targets
935
- ? [
936
- {
937
- id: "target",
938
- label: `${targetState.status === "selection-required" ? "Select" : "Change"} ${targetAdapter.targets.singularLabel}…`,
939
- action: "target" as const,
940
- },
941
- ]
942
- : []),
943
- ...(fastAvailability.kind === "available"
944
- ? [
945
- {
946
- id: "toggle-fast",
947
- label: fastAvailability.enabled
948
- ? "Turn Fast mode off"
949
- : "Turn Fast mode on",
950
- description:
951
- fastState.kind === "invalid"
952
- ? "Repair pi-usage.json and reload before changing Fast mode."
953
- : FAST_USAGE_WARNING,
954
- disabled: fastState.kind === "invalid",
955
- action: "toggle-fast" as const,
956
- },
957
- ]
958
- : []),
959
- ...(current.state.status === "ready" && current.state.providerId === "openai-codex"
960
- ? [
961
- {
962
- id: "open-resets",
963
- label: REDEEM_CODEX_RESET,
964
- description: codexResetActionDescription(current.state.report),
965
- disabled: codexResetCount(current.state.report) === 0,
966
- action: "open-resets" as const,
967
- },
968
- ]
969
- : []),
970
- { id: "another", label: VIEW_ANOTHER, action: "another" },
971
- { id: "all", label: VIEW_ALL, action: "all" },
972
- { id: "close", label: CLOSE, close: true },
973
- ],
974
- hint: "close",
975
- };
976
- },
977
- providers: () => ({
978
- kind: "actions",
979
- title: "Select a configured provider",
980
- items: configuredAdapters(ctx)
981
- .filter((adapter) => adapter.id !== ctx.model?.provider)
982
- .map((adapter) => ({
983
- id: adapter.id,
984
- label: providerDisplayName(ctx, adapter.id),
985
- action: "provider" as const,
986
- })),
987
- hint: "back",
988
- }),
989
- "reset-picker": () => ({
990
- kind: "choice",
991
- title: "Usage limit resets",
992
- lines: [
993
- `${resetAvailability?.availableCount ?? 0} ${resetLabel(resetAvailability?.availableCount ?? 0)} available.`,
994
- ],
995
- items: (resetAvailability?.options ?? []).map((option, index) => ({
996
- id: `reset-${index}`,
997
- label: option.title,
998
- description: resetOptionExpiration(option),
999
- details: [option.description],
1000
- })),
1001
- action: "select-reset",
1002
- initialItemId: "reset-0",
1003
- hint: "back",
1004
- }),
1005
- "reset-confirm": () => ({
1006
- kind: "actions",
1007
- title: "Use this reset?",
1008
- lines: resetConfirmationLines(selectedReset),
1009
- items: [
1010
- { id: "cancel-reset", label: "No, go back", action: "cancel-reset" },
1011
- { id: "consume-reset", label: "Yes, use reset", action: "consume-reset" },
1012
- ],
1013
- hint: "back",
1014
- }),
1015
- "reset-result": () => ({
1016
- kind: "actions",
1017
- title: "Usage limit resets",
1018
- lines: [
1019
- formatCodexResetOutcome(
1020
- resetOutcome,
1021
- current.state.status === "ready"
1022
- ? codexResetCount(current.state.report)
1023
- : undefined,
1024
- ),
1025
- ],
1026
- items: [
1027
- {
1028
- id: "back-to-usage",
1029
- label: "Back to usage",
1030
- action: "back-to-usage",
1031
- },
1032
- { id: "close", label: CLOSE, close: true },
1033
- ],
1034
- hint: "back",
1035
- }),
1036
- "reset-error": () => ({
1037
- kind: "actions",
1038
- title: "Usage limit resets",
1039
- lines: [resetFailure ?? "Couldn't reset usage. Please try again."],
1040
- items: [
1041
- { id: "consume-reset", label: "Try again", action: "consume-reset" },
1042
- { id: "back-to-resets", label: "Back", action: "back-to-resets" },
1043
- ],
1044
- hint: "back",
1045
- }),
1046
- },
1047
- actions: {
1048
- target: async () => {
1049
- const targetState = actionableTargetState();
1050
- if (!targetState) return { kind: "rejected" };
1051
- try {
1052
- const stateFingerprint =
1053
- targetState === current.state ? current.fingerprint : undefined;
1054
- if (!(await promptForTarget(targetState, stateFingerprint))) {
1055
- return { kind: "stay" };
1056
- }
1057
- const adapter = adapterForProvider(targetState.providerId);
1058
- if (!adapter) return { kind: "rejected" };
1059
- if (targetState.providerId === ctx.model?.provider) {
1060
- const refreshed = await queryStableCurrent(
1061
- ctx,
1062
- true,
1063
- controller,
1064
- `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1065
- );
1066
- if (!refreshed) return { kind: "stay" };
1067
- stableCurrent = refreshed;
1068
- current = refreshed.outcome;
1069
- visibleStates = [current.state];
1070
- publishStableCurrent(ctx, refreshed);
1071
- return { kind: "stay" };
1072
- }
1073
- const outcome = await runMenuOperation(
1074
- ctx,
1075
- `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1076
- controller.signal,
1077
- (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
1078
- );
1079
- if (!outcome) return { kind: "stay" };
1080
- const revalidated = await queryStableCurrent(
1081
- ctx,
1082
- false,
1083
- controller,
1084
- "Revalidating current usage…",
1085
- );
1086
- if (!revalidated) return { kind: "stay" };
1087
- stableCurrent = revalidated;
1088
- current = revalidated.outcome;
1089
- visibleStates = [
1090
- outcome.state.providerId === current.state.providerId
1091
- ? current.state
1092
- : { ...outcome.state, displayState: "configured" },
1093
- ];
1094
- return { kind: "stay" };
1095
- } catch (error) {
1096
- if (isAbortError(error) || isStaleExtensionContextError(error)) {
1097
- return { kind: "stay" };
1098
- }
1099
- ctx.ui.notify(`Could not select target: ${errorMessage(error)}`, "error");
1100
- return { kind: "stay" };
1101
- }
1102
- },
1103
- settings: async () => {
1104
- await showUsageSettings(
1105
- ctx,
1106
- settingsRuntime,
1107
- controller.signal,
1108
- () => statusGeneration === menuGeneration && !controller.signal.aborted,
1109
- (id) => {
1110
- if (
1111
- id === "codexStatusResetCountdown" &&
1112
- stableCurrent &&
1113
- statusGeneration === menuGeneration &&
1114
- !controller.signal.aborted
1115
- ) {
1116
- publishStableCurrent(ctx, stableCurrent);
1117
- }
1118
- },
1119
- );
1120
- fastState = settingsRuntime.get();
1121
- const revalidated = await queryStableCurrent(
1122
- ctx,
1123
- false,
1124
- controller,
1125
- "Applying usage settings…",
1126
- );
1127
- if (!revalidated) return { kind: "stay" };
1128
- stableCurrent = revalidated;
1129
- current = revalidated.outcome;
1130
- visibleStates = [current.state];
1131
- publishStableCurrent(ctx, revalidated);
1132
- return { kind: "stay" };
1133
- },
1134
- "toggle-fast": async () => {
1135
- const availability = fastRuntime.availability(ctx.model);
1136
- if (availability.kind !== "available" || fastState.kind === "invalid") {
1137
- return { kind: "rejected" };
1138
- }
1139
- const changed = await fastRuntime.toggle(ctx, !availability.enabled, controller.signal);
1140
- if (!changed) return { kind: "rejected" };
1141
- fastState = settingsRuntime.get();
1142
- return { kind: "stay" };
1143
- },
1144
- "open-resets": async () => {
1145
- const summaryCount =
1146
- current.state.status === "ready" && current.state.providerId === "openai-codex"
1147
- ? codexResetCount(current.state.report)
1148
- : undefined;
1149
- try {
1150
- const loaded = await runMenuOperation(
1151
- ctx,
1152
- "Checking usage limit resets…",
1153
- controller.signal,
1154
- async (signal) => {
1155
- const expectedModel = modelIdentity(ctx.model);
1156
- const auth = await awaitWithDeadline(
1157
- resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
1158
- signal,
1159
- DEFAULT_TIMEOUT_MS,
1160
- "resolving current Codex reset authentication",
1161
- );
1162
- let availability: CodexResetAvailability;
1163
- try {
1164
- availability = await listCodexResetCredits(auth, signal, DEFAULT_TIMEOUT_MS);
1165
- } catch (error) {
1166
- if (isAbortError(error) || summaryCount === undefined || summaryCount <= 0) {
1167
- throw error;
1168
- }
1169
- availability = {
1170
- availableCount: summaryCount,
1171
- options: [genericCodexResetOption()],
1172
- };
1173
- }
1174
- const revalidated = await awaitWithDeadline(
1175
- resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
1176
- signal,
1177
- DEFAULT_TIMEOUT_MS,
1178
- "revalidating current Codex reset authentication",
1179
- );
1180
- if (
1181
- modelIdentity(ctx.model) !== expectedModel ||
1182
- revalidated.fingerprint !== auth.fingerprint
1183
- ) {
1184
- throw new Error(
1185
- "The active Codex model or account changed while loading usage limit resets.",
1186
- );
1187
- }
1188
- return { availability, auth, expectedModel };
1189
- },
1190
- );
1191
- if (!loaded) return { kind: "stay" };
1192
- resetAvailability = loaded.availability;
1193
- resetAuthFingerprint = loaded.auth.fingerprint;
1194
- resetModelIdentity = loaded.expectedModel;
1195
- selectedReset = undefined;
1196
- redemptionId = undefined;
1197
- resetOutcome = undefined;
1198
- resetFailure = undefined;
1199
- return {
1200
- kind: "to",
1201
- screen: loaded.availability.availableCount > 0 ? "reset-picker" : "reset-result",
1202
- };
1203
- } catch (error) {
1204
- if (isAbortError(error) || isStaleExtensionContextError(error)) {
1205
- return { kind: "stay" };
1206
- }
1207
- ctx.ui.notify(`Couldn't load usage limit resets: ${errorMessage(error)}`, "error");
1208
- return { kind: "stay" };
1209
- }
1210
- },
1211
- "select-reset": ({ itemId }) => {
1212
- const index = Number(itemId.replace(/^reset-/u, ""));
1213
- const option = Number.isSafeInteger(index)
1214
- ? resetAvailability?.options[index]
1215
- : undefined;
1216
- if (!option) return { kind: "rejected" };
1217
- selectedReset = option;
1218
- redemptionId = undefined;
1219
- resetFailure = undefined;
1220
- return { kind: "to", screen: "reset-confirm" };
1221
- },
1222
- "cancel-reset": () => ({ kind: "back" }),
1223
- "consume-reset": async () => {
1224
- if (!selectedReset || !resetAuthFingerprint || !resetModelIdentity) {
1225
- return { kind: "rejected" };
1226
- }
1227
- try {
1228
- redemptionId ??= createRedemptionId();
1229
- const attemptId = redemptionId;
1230
- const option = selectedReset;
1231
- const expectedFingerprint = resetAuthFingerprint;
1232
- const expectedModel = resetModelIdentity;
1233
- const result = await runMenuOperation(
1234
- ctx,
1235
- "Resetting your usage…",
1236
- controller.signal,
1237
- async (signal) => {
1238
- const auth = await awaitWithDeadline(
1239
- resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
1240
- signal,
1241
- DEFAULT_TIMEOUT_MS,
1242
- "revalidating current Codex reset authentication",
1243
- );
1244
- if (
1245
- modelIdentity(ctx.model) !== expectedModel ||
1246
- auth.fingerprint !== expectedFingerprint
1247
- ) {
1248
- throw new Error(
1249
- "The active Codex model or account changed; the reset was not used.",
1250
- );
1251
- }
1252
- const outcome = await consumeCodexResetCredit(
1253
- auth,
1254
- option,
1255
- attemptId,
1256
- signal,
1257
- DEFAULT_TIMEOUT_MS,
1258
- );
1259
- invalidateProviderState("openai-codex");
1260
- const model = ctx.model;
1261
- if (modelIdentity(model) !== expectedModel) return { outcome };
1262
- const refreshed = await queryCurrentState(ctx, model, true, signal);
1263
- const stable = await outcomeStillCurrent(
1264
- ctx,
1265
- model,
1266
- menuGeneration,
1267
- refreshed,
1268
- signal,
1269
- );
1270
- return stable ? { outcome, refreshed, model } : { outcome };
1271
- },
1272
- false,
1273
- );
1274
- if (!result) return { kind: "close" };
1275
- resetOutcome = result.outcome;
1276
- resetFailure = undefined;
1277
- if (result.refreshed && result.model) {
1278
- stableCurrent = { outcome: result.refreshed, model: result.model };
1279
- current = result.refreshed;
1280
- visibleStates = [current.state];
1281
- publishStableCurrent(ctx, stableCurrent);
1282
- }
1283
- return { kind: "to", screen: "reset-result" };
1284
- } catch (error) {
1285
- if (isAbortError(error) || isStaleExtensionContextError(error)) {
1286
- return { kind: "close" };
1287
- }
1288
- resetFailure = `Couldn't reset usage: ${errorMessage(error)}. Try again with the same request.`;
1289
- return { kind: "to", screen: "reset-error" };
1290
- }
1291
- },
1292
- "back-to-usage": () => ({ kind: "to", screen: "main" }),
1293
- "back-to-resets": () => {
1294
- redemptionId = undefined;
1295
- resetFailure = undefined;
1296
- return { kind: "to", screen: "reset-picker" };
1297
- },
1298
- refresh: async () => {
1299
- const refreshed = await queryStableCurrent(
1300
- ctx,
1301
- true,
1302
- controller,
1303
- "Refreshing current usage…",
1304
- );
1305
- if (!refreshed) return { kind: "stay" };
1306
- stableCurrent = refreshed;
1307
- publishStableCurrent(ctx, refreshed);
1308
- current = refreshed.outcome;
1309
- visibleStates = [current.state];
1310
- return { kind: "stay" };
1311
- },
1312
- another: async () => {
1313
- const others = configuredAdapters(ctx).filter(
1314
- (adapter) => adapter.id !== ctx.model?.provider,
1315
- );
1316
- if (others.length === 0) {
1317
- ctx.ui.notify("No other supported provider has configured runtime auth.", "info");
1318
- return { kind: "stay" };
1319
- }
1320
- return { kind: "to", screen: "providers" };
1321
- },
1322
- provider: async ({ itemId }) => {
1323
- const adapter = configuredAdapters(ctx).find(
1324
- (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
1325
- );
1326
- if (!adapter) return { kind: "back" };
1327
- let outcome = await runMenuOperation(
1328
- ctx,
1329
- `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1330
- controller.signal,
1331
- (signal) => queryAdapterState(ctx, adapter, "configured", false, signal),
1332
- );
1333
- if (!outcome) return { kind: "back" };
1334
- if (outcome.state.status === "selection-required") {
1335
- if (!(await promptForTarget(outcome.state, outcome.fingerprint))) {
1336
- return { kind: "back" };
1337
- }
1338
- outcome = await runMenuOperation(
1339
- ctx,
1340
- `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1341
- controller.signal,
1342
- (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
1343
- );
1344
- if (!outcome) return { kind: "back" };
1345
- }
1346
- const revalidated = await queryStableCurrent(
1347
- ctx,
1348
- false,
1349
- controller,
1350
- "Revalidating current usage…",
1351
- );
1352
- if (!revalidated) return { kind: "back" };
1353
- stableCurrent = revalidated;
1354
- current = revalidated.outcome;
1355
- visibleStates = [
1356
- outcome.state.providerId === current.state.providerId
1357
- ? current.state
1358
- : { ...outcome.state, displayState: "configured" },
1359
- ];
1360
- return { kind: "back" };
1361
- },
1362
- all: async () => {
1363
- const adapters = configuredAdapters(ctx);
1364
- const currentProviderId = ctx.model?.provider;
1365
- const settled = await runMenuOperation(
1366
- ctx,
1367
- "Checking configured provider usage…",
1368
- controller.signal,
1369
- (signal) =>
1370
- runWithConcurrency(
1371
- adapters,
1372
- ALL_PROVIDER_CONCURRENCY,
1373
- (adapter, _index, workerSignal) =>
1374
- queryAdapterState(
1375
- ctx,
1376
- adapter,
1377
- adapter.id === currentProviderId ? "current" : "configured",
1378
- true,
1379
- workerSignal,
1380
- ),
1381
- signal,
1382
- ),
1383
- );
1384
- if (!settled) return { kind: "stay" };
1385
- const queriedStates: ProviderUsageState[] = settled.map((result, index) => {
1386
- if (result.status === "fulfilled") {
1387
- return { ...result.value.state, displayState: "configured" };
1388
- }
1389
- const adapter = adapters[index] as UsageProviderAdapter;
1390
- return {
1391
- providerId: adapter.id,
1392
- providerName: providerDisplayName(ctx, adapter.id),
1393
- displayState: "configured",
1394
- status: "query-failed",
1395
- message: errorMessage(result.reason),
1396
- };
1397
- });
1398
- const revalidated = await queryStableCurrent(
1399
- ctx,
1400
- false,
1401
- controller,
1402
- "Revalidating current usage…",
1403
- );
1404
- if (!revalidated) return { kind: "stay" };
1405
- stableCurrent = revalidated;
1406
- current = revalidated.outcome;
1407
- visibleStates = [
1408
- current.state,
1409
- ...queriedStates.filter((state) => state.providerId !== current.state.providerId),
1410
- ];
1411
- return { kind: "stay" };
1412
- },
1413
- },
1414
- });
1415
- await runMenu(ctx, menu, {
1416
- getState: () => undefined,
1417
- signal: controller.signal,
1418
- isCurrent: () => statusGeneration === menuGeneration && !controller.signal.aborted,
1419
- });
1420
- } finally {
1421
- controller.abort(new DOMException("Usage menu closed", "AbortError"));
1422
- activeControllers.delete(controller);
1423
- }
1424
- };
670
+ const showMenu = async (ctx: ExtensionCommandContext): Promise<void> => {
671
+ if (!ctx.hasUI) throw new Error("/usage requires TUI or RPC mode.");
672
+ statusGeneration += 1;
673
+ const menuGeneration = statusGeneration;
674
+ statusController?.abort();
675
+ statusController = undefined;
676
+ clearStatusTimers();
677
+ const controller = new AbortController();
678
+ activeControllers.add(controller);
679
+ try {
680
+ let stableCurrent = await queryStableCurrent(ctx, false, controller, "Checking current usage…");
681
+ if (!stableCurrent) return;
682
+ publishStableCurrent(ctx, stableCurrent);
683
+ let current = stableCurrent.outcome;
684
+ let visibleStates: ProviderUsageState[] = [current.state];
685
+ let fastState = settingsRuntime.get();
686
+ let resetAvailability: CodexResetAvailability | undefined;
687
+ let selectedReset: CodexResetOption | undefined;
688
+ let resetAuthFingerprint: string | undefined;
689
+ let resetModelIdentity: string | undefined;
690
+ let redemptionId: string | undefined;
691
+ let resetOutcome: CodexResetOutcome | undefined;
692
+ let resetFailure: string | undefined;
693
+ const actionableTargetState = (): ProviderUsageState | undefined => {
694
+ if (visibleStates.length !== 1) return undefined;
695
+ const state = visibleStates[0];
696
+ return state &&
697
+ (state.status === "ready" || state.status === "selection-required") &&
698
+ adapterForProvider(state.providerId)?.targets
699
+ ? state
700
+ : undefined;
701
+ };
702
+ const promptForTarget = async (state: ProviderUsageState, stateFingerprint?: string): Promise<boolean> => {
703
+ const adapter = adapterForProvider(state.providerId);
704
+ if (!adapter?.targets) return false;
705
+ const expectedRememberedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
706
+ const snapshot =
707
+ state.status === "selection-required" && stateFingerprint
708
+ ? { choices: state.choices, fingerprint: stateFingerprint }
709
+ : await runMenuOperation(ctx, `Loading ${adapter.targets.pluralLabel}…`, controller.signal, (signal) =>
710
+ loadTargetChoices(ctx, adapter, signal),
711
+ );
712
+ if (!snapshot || controller.signal.aborted || statusGeneration !== menuGeneration) {
713
+ return false;
714
+ }
715
+ const selectOptions = createUsageTargetSelectOptions(snapshot.choices);
716
+ const selected = await ctx.ui.select(
717
+ `Select ${adapter.targets.singularLabel} for ${providerDisplayName(ctx, adapter.id)}`,
718
+ [...selectOptions.options],
719
+ { signal: controller.signal },
720
+ );
721
+ if (
722
+ selected === undefined ||
723
+ controller.signal.aborted ||
724
+ statusGeneration !== menuGeneration ||
725
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
726
+ ) {
727
+ return false;
728
+ }
729
+ const targetId = selectOptions.targetIdFor(selected);
730
+ if (!targetId) return false;
731
+ const revalidated = await runMenuOperation(
732
+ ctx,
733
+ `Revalidating ${adapter.targets.singularLabel}…`,
734
+ controller.signal,
735
+ (signal) => loadTargetChoices(ctx, adapter, signal),
736
+ );
737
+ if (
738
+ !revalidated ||
739
+ controller.signal.aborted ||
740
+ statusGeneration !== menuGeneration ||
741
+ settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
742
+ ) {
743
+ return false;
744
+ }
745
+ if (
746
+ revalidated.fingerprint !== snapshot.fingerprint ||
747
+ !revalidated.choices.some((choice) => choice.id === targetId)
748
+ ) {
749
+ ctx.ui.notify(
750
+ `${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
751
+ "warning",
752
+ );
753
+ return false;
754
+ }
755
+ let saved: Readonly<UsageSettingsState> | undefined;
756
+ try {
757
+ saved = await runMenuOperation(ctx, `Saving ${adapter.targets.singularLabel}…`, controller.signal, (signal) =>
758
+ settingsRuntime.updateSelectedTarget(adapter.id, targetId, signal, async () => {
759
+ let published: Awaited<ReturnType<typeof loadTargetChoices>>;
760
+ try {
761
+ published = await loadTargetChoices(ctx, adapter, signal);
762
+ } catch (error) {
763
+ if (
764
+ signal.aborted ||
765
+ controller.signal.aborted ||
766
+ statusGeneration !== menuGeneration ||
767
+ isStaleExtensionContextError(error)
768
+ ) {
769
+ throw error;
770
+ }
771
+ throw new UsageTargetSelectionChangedError();
772
+ }
773
+ if (
774
+ published.fingerprint !== snapshot.fingerprint ||
775
+ !published.choices.some((choice) => choice.id === targetId)
776
+ ) {
777
+ throw new UsageTargetSelectionChangedError();
778
+ }
779
+ }),
780
+ );
781
+ } catch (error) {
782
+ if (error instanceof UsageTargetSelectionChangedError) {
783
+ ctx.ui.notify(
784
+ `${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
785
+ "warning",
786
+ );
787
+ return false;
788
+ }
789
+ throw error;
790
+ }
791
+ if (!saved || controller.signal.aborted || statusGeneration !== menuGeneration) {
792
+ return false;
793
+ }
794
+ invalidateProviderState(adapter.id);
795
+ return true;
796
+ };
797
+ const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
798
+ if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
799
+ type Screen = "main" | "providers" | "reset-picker" | "reset-confirm" | "reset-result" | "reset-error";
800
+ type Action =
801
+ | "refresh"
802
+ | "settings"
803
+ | "target"
804
+ | "toggle-fast"
805
+ | "another"
806
+ | "all"
807
+ | "provider"
808
+ | "open-resets"
809
+ | "select-reset"
810
+ | "cancel-reset"
811
+ | "consume-reset"
812
+ | "back-to-usage"
813
+ | "back-to-resets";
814
+ const menu = defineMenu<undefined, Screen, Action, ExtensionCommandContext>({
815
+ start: "main",
816
+ screens: {
817
+ main: () => {
818
+ const fastAvailability = fastRuntime.availability(ctx.model);
819
+ const targetState = actionableTargetState();
820
+ const targetAdapter = adapterForProvider(targetState?.providerId);
821
+ const fastLines =
822
+ fastAvailability.kind === "available"
823
+ ? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
824
+ : fastAvailability.kind === "unavailable"
825
+ ? [`Fast mode: Unavailable · ${fastAvailability.reason}`]
826
+ : [];
827
+ return {
828
+ kind: "actions",
829
+ title: "Provider usage",
830
+ lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
831
+ items: [
832
+ { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
833
+ { id: "settings", label: SETTINGS, action: "settings" },
834
+ ...(targetState && targetAdapter?.targets
835
+ ? [
836
+ {
837
+ id: "target",
838
+ label: `${targetState.status === "selection-required" ? "Select" : "Change"} ${targetAdapter.targets.singularLabel}…`,
839
+ action: "target" as const,
840
+ },
841
+ ]
842
+ : []),
843
+ ...(fastAvailability.kind === "available"
844
+ ? [
845
+ {
846
+ id: "toggle-fast",
847
+ label: fastAvailability.enabled ? "Turn Fast mode off" : "Turn Fast mode on",
848
+ description:
849
+ fastState.kind === "invalid"
850
+ ? "Repair pi-usage.json and reload before changing Fast mode."
851
+ : FAST_USAGE_WARNING,
852
+ disabled: fastState.kind === "invalid",
853
+ action: "toggle-fast" as const,
854
+ },
855
+ ]
856
+ : []),
857
+ ...(current.state.status === "ready" && current.state.providerId === "openai-codex"
858
+ ? [
859
+ {
860
+ id: "open-resets",
861
+ label: REDEEM_CODEX_RESET,
862
+ description: codexResetActionDescription(current.state.report),
863
+ disabled: codexResetCount(current.state.report) === 0,
864
+ action: "open-resets" as const,
865
+ },
866
+ ]
867
+ : []),
868
+ { id: "another", label: VIEW_ANOTHER, action: "another" },
869
+ { id: "all", label: VIEW_ALL, action: "all" },
870
+ { id: "close", label: CLOSE, close: true },
871
+ ],
872
+ hint: "close",
873
+ };
874
+ },
875
+ providers: () => ({
876
+ kind: "actions",
877
+ title: "Select a configured provider",
878
+ items: configuredAdapters(ctx)
879
+ .filter((adapter) => adapter.id !== ctx.model?.provider)
880
+ .map((adapter) => ({
881
+ id: adapter.id,
882
+ label: providerDisplayName(ctx, adapter.id),
883
+ action: "provider" as const,
884
+ })),
885
+ hint: "back",
886
+ }),
887
+ "reset-picker": () => ({
888
+ kind: "choice",
889
+ title: "Usage limit resets",
890
+ lines: [
891
+ `${resetAvailability?.availableCount ?? 0} ${resetLabel(resetAvailability?.availableCount ?? 0)} available.`,
892
+ ],
893
+ items: (resetAvailability?.options ?? []).map((option, index) => ({
894
+ id: `reset-${index}`,
895
+ label: option.title,
896
+ description: resetOptionExpiration(option),
897
+ details: [option.description],
898
+ })),
899
+ action: "select-reset",
900
+ initialItemId: "reset-0",
901
+ hint: "back",
902
+ }),
903
+ "reset-confirm": () => ({
904
+ kind: "actions",
905
+ title: "Use this reset?",
906
+ lines: resetConfirmationLines(selectedReset),
907
+ items: [
908
+ { id: "cancel-reset", label: "No, go back", action: "cancel-reset" },
909
+ { id: "consume-reset", label: "Yes, use reset", action: "consume-reset" },
910
+ ],
911
+ hint: "back",
912
+ }),
913
+ "reset-result": () => ({
914
+ kind: "actions",
915
+ title: "Usage limit resets",
916
+ lines: [
917
+ formatCodexResetOutcome(
918
+ resetOutcome,
919
+ current.state.status === "ready" ? codexResetCount(current.state.report) : undefined,
920
+ ),
921
+ ],
922
+ items: [
923
+ {
924
+ id: "back-to-usage",
925
+ label: "Back to usage",
926
+ action: "back-to-usage",
927
+ },
928
+ { id: "close", label: CLOSE, close: true },
929
+ ],
930
+ hint: "back",
931
+ }),
932
+ "reset-error": () => ({
933
+ kind: "actions",
934
+ title: "Usage limit resets",
935
+ lines: [resetFailure ?? "Couldn't reset usage. Please try again."],
936
+ items: [
937
+ { id: "consume-reset", label: "Try again", action: "consume-reset" },
938
+ { id: "back-to-resets", label: "Back", action: "back-to-resets" },
939
+ ],
940
+ hint: "back",
941
+ }),
942
+ },
943
+ actions: {
944
+ target: async () => {
945
+ const targetState = actionableTargetState();
946
+ if (!targetState) return { kind: "rejected" };
947
+ try {
948
+ const stateFingerprint = targetState === current.state ? current.fingerprint : undefined;
949
+ if (!(await promptForTarget(targetState, stateFingerprint))) {
950
+ return { kind: "stay" };
951
+ }
952
+ const adapter = adapterForProvider(targetState.providerId);
953
+ if (!adapter) return { kind: "rejected" };
954
+ if (targetState.providerId === ctx.model?.provider) {
955
+ const refreshed = await queryStableCurrent(
956
+ ctx,
957
+ true,
958
+ controller,
959
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
960
+ );
961
+ if (!refreshed) return { kind: "stay" };
962
+ stableCurrent = refreshed;
963
+ current = refreshed.outcome;
964
+ visibleStates = [current.state];
965
+ publishStableCurrent(ctx, refreshed);
966
+ return { kind: "stay" };
967
+ }
968
+ const outcome = await runMenuOperation(
969
+ ctx,
970
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
971
+ controller.signal,
972
+ (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
973
+ );
974
+ if (!outcome) return { kind: "stay" };
975
+ const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
976
+ if (!revalidated) return { kind: "stay" };
977
+ stableCurrent = revalidated;
978
+ current = revalidated.outcome;
979
+ visibleStates = [
980
+ outcome.state.providerId === current.state.providerId
981
+ ? current.state
982
+ : { ...outcome.state, displayState: "configured" },
983
+ ];
984
+ return { kind: "stay" };
985
+ } catch (error) {
986
+ if (isAbortError(error) || isStaleExtensionContextError(error)) {
987
+ return { kind: "stay" };
988
+ }
989
+ ctx.ui.notify(`Could not select target: ${errorMessage(error)}`, "error");
990
+ return { kind: "stay" };
991
+ }
992
+ },
993
+ settings: async () => {
994
+ await showUsageSettings(
995
+ ctx,
996
+ settingsRuntime,
997
+ controller.signal,
998
+ () => statusGeneration === menuGeneration && !controller.signal.aborted,
999
+ (id) => {
1000
+ if (
1001
+ id === "codexStatusResetCountdown" &&
1002
+ stableCurrent &&
1003
+ statusGeneration === menuGeneration &&
1004
+ !controller.signal.aborted
1005
+ ) {
1006
+ publishStableCurrent(ctx, stableCurrent);
1007
+ }
1008
+ },
1009
+ );
1010
+ fastState = settingsRuntime.get();
1011
+ const revalidated = await queryStableCurrent(ctx, false, controller, "Applying usage settings…");
1012
+ if (!revalidated) return { kind: "stay" };
1013
+ stableCurrent = revalidated;
1014
+ current = revalidated.outcome;
1015
+ visibleStates = [current.state];
1016
+ publishStableCurrent(ctx, revalidated);
1017
+ return { kind: "stay" };
1018
+ },
1019
+ "toggle-fast": async () => {
1020
+ const availability = fastRuntime.availability(ctx.model);
1021
+ if (availability.kind !== "available" || fastState.kind === "invalid") {
1022
+ return { kind: "rejected" };
1023
+ }
1024
+ const changed = await fastRuntime.toggle(ctx, !availability.enabled, controller.signal);
1025
+ if (!changed) return { kind: "rejected" };
1026
+ fastState = settingsRuntime.get();
1027
+ return { kind: "stay" };
1028
+ },
1029
+ "open-resets": async () => {
1030
+ const summaryCount =
1031
+ current.state.status === "ready" && current.state.providerId === "openai-codex"
1032
+ ? codexResetCount(current.state.report)
1033
+ : undefined;
1034
+ try {
1035
+ const loaded = await runMenuOperation(
1036
+ ctx,
1037
+ "Checking usage limit resets…",
1038
+ controller.signal,
1039
+ async (signal) => {
1040
+ const expectedModel = modelIdentity(ctx.model);
1041
+ const auth = await awaitWithDeadline(
1042
+ resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
1043
+ signal,
1044
+ DEFAULT_TIMEOUT_MS,
1045
+ "resolving current Codex reset authentication",
1046
+ );
1047
+ let availability: CodexResetAvailability;
1048
+ try {
1049
+ availability = await listCodexResetCredits(auth, signal, DEFAULT_TIMEOUT_MS);
1050
+ } catch (error) {
1051
+ if (isAbortError(error) || summaryCount === undefined || summaryCount <= 0) {
1052
+ throw error;
1053
+ }
1054
+ availability = {
1055
+ availableCount: summaryCount,
1056
+ options: [genericCodexResetOption()],
1057
+ };
1058
+ }
1059
+ const revalidated = await awaitWithDeadline(
1060
+ resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
1061
+ signal,
1062
+ DEFAULT_TIMEOUT_MS,
1063
+ "revalidating current Codex reset authentication",
1064
+ );
1065
+ if (modelIdentity(ctx.model) !== expectedModel || revalidated.fingerprint !== auth.fingerprint) {
1066
+ throw new Error("The active Codex model or account changed while loading usage limit resets.");
1067
+ }
1068
+ return { availability, auth, expectedModel };
1069
+ },
1070
+ );
1071
+ if (!loaded) return { kind: "stay" };
1072
+ resetAvailability = loaded.availability;
1073
+ resetAuthFingerprint = loaded.auth.fingerprint;
1074
+ resetModelIdentity = loaded.expectedModel;
1075
+ selectedReset = undefined;
1076
+ redemptionId = undefined;
1077
+ resetOutcome = undefined;
1078
+ resetFailure = undefined;
1079
+ return {
1080
+ kind: "to",
1081
+ screen: loaded.availability.availableCount > 0 ? "reset-picker" : "reset-result",
1082
+ };
1083
+ } catch (error) {
1084
+ if (isAbortError(error) || isStaleExtensionContextError(error)) {
1085
+ return { kind: "stay" };
1086
+ }
1087
+ ctx.ui.notify(`Couldn't load usage limit resets: ${errorMessage(error)}`, "error");
1088
+ return { kind: "stay" };
1089
+ }
1090
+ },
1091
+ "select-reset": ({ itemId }) => {
1092
+ const index = Number(itemId.replace(/^reset-/u, ""));
1093
+ const option = Number.isSafeInteger(index) ? resetAvailability?.options[index] : undefined;
1094
+ if (!option) return { kind: "rejected" };
1095
+ selectedReset = option;
1096
+ redemptionId = undefined;
1097
+ resetFailure = undefined;
1098
+ return { kind: "to", screen: "reset-confirm" };
1099
+ },
1100
+ "cancel-reset": () => ({ kind: "back" }),
1101
+ "consume-reset": async () => {
1102
+ if (!selectedReset || !resetAuthFingerprint || !resetModelIdentity) {
1103
+ return { kind: "rejected" };
1104
+ }
1105
+ try {
1106
+ redemptionId ??= createRedemptionId();
1107
+ const attemptId = redemptionId;
1108
+ const option = selectedReset;
1109
+ const expectedFingerprint = resetAuthFingerprint;
1110
+ const expectedModel = resetModelIdentity;
1111
+ const result = await runMenuOperation(
1112
+ ctx,
1113
+ "Resetting your usage…",
1114
+ controller.signal,
1115
+ async (signal) => {
1116
+ const auth = await awaitWithDeadline(
1117
+ resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
1118
+ signal,
1119
+ DEFAULT_TIMEOUT_MS,
1120
+ "revalidating current Codex reset authentication",
1121
+ );
1122
+ if (modelIdentity(ctx.model) !== expectedModel || auth.fingerprint !== expectedFingerprint) {
1123
+ throw new Error("The active Codex model or account changed; the reset was not used.");
1124
+ }
1125
+ const outcome = await consumeCodexResetCredit(auth, option, attemptId, signal, DEFAULT_TIMEOUT_MS);
1126
+ invalidateProviderState("openai-codex");
1127
+ const model = ctx.model;
1128
+ if (modelIdentity(model) !== expectedModel) return { outcome };
1129
+ const refreshed = await queryCurrentState(ctx, model, true, signal);
1130
+ const stable = await outcomeStillCurrent(ctx, model, menuGeneration, refreshed, signal);
1131
+ return stable ? { outcome, refreshed, model } : { outcome };
1132
+ },
1133
+ false,
1134
+ );
1135
+ if (!result) return { kind: "close" };
1136
+ resetOutcome = result.outcome;
1137
+ resetFailure = undefined;
1138
+ if (result.refreshed && result.model) {
1139
+ stableCurrent = { outcome: result.refreshed, model: result.model };
1140
+ current = result.refreshed;
1141
+ visibleStates = [current.state];
1142
+ publishStableCurrent(ctx, stableCurrent);
1143
+ }
1144
+ return { kind: "to", screen: "reset-result" };
1145
+ } catch (error) {
1146
+ if (isAbortError(error) || isStaleExtensionContextError(error)) {
1147
+ return { kind: "close" };
1148
+ }
1149
+ resetFailure = `Couldn't reset usage: ${errorMessage(error)}. Try again with the same request.`;
1150
+ return { kind: "to", screen: "reset-error" };
1151
+ }
1152
+ },
1153
+ "back-to-usage": () => ({ kind: "to", screen: "main" }),
1154
+ "back-to-resets": () => {
1155
+ redemptionId = undefined;
1156
+ resetFailure = undefined;
1157
+ return { kind: "to", screen: "reset-picker" };
1158
+ },
1159
+ refresh: async () => {
1160
+ const refreshed = await queryStableCurrent(ctx, true, controller, "Refreshing current usage…");
1161
+ if (!refreshed) return { kind: "stay" };
1162
+ stableCurrent = refreshed;
1163
+ publishStableCurrent(ctx, refreshed);
1164
+ current = refreshed.outcome;
1165
+ visibleStates = [current.state];
1166
+ return { kind: "stay" };
1167
+ },
1168
+ another: async () => {
1169
+ const others = configuredAdapters(ctx).filter((adapter) => adapter.id !== ctx.model?.provider);
1170
+ if (others.length === 0) {
1171
+ ctx.ui.notify("No other supported provider has configured runtime auth.", "info");
1172
+ return { kind: "stay" };
1173
+ }
1174
+ return { kind: "to", screen: "providers" };
1175
+ },
1176
+ provider: async ({ itemId }) => {
1177
+ const adapter = configuredAdapters(ctx).find(
1178
+ (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
1179
+ );
1180
+ if (!adapter) return { kind: "back" };
1181
+ let outcome = await runMenuOperation(
1182
+ ctx,
1183
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1184
+ controller.signal,
1185
+ (signal) => queryAdapterState(ctx, adapter, "configured", false, signal),
1186
+ );
1187
+ if (!outcome) return { kind: "back" };
1188
+ if (outcome.state.status === "selection-required") {
1189
+ if (!(await promptForTarget(outcome.state, outcome.fingerprint))) {
1190
+ return { kind: "back" };
1191
+ }
1192
+ outcome = await runMenuOperation(
1193
+ ctx,
1194
+ `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1195
+ controller.signal,
1196
+ (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
1197
+ );
1198
+ if (!outcome) return { kind: "back" };
1199
+ }
1200
+ const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
1201
+ if (!revalidated) return { kind: "back" };
1202
+ stableCurrent = revalidated;
1203
+ current = revalidated.outcome;
1204
+ visibleStates = [
1205
+ outcome.state.providerId === current.state.providerId
1206
+ ? current.state
1207
+ : { ...outcome.state, displayState: "configured" },
1208
+ ];
1209
+ return { kind: "back" };
1210
+ },
1211
+ all: async () => {
1212
+ const adapters = configuredAdapters(ctx);
1213
+ const currentProviderId = ctx.model?.provider;
1214
+ const settled = await runMenuOperation(
1215
+ ctx,
1216
+ "Checking configured provider usage…",
1217
+ controller.signal,
1218
+ (signal) =>
1219
+ runWithConcurrency(
1220
+ adapters,
1221
+ ALL_PROVIDER_CONCURRENCY,
1222
+ (adapter, _index, workerSignal) =>
1223
+ queryAdapterState(
1224
+ ctx,
1225
+ adapter,
1226
+ adapter.id === currentProviderId ? "current" : "configured",
1227
+ true,
1228
+ workerSignal,
1229
+ ),
1230
+ signal,
1231
+ ),
1232
+ );
1233
+ if (!settled) return { kind: "stay" };
1234
+ const queriedStates: ProviderUsageState[] = settled.map((result, index) => {
1235
+ if (result.status === "fulfilled") {
1236
+ return { ...result.value.state, displayState: "configured" };
1237
+ }
1238
+ const adapter = adapters[index] as UsageProviderAdapter;
1239
+ return {
1240
+ providerId: adapter.id,
1241
+ providerName: providerDisplayName(ctx, adapter.id),
1242
+ displayState: "configured",
1243
+ status: "query-failed",
1244
+ message: errorMessage(result.reason),
1245
+ };
1246
+ });
1247
+ const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
1248
+ if (!revalidated) return { kind: "stay" };
1249
+ stableCurrent = revalidated;
1250
+ current = revalidated.outcome;
1251
+ visibleStates = [
1252
+ current.state,
1253
+ ...queriedStates.filter((state) => state.providerId !== current.state.providerId),
1254
+ ];
1255
+ return { kind: "stay" };
1256
+ },
1257
+ },
1258
+ });
1259
+ await runMenu(ctx, menu, {
1260
+ getState: () => undefined,
1261
+ signal: controller.signal,
1262
+ isCurrent: () => statusGeneration === menuGeneration && !controller.signal.aborted,
1263
+ });
1264
+ } finally {
1265
+ controller.abort(new DOMException("Usage menu closed", "AbortError"));
1266
+ activeControllers.delete(controller);
1267
+ }
1268
+ };
1425
1269
 
1426
- pi.registerCommand("usage", {
1427
- description: "Show usage for the current runtime account",
1428
- handler: async (args, ctx) => {
1429
- if (args.trim()) {
1430
- ctx.ui.notify(
1431
- "/usage does not accept arguments; choose an action from its menu.",
1432
- "warning",
1433
- );
1434
- return;
1435
- }
1436
- try {
1437
- await showMenu(ctx);
1438
- } catch (error) {
1439
- if (isStaleExtensionContextError(error) || isAbortError(error)) return;
1440
- throw error;
1441
- }
1442
- },
1443
- });
1444
- pi.on("session_start", async (_event, ctx) => {
1445
- sessionGeneration += 1;
1446
- statusGeneration += 1;
1447
- clearStatusTimers();
1448
- for (const controller of activeControllers) controller.abort();
1449
- activeControllers.clear();
1450
- statusController = undefined;
1451
- sessionActive = true;
1452
- const ownerGeneration = sessionGeneration;
1453
- try {
1454
- await fastRuntime.prepareSession(ctx);
1455
- } catch (error) {
1456
- if (isStaleExtensionContextError(error) || ownerGeneration !== sessionGeneration) return;
1457
- throw error;
1458
- }
1459
- });
1460
- pi.on("session_tree", (_event, ctx) => {
1461
- startStatusRefresh(ctx, ctx.model, false);
1462
- });
1463
- pi.on("model_select", (event, ctx) => {
1464
- startStatusRefresh(ctx, event.model, false);
1465
- });
1466
- pi.on("turn_start", (_event, ctx) => {
1467
- startStatusRefresh(ctx, ctx.model, false);
1468
- });
1469
- pi.on("session_shutdown", (_event, ctx) => {
1470
- sessionActive = false;
1471
- sessionGeneration += 1;
1472
- statusGeneration += 1;
1473
- clearStatusTimers();
1474
- for (const controller of activeControllers) controller.abort();
1475
- activeControllers.clear();
1476
- statusController = undefined;
1477
- cache.clear();
1478
- failureBackoff.clear();
1479
- latestQueries.clear();
1480
- activeCurrentIdentity = undefined;
1481
- safeSetStatus(ctx, undefined);
1482
- });
1270
+ pi.registerCommand("usage", {
1271
+ description: "Show usage for the current runtime account",
1272
+ handler: async (args, ctx) => {
1273
+ if (args.trim()) {
1274
+ ctx.ui.notify("/usage does not accept arguments; choose an action from its menu.", "warning");
1275
+ return;
1276
+ }
1277
+ try {
1278
+ await showMenu(ctx);
1279
+ } catch (error) {
1280
+ if (isStaleExtensionContextError(error) || isAbortError(error)) return;
1281
+ throw error;
1282
+ }
1283
+ },
1284
+ });
1285
+ pi.on("session_start", async (_event, ctx) => {
1286
+ sessionGeneration += 1;
1287
+ statusGeneration += 1;
1288
+ clearStatusTimers();
1289
+ for (const controller of activeControllers) controller.abort();
1290
+ activeControllers.clear();
1291
+ statusController = undefined;
1292
+ sessionActive = true;
1293
+ const ownerGeneration = sessionGeneration;
1294
+ try {
1295
+ await fastRuntime.prepareSession(ctx);
1296
+ } catch (error) {
1297
+ if (isStaleExtensionContextError(error) || ownerGeneration !== sessionGeneration) return;
1298
+ throw error;
1299
+ }
1300
+ });
1301
+ pi.on("session_tree", (_event, ctx) => {
1302
+ startStatusRefresh(ctx, ctx.model, false);
1303
+ });
1304
+ pi.on("model_select", (event, ctx) => {
1305
+ startStatusRefresh(ctx, event.model, false);
1306
+ });
1307
+ pi.on("turn_start", (_event, ctx) => {
1308
+ startStatusRefresh(ctx, ctx.model, false);
1309
+ });
1310
+ pi.on("session_shutdown", (_event, ctx) => {
1311
+ sessionActive = false;
1312
+ sessionGeneration += 1;
1313
+ statusGeneration += 1;
1314
+ clearStatusTimers();
1315
+ for (const controller of activeControllers) controller.abort();
1316
+ activeControllers.clear();
1317
+ statusController = undefined;
1318
+ cache.clear();
1319
+ failureBackoff.clear();
1320
+ latestQueries.clear();
1321
+ activeCurrentIdentity = undefined;
1322
+ safeSetStatus(ctx, undefined);
1323
+ });
1483
1324
 
1484
- fastRuntime = registerCodexFastMode(
1485
- pi,
1486
- settingsRuntime,
1487
- (ctx) => startStatusRefresh(ctx, ctx.model, false),
1488
- { registerSessionStart: false },
1489
- );
1325
+ fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) => startStatusRefresh(ctx, ctx.model, false), {
1326
+ registerSessionStart: false,
1327
+ });
1490
1328
  }