@llblab/pi-codex-usage 0.6.0 → 0.8.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/BACKLOG.md +1 -2
- package/CHANGELOG.md +11 -0
- package/README.md +23 -6
- package/index.ts +200 -60
- package/package.json +1 -1
package/BACKLOG.md
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
# Backlog
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- [ ] `Statusline polish` Tune the successful-refresh blink duration if the current 150ms redraw feels too visible or too subtle in the Pi footer.
|
|
3
|
+
No open work.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0: Spark Model Usage Status
|
|
6
|
+
|
|
7
|
+
- Added active-bucket status support for `GPT-5.3-Codex-Spark`. Impact: selecting the Spark Codex subscription model now shows the parallel Spark rate-limit bucket with a `spark` label, while regular Codex models keep the existing `codex` bucket/status behavior.
|
|
8
|
+
- Added parsing for backend `additional_rate_limits`, including the Spark `GPT-5.3-Codex-Spark` / `codex_bengalfox` limit. Impact: Spark quota can be shown from the same Pi-auth usage response path as normal Codex quota instead of relying only on app-server fallback.
|
|
9
|
+
|
|
10
|
+
## 0.7.0: Exhausted Quota Visibility
|
|
11
|
+
|
|
12
|
+
- Added exhausted-quota warning background for the statusline quota bar. Impact: when either the 5-hour or weekly Codex window has 0% remaining, the bar keeps its shape but switches from the selected background to the error background for faster visual detection.
|
|
13
|
+
- Added exhausted-primary countdown display for cases where the 5-hour Codex window has 0% remaining and exposes a reset time. Impact: the statusline keeps the normal ten-cell dual bar and appends `<5-hour-reset>/<weekly-reset>` countdowns, so operators can see both how long to wait for short-window recovery and how much weekly quota remains.
|
|
14
|
+
- Fixed quota-bar rounding so a truly empty segment state is shown only when a quota window has 0% remaining, while any positive sub-5% remainder still renders as one visible step. Impact: the statusline now distinguishes exhausted Codex limits from tiny remaining quota without changing normal round-to-nearest behavior elsewhere.
|
|
15
|
+
|
|
5
16
|
## 0.6.0: Telegram Status Integration
|
|
6
17
|
|
|
7
18
|
- Added optional `pi-telegram` status-menu integration through the public Telegram status-line provider API. When `pi-telegram` is available and the active model is an OpenAI Codex subscription model, the `/start` menu status text now includes `codex: <value>` using the same compact quota bar and reset countdown value as the terminal statusline. Impact: Telegram operators can see Codex quota/reset state in the main control menu without any extra configuration, while non-Codex models and missing `pi-telegram` installs stay unchanged.
|
package/README.md
CHANGED
|
@@ -15,9 +15,11 @@ This repository is a minimal fork of [`narumiruna/pi-extensions/extensions/pi-co
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
17
|
- Shows an empty statusline bar immediately, then refreshes every 30 seconds while the active Pi model uses `openai-codex`
|
|
18
|
-
- Statusline output stays compact, with the
|
|
19
|
-
-
|
|
20
|
-
-
|
|
18
|
+
- Statusline output stays compact, with the active usage label accented and the quota bar plus weekly reset countdown drawn on a themed background
|
|
19
|
+
- Regular Codex subscription models show the primary `codex` quota bucket
|
|
20
|
+
- `GPT-5.3-Codex-Spark` shows the parallel Spark quota bucket with the `spark` label
|
|
21
|
+
- When `pi-telegram` is available, the same compact value appears as `codex: <value>` or `spark: <value>` in the `/start` menu status text for active OpenAI Codex subscription models
|
|
22
|
+
- Additional returned buckets unrelated to the active Codex/Spark model are ignored
|
|
21
23
|
- Pi OpenAI Codex provider auth is used first
|
|
22
24
|
- Codex CLI app-server remains available as a fallback
|
|
23
25
|
- Missing auth, subscription, plan, or quota windows are shown as `n/a`, not as an error
|
|
@@ -41,16 +43,30 @@ pi install git:github.com/llblab/pi-codex-usage
|
|
|
41
43
|
|
|
42
44
|
## Statusline
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
Regular Codex usage:
|
|
45
47
|
|
|
46
48
|
```text
|
|
47
49
|
codex ██████▀▀▀▀ 6d
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
Spark model usage:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
spark ██████████ 7d
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The ten-character bar encodes two twenty-step limits at once: 40 total bits of quota state in 10 terminal cells. Each step is 5%: the top quadrants are the 5-hour limit, and the bottom quadrants are the weekly limit. If either quota window is exhausted, the bar keeps the same shape but switches to the error background color.
|
|
51
59
|
|
|
52
60
|
When the weekly reset time is available, the bar is followed by a countdown. More than a day remains is shown in 144-minute day-tenth steps such as `7d`, `6.9d`, `6.6d`, `5.1d`, `5d`, `3.7d`, `3d`, `2d`, `1.9d`, `1.5d`, and `1.1d`, rounded upward to the next tenth. At 24 hours and below it switches to upward-rounded 6-minute hour-tenth steps such as `24h`, `23.7h`, `20.1h`, `20h`, `19.9h`, `1.4h`, `1.3h`, `1.2h`, `1.1h`, and `1h`. Under an hour it switches to floored minutes, and under a minute to seconds. After the reset timestamp passes, `0s` is held until the next successful quota refresh reports the new weekly window.
|
|
53
61
|
|
|
62
|
+
When the 5-hour window is exhausted and exposes its own reset time, the statusline adds the 5-hour reset before the weekly reset:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
codex ▄▄▄▄▄⠀⠀⠀⠀⠀ 5h/7d
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The ten-character dual bar stays unchanged. The first countdown is the 5-hour reset, and the second countdown after `/` is the weekly reset.
|
|
69
|
+
|
|
54
70
|
Unavailable because Codex auth or subscription quota is not available:
|
|
55
71
|
|
|
56
72
|
```text
|
|
@@ -69,9 +85,10 @@ If `@llblab/pi-telegram` is loaded with the public status-line provider API, thi
|
|
|
69
85
|
|
|
70
86
|
```text
|
|
71
87
|
codex: ██████▀▀▀▀ 6d
|
|
88
|
+
spark: ██████████ 7d
|
|
72
89
|
```
|
|
73
90
|
|
|
74
|
-
The value is the same compact quota bar plus weekly reset countdown used by the terminal statusline. If `pi-telegram` is absent, older, or the active model is not a Codex subscription model, no Telegram row is added.
|
|
91
|
+
The value is the same compact quota bar plus weekly reset countdown used by the terminal statusline, and the label follows the active Codex/Spark model. If `pi-telegram` is absent, older, or the active model is not a Codex subscription model, no Telegram row is added.
|
|
75
92
|
|
|
76
93
|
## Auth
|
|
77
94
|
|
package/index.ts
CHANGED
|
@@ -20,7 +20,11 @@ const REFRESH_INTERVAL_MS = 30 * SECOND_MS;
|
|
|
20
20
|
const REDRAW_BLINK_MS = 150;
|
|
21
21
|
const STATUS_KEY = "aa-codex-usage";
|
|
22
22
|
const MAX_ERROR_BODY_CHARS = 600;
|
|
23
|
-
const
|
|
23
|
+
const DEFAULT_STATUS_LABEL_TEXT = "codex";
|
|
24
|
+
const SPARK_STATUS_LABEL_TEXT = "spark";
|
|
25
|
+
const CODEX_USAGE_LIMIT_ID = "codex";
|
|
26
|
+
const SPARK_USAGE_LIMIT_ID = "spark";
|
|
27
|
+
const SPARK_MODEL_KEY = "gpt-5.3-codex-spark";
|
|
24
28
|
const DUAL_BAR_WIDTH = 10;
|
|
25
29
|
const TELEGRAM_STATUS_IMPORT_SPECIFIERS = [
|
|
26
30
|
"@llblab/pi-telegram/status",
|
|
@@ -49,7 +53,7 @@ type UsageSource = "pi-auth" | "codex-app-server";
|
|
|
49
53
|
type TimeoutHandle = ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
50
54
|
type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
51
55
|
export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
52
|
-
type CodexUsageTelegramStatusModel = Pick<PiModel, "provider">;
|
|
56
|
+
type CodexUsageTelegramStatusModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
53
57
|
type TelegramStatusLineProviderResult =
|
|
54
58
|
| { label: string; value: string }
|
|
55
59
|
| undefined;
|
|
@@ -99,6 +103,13 @@ export type NormalizedRateLimitWindow = {
|
|
|
99
103
|
|
|
100
104
|
type RateLimitStatusPayload = {
|
|
101
105
|
rate_limit?: unknown;
|
|
106
|
+
additional_rate_limits?: unknown;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
type BackendAdditionalRateLimit = {
|
|
110
|
+
limit_name?: unknown;
|
|
111
|
+
metered_feature?: unknown;
|
|
112
|
+
rate_limit?: unknown;
|
|
102
113
|
};
|
|
103
114
|
|
|
104
115
|
type BackendRateLimitDetails = {
|
|
@@ -167,8 +178,8 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
167
178
|
({ activeModel }) => {
|
|
168
179
|
if (!isOpenAICodexModel(activeModel)) return undefined;
|
|
169
180
|
if (!cache) return undefined;
|
|
170
|
-
const value = formatCodexUsageStatusValue(cache.report);
|
|
171
|
-
return value ? { label:
|
|
181
|
+
const value = formatCodexUsageStatusValue(cache.report, activeModel);
|
|
182
|
+
return value ? { label: activeUsageLabel(activeModel), value } : undefined;
|
|
172
183
|
},
|
|
173
184
|
)
|
|
174
185
|
.then((unregister) => {
|
|
@@ -219,7 +230,7 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
219
230
|
model: CodexUsageModel | undefined,
|
|
220
231
|
) => {
|
|
221
232
|
if (statuslineCountdownTimer) clearTimeout(statuslineCountdownTimer);
|
|
222
|
-
const delayMs = nextResetCountdownDelayMs(report);
|
|
233
|
+
const delayMs = nextResetCountdownDelayMs(report, Date.now(), model);
|
|
223
234
|
if (delayMs === undefined) {
|
|
224
235
|
statuslineCountdownTimer = undefined;
|
|
225
236
|
return;
|
|
@@ -268,11 +279,16 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
268
279
|
else scheduleTemporaryStatuslineClear(ctx);
|
|
269
280
|
};
|
|
270
281
|
|
|
271
|
-
const queryCurrentUsage = (
|
|
282
|
+
const queryCurrentUsage = (
|
|
283
|
+
ctx: ExtensionContext,
|
|
284
|
+
model: CodexUsageModel | undefined,
|
|
285
|
+
) => {
|
|
272
286
|
if (!inFlightUsageQuery) {
|
|
273
|
-
inFlightUsageQuery = queryUsage(
|
|
274
|
-
|
|
275
|
-
|
|
287
|
+
inFlightUsageQuery = queryUsage(
|
|
288
|
+
ctx,
|
|
289
|
+
{ timeoutMs: DEFAULT_TIMEOUT_MS },
|
|
290
|
+
model,
|
|
291
|
+
).finally(() => {
|
|
276
292
|
inFlightUsageQuery = undefined;
|
|
277
293
|
});
|
|
278
294
|
}
|
|
@@ -305,7 +321,7 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
305
321
|
return;
|
|
306
322
|
}
|
|
307
323
|
|
|
308
|
-
const result = await queryCurrentUsage(ctx);
|
|
324
|
+
const result = await queryCurrentUsage(ctx, model);
|
|
309
325
|
if (requestId !== statuslineRequestId) return;
|
|
310
326
|
if (!isOpenAICodexModel(ctx.model)) {
|
|
311
327
|
clearUsageStatusline(ctx);
|
|
@@ -373,6 +389,26 @@ function isOpenAICodexModel(
|
|
|
373
389
|
return model?.provider === CODEX_PROVIDER_ID;
|
|
374
390
|
}
|
|
375
391
|
|
|
392
|
+
function isSparkCodexModel(
|
|
393
|
+
model: Pick<PiModel, "id" | "name" | "provider"> | undefined,
|
|
394
|
+
): boolean {
|
|
395
|
+
if (!isOpenAICodexModel(model)) return false;
|
|
396
|
+
const key = `${model?.id ?? ""} ${model?.name ?? ""}`.toLowerCase();
|
|
397
|
+
return key.includes(SPARK_MODEL_KEY);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function activeUsageLimitId(
|
|
401
|
+
model: Pick<PiModel, "id" | "name" | "provider"> | undefined,
|
|
402
|
+
): string {
|
|
403
|
+
return isSparkCodexModel(model) ? SPARK_USAGE_LIMIT_ID : CODEX_USAGE_LIMIT_ID;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function activeUsageLabel(
|
|
407
|
+
model: Pick<PiModel, "id" | "name" | "provider"> | undefined,
|
|
408
|
+
): string {
|
|
409
|
+
return isSparkCodexModel(model) ? SPARK_STATUS_LABEL_TEXT : DEFAULT_STATUS_LABEL_TEXT;
|
|
410
|
+
}
|
|
411
|
+
|
|
376
412
|
async function importTelegramStatusLineModule(): Promise<
|
|
377
413
|
TelegramStatusLineModule | undefined
|
|
378
414
|
> {
|
|
@@ -401,25 +437,29 @@ async function registerCodexUsageTelegramStatusLine(
|
|
|
401
437
|
async function queryUsage(
|
|
402
438
|
ctx: ExtensionContext,
|
|
403
439
|
options: Pick<QueryUsageOptions, "timeoutMs">,
|
|
440
|
+
model: CodexUsageModel | undefined,
|
|
404
441
|
): Promise<QueryUsageResult> {
|
|
405
442
|
const errors: UsageQueryError[] = [];
|
|
443
|
+
const sources = isSparkCodexModel(model)
|
|
444
|
+
? (["codex-app-server", "pi-auth"] as const)
|
|
445
|
+
: (["pi-auth", "codex-app-server"] as const);
|
|
406
446
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
cause,
|
|
422
|
-
}
|
|
447
|
+
for (const source of sources) {
|
|
448
|
+
try {
|
|
449
|
+
const report =
|
|
450
|
+
source === "pi-auth"
|
|
451
|
+
? await queryViaPiAuth(ctx, options.timeoutMs)
|
|
452
|
+
: await queryViaCodexAppServer(options.timeoutMs);
|
|
453
|
+
if (selectUsageSnapshot(report, activeUsageLimitId(model))) {
|
|
454
|
+
return { ok: true, report };
|
|
455
|
+
}
|
|
456
|
+
errors.push({
|
|
457
|
+
source,
|
|
458
|
+
message: `${source} returned no displayable ${activeUsageLabel(model)} rate-limit windows`,
|
|
459
|
+
});
|
|
460
|
+
} catch (cause) {
|
|
461
|
+
errors.push({ source, message: errorMessage(cause), cause });
|
|
462
|
+
}
|
|
423
463
|
}
|
|
424
464
|
|
|
425
465
|
return { ok: false, errors };
|
|
@@ -711,17 +751,42 @@ export function normalizeBackendPayload(
|
|
|
711
751
|
_capturedAt: number,
|
|
712
752
|
_source: UsageSource,
|
|
713
753
|
): CodexUsageReport {
|
|
714
|
-
const
|
|
715
|
-
|
|
754
|
+
const snapshots: NormalizedRateLimitSnapshot[] = [];
|
|
755
|
+
const primarySnapshot = normalizeBackendSnapshot(
|
|
756
|
+
CODEX_USAGE_LIMIT_ID,
|
|
716
757
|
payload.rate_limit,
|
|
717
758
|
_capturedAt,
|
|
718
759
|
);
|
|
719
|
-
if (
|
|
760
|
+
if (primarySnapshot) snapshots.push(primarySnapshot);
|
|
761
|
+
|
|
762
|
+
if (Array.isArray(payload.additional_rate_limits)) {
|
|
763
|
+
for (const item of payload.additional_rate_limits) {
|
|
764
|
+
const additional = assertObject(
|
|
765
|
+
item,
|
|
766
|
+
"additional rate limit",
|
|
767
|
+
) as BackendAdditionalRateLimit;
|
|
768
|
+
const snapshot = normalizeBackendSnapshot(
|
|
769
|
+
backendAdditionalLimitId(additional),
|
|
770
|
+
additional.rate_limit,
|
|
771
|
+
_capturedAt,
|
|
772
|
+
);
|
|
773
|
+
if (snapshot) snapshots.push(snapshot);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (snapshots.length === 0) {
|
|
720
778
|
throw new Error(
|
|
721
779
|
"Codex usage endpoint returned no displayable rate-limit windows.",
|
|
722
780
|
);
|
|
723
781
|
}
|
|
724
|
-
return { snapshots
|
|
782
|
+
return { snapshots };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function backendAdditionalLimitId(limit: BackendAdditionalRateLimit): string {
|
|
786
|
+
const raw = `${asString(limit.limit_name) ?? ""} ${asString(limit.metered_feature) ?? ""}`;
|
|
787
|
+
return raw.toLowerCase().includes("spark")
|
|
788
|
+
? SPARK_USAGE_LIMIT_ID
|
|
789
|
+
: normalizedUsageKey(raw) ?? CODEX_USAGE_LIMIT_ID;
|
|
725
790
|
}
|
|
726
791
|
|
|
727
792
|
function normalizeBackendSnapshot(
|
|
@@ -859,12 +924,17 @@ function mergeSnapshot(
|
|
|
859
924
|
export function formatCodexUsageStatusline(
|
|
860
925
|
report: CodexUsageReport,
|
|
861
926
|
ctx: ExtensionContext,
|
|
862
|
-
|
|
927
|
+
model?: CodexUsageModel,
|
|
863
928
|
): string {
|
|
864
|
-
const value = formatCodexUsageStatusValue(report);
|
|
865
|
-
if (!value) return formatStatuslineText(ctx, "n/a");
|
|
929
|
+
const value = formatCodexUsageStatusValue(report, model);
|
|
930
|
+
if (!value) return formatStatuslineText(ctx, "n/a", model);
|
|
866
931
|
const [bar, countdown] = value.split(" ", 2);
|
|
867
|
-
const barText = formatStatuslineBarText(
|
|
932
|
+
const barText = formatStatuslineBarText(
|
|
933
|
+
ctx,
|
|
934
|
+
bar ?? "",
|
|
935
|
+
hasExhaustedQuotaWindow(report, model) ? "toolErrorBg" : "selectedBg",
|
|
936
|
+
model,
|
|
937
|
+
);
|
|
868
938
|
return countdown
|
|
869
939
|
? `${barText} ${ctx.ui.theme.fg("dim", countdown)}`
|
|
870
940
|
: barText;
|
|
@@ -878,21 +948,40 @@ export function formatCodexUsageBar(
|
|
|
878
948
|
|
|
879
949
|
export function formatCodexUsageStatusValue(
|
|
880
950
|
report: CodexUsageReport,
|
|
951
|
+
modelOrNow?: CodexUsageModel | number,
|
|
881
952
|
now = Date.now(),
|
|
882
953
|
): string | undefined {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
const
|
|
954
|
+
const model = typeof modelOrNow === "number" ? undefined : modelOrNow;
|
|
955
|
+
const capturedNow = typeof modelOrNow === "number" ? modelOrNow : now;
|
|
956
|
+
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
957
|
+
if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
|
|
958
|
+
const bar = formatDualLimitBar(snapshot.primary, snapshot.secondary);
|
|
959
|
+
if (isQuotaWindowExhausted(snapshot.primary) && snapshot.primary?.resetAt) {
|
|
960
|
+
const primaryCountdown = formatResetCountdown(
|
|
961
|
+
snapshot.primary.resetAt,
|
|
962
|
+
capturedNow,
|
|
963
|
+
);
|
|
964
|
+
const weeklyCountdown = snapshot.secondary?.resetAt
|
|
965
|
+
? formatResetCountdown(snapshot.secondary.resetAt, capturedNow)
|
|
966
|
+
: undefined;
|
|
967
|
+
return weeklyCountdown
|
|
968
|
+
? `${bar} ${primaryCountdown}/${weeklyCountdown}`
|
|
969
|
+
: `${bar} ${primaryCountdown}`;
|
|
970
|
+
}
|
|
971
|
+
const countdown = formatWeeklyResetCountdown(report, model, capturedNow);
|
|
886
972
|
return countdown ? `${bar} ${countdown}` : bar;
|
|
887
973
|
}
|
|
888
974
|
|
|
889
975
|
export function formatWeeklyResetCountdown(
|
|
890
976
|
report: CodexUsageReport,
|
|
977
|
+
modelOrNow?: CodexUsageModel | number,
|
|
891
978
|
now = Date.now(),
|
|
892
979
|
): string | undefined {
|
|
893
|
-
const
|
|
980
|
+
const model = typeof modelOrNow === "number" ? undefined : modelOrNow;
|
|
981
|
+
const capturedNow = typeof modelOrNow === "number" ? modelOrNow : now;
|
|
982
|
+
const resetAt = selectActiveUsageSnapshot(report, model)?.secondary?.resetAt;
|
|
894
983
|
if (resetAt === undefined) return undefined;
|
|
895
|
-
return formatResetCountdown(resetAt,
|
|
984
|
+
return formatResetCountdown(resetAt, capturedNow);
|
|
896
985
|
}
|
|
897
986
|
|
|
898
987
|
export function formatResetCountdown(
|
|
@@ -916,10 +1005,18 @@ export function formatResetCountdown(
|
|
|
916
1005
|
export function nextResetCountdownDelayMs(
|
|
917
1006
|
report: CodexUsageReport,
|
|
918
1007
|
now = Date.now(),
|
|
1008
|
+
model?: CodexUsageModel,
|
|
919
1009
|
): number | undefined {
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
1010
|
+
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
1011
|
+
const resetTimes = [snapshot?.secondary?.resetAt];
|
|
1012
|
+
if (isQuotaWindowExhausted(snapshot?.primary)) {
|
|
1013
|
+
resetTimes.push(snapshot?.primary?.resetAt);
|
|
1014
|
+
}
|
|
1015
|
+
const delays = resetTimes
|
|
1016
|
+
.filter((resetAt): resetAt is number => resetAt !== undefined)
|
|
1017
|
+
.map((resetAt) => nextResetCountdownDelayForRemainingMs(resetAt - now))
|
|
1018
|
+
.filter((delay): delay is number => delay !== undefined);
|
|
1019
|
+
return delays.length > 0 ? Math.min(...delays) : undefined;
|
|
923
1020
|
}
|
|
924
1021
|
|
|
925
1022
|
export function nextResetCountdownDelayForRemainingMs(
|
|
@@ -951,20 +1048,32 @@ function formatTenths(value: number): string {
|
|
|
951
1048
|
return value % 10 === 0 ? String(value / 10) : (value / 10).toFixed(1);
|
|
952
1049
|
}
|
|
953
1050
|
|
|
954
|
-
function formatReportBar(
|
|
955
|
-
|
|
1051
|
+
function formatReportBar(
|
|
1052
|
+
report: CodexUsageReport,
|
|
1053
|
+
model?: CodexUsageModel,
|
|
1054
|
+
): string | undefined {
|
|
1055
|
+
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
956
1056
|
if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
|
|
957
1057
|
return formatDualLimitBar(snapshot.primary, snapshot.secondary);
|
|
958
1058
|
}
|
|
959
1059
|
|
|
960
|
-
function formatStatuslineText(
|
|
961
|
-
|
|
1060
|
+
function formatStatuslineText(
|
|
1061
|
+
ctx: ExtensionContext,
|
|
1062
|
+
value: string,
|
|
1063
|
+
model?: CodexUsageModel,
|
|
1064
|
+
): string {
|
|
1065
|
+
const label = ctx.ui.theme.fg("accent", activeUsageLabel(model));
|
|
962
1066
|
return `${label} ${ctx.ui.theme.fg("dim", value)}`;
|
|
963
1067
|
}
|
|
964
1068
|
|
|
965
|
-
function formatStatuslineBarText(
|
|
966
|
-
|
|
967
|
-
|
|
1069
|
+
function formatStatuslineBarText(
|
|
1070
|
+
ctx: ExtensionContext,
|
|
1071
|
+
bar: string,
|
|
1072
|
+
background: "selectedBg" | "toolErrorBg" = "selectedBg",
|
|
1073
|
+
model?: CodexUsageModel,
|
|
1074
|
+
): string {
|
|
1075
|
+
const label = ctx.ui.theme.fg("accent", activeUsageLabel(model));
|
|
1076
|
+
const value = ctx.ui.theme.bg(background, ctx.ui.theme.fg("dim", bar));
|
|
968
1077
|
return `${label} ${value}`;
|
|
969
1078
|
}
|
|
970
1079
|
|
|
@@ -976,7 +1085,7 @@ function formatStatuslineProblem(
|
|
|
976
1085
|
ctx: ExtensionContext,
|
|
977
1086
|
errors: UsageQueryError[],
|
|
978
1087
|
): string {
|
|
979
|
-
const label = ctx.ui.theme.fg("accent",
|
|
1088
|
+
const label = ctx.ui.theme.fg("accent", DEFAULT_STATUS_LABEL_TEXT);
|
|
980
1089
|
const value = isUsageUnavailable(errors)
|
|
981
1090
|
? ctx.ui.theme.fg("muted", "n/a")
|
|
982
1091
|
: ctx.ui.theme.fg("error", "error");
|
|
@@ -1005,12 +1114,6 @@ function isUnavailableError(error: UsageQueryError): boolean {
|
|
|
1005
1114
|
);
|
|
1006
1115
|
}
|
|
1007
1116
|
|
|
1008
|
-
function selectPrimaryCodexSnapshot(
|
|
1009
|
-
report: CodexUsageReport,
|
|
1010
|
-
): NormalizedRateLimitSnapshot | undefined {
|
|
1011
|
-
return report.snapshots.find(isPrimaryCodexSnapshot);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
1117
|
function normalizedUsageKey(value: string | undefined): string | undefined {
|
|
1015
1118
|
const key = value
|
|
1016
1119
|
?.toLowerCase()
|
|
@@ -1041,19 +1144,56 @@ function formatDualLimitBar(
|
|
|
1041
1144
|
|
|
1042
1145
|
function filledTwentieths(
|
|
1043
1146
|
window: NormalizedRateLimitWindow | undefined,
|
|
1147
|
+
): number {
|
|
1148
|
+
return filledParts(window, 20);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function filledParts(
|
|
1152
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
1153
|
+
totalParts: number,
|
|
1044
1154
|
): number {
|
|
1045
1155
|
if (!window) return 0;
|
|
1046
|
-
|
|
1156
|
+
const remaining = remainingPercent(window);
|
|
1157
|
+
if (remaining <= 0) return 0;
|
|
1158
|
+
return Math.max(1, Math.round(remaining / (100 / totalParts)));
|
|
1047
1159
|
}
|
|
1048
1160
|
|
|
1049
1161
|
function remainingPercent(window: NormalizedRateLimitWindow): number {
|
|
1050
1162
|
return 100 - clampPercent(window.usedPercent);
|
|
1051
1163
|
}
|
|
1052
1164
|
|
|
1053
|
-
function
|
|
1054
|
-
|
|
1165
|
+
function isQuotaWindowExhausted(
|
|
1166
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
1167
|
+
): boolean {
|
|
1168
|
+
return window !== undefined && remainingPercent(window) <= 0;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function hasExhaustedQuotaWindow(
|
|
1172
|
+
report: CodexUsageReport,
|
|
1173
|
+
model?: CodexUsageModel,
|
|
1055
1174
|
): boolean {
|
|
1056
|
-
|
|
1175
|
+
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
1176
|
+
return (
|
|
1177
|
+
isQuotaWindowExhausted(snapshot?.primary) ||
|
|
1178
|
+
isQuotaWindowExhausted(snapshot?.secondary)
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function selectActiveUsageSnapshot(
|
|
1183
|
+
report: CodexUsageReport,
|
|
1184
|
+
model: CodexUsageModel | undefined,
|
|
1185
|
+
): NormalizedRateLimitSnapshot | undefined {
|
|
1186
|
+
return selectUsageSnapshot(report, activeUsageLimitId(model));
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function selectUsageSnapshot(
|
|
1190
|
+
report: CodexUsageReport,
|
|
1191
|
+
limitId: string,
|
|
1192
|
+
): NormalizedRateLimitSnapshot | undefined {
|
|
1193
|
+
const normalizedLimitId = normalizedUsageKey(limitId);
|
|
1194
|
+
return report.snapshots.find(
|
|
1195
|
+
(snapshot) => normalizedUsageKey(snapshot.limitId) === normalizedLimitId,
|
|
1196
|
+
);
|
|
1057
1197
|
}
|
|
1058
1198
|
|
|
1059
1199
|
function clampPercent(value: number): number {
|