@narumitw/pi-usage 0.49.3 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -9
- package/package.json +8 -3
- package/src/codex-fast-runtime.ts +212 -0
- package/src/codex-fast.ts +124 -0
- package/src/codex-resets.ts +308 -0
- package/src/index.ts +35 -0
- package/src/query.ts +14 -2
- package/src/settings.ts +208 -0
- package/src/usage-helpers.ts +40 -0
- package/src/usage.ts +374 -81
package/src/usage.ts
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type {
|
|
2
3
|
ExtensionAPI,
|
|
3
4
|
ExtensionCommandContext,
|
|
4
5
|
ExtensionContext,
|
|
5
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { FAST_USAGE_WARNING, registerCodexFastMode } from "./codex-fast-runtime.js";
|
|
6
8
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
type CodexResetAvailability,
|
|
10
|
+
type CodexResetOption,
|
|
11
|
+
type CodexResetOutcome,
|
|
12
|
+
codexResetActionDescription,
|
|
13
|
+
codexResetCount,
|
|
14
|
+
consumeCodexResetCredit,
|
|
15
|
+
formatCodexResetOutcome,
|
|
16
|
+
genericCodexResetOption,
|
|
17
|
+
listCodexResetCredits,
|
|
18
|
+
resetConfirmationLines,
|
|
19
|
+
resetLabel,
|
|
20
|
+
resetOptionExpiration,
|
|
21
|
+
resolveCodexResetAuth,
|
|
22
|
+
} from "./codex-resets.js";
|
|
23
|
+
import { awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
|
|
13
24
|
import { formatProviderStates, formatUsageStatusline } from "./format.js";
|
|
14
25
|
import {
|
|
15
26
|
adapterForProvider,
|
|
16
27
|
isStaleExtensionContextError,
|
|
17
|
-
providerIsConfigured,
|
|
18
28
|
queryProviderUsage,
|
|
19
29
|
resolveUsageAuth,
|
|
20
|
-
SUPPORTED_ADAPTERS,
|
|
21
30
|
} from "./query.js";
|
|
31
|
+
import { createUsageSettingsRuntime, type UsageSettingsRuntime } from "./settings.js";
|
|
22
32
|
import type {
|
|
23
33
|
PiModel,
|
|
24
34
|
ProviderUsageState,
|
|
@@ -26,6 +36,14 @@ import type {
|
|
|
26
36
|
UsageDisplayState,
|
|
27
37
|
UsageProviderAdapter,
|
|
28
38
|
} from "./types.js";
|
|
39
|
+
import {
|
|
40
|
+
configuredAdapters,
|
|
41
|
+
isAbortError,
|
|
42
|
+
isTimeoutError,
|
|
43
|
+
modelIdentity,
|
|
44
|
+
providerDisplayName,
|
|
45
|
+
setBoundedMap,
|
|
46
|
+
} from "./usage-helpers.js";
|
|
29
47
|
|
|
30
48
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
31
49
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
@@ -38,6 +56,13 @@ const REFRESH_CURRENT = "Refresh current usage";
|
|
|
38
56
|
const VIEW_ANOTHER = "View another configured provider…";
|
|
39
57
|
const VIEW_ALL = "View all configured providers…";
|
|
40
58
|
const CLOSE = "Close";
|
|
59
|
+
const REDEEM_CODEX_RESET = "Redeem usage limit reset…";
|
|
60
|
+
|
|
61
|
+
type UsageExtensionDependencies = {
|
|
62
|
+
credentialReader?: (providerId: string) => unknown;
|
|
63
|
+
createRedemptionId?: () => string;
|
|
64
|
+
settingsRuntime?: UsageSettingsRuntime;
|
|
65
|
+
};
|
|
41
66
|
|
|
42
67
|
type QueryOutcome = {
|
|
43
68
|
state: ProviderUsageState;
|
|
@@ -50,7 +75,13 @@ type StableCurrent = {
|
|
|
50
75
|
model: PiModel | undefined;
|
|
51
76
|
};
|
|
52
77
|
|
|
53
|
-
export default function usageExtension(
|
|
78
|
+
export default function usageExtension(
|
|
79
|
+
pi: ExtensionAPI,
|
|
80
|
+
dependencies: UsageExtensionDependencies = {},
|
|
81
|
+
) {
|
|
82
|
+
const credentialReader = dependencies.credentialReader;
|
|
83
|
+
const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
|
|
84
|
+
const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
|
|
54
85
|
const cache = new UsageCache(CACHE_TTL_MS);
|
|
55
86
|
const failureBackoff = new Map<string, { until: number; message: string }>();
|
|
56
87
|
const latestQueries = new Map<string, number>();
|
|
@@ -61,6 +92,7 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
61
92
|
let statusGeneration = 0;
|
|
62
93
|
let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
63
94
|
let statusController: AbortController | undefined;
|
|
95
|
+
let fastRuntime: ReturnType<typeof registerCodexFastMode>;
|
|
64
96
|
|
|
65
97
|
const clearStatusTimer = () => {
|
|
66
98
|
if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
|
|
@@ -118,11 +150,22 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
118
150
|
}
|
|
119
151
|
return;
|
|
120
152
|
}
|
|
121
|
-
const
|
|
153
|
+
const rawValue = formatUsageStatusline(outcome.state.report, model);
|
|
154
|
+
const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
|
|
122
155
|
if (!safeSetStatus(ctx, value)) return;
|
|
123
156
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
124
157
|
};
|
|
125
158
|
|
|
159
|
+
const invalidateProviderState = (providerId: string) => {
|
|
160
|
+
cache.clearProvider(providerId);
|
|
161
|
+
for (const key of failureBackoff.keys()) {
|
|
162
|
+
if (key.startsWith(`${providerId}:`)) failureBackoff.delete(key);
|
|
163
|
+
}
|
|
164
|
+
for (const key of latestQueries.keys()) {
|
|
165
|
+
if (key.startsWith(`${providerId}:`)) latestQueries.delete(key);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
126
169
|
const transitionCurrentIdentity = (nextIdentity: string, providerId: string) => {
|
|
127
170
|
if (!activeCurrentIdentity || activeCurrentIdentity === nextIdentity) {
|
|
128
171
|
activeCurrentIdentity = nextIdentity;
|
|
@@ -130,14 +173,7 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
130
173
|
}
|
|
131
174
|
const previousProviderId = activeCurrentIdentity.split(":", 1)[0] ?? "";
|
|
132
175
|
for (const id of new Set([previousProviderId, providerId])) {
|
|
133
|
-
if (
|
|
134
|
-
cache.clearProvider(id);
|
|
135
|
-
for (const key of failureBackoff.keys()) {
|
|
136
|
-
if (key.startsWith(`${id}:`)) failureBackoff.delete(key);
|
|
137
|
-
}
|
|
138
|
-
for (const key of latestQueries.keys()) {
|
|
139
|
-
if (key.startsWith(`${id}:`)) latestQueries.delete(key);
|
|
140
|
-
}
|
|
176
|
+
if (id) invalidateProviderState(id);
|
|
141
177
|
}
|
|
142
178
|
activeCurrentIdentity = nextIdentity;
|
|
143
179
|
};
|
|
@@ -346,12 +382,14 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
346
382
|
label: string,
|
|
347
383
|
parentSignal: AbortSignal,
|
|
348
384
|
operation: (signal: AbortSignal) => Promise<T>,
|
|
385
|
+
cancellable = true,
|
|
349
386
|
): Promise<T | undefined> => {
|
|
350
387
|
const { runTask } = await import("@narumitw/pi-tui-kit");
|
|
351
388
|
if (parentSignal.aborted) return undefined;
|
|
352
389
|
const result = await runTask(ctx, {
|
|
353
390
|
label,
|
|
354
391
|
signal: parentSignal,
|
|
392
|
+
cancellable,
|
|
355
393
|
onError: () => undefined,
|
|
356
394
|
task: ({ signal }) => operation(signal),
|
|
357
395
|
});
|
|
@@ -465,25 +503,86 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
465
503
|
publishStableCurrent(ctx, stableCurrent);
|
|
466
504
|
let current = stableCurrent.outcome;
|
|
467
505
|
let visibleStates: ProviderUsageState[] = [current.state];
|
|
506
|
+
let fastState = settingsRuntime.get();
|
|
507
|
+
let resetAvailability: CodexResetAvailability | undefined;
|
|
508
|
+
let selectedReset: CodexResetOption | undefined;
|
|
509
|
+
let resetAuthFingerprint: string | undefined;
|
|
510
|
+
let resetModelIdentity: string | undefined;
|
|
511
|
+
let redemptionId: string | undefined;
|
|
512
|
+
let resetOutcome: CodexResetOutcome | undefined;
|
|
513
|
+
let resetFailure: string | undefined;
|
|
468
514
|
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
469
515
|
if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
|
|
470
|
-
type Screen =
|
|
471
|
-
|
|
516
|
+
type Screen =
|
|
517
|
+
| "main"
|
|
518
|
+
| "providers"
|
|
519
|
+
| "reset-picker"
|
|
520
|
+
| "reset-confirm"
|
|
521
|
+
| "reset-result"
|
|
522
|
+
| "reset-error";
|
|
523
|
+
type Action =
|
|
524
|
+
| "refresh"
|
|
525
|
+
| "toggle-fast"
|
|
526
|
+
| "another"
|
|
527
|
+
| "all"
|
|
528
|
+
| "provider"
|
|
529
|
+
| "open-resets"
|
|
530
|
+
| "select-reset"
|
|
531
|
+
| "cancel-reset"
|
|
532
|
+
| "consume-reset"
|
|
533
|
+
| "back-to-usage"
|
|
534
|
+
| "back-to-resets";
|
|
472
535
|
const menu = defineMenu<undefined, Screen, Action, ExtensionCommandContext>({
|
|
473
536
|
start: "main",
|
|
474
537
|
screens: {
|
|
475
|
-
main: () =>
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
538
|
+
main: () => {
|
|
539
|
+
const fastAvailability = fastRuntime.availability(ctx.model);
|
|
540
|
+
const fastLines =
|
|
541
|
+
fastAvailability.kind === "available"
|
|
542
|
+
? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
|
|
543
|
+
: fastAvailability.kind === "unavailable"
|
|
544
|
+
? [`Fast mode: Unavailable · ${fastAvailability.reason}`]
|
|
545
|
+
: [];
|
|
546
|
+
return {
|
|
547
|
+
kind: "actions",
|
|
548
|
+
title: "Provider usage",
|
|
549
|
+
lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
|
|
550
|
+
items: [
|
|
551
|
+
{ id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
|
|
552
|
+
...(fastAvailability.kind === "available"
|
|
553
|
+
? [
|
|
554
|
+
{
|
|
555
|
+
id: "toggle-fast",
|
|
556
|
+
label: fastAvailability.enabled
|
|
557
|
+
? "Turn Fast mode off"
|
|
558
|
+
: "Turn Fast mode on",
|
|
559
|
+
description:
|
|
560
|
+
fastState.kind === "invalid"
|
|
561
|
+
? "Repair pi-usage.json and reload before changing Fast mode."
|
|
562
|
+
: FAST_USAGE_WARNING,
|
|
563
|
+
disabled: fastState.kind === "invalid",
|
|
564
|
+
action: "toggle-fast" as const,
|
|
565
|
+
},
|
|
566
|
+
]
|
|
567
|
+
: []),
|
|
568
|
+
...(current.state.status === "ready" && current.state.providerId === "openai-codex"
|
|
569
|
+
? [
|
|
570
|
+
{
|
|
571
|
+
id: "open-resets",
|
|
572
|
+
label: REDEEM_CODEX_RESET,
|
|
573
|
+
description: codexResetActionDescription(current.state.report),
|
|
574
|
+
disabled: codexResetCount(current.state.report) === 0,
|
|
575
|
+
action: "open-resets" as const,
|
|
576
|
+
},
|
|
577
|
+
]
|
|
578
|
+
: []),
|
|
579
|
+
{ id: "another", label: VIEW_ANOTHER, action: "another" },
|
|
580
|
+
{ id: "all", label: VIEW_ALL, action: "all" },
|
|
581
|
+
{ id: "close", label: CLOSE, close: true },
|
|
582
|
+
],
|
|
583
|
+
hint: "close",
|
|
584
|
+
};
|
|
585
|
+
},
|
|
487
586
|
providers: () => ({
|
|
488
587
|
kind: "actions",
|
|
489
588
|
title: "Select a configured provider",
|
|
@@ -496,8 +595,229 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
496
595
|
})),
|
|
497
596
|
hint: "back",
|
|
498
597
|
}),
|
|
598
|
+
"reset-picker": () => ({
|
|
599
|
+
kind: "choice",
|
|
600
|
+
title: "Usage limit resets",
|
|
601
|
+
lines: [
|
|
602
|
+
`${resetAvailability?.availableCount ?? 0} ${resetLabel(resetAvailability?.availableCount ?? 0)} available.`,
|
|
603
|
+
],
|
|
604
|
+
items: (resetAvailability?.options ?? []).map((option, index) => ({
|
|
605
|
+
id: `reset-${index}`,
|
|
606
|
+
label: option.title,
|
|
607
|
+
description: resetOptionExpiration(option),
|
|
608
|
+
details: [option.description],
|
|
609
|
+
})),
|
|
610
|
+
action: "select-reset",
|
|
611
|
+
initialItemId: "reset-0",
|
|
612
|
+
hint: "back",
|
|
613
|
+
}),
|
|
614
|
+
"reset-confirm": () => ({
|
|
615
|
+
kind: "actions",
|
|
616
|
+
title: "Use this reset?",
|
|
617
|
+
lines: resetConfirmationLines(selectedReset),
|
|
618
|
+
items: [
|
|
619
|
+
{ id: "cancel-reset", label: "No, go back", action: "cancel-reset" },
|
|
620
|
+
{ id: "consume-reset", label: "Yes, use reset", action: "consume-reset" },
|
|
621
|
+
],
|
|
622
|
+
hint: "back",
|
|
623
|
+
}),
|
|
624
|
+
"reset-result": () => ({
|
|
625
|
+
kind: "actions",
|
|
626
|
+
title: "Usage limit resets",
|
|
627
|
+
lines: [
|
|
628
|
+
formatCodexResetOutcome(
|
|
629
|
+
resetOutcome,
|
|
630
|
+
current.state.status === "ready"
|
|
631
|
+
? codexResetCount(current.state.report)
|
|
632
|
+
: undefined,
|
|
633
|
+
),
|
|
634
|
+
],
|
|
635
|
+
items: [
|
|
636
|
+
{
|
|
637
|
+
id: "back-to-usage",
|
|
638
|
+
label: "Back to usage",
|
|
639
|
+
action: "back-to-usage",
|
|
640
|
+
},
|
|
641
|
+
{ id: "close", label: CLOSE, close: true },
|
|
642
|
+
],
|
|
643
|
+
hint: "back",
|
|
644
|
+
}),
|
|
645
|
+
"reset-error": () => ({
|
|
646
|
+
kind: "actions",
|
|
647
|
+
title: "Usage limit resets",
|
|
648
|
+
lines: [resetFailure ?? "Couldn't reset usage. Please try again."],
|
|
649
|
+
items: [
|
|
650
|
+
{ id: "consume-reset", label: "Try again", action: "consume-reset" },
|
|
651
|
+
{ id: "back-to-resets", label: "Back", action: "back-to-resets" },
|
|
652
|
+
],
|
|
653
|
+
hint: "back",
|
|
654
|
+
}),
|
|
499
655
|
},
|
|
500
656
|
actions: {
|
|
657
|
+
"toggle-fast": async () => {
|
|
658
|
+
const availability = fastRuntime.availability(ctx.model);
|
|
659
|
+
if (availability.kind !== "available" || fastState.kind === "invalid") {
|
|
660
|
+
return { kind: "rejected" };
|
|
661
|
+
}
|
|
662
|
+
const changed = await fastRuntime.toggle(ctx, !availability.enabled, controller.signal);
|
|
663
|
+
if (!changed) return { kind: "rejected" };
|
|
664
|
+
fastState = settingsRuntime.get();
|
|
665
|
+
return { kind: "stay" };
|
|
666
|
+
},
|
|
667
|
+
"open-resets": async () => {
|
|
668
|
+
const summaryCount =
|
|
669
|
+
current.state.status === "ready" && current.state.providerId === "openai-codex"
|
|
670
|
+
? codexResetCount(current.state.report)
|
|
671
|
+
: undefined;
|
|
672
|
+
try {
|
|
673
|
+
const loaded = await runMenuOperation(
|
|
674
|
+
ctx,
|
|
675
|
+
"Checking usage limit resets…",
|
|
676
|
+
controller.signal,
|
|
677
|
+
async (signal) => {
|
|
678
|
+
const expectedModel = modelIdentity(ctx.model);
|
|
679
|
+
const auth = await awaitWithDeadline(
|
|
680
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader),
|
|
681
|
+
signal,
|
|
682
|
+
DEFAULT_TIMEOUT_MS,
|
|
683
|
+
"resolving current Codex reset authentication",
|
|
684
|
+
);
|
|
685
|
+
let availability: CodexResetAvailability;
|
|
686
|
+
try {
|
|
687
|
+
availability = await listCodexResetCredits(auth, signal, DEFAULT_TIMEOUT_MS);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (isAbortError(error) || summaryCount === undefined || summaryCount <= 0) {
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
availability = {
|
|
693
|
+
availableCount: summaryCount,
|
|
694
|
+
options: [genericCodexResetOption()],
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const revalidated = await awaitWithDeadline(
|
|
698
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader),
|
|
699
|
+
signal,
|
|
700
|
+
DEFAULT_TIMEOUT_MS,
|
|
701
|
+
"revalidating current Codex reset authentication",
|
|
702
|
+
);
|
|
703
|
+
if (
|
|
704
|
+
modelIdentity(ctx.model) !== expectedModel ||
|
|
705
|
+
revalidated.fingerprint !== auth.fingerprint
|
|
706
|
+
) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
"The active Codex model or account changed while loading usage limit resets.",
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return { availability, auth, expectedModel };
|
|
712
|
+
},
|
|
713
|
+
);
|
|
714
|
+
if (!loaded) return { kind: "stay" };
|
|
715
|
+
resetAvailability = loaded.availability;
|
|
716
|
+
resetAuthFingerprint = loaded.auth.fingerprint;
|
|
717
|
+
resetModelIdentity = loaded.expectedModel;
|
|
718
|
+
selectedReset = undefined;
|
|
719
|
+
redemptionId = undefined;
|
|
720
|
+
resetOutcome = undefined;
|
|
721
|
+
resetFailure = undefined;
|
|
722
|
+
return {
|
|
723
|
+
kind: "to",
|
|
724
|
+
screen: loaded.availability.availableCount > 0 ? "reset-picker" : "reset-result",
|
|
725
|
+
};
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) {
|
|
728
|
+
return { kind: "stay" };
|
|
729
|
+
}
|
|
730
|
+
ctx.ui.notify(`Couldn't load usage limit resets: ${errorMessage(error)}`, "error");
|
|
731
|
+
return { kind: "stay" };
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
"select-reset": ({ itemId }) => {
|
|
735
|
+
const index = Number(itemId.replace(/^reset-/u, ""));
|
|
736
|
+
const option = Number.isSafeInteger(index)
|
|
737
|
+
? resetAvailability?.options[index]
|
|
738
|
+
: undefined;
|
|
739
|
+
if (!option) return { kind: "rejected" };
|
|
740
|
+
selectedReset = option;
|
|
741
|
+
redemptionId = undefined;
|
|
742
|
+
resetFailure = undefined;
|
|
743
|
+
return { kind: "to", screen: "reset-confirm" };
|
|
744
|
+
},
|
|
745
|
+
"cancel-reset": () => ({ kind: "back" }),
|
|
746
|
+
"consume-reset": async () => {
|
|
747
|
+
if (!selectedReset || !resetAuthFingerprint || !resetModelIdentity) {
|
|
748
|
+
return { kind: "rejected" };
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
redemptionId ??= createRedemptionId();
|
|
752
|
+
const attemptId = redemptionId;
|
|
753
|
+
const option = selectedReset;
|
|
754
|
+
const expectedFingerprint = resetAuthFingerprint;
|
|
755
|
+
const expectedModel = resetModelIdentity;
|
|
756
|
+
const result = await runMenuOperation(
|
|
757
|
+
ctx,
|
|
758
|
+
"Resetting your usage…",
|
|
759
|
+
controller.signal,
|
|
760
|
+
async (signal) => {
|
|
761
|
+
const auth = await awaitWithDeadline(
|
|
762
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader),
|
|
763
|
+
signal,
|
|
764
|
+
DEFAULT_TIMEOUT_MS,
|
|
765
|
+
"revalidating current Codex reset authentication",
|
|
766
|
+
);
|
|
767
|
+
if (
|
|
768
|
+
modelIdentity(ctx.model) !== expectedModel ||
|
|
769
|
+
auth.fingerprint !== expectedFingerprint
|
|
770
|
+
) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
"The active Codex model or account changed; the reset was not used.",
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const outcome = await consumeCodexResetCredit(
|
|
776
|
+
auth,
|
|
777
|
+
option,
|
|
778
|
+
attemptId,
|
|
779
|
+
signal,
|
|
780
|
+
DEFAULT_TIMEOUT_MS,
|
|
781
|
+
);
|
|
782
|
+
invalidateProviderState("openai-codex");
|
|
783
|
+
const model = ctx.model;
|
|
784
|
+
if (modelIdentity(model) !== expectedModel) return { outcome };
|
|
785
|
+
const refreshed = await queryCurrentState(ctx, model, true, signal);
|
|
786
|
+
const stable = await outcomeStillCurrent(
|
|
787
|
+
ctx,
|
|
788
|
+
model,
|
|
789
|
+
menuGeneration,
|
|
790
|
+
refreshed,
|
|
791
|
+
signal,
|
|
792
|
+
);
|
|
793
|
+
return stable ? { outcome, refreshed, model } : { outcome };
|
|
794
|
+
},
|
|
795
|
+
false,
|
|
796
|
+
);
|
|
797
|
+
if (!result) return { kind: "close" };
|
|
798
|
+
resetOutcome = result.outcome;
|
|
799
|
+
resetFailure = undefined;
|
|
800
|
+
if (result.refreshed && result.model) {
|
|
801
|
+
stableCurrent = { outcome: result.refreshed, model: result.model };
|
|
802
|
+
current = result.refreshed;
|
|
803
|
+
visibleStates = [current.state];
|
|
804
|
+
publishStableCurrent(ctx, stableCurrent);
|
|
805
|
+
}
|
|
806
|
+
return { kind: "to", screen: "reset-result" };
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) {
|
|
809
|
+
return { kind: "close" };
|
|
810
|
+
}
|
|
811
|
+
resetFailure = `Couldn't reset usage: ${errorMessage(error)}. Try again with the same request.`;
|
|
812
|
+
return { kind: "to", screen: "reset-error" };
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
"back-to-usage": () => ({ kind: "to", screen: "main" }),
|
|
816
|
+
"back-to-resets": () => {
|
|
817
|
+
redemptionId = undefined;
|
|
818
|
+
resetFailure = undefined;
|
|
819
|
+
return { kind: "to", screen: "reset-picker" };
|
|
820
|
+
},
|
|
501
821
|
refresh: async () => {
|
|
502
822
|
const refreshed = await queryStableCurrent(
|
|
503
823
|
ctx,
|
|
@@ -614,25 +934,30 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
614
934
|
}
|
|
615
935
|
};
|
|
616
936
|
|
|
617
|
-
const commandHandler = async (args: string, ctx: ExtensionCommandContext) => {
|
|
618
|
-
if (args.trim()) {
|
|
619
|
-
ctx.ui.notify("/usage does not accept arguments; choose an action from its menu.", "warning");
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
try {
|
|
623
|
-
await showMenu(ctx);
|
|
624
|
-
} catch (error) {
|
|
625
|
-
if (isStaleExtensionContextError(error) || isAbortError(error)) return;
|
|
626
|
-
throw error;
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
|
|
630
937
|
pi.registerCommand("usage", {
|
|
631
938
|
description: "Show usage for the current runtime account",
|
|
632
|
-
handler:
|
|
939
|
+
handler: async (args, ctx) => {
|
|
940
|
+
if (args.trim()) {
|
|
941
|
+
ctx.ui.notify(
|
|
942
|
+
"/usage does not accept arguments; choose an action from its menu.",
|
|
943
|
+
"warning",
|
|
944
|
+
);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
await showMenu(ctx);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (isStaleExtensionContextError(error) || isAbortError(error)) return;
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
},
|
|
633
954
|
});
|
|
634
|
-
|
|
635
955
|
pi.on("session_start", (_event, ctx) => {
|
|
956
|
+
statusGeneration += 1;
|
|
957
|
+
clearStatusTimer();
|
|
958
|
+
for (const controller of activeControllers) controller.abort();
|
|
959
|
+
activeControllers.clear();
|
|
960
|
+
statusController = undefined;
|
|
636
961
|
sessionActive = true;
|
|
637
962
|
startStatusRefresh(ctx, ctx.model, false);
|
|
638
963
|
});
|
|
@@ -658,40 +983,8 @@ export default function usageExtension(pi: ExtensionAPI) {
|
|
|
658
983
|
activeCurrentIdentity = undefined;
|
|
659
984
|
safeSetStatus(ctx, undefined);
|
|
660
985
|
});
|
|
661
|
-
}
|
|
662
986
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
(adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id),
|
|
987
|
+
fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) =>
|
|
988
|
+
startStatusRefresh(ctx, ctx.model, false),
|
|
666
989
|
);
|
|
667
990
|
}
|
|
668
|
-
|
|
669
|
-
function providerDisplayName(ctx: ExtensionContext, providerId: string): string {
|
|
670
|
-
try {
|
|
671
|
-
return sanitizeDisplayText(ctx.modelRegistry.getProviderDisplayName(providerId), 80);
|
|
672
|
-
} catch {
|
|
673
|
-
return sanitizeDisplayText(providerId, 80);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function setBoundedMap<T>(map: Map<string, T>, key: string, value: T, limit: number): void {
|
|
678
|
-
map.delete(key);
|
|
679
|
-
while (map.size >= limit) {
|
|
680
|
-
const oldest = map.keys().next().value;
|
|
681
|
-
if (oldest === undefined) break;
|
|
682
|
-
map.delete(oldest);
|
|
683
|
-
}
|
|
684
|
-
map.set(key, value);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function modelIdentity(model: PiModel | undefined): string | undefined {
|
|
688
|
-
return model ? `${model.provider}/${model.id}` : undefined;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function isAbortError(error: unknown): boolean {
|
|
692
|
-
return error instanceof Error && error.name === "AbortError";
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function isTimeoutError(error: unknown): boolean {
|
|
696
|
-
return error instanceof Error && error.name === "TimeoutError";
|
|
697
|
-
}
|