@llblab/pi-codex-usage 0.5.2 → 0.7.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/AGENTS.md +2 -2
- package/BACKLOG.md +1 -2
- package/CHANGELOG.md +10 -0
- package/README.md +20 -1
- package/index.ts +139 -11
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Agent Notes
|
|
2
2
|
|
|
3
|
-
- `Statusline-
|
|
3
|
+
- `Statusline-first scope`: Keep this extension zero-configuration and focused on compact status surfaces.
|
|
4
4
|
- Trigger: Considering commands, menus, persisted settings, or notification output.
|
|
5
|
-
- Action: Prefer deleting the surface unless it is required for the optimistic status widget.
|
|
5
|
+
- Action: Prefer deleting the surface unless it is required for the optimistic TUI status widget or the optional `pi-telegram` `/start` status-line mirror.
|
|
6
6
|
|
|
7
7
|
- `Optimistic refresh`: Preserve the last good statusline bar during refresh and transient failures.
|
|
8
8
|
- Trigger: Updating quota polling or error handling.
|
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,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.0: Exhausted Quota Visibility
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
11
|
+
## 0.6.0: Telegram Status Integration
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
5
15
|
## 0.5.2: Sub-Day Reset Countdown And JPEG Banner
|
|
6
16
|
|
|
7
17
|
- Refined the weekly reset countdown below 24 hours to use upward-rounded 6-minute hour-tenth steps (`24h`, `23.7h`, `20.1h`, `20h`, `19.9h`, …, `1h`) instead of coarse whole-hour floors. Impact: the statusline gives more useful sub-day reset timing without growing wider than one decimal place.
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ This repository is a minimal fork of [`narumiruna/pi-extensions/extensions/pi-co
|
|
|
16
16
|
|
|
17
17
|
- Shows an empty statusline bar immediately, then refreshes every 30 seconds while the active Pi model uses `openai-codex`
|
|
18
18
|
- Statusline output stays compact, with the `codex` label accented and the quota bar plus weekly reset countdown drawn on a themed background
|
|
19
|
+
- When `pi-telegram` is available, the same compact value appears as `codex: <value>` in the `/start` menu status text for active OpenAI Codex subscription models
|
|
19
20
|
- Additional returned buckets, including Spark-specific limits, are ignored
|
|
20
21
|
- Pi OpenAI Codex provider auth is used first
|
|
21
22
|
- Codex CLI app-server remains available as a fallback
|
|
@@ -46,10 +47,18 @@ Normal usage:
|
|
|
46
47
|
codex ██████▀▀▀▀ 6d
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
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.
|
|
50
|
+
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.
|
|
50
51
|
|
|
51
52
|
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.
|
|
52
53
|
|
|
54
|
+
When the 5-hour window is exhausted and exposes its own reset time, the statusline adds the 5-hour reset before the weekly reset:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
codex ▄▄▄▄▄⠀⠀⠀⠀⠀ 5h/7d
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The ten-character dual bar stays unchanged. The first countdown is the 5-hour reset, and the second countdown after `/` is the weekly reset.
|
|
61
|
+
|
|
53
62
|
Unavailable because Codex auth or subscription quota is not available:
|
|
54
63
|
|
|
55
64
|
```text
|
|
@@ -62,6 +71,16 @@ Runtime failure, such as a network or provider error:
|
|
|
62
71
|
codex error
|
|
63
72
|
```
|
|
64
73
|
|
|
74
|
+
## Telegram Status Menu
|
|
75
|
+
|
|
76
|
+
If `@llblab/pi-telegram` is loaded with the public status-line provider API, this extension registers an optional `/start` menu status row. The row is shown only while the active model uses the OpenAI Codex subscription provider:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
codex: ██████▀▀▀▀ 6d
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
65
84
|
## Auth
|
|
66
85
|
|
|
67
86
|
The extension tries usage sources in this order:
|
package/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
const CODEX_PROVIDER_ID = "openai-codex";
|
|
10
|
+
const CODEX_USAGE_EXTENSION_ID = "@llblab/pi-codex-usage";
|
|
10
11
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
11
12
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
12
13
|
const SECOND_MS = 1000;
|
|
@@ -21,6 +22,10 @@ const STATUS_KEY = "aa-codex-usage";
|
|
|
21
22
|
const MAX_ERROR_BODY_CHARS = 600;
|
|
22
23
|
const STATUS_LABEL_TEXT = "codex";
|
|
23
24
|
const DUAL_BAR_WIDTH = 10;
|
|
25
|
+
const TELEGRAM_STATUS_IMPORT_SPECIFIERS = [
|
|
26
|
+
"@llblab/pi-telegram/status",
|
|
27
|
+
new URL("../pi-telegram/api/status.ts", import.meta.url).href,
|
|
28
|
+
];
|
|
24
29
|
const DUAL_BAR_CHARS = [
|
|
25
30
|
"⠀",
|
|
26
31
|
"▘",
|
|
@@ -44,6 +49,19 @@ type UsageSource = "pi-auth" | "codex-app-server";
|
|
|
44
49
|
type TimeoutHandle = ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
45
50
|
type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
46
51
|
export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
52
|
+
type CodexUsageTelegramStatusModel = Pick<PiModel, "provider">;
|
|
53
|
+
type TelegramStatusLineProviderResult =
|
|
54
|
+
| { label: string; value: string }
|
|
55
|
+
| undefined;
|
|
56
|
+
type TelegramStatusLineProvider = (ctx: {
|
|
57
|
+
activeModel?: CodexUsageTelegramStatusModel;
|
|
58
|
+
}) => TelegramStatusLineProviderResult;
|
|
59
|
+
type TelegramStatusLineModule = {
|
|
60
|
+
registerTelegramStatusLineProvider?: (
|
|
61
|
+
provider: TelegramStatusLineProvider,
|
|
62
|
+
options: { id: string },
|
|
63
|
+
) => () => void;
|
|
64
|
+
};
|
|
47
65
|
|
|
48
66
|
type QueryUsageOptions = {
|
|
49
67
|
timeoutMs: number;
|
|
@@ -140,6 +158,26 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
140
158
|
let statuslineCountdownTimer: TimeoutHandle | undefined;
|
|
141
159
|
let statuslineRefreshTimer: TimeoutHandle | undefined;
|
|
142
160
|
let statuslineRequestId = 0;
|
|
161
|
+
let unregisterTelegramStatusLine: (() => void) | undefined;
|
|
162
|
+
let telegramStatusLineRegistration: Promise<void> | undefined;
|
|
163
|
+
|
|
164
|
+
const ensureTelegramStatusLineRegistered = () => {
|
|
165
|
+
if (unregisterTelegramStatusLine || telegramStatusLineRegistration) return;
|
|
166
|
+
telegramStatusLineRegistration = registerCodexUsageTelegramStatusLine(
|
|
167
|
+
({ activeModel }) => {
|
|
168
|
+
if (!isOpenAICodexModel(activeModel)) return undefined;
|
|
169
|
+
if (!cache) return undefined;
|
|
170
|
+
const value = formatCodexUsageStatusValue(cache.report);
|
|
171
|
+
return value ? { label: "codex", value } : undefined;
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
.then((unregister) => {
|
|
175
|
+
unregisterTelegramStatusLine = unregister;
|
|
176
|
+
})
|
|
177
|
+
.finally(() => {
|
|
178
|
+
telegramStatusLineRegistration = undefined;
|
|
179
|
+
});
|
|
180
|
+
};
|
|
143
181
|
|
|
144
182
|
const clearStatuslineTimers = () => {
|
|
145
183
|
if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
|
|
@@ -299,7 +337,10 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
299
337
|
});
|
|
300
338
|
};
|
|
301
339
|
|
|
340
|
+
ensureTelegramStatusLineRegistered();
|
|
341
|
+
|
|
302
342
|
pi.on("session_start", (_event, ctx) => {
|
|
343
|
+
ensureTelegramStatusLineRegistered();
|
|
303
344
|
if (isOpenAICodexModel(ctx.model))
|
|
304
345
|
void refreshCurrentCodexUsageStatusline(ctx, false);
|
|
305
346
|
else clearUsageStatusline(ctx);
|
|
@@ -319,7 +360,11 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
319
360
|
}
|
|
320
361
|
});
|
|
321
362
|
|
|
322
|
-
pi.on("session_shutdown", (_event, ctx) =>
|
|
363
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
364
|
+
clearUsageStatusline(ctx);
|
|
365
|
+
unregisterTelegramStatusLine?.();
|
|
366
|
+
unregisterTelegramStatusLine = undefined;
|
|
367
|
+
});
|
|
323
368
|
}
|
|
324
369
|
|
|
325
370
|
function isOpenAICodexModel(
|
|
@@ -328,6 +373,31 @@ function isOpenAICodexModel(
|
|
|
328
373
|
return model?.provider === CODEX_PROVIDER_ID;
|
|
329
374
|
}
|
|
330
375
|
|
|
376
|
+
async function importTelegramStatusLineModule(): Promise<
|
|
377
|
+
TelegramStatusLineModule | undefined
|
|
378
|
+
> {
|
|
379
|
+
for (const specifier of TELEGRAM_STATUS_IMPORT_SPECIFIERS) {
|
|
380
|
+
try {
|
|
381
|
+
const imported = (await import(specifier)) as TelegramStatusLineModule;
|
|
382
|
+
if (typeof imported.registerTelegramStatusLineProvider === "function") {
|
|
383
|
+
return imported;
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// pi-telegram is optional; absence just disables the Telegram status line.
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function registerCodexUsageTelegramStatusLine(
|
|
393
|
+
provider: TelegramStatusLineProvider,
|
|
394
|
+
): Promise<(() => void) | undefined> {
|
|
395
|
+
const telegramStatus = await importTelegramStatusLineModule();
|
|
396
|
+
return telegramStatus?.registerTelegramStatusLineProvider?.(provider, {
|
|
397
|
+
id: CODEX_USAGE_EXTENSION_ID,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
331
401
|
async function queryUsage(
|
|
332
402
|
ctx: ExtensionContext,
|
|
333
403
|
options: Pick<QueryUsageOptions, "timeoutMs">,
|
|
@@ -791,10 +861,14 @@ export function formatCodexUsageStatusline(
|
|
|
791
861
|
ctx: ExtensionContext,
|
|
792
862
|
_model?: CodexUsageModel,
|
|
793
863
|
): string {
|
|
794
|
-
const
|
|
795
|
-
if (!
|
|
796
|
-
const countdown =
|
|
797
|
-
const barText = formatStatuslineBarText(
|
|
864
|
+
const value = formatCodexUsageStatusValue(report);
|
|
865
|
+
if (!value) return formatStatuslineText(ctx, "n/a");
|
|
866
|
+
const [bar, countdown] = value.split(" ", 2);
|
|
867
|
+
const barText = formatStatuslineBarText(
|
|
868
|
+
ctx,
|
|
869
|
+
bar ?? "",
|
|
870
|
+
hasExhaustedQuotaWindow(report) ? "toolErrorBg" : "selectedBg",
|
|
871
|
+
);
|
|
798
872
|
return countdown
|
|
799
873
|
? `${barText} ${ctx.ui.theme.fg("dim", countdown)}`
|
|
800
874
|
: barText;
|
|
@@ -806,6 +880,26 @@ export function formatCodexUsageBar(
|
|
|
806
880
|
return formatReportBar(report);
|
|
807
881
|
}
|
|
808
882
|
|
|
883
|
+
export function formatCodexUsageStatusValue(
|
|
884
|
+
report: CodexUsageReport,
|
|
885
|
+
now = Date.now(),
|
|
886
|
+
): string | undefined {
|
|
887
|
+
const snapshot = selectPrimaryCodexSnapshot(report);
|
|
888
|
+
if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
|
|
889
|
+
const bar = formatDualLimitBar(snapshot.primary, snapshot.secondary);
|
|
890
|
+
if (isQuotaWindowExhausted(snapshot.primary) && snapshot.primary?.resetAt) {
|
|
891
|
+
const primaryCountdown = formatResetCountdown(snapshot.primary.resetAt, now);
|
|
892
|
+
const weeklyCountdown = snapshot.secondary?.resetAt
|
|
893
|
+
? formatResetCountdown(snapshot.secondary.resetAt, now)
|
|
894
|
+
: undefined;
|
|
895
|
+
return weeklyCountdown
|
|
896
|
+
? `${bar} ${primaryCountdown}/${weeklyCountdown}`
|
|
897
|
+
: `${bar} ${primaryCountdown}`;
|
|
898
|
+
}
|
|
899
|
+
const countdown = formatWeeklyResetCountdown(report, now);
|
|
900
|
+
return countdown ? `${bar} ${countdown}` : bar;
|
|
901
|
+
}
|
|
902
|
+
|
|
809
903
|
export function formatWeeklyResetCountdown(
|
|
810
904
|
report: CodexUsageReport,
|
|
811
905
|
now = Date.now(),
|
|
@@ -837,9 +931,16 @@ export function nextResetCountdownDelayMs(
|
|
|
837
931
|
report: CodexUsageReport,
|
|
838
932
|
now = Date.now(),
|
|
839
933
|
): number | undefined {
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
934
|
+
const snapshot = selectPrimaryCodexSnapshot(report);
|
|
935
|
+
const resetTimes = [snapshot?.secondary?.resetAt];
|
|
936
|
+
if (isQuotaWindowExhausted(snapshot?.primary)) {
|
|
937
|
+
resetTimes.push(snapshot?.primary?.resetAt);
|
|
938
|
+
}
|
|
939
|
+
const delays = resetTimes
|
|
940
|
+
.filter((resetAt): resetAt is number => resetAt !== undefined)
|
|
941
|
+
.map((resetAt) => nextResetCountdownDelayForRemainingMs(resetAt - now))
|
|
942
|
+
.filter((delay): delay is number => delay !== undefined);
|
|
943
|
+
return delays.length > 0 ? Math.min(...delays) : undefined;
|
|
843
944
|
}
|
|
844
945
|
|
|
845
946
|
export function nextResetCountdownDelayForRemainingMs(
|
|
@@ -882,9 +983,13 @@ function formatStatuslineText(ctx: ExtensionContext, value: string): string {
|
|
|
882
983
|
return `${label} ${ctx.ui.theme.fg("dim", value)}`;
|
|
883
984
|
}
|
|
884
985
|
|
|
885
|
-
function formatStatuslineBarText(
|
|
986
|
+
function formatStatuslineBarText(
|
|
987
|
+
ctx: ExtensionContext,
|
|
988
|
+
bar: string,
|
|
989
|
+
background: "selectedBg" | "toolErrorBg" = "selectedBg",
|
|
990
|
+
): string {
|
|
886
991
|
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
887
|
-
const value = ctx.ui.theme.bg(
|
|
992
|
+
const value = ctx.ui.theme.bg(background, ctx.ui.theme.fg("dim", bar));
|
|
888
993
|
return `${label} ${value}`;
|
|
889
994
|
}
|
|
890
995
|
|
|
@@ -961,15 +1066,38 @@ function formatDualLimitBar(
|
|
|
961
1066
|
|
|
962
1067
|
function filledTwentieths(
|
|
963
1068
|
window: NormalizedRateLimitWindow | undefined,
|
|
1069
|
+
): number {
|
|
1070
|
+
return filledParts(window, 20);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function filledParts(
|
|
1074
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
1075
|
+
totalParts: number,
|
|
964
1076
|
): number {
|
|
965
1077
|
if (!window) return 0;
|
|
966
|
-
|
|
1078
|
+
const remaining = remainingPercent(window);
|
|
1079
|
+
if (remaining <= 0) return 0;
|
|
1080
|
+
return Math.max(1, Math.round(remaining / (100 / totalParts)));
|
|
967
1081
|
}
|
|
968
1082
|
|
|
969
1083
|
function remainingPercent(window: NormalizedRateLimitWindow): number {
|
|
970
1084
|
return 100 - clampPercent(window.usedPercent);
|
|
971
1085
|
}
|
|
972
1086
|
|
|
1087
|
+
function isQuotaWindowExhausted(
|
|
1088
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
1089
|
+
): boolean {
|
|
1090
|
+
return window !== undefined && remainingPercent(window) <= 0;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function hasExhaustedQuotaWindow(report: CodexUsageReport): boolean {
|
|
1094
|
+
const snapshot = selectPrimaryCodexSnapshot(report);
|
|
1095
|
+
return (
|
|
1096
|
+
isQuotaWindowExhausted(snapshot?.primary) ||
|
|
1097
|
+
isQuotaWindowExhausted(snapshot?.secondary)
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
973
1101
|
function isPrimaryCodexSnapshot(
|
|
974
1102
|
snapshot: NormalizedRateLimitSnapshot,
|
|
975
1103
|
): boolean {
|