@llblab/pi-codex-usage 0.8.1 → 0.9.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 +4 -4
- package/BACKLOG.md +1 -0
- package/CHANGELOG.md +11 -0
- package/README.md +7 -4
- package/index.ts +206 -34
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
- Trigger: Updating quota polling or error handling.
|
|
9
9
|
- Action: Do not collapse the bar while a request is in flight; only show `n/a` or `error` after repeated failures or no usable quota.
|
|
10
10
|
|
|
11
|
-
- `
|
|
11
|
+
- `Adaptive compact bar`: Encode the server-provided quota windows in the ten-character separated-sextant bar.
|
|
12
12
|
- Trigger: Changing statusline formatting.
|
|
13
|
-
- Action:
|
|
13
|
+
- Action: When both windows exist, keep the classic dual bar with 20 top steps for the 5-hour window and 20 bottom steps for the weekly window. When only one weekly window exists, combine both tiers into one 40-step bar.
|
|
14
14
|
|
|
15
|
-
- `Weekly reset countdown`: Append the weekly reset countdown
|
|
15
|
+
- `Weekly reset countdown`: Append the weekly reset countdown whenever the available weekly window exposes a reset time.
|
|
16
16
|
- Trigger: Changing reset-time normalization or statusline refresh cadence.
|
|
17
|
-
- Action: Keep `d` labels rounded upward in 144-minute day-tenth steps above 24h, show 24h..1h labels in upward-rounded 6-minute hour-tenth steps, keep `m`/`s` labels floored, and hold `0s` until a successful quota refresh reports the next window.
|
|
17
|
+
- Action: Treat the secondary window as weekly in dual-window responses and the sole window as weekly in single-window responses. Keep `d` labels rounded upward in 144-minute day-tenth steps above 24h, show 24h..1h labels in upward-rounded 6-minute hour-tenth steps, keep `m`/`s` labels floored, and hold `0s` until a successful quota refresh reports the next window.
|
package/BACKLOG.md
CHANGED
|
@@ -5,3 +5,4 @@
|
|
|
5
5
|
- [x] Fix stale extension context usage after session replacement or reload. Source: GitHub issue #1. Timers must not keep using captured stale `ctx` after Pi session lifecycle changes.
|
|
6
6
|
- [x] Make usage refresh cache/in-flight state model-aware for Codex vs Spark. A regular Codex cache or request must not temporarily render Spark as `n/a` after model switching.
|
|
7
7
|
- [x] Preserve the active `codex`/`spark` label for failure states. Spark failures should render `spark n/a` or `spark error`, not `codex ...`.
|
|
8
|
+
- [x] Replace the visually full startup/request placeholder with counter-moving upper/lower in-bar loading markers. Keep the last usable active-bucket bar visible during ordinary refreshes, and hold provisional zero-usage reports behind the loader until confirmed stable.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.9.0: Adaptive Weekly Quota Bar
|
|
6
|
+
|
|
7
|
+
- Added server-driven quota-window display selection. Impact: responses containing both 5-hour and weekly windows keep the classic dual-tier bar, while responses containing only the new weekly window use all 40 bar steps as one unified quota scale instead of leaving one tier empty.
|
|
8
|
+
- Added single-window reset countdown and redraw scheduling. Impact: the weekly reset time remains visible and updates at the correct countdown boundaries regardless of whether OpenAI returns the weekly limit as the primary or secondary window.
|
|
9
|
+
- Extended provisional full-quota stabilization to single-window responses. Impact: transient 0%-used weekly reports remain behind the loading indicator until confirmed, matching the existing dual-window safety behavior.
|
|
10
|
+
|
|
11
|
+
## 0.8.2: Quota Loading Hotfix
|
|
12
|
+
|
|
13
|
+
- Replaced the static empty request placeholder with fixed-width upper and lower markers that move in opposite directions through their respective quota bars, reverse at the ends, and randomly start from either mirrored endpoint phase, using the normal themed bar background. Impact: motion distinguishes startup and model-bucket loading from 100% available quota while preserving the dual-window visual language.
|
|
14
|
+
- Kept the last usable active-bucket report visible while refreshing, including when returning from another model bucket, and stabilized first reports claiming both windows are completely unused for 15 seconds with one-second retries. Impact: routine polling remains visually stable while transient provider initialization values stay behind the loader instead of appearing as false 100% availability.
|
|
15
|
+
|
|
5
16
|
## 0.8.1: Stale Context Hotfix
|
|
6
17
|
|
|
7
18
|
- Fixed stale extension context crashes from delayed statusline timers after Pi session replacement or reload. Impact: timers now ignore stale `ctx` failures instead of letting issue #1 crash Pi after idle/session lifecycle changes.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
This repository is a minimal fork of [`narumiruna/pi-extensions/extensions/pi-codex-usage`](https://github.com/narumiruna/pi-extensions/tree/main/extensions/pi-codex-usage). It keeps the auth and quota-fetching path, but intentionally narrows the interface to the
|
|
7
|
+
This repository is a minimal fork of [`narumiruna/pi-extensions/extensions/pi-codex-usage`](https://github.com/narumiruna/pi-extensions/tree/main/extensions/pi-codex-usage). It keeps the auth and quota-fetching path, but intentionally narrows the interface to the Codex quota windows returned by OpenAI.
|
|
8
8
|
|
|
9
9
|
## Start Here
|
|
10
10
|
|
|
@@ -14,7 +14,8 @@ This repository is a minimal fork of [`narumiruna/pi-extensions/extensions/pi-co
|
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
- Shows
|
|
17
|
+
- Shows two counter-moving half-height markers in the statusline bar while the active Codex/Spark quota bucket is loading, then refreshes every 30 seconds
|
|
18
|
+
- Keeps the last usable active-bucket bar visible during ordinary refreshes instead of replacing known quota with a loading state
|
|
18
19
|
- Statusline output stays compact, with the active usage label accented and the quota bar plus weekly reset countdown drawn on a themed background
|
|
19
20
|
- Regular Codex subscription models show the primary `codex` quota bucket
|
|
20
21
|
- `GPT-5.3-Codex-Spark` shows the parallel Spark quota bucket with the `spark` label
|
|
@@ -55,9 +56,11 @@ Spark model usage:
|
|
|
55
56
|
spark ██████████ 7d
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
The ten-character bar
|
|
59
|
+
The ten-character bar adapts to the quota windows returned by OpenAI. When both windows exist, it encodes two twenty-step limits at once: the top quadrants show the 5-hour limit and the bottom quadrants show the weekly limit, with each step representing 5%. When OpenAI returns only the weekly window, both tiers combine into one conventional 40-step bar, with each step representing 2.5%. If any available quota window is exhausted, the bar keeps its shape but switches to the error background color.
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Before the first usable report for the active quota bucket arrives, two half-height markers move through the same fixed-width themed bar. The upper 5-hour marker travels opposite the lower weekly marker; both reverse smoothly at the ends, and each loader run randomly starts from one of the two mirrored endpoint phases. Their motion distinguishes loading from 100% remaining quota while preserving the normal bar background. A first report claiming both windows are completely unused is treated as provisional for 15 seconds and retried every second, because providers can briefly emit zeroed windows while initializing. Once a usable report exists, refresh requests preserve that last good bar.
|
|
62
|
+
|
|
63
|
+
When the weekly reset time is available, including in a single-window response, 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.
|
|
61
64
|
|
|
62
65
|
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
66
|
|
package/index.ts
CHANGED
|
@@ -17,6 +17,9 @@ const HOUR_TENTH_MS = 6 * MINUTE_MS;
|
|
|
17
17
|
const DAY_MS = 24 * HOUR_MS;
|
|
18
18
|
const DAY_TENTH_MS = 144 * MINUTE_MS;
|
|
19
19
|
const REFRESH_INTERVAL_MS = 30 * SECOND_MS;
|
|
20
|
+
const PROVISIONAL_RETRY_MS = SECOND_MS;
|
|
21
|
+
const FULL_AVAILABILITY_CONFIRMATION_MS = 15 * SECOND_MS;
|
|
22
|
+
const LOADING_FRAME_MS = 30;
|
|
20
23
|
const REDRAW_BLINK_MS = 150;
|
|
21
24
|
const STATUS_KEY = "aa-codex-usage";
|
|
22
25
|
const MAX_ERROR_BODY_CHARS = 600;
|
|
@@ -55,8 +58,7 @@ type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
|
55
58
|
export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
56
59
|
type CodexUsageTelegramStatusModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
57
60
|
type TelegramStatusLineProviderResult =
|
|
58
|
-
|
|
59
|
-
| undefined;
|
|
61
|
+
{ label: string; value: string } | undefined;
|
|
60
62
|
type TelegramStatusLineProvider = (ctx: {
|
|
61
63
|
activeModel?: CodexUsageTelegramStatusModel;
|
|
62
64
|
}) => TelegramStatusLineProviderResult;
|
|
@@ -164,13 +166,17 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
164
166
|
let cache: CachedReport | undefined;
|
|
165
167
|
let failedRefreshes = 0;
|
|
166
168
|
let inFlightUsageQuery:
|
|
167
|
-
|
|
168
|
-
| undefined;
|
|
169
|
+
{ limitId: string; promise: Promise<QueryUsageResult> } | undefined;
|
|
169
170
|
let statuslineBlinkTimer: TimeoutHandle | undefined;
|
|
170
171
|
let statuslineClearTimer: TimeoutHandle | undefined;
|
|
171
172
|
let statuslineCountdownTimer: TimeoutHandle | undefined;
|
|
173
|
+
let statuslineLoadingTimer: TimeoutHandle | undefined;
|
|
172
174
|
let statuslineRefreshTimer: TimeoutHandle | undefined;
|
|
175
|
+
let statuslineLoadingFrame = 0;
|
|
176
|
+
let statuslineLoadingLimitId: string | undefined;
|
|
173
177
|
let statuslineRequestId = 0;
|
|
178
|
+
let provisionalFullReport:
|
|
179
|
+
{ limitId: string; firstSeenAt: number } | undefined;
|
|
174
180
|
let unregisterTelegramStatusLine: (() => void) | undefined;
|
|
175
181
|
let telegramStatusLineRegistration: Promise<void> | undefined;
|
|
176
182
|
|
|
@@ -181,7 +187,9 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
181
187
|
if (!isOpenAICodexModel(activeModel)) return undefined;
|
|
182
188
|
if (!cache) return undefined;
|
|
183
189
|
const value = formatCodexUsageStatusValue(cache.report, activeModel);
|
|
184
|
-
return value
|
|
190
|
+
return value
|
|
191
|
+
? { label: activeUsageLabel(activeModel), value }
|
|
192
|
+
: undefined;
|
|
185
193
|
},
|
|
186
194
|
)
|
|
187
195
|
.then((unregister) => {
|
|
@@ -196,13 +204,51 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
196
204
|
if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
|
|
197
205
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
198
206
|
if (statuslineCountdownTimer) clearTimeout(statuslineCountdownTimer);
|
|
207
|
+
if (statuslineLoadingTimer) clearTimeout(statuslineLoadingTimer);
|
|
199
208
|
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
200
209
|
statuslineBlinkTimer = undefined;
|
|
201
210
|
statuslineClearTimer = undefined;
|
|
202
211
|
statuslineCountdownTimer = undefined;
|
|
212
|
+
statuslineLoadingTimer = undefined;
|
|
213
|
+
statuslineLoadingLimitId = undefined;
|
|
203
214
|
statuslineRefreshTimer = undefined;
|
|
204
215
|
};
|
|
205
216
|
|
|
217
|
+
const stopStatuslineLoading = () => {
|
|
218
|
+
if (statuslineLoadingTimer) clearTimeout(statuslineLoadingTimer);
|
|
219
|
+
statuslineLoadingTimer = undefined;
|
|
220
|
+
statuslineLoadingLimitId = undefined;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const startStatuslineLoading = (
|
|
224
|
+
ctx: ExtensionContext,
|
|
225
|
+
model: CodexUsageModel | undefined,
|
|
226
|
+
) => {
|
|
227
|
+
const limitId = activeUsageLimitId(model);
|
|
228
|
+
if (statuslineLoadingTimer && statuslineLoadingLimitId === limitId) return;
|
|
229
|
+
stopStatuslineLoading();
|
|
230
|
+
statuslineLoadingFrame = Math.random() < 0.5 ? 0 : DUAL_BAR_WIDTH * 2 - 1;
|
|
231
|
+
statuslineLoadingLimitId = limitId;
|
|
232
|
+
const drawNextFrame = () => {
|
|
233
|
+
try {
|
|
234
|
+
ctx.ui.setStatus(
|
|
235
|
+
STATUS_KEY,
|
|
236
|
+
formatStatuslineLoading(ctx, statuslineLoadingFrame, model),
|
|
237
|
+
);
|
|
238
|
+
statuslineLoadingFrame += 1;
|
|
239
|
+
statuslineLoadingTimer = setTimeout(
|
|
240
|
+
drawNextFrame,
|
|
241
|
+
LOADING_FRAME_MS,
|
|
242
|
+
) as TimeoutHandle;
|
|
243
|
+
statuslineLoadingTimer.unref?.();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
statuslineLoadingTimer = undefined;
|
|
246
|
+
handleTimerError(error);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
drawNextFrame();
|
|
250
|
+
};
|
|
251
|
+
|
|
206
252
|
const clearUsageStatusline = (ctx: ExtensionContext) => {
|
|
207
253
|
statuslineRequestId += 1;
|
|
208
254
|
clearStatuslineTimers();
|
|
@@ -222,11 +268,16 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
222
268
|
statuslineClearTimer.unref?.();
|
|
223
269
|
};
|
|
224
270
|
|
|
225
|
-
const scheduleStatuslineRefresh = (
|
|
271
|
+
const scheduleStatuslineRefresh = (
|
|
272
|
+
ctx: ExtensionContext,
|
|
273
|
+
delayMs = REFRESH_INTERVAL_MS,
|
|
274
|
+
) => {
|
|
226
275
|
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
227
276
|
statuslineRefreshTimer = setTimeout(() => {
|
|
228
|
-
void refreshCurrentCodexUsageStatusline(ctx, true).catch(
|
|
229
|
-
|
|
277
|
+
void refreshCurrentCodexUsageStatusline(ctx, true).catch(
|
|
278
|
+
handleAsyncTimerError,
|
|
279
|
+
);
|
|
280
|
+
}, delayMs) as TimeoutHandle;
|
|
230
281
|
statuslineRefreshTimer.unref?.();
|
|
231
282
|
};
|
|
232
283
|
|
|
@@ -269,12 +320,16 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
269
320
|
if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
|
|
270
321
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
271
322
|
if (statuslineCountdownTimer) clearTimeout(statuslineCountdownTimer);
|
|
323
|
+
stopStatuslineLoading();
|
|
272
324
|
statuslineBlinkTimer = undefined;
|
|
273
325
|
statuslineClearTimer = undefined;
|
|
274
326
|
statuslineCountdownTimer = undefined;
|
|
275
327
|
const text = formatCodexUsageStatusline(report, ctx, options.model);
|
|
276
328
|
if (options.blink) {
|
|
277
|
-
ctx.ui.setStatus(
|
|
329
|
+
ctx.ui.setStatus(
|
|
330
|
+
STATUS_KEY,
|
|
331
|
+
formatStatuslineLoading(ctx, statuslineLoadingFrame, options.model),
|
|
332
|
+
);
|
|
278
333
|
statuslineBlinkTimer = setTimeout(() => {
|
|
279
334
|
try {
|
|
280
335
|
ctx.ui.setStatus(STATUS_KEY, text);
|
|
@@ -298,13 +353,15 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
298
353
|
model: CodexUsageModel | undefined,
|
|
299
354
|
) => {
|
|
300
355
|
const limitId = activeUsageLimitId(model);
|
|
301
|
-
if (inFlightUsageQuery?.limitId === limitId)
|
|
356
|
+
if (inFlightUsageQuery?.limitId === limitId)
|
|
357
|
+
return inFlightUsageQuery.promise;
|
|
302
358
|
const promise = queryUsage(
|
|
303
359
|
ctx,
|
|
304
360
|
{ timeoutMs: DEFAULT_TIMEOUT_MS },
|
|
305
361
|
model,
|
|
306
362
|
).finally(() => {
|
|
307
|
-
if (inFlightUsageQuery?.promise === promise)
|
|
363
|
+
if (inFlightUsageQuery?.promise === promise)
|
|
364
|
+
inFlightUsageQuery = undefined;
|
|
308
365
|
});
|
|
309
366
|
inFlightUsageQuery = { limitId, promise };
|
|
310
367
|
return promise;
|
|
@@ -322,17 +379,27 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
322
379
|
return;
|
|
323
380
|
}
|
|
324
381
|
|
|
325
|
-
|
|
382
|
+
const usableCache =
|
|
383
|
+
cache && canReuseCachedReport(cache.report, activeModel)
|
|
384
|
+
? cache
|
|
385
|
+
: undefined;
|
|
386
|
+
if (usableCache) {
|
|
387
|
+
setUsageStatusline(ctx, usableCache.report, {
|
|
388
|
+
autoRefresh: true,
|
|
389
|
+
blink: false,
|
|
390
|
+
model: activeModel,
|
|
391
|
+
});
|
|
392
|
+
} else {
|
|
393
|
+
startStatuslineLoading(ctx, activeModel);
|
|
394
|
+
}
|
|
326
395
|
const requestId = statuslineRequestId + 1;
|
|
327
396
|
statuslineRequestId = requestId;
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
canReuseCachedReport(cache.report, activeModel)
|
|
332
|
-
? cache
|
|
397
|
+
const freshCache =
|
|
398
|
+
usableCache && Date.now() - usableCache.createdAt < REFRESH_INTERVAL_MS
|
|
399
|
+
? usableCache
|
|
333
400
|
: undefined;
|
|
334
|
-
if (
|
|
335
|
-
setUsageStatusline(ctx,
|
|
401
|
+
if (freshCache && !force) {
|
|
402
|
+
setUsageStatusline(ctx, freshCache.report, {
|
|
336
403
|
autoRefresh: true,
|
|
337
404
|
blink: false,
|
|
338
405
|
model: activeModel,
|
|
@@ -349,7 +416,12 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
349
416
|
|
|
350
417
|
if (!result.ok) {
|
|
351
418
|
failedRefreshes += 1;
|
|
352
|
-
|
|
419
|
+
const activeCache =
|
|
420
|
+
cache && canReuseCachedReport(cache.report, activeModel)
|
|
421
|
+
? cache
|
|
422
|
+
: undefined;
|
|
423
|
+
if (!activeCache || failedRefreshes >= 5) {
|
|
424
|
+
stopStatuslineLoading();
|
|
353
425
|
ctx.ui.setStatus(
|
|
354
426
|
STATUS_KEY,
|
|
355
427
|
formatStatuslineProblem(ctx, result.errors, activeModel),
|
|
@@ -360,11 +432,34 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
360
432
|
}
|
|
361
433
|
|
|
362
434
|
const previousReport = cache?.report;
|
|
435
|
+
const previousWasFullyAvailable = previousReport
|
|
436
|
+
? isFullyAvailableReport(previousReport, activeModel)
|
|
437
|
+
: false;
|
|
438
|
+
if (
|
|
439
|
+
isFullyAvailableReport(result.report, activeModel) &&
|
|
440
|
+
!previousWasFullyAvailable
|
|
441
|
+
) {
|
|
442
|
+
const now = Date.now();
|
|
443
|
+
const limitId = activeUsageLimitId(activeModel);
|
|
444
|
+
if (provisionalFullReport?.limitId !== limitId) {
|
|
445
|
+
provisionalFullReport = { limitId, firstSeenAt: now };
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
now - provisionalFullReport.firstSeenAt <
|
|
449
|
+
FULL_AVAILABILITY_CONFIRMATION_MS
|
|
450
|
+
) {
|
|
451
|
+
scheduleStatuslineRefresh(ctx, PROVISIONAL_RETRY_MS);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
provisionalFullReport = undefined;
|
|
456
|
+
}
|
|
363
457
|
const blink = previousReport
|
|
364
458
|
? formatReportBar(previousReport, activeModel) !==
|
|
365
459
|
formatReportBar(result.report, activeModel)
|
|
366
460
|
: false;
|
|
367
461
|
failedRefreshes = 0;
|
|
462
|
+
provisionalFullReport = undefined;
|
|
368
463
|
cache = { createdAt: Date.now(), report: result.report };
|
|
369
464
|
setUsageStatusline(ctx, result.report, {
|
|
370
465
|
autoRefresh: true,
|
|
@@ -385,19 +480,25 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
385
480
|
pi.on("session_start", (_event, ctx) => {
|
|
386
481
|
ensureTelegramStatusLineRegistered();
|
|
387
482
|
if (isOpenAICodexModel(ctx.model))
|
|
388
|
-
void refreshCurrentCodexUsageStatusline(ctx, false).catch(
|
|
483
|
+
void refreshCurrentCodexUsageStatusline(ctx, false).catch(
|
|
484
|
+
handleAsyncTimerError,
|
|
485
|
+
);
|
|
389
486
|
else clearUsageStatusline(ctx);
|
|
390
487
|
});
|
|
391
488
|
|
|
392
489
|
pi.on("session_tree", (_event, ctx) => {
|
|
393
490
|
if (isOpenAICodexModel(ctx.model))
|
|
394
|
-
void refreshCurrentCodexUsageStatusline(ctx, false).catch(
|
|
491
|
+
void refreshCurrentCodexUsageStatusline(ctx, false).catch(
|
|
492
|
+
handleAsyncTimerError,
|
|
493
|
+
);
|
|
395
494
|
else clearUsageStatusline(ctx);
|
|
396
495
|
});
|
|
397
496
|
|
|
398
497
|
pi.on("model_select", (event, ctx) => {
|
|
399
498
|
if (isOpenAICodexModel(event.model)) {
|
|
400
|
-
void refreshCurrentCodexUsageStatusline(ctx, false, event.model).catch(
|
|
499
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, event.model).catch(
|
|
500
|
+
handleAsyncTimerError,
|
|
501
|
+
);
|
|
401
502
|
} else {
|
|
402
503
|
clearUsageStatusline(ctx);
|
|
403
504
|
}
|
|
@@ -446,7 +547,9 @@ function activeUsageLimitId(
|
|
|
446
547
|
function activeUsageLabel(
|
|
447
548
|
model: Pick<PiModel, "id" | "name" | "provider"> | undefined,
|
|
448
549
|
): string {
|
|
449
|
-
return isSparkCodexModel(model)
|
|
550
|
+
return isSparkCodexModel(model)
|
|
551
|
+
? SPARK_STATUS_LABEL_TEXT
|
|
552
|
+
: DEFAULT_STATUS_LABEL_TEXT;
|
|
450
553
|
}
|
|
451
554
|
|
|
452
555
|
async function importTelegramStatusLineModule(): Promise<
|
|
@@ -826,7 +929,7 @@ function backendAdditionalLimitId(limit: BackendAdditionalRateLimit): string {
|
|
|
826
929
|
const raw = `${asString(limit.limit_name) ?? ""} ${asString(limit.metered_feature) ?? ""}`;
|
|
827
930
|
return raw.toLowerCase().includes("spark")
|
|
828
931
|
? SPARK_USAGE_LIMIT_ID
|
|
829
|
-
: normalizedUsageKey(raw) ?? CODEX_USAGE_LIMIT_ID;
|
|
932
|
+
: (normalizedUsageKey(raw) ?? CODEX_USAGE_LIMIT_ID);
|
|
830
933
|
}
|
|
831
934
|
|
|
832
935
|
function normalizeBackendSnapshot(
|
|
@@ -995,7 +1098,7 @@ export function formatCodexUsageStatusValue(
|
|
|
995
1098
|
const capturedNow = typeof modelOrNow === "number" ? modelOrNow : now;
|
|
996
1099
|
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
997
1100
|
if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
|
|
998
|
-
const bar =
|
|
1101
|
+
const bar = formatAdaptiveLimitBar(snapshot.primary, snapshot.secondary);
|
|
999
1102
|
if (isQuotaWindowExhausted(snapshot.primary) && snapshot.primary?.resetAt) {
|
|
1000
1103
|
const primaryCountdown = formatResetCountdown(
|
|
1001
1104
|
snapshot.primary.resetAt,
|
|
@@ -1019,7 +1122,8 @@ export function formatWeeklyResetCountdown(
|
|
|
1019
1122
|
): string | undefined {
|
|
1020
1123
|
const model = typeof modelOrNow === "number" ? undefined : modelOrNow;
|
|
1021
1124
|
const capturedNow = typeof modelOrNow === "number" ? modelOrNow : now;
|
|
1022
|
-
const
|
|
1125
|
+
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
1126
|
+
const resetAt = weeklyWindow(snapshot)?.resetAt;
|
|
1023
1127
|
if (resetAt === undefined) return undefined;
|
|
1024
1128
|
return formatResetCountdown(resetAt, capturedNow);
|
|
1025
1129
|
}
|
|
@@ -1048,9 +1152,9 @@ export function nextResetCountdownDelayMs(
|
|
|
1048
1152
|
model?: CodexUsageModel,
|
|
1049
1153
|
): number | undefined {
|
|
1050
1154
|
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
1051
|
-
const resetTimes = [snapshot?.
|
|
1052
|
-
if (isQuotaWindowExhausted(snapshot
|
|
1053
|
-
resetTimes.push(snapshot
|
|
1155
|
+
const resetTimes = [weeklyWindow(snapshot)?.resetAt];
|
|
1156
|
+
if (snapshot?.secondary && isQuotaWindowExhausted(snapshot.primary)) {
|
|
1157
|
+
resetTimes.push(snapshot.primary?.resetAt);
|
|
1054
1158
|
}
|
|
1055
1159
|
const delays = resetTimes
|
|
1056
1160
|
.filter((resetAt): resetAt is number => resetAt !== undefined)
|
|
@@ -1094,7 +1198,7 @@ function formatReportBar(
|
|
|
1094
1198
|
): string | undefined {
|
|
1095
1199
|
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
1096
1200
|
if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
|
|
1097
|
-
return
|
|
1201
|
+
return formatAdaptiveLimitBar(snapshot.primary, snapshot.secondary);
|
|
1098
1202
|
}
|
|
1099
1203
|
|
|
1100
1204
|
function formatStatuslineText(
|
|
@@ -1117,8 +1221,33 @@ function formatStatuslineBarText(
|
|
|
1117
1221
|
return `${label} ${value}`;
|
|
1118
1222
|
}
|
|
1119
1223
|
|
|
1120
|
-
function
|
|
1121
|
-
|
|
1224
|
+
export function formatCodexUsageLoadingBar(frame: number): string {
|
|
1225
|
+
const totalParts = DUAL_BAR_WIDTH * 2;
|
|
1226
|
+
const cycleFrames = (totalParts - 1) * 2;
|
|
1227
|
+
const finiteFrame = Number.isFinite(frame) ? frame : 0;
|
|
1228
|
+
const cycleFrame = Math.abs(Math.trunc(finiteFrame)) % cycleFrames;
|
|
1229
|
+
const primaryPart =
|
|
1230
|
+
cycleFrame < totalParts ? cycleFrame : cycleFrames - cycleFrame;
|
|
1231
|
+
const secondaryPart = totalParts - primaryPart - 1;
|
|
1232
|
+
const masks = Array<number>(DUAL_BAR_WIDTH).fill(0);
|
|
1233
|
+
const primaryCell = Math.floor(primaryPart / 2);
|
|
1234
|
+
const secondaryCell = Math.floor(secondaryPart / 2);
|
|
1235
|
+
masks[primaryCell] |= primaryPart % 2 === 0 ? 1 : 2;
|
|
1236
|
+
masks[secondaryCell] |= secondaryPart % 2 === 0 ? 4 : 8;
|
|
1237
|
+
return masks.map((mask) => DUAL_BAR_CHARS[mask]).join("");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function formatStatuslineLoading(
|
|
1241
|
+
ctx: ExtensionContext,
|
|
1242
|
+
frame: number,
|
|
1243
|
+
model?: CodexUsageModel,
|
|
1244
|
+
): string {
|
|
1245
|
+
return formatStatuslineBarText(
|
|
1246
|
+
ctx,
|
|
1247
|
+
formatCodexUsageLoadingBar(frame),
|
|
1248
|
+
"selectedBg",
|
|
1249
|
+
model,
|
|
1250
|
+
);
|
|
1122
1251
|
}
|
|
1123
1252
|
|
|
1124
1253
|
function formatStatuslineProblem(
|
|
@@ -1163,9 +1292,32 @@ function normalizedUsageKey(value: string | undefined): string | undefined {
|
|
|
1163
1292
|
return key || undefined;
|
|
1164
1293
|
}
|
|
1165
1294
|
|
|
1166
|
-
function
|
|
1295
|
+
function formatAdaptiveLimitBar(
|
|
1167
1296
|
primary: NormalizedRateLimitWindow | undefined,
|
|
1168
1297
|
secondary: NormalizedRateLimitWindow | undefined,
|
|
1298
|
+
): string {
|
|
1299
|
+
if (!primary || !secondary) return formatSingleLimitBar(primary ?? secondary);
|
|
1300
|
+
return formatDualLimitBar(primary, secondary);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function formatSingleLimitBar(
|
|
1304
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
1305
|
+
): string {
|
|
1306
|
+
const filled = filledParts(window, DUAL_BAR_WIDTH * 4);
|
|
1307
|
+
const partMasks = [1, 4, 2, 8];
|
|
1308
|
+
let value = "";
|
|
1309
|
+
for (let index = 0; index < DUAL_BAR_WIDTH; index++) {
|
|
1310
|
+
const cellParts = Math.min(4, Math.max(0, filled - index * 4));
|
|
1311
|
+
let mask = 0;
|
|
1312
|
+
for (let part = 0; part < cellParts; part++) mask |= partMasks[part] ?? 0;
|
|
1313
|
+
value += DUAL_BAR_CHARS[mask];
|
|
1314
|
+
}
|
|
1315
|
+
return value;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function formatDualLimitBar(
|
|
1319
|
+
primary: NormalizedRateLimitWindow,
|
|
1320
|
+
secondary: NormalizedRateLimitWindow,
|
|
1169
1321
|
): string {
|
|
1170
1322
|
const primaryParts = filledTwentieths(primary);
|
|
1171
1323
|
const secondaryParts = filledTwentieths(secondary);
|
|
@@ -1227,6 +1379,26 @@ export function canReuseCachedReport(
|
|
|
1227
1379
|
return selectActiveUsageSnapshot(report, model) !== undefined;
|
|
1228
1380
|
}
|
|
1229
1381
|
|
|
1382
|
+
export function isFullyAvailableReport(
|
|
1383
|
+
report: CodexUsageReport,
|
|
1384
|
+
model?: CodexUsageModel,
|
|
1385
|
+
): boolean {
|
|
1386
|
+
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
1387
|
+
const windows = [snapshot?.primary, snapshot?.secondary].filter(
|
|
1388
|
+
(window): window is NormalizedRateLimitWindow => window !== undefined,
|
|
1389
|
+
);
|
|
1390
|
+
return (
|
|
1391
|
+
windows.length > 0 &&
|
|
1392
|
+
windows.every((window) => clampPercent(window.usedPercent) === 0)
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function weeklyWindow(
|
|
1397
|
+
snapshot: NormalizedRateLimitSnapshot | undefined,
|
|
1398
|
+
): NormalizedRateLimitWindow | undefined {
|
|
1399
|
+
return snapshot?.secondary ?? snapshot?.primary;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1230
1402
|
function selectActiveUsageSnapshot(
|
|
1231
1403
|
report: CodexUsageReport,
|
|
1232
1404
|
model: CodexUsageModel | undefined,
|