@llblab/pi-codex-usage 0.8.0 → 0.8.2
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 +6 -1
- package/CHANGELOG.md +12 -0
- package/README.md +4 -1
- package/index.ts +272 -83
- package/package.json +1 -1
package/BACKLOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
# Backlog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Pending Release Verification
|
|
4
|
+
|
|
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
|
+
- [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
|
+
- [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,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.2: Quota Loading Hotfix
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
## 0.8.1: Stale Context Hotfix
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- Fixed Codex/Spark refresh state to key in-flight usage lookups by active quota bucket and only reuse cached reports that contain the active bucket. Impact: switching between regular Codex and Spark no longer temporarily renders the active bucket as unavailable because of a stale request/cache.
|
|
14
|
+
- Fixed Spark failure labels. Impact: Spark unavailable/error states now render with the `spark` label instead of falling back to `codex`.
|
|
15
|
+
- Added regression coverage for active-bucket cache reuse and stale extension context error detection.
|
|
16
|
+
|
|
5
17
|
## 0.8.0: Spark Model Usage Status
|
|
6
18
|
|
|
7
19
|
- 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.
|
package/README.md
CHANGED
|
@@ -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
|
|
@@ -57,6 +58,8 @@ spark ██████████ 7d
|
|
|
57
58
|
|
|
58
59
|
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.
|
|
59
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
|
+
|
|
60
63
|
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.
|
|
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:
|
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;
|
|
@@ -163,12 +165,18 @@ type PendingRpc = {
|
|
|
163
165
|
export default function codexUsage(pi: ExtensionAPI) {
|
|
164
166
|
let cache: CachedReport | undefined;
|
|
165
167
|
let failedRefreshes = 0;
|
|
166
|
-
let inFlightUsageQuery:
|
|
168
|
+
let inFlightUsageQuery:
|
|
169
|
+
{ limitId: string; promise: Promise<QueryUsageResult> } | undefined;
|
|
167
170
|
let statuslineBlinkTimer: TimeoutHandle | undefined;
|
|
168
171
|
let statuslineClearTimer: TimeoutHandle | undefined;
|
|
169
172
|
let statuslineCountdownTimer: TimeoutHandle | undefined;
|
|
173
|
+
let statuslineLoadingTimer: TimeoutHandle | undefined;
|
|
170
174
|
let statuslineRefreshTimer: TimeoutHandle | undefined;
|
|
175
|
+
let statuslineLoadingFrame = 0;
|
|
176
|
+
let statuslineLoadingLimitId: string | undefined;
|
|
171
177
|
let statuslineRequestId = 0;
|
|
178
|
+
let provisionalFullReport:
|
|
179
|
+
{ limitId: string; firstSeenAt: number } | undefined;
|
|
172
180
|
let unregisterTelegramStatusLine: (() => void) | undefined;
|
|
173
181
|
let telegramStatusLineRegistration: Promise<void> | undefined;
|
|
174
182
|
|
|
@@ -179,7 +187,9 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
179
187
|
if (!isOpenAICodexModel(activeModel)) return undefined;
|
|
180
188
|
if (!cache) return undefined;
|
|
181
189
|
const value = formatCodexUsageStatusValue(cache.report, activeModel);
|
|
182
|
-
return value
|
|
190
|
+
return value
|
|
191
|
+
? { label: activeUsageLabel(activeModel), value }
|
|
192
|
+
: undefined;
|
|
183
193
|
},
|
|
184
194
|
)
|
|
185
195
|
.then((unregister) => {
|
|
@@ -194,13 +204,51 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
194
204
|
if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
|
|
195
205
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
196
206
|
if (statuslineCountdownTimer) clearTimeout(statuslineCountdownTimer);
|
|
207
|
+
if (statuslineLoadingTimer) clearTimeout(statuslineLoadingTimer);
|
|
197
208
|
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
198
209
|
statuslineBlinkTimer = undefined;
|
|
199
210
|
statuslineClearTimer = undefined;
|
|
200
211
|
statuslineCountdownTimer = undefined;
|
|
212
|
+
statuslineLoadingTimer = undefined;
|
|
213
|
+
statuslineLoadingLimitId = undefined;
|
|
201
214
|
statuslineRefreshTimer = undefined;
|
|
202
215
|
};
|
|
203
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
|
+
|
|
204
252
|
const clearUsageStatusline = (ctx: ExtensionContext) => {
|
|
205
253
|
statuslineRequestId += 1;
|
|
206
254
|
clearStatuslineTimers();
|
|
@@ -210,17 +258,26 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
210
258
|
const scheduleTemporaryStatuslineClear = (ctx: ExtensionContext) => {
|
|
211
259
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
212
260
|
statuslineClearTimer = setTimeout(() => {
|
|
213
|
-
|
|
214
|
-
|
|
261
|
+
try {
|
|
262
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
263
|
+
statuslineClearTimer = undefined;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
handleTimerError(error);
|
|
266
|
+
}
|
|
215
267
|
}, REFRESH_INTERVAL_MS) as TimeoutHandle;
|
|
216
268
|
statuslineClearTimer.unref?.();
|
|
217
269
|
};
|
|
218
270
|
|
|
219
|
-
const scheduleStatuslineRefresh = (
|
|
271
|
+
const scheduleStatuslineRefresh = (
|
|
272
|
+
ctx: ExtensionContext,
|
|
273
|
+
delayMs = REFRESH_INTERVAL_MS,
|
|
274
|
+
) => {
|
|
220
275
|
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
221
276
|
statuslineRefreshTimer = setTimeout(() => {
|
|
222
|
-
void refreshCurrentCodexUsageStatusline(ctx, true)
|
|
223
|
-
|
|
277
|
+
void refreshCurrentCodexUsageStatusline(ctx, true).catch(
|
|
278
|
+
handleAsyncTimerError,
|
|
279
|
+
);
|
|
280
|
+
}, delayMs) as TimeoutHandle;
|
|
224
281
|
statuslineRefreshTimer.unref?.();
|
|
225
282
|
};
|
|
226
283
|
|
|
@@ -236,12 +293,16 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
236
293
|
return;
|
|
237
294
|
}
|
|
238
295
|
statuslineCountdownTimer = setTimeout(() => {
|
|
239
|
-
|
|
240
|
-
ctx.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
296
|
+
try {
|
|
297
|
+
if (isOpenAICodexModel(ctx.model)) {
|
|
298
|
+
ctx.ui.setStatus(
|
|
299
|
+
STATUS_KEY,
|
|
300
|
+
formatCodexUsageStatusline(report, ctx, model),
|
|
301
|
+
);
|
|
302
|
+
scheduleStatuslineCountdown(ctx, report, model);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
handleTimerError(error);
|
|
245
306
|
}
|
|
246
307
|
}, delayMs) as TimeoutHandle;
|
|
247
308
|
statuslineCountdownTimer.unref?.();
|
|
@@ -259,16 +320,24 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
259
320
|
if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
|
|
260
321
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
261
322
|
if (statuslineCountdownTimer) clearTimeout(statuslineCountdownTimer);
|
|
323
|
+
stopStatuslineLoading();
|
|
262
324
|
statuslineBlinkTimer = undefined;
|
|
263
325
|
statuslineClearTimer = undefined;
|
|
264
326
|
statuslineCountdownTimer = undefined;
|
|
265
327
|
const text = formatCodexUsageStatusline(report, ctx, options.model);
|
|
266
328
|
if (options.blink) {
|
|
267
|
-
ctx.ui.setStatus(
|
|
329
|
+
ctx.ui.setStatus(
|
|
330
|
+
STATUS_KEY,
|
|
331
|
+
formatStatuslineLoading(ctx, statuslineLoadingFrame, options.model),
|
|
332
|
+
);
|
|
268
333
|
statuslineBlinkTimer = setTimeout(() => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
334
|
+
try {
|
|
335
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
336
|
+
scheduleStatuslineCountdown(ctx, report, options.model);
|
|
337
|
+
statuslineBlinkTimer = undefined;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
handleTimerError(error);
|
|
340
|
+
}
|
|
272
341
|
}, REDRAW_BLINK_MS) as TimeoutHandle;
|
|
273
342
|
statuslineBlinkTimer.unref?.();
|
|
274
343
|
} else {
|
|
@@ -283,74 +352,127 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
283
352
|
ctx: ExtensionContext,
|
|
284
353
|
model: CodexUsageModel | undefined,
|
|
285
354
|
) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
355
|
+
const limitId = activeUsageLimitId(model);
|
|
356
|
+
if (inFlightUsageQuery?.limitId === limitId)
|
|
357
|
+
return inFlightUsageQuery.promise;
|
|
358
|
+
const promise = queryUsage(
|
|
359
|
+
ctx,
|
|
360
|
+
{ timeoutMs: DEFAULT_TIMEOUT_MS },
|
|
361
|
+
model,
|
|
362
|
+
).finally(() => {
|
|
363
|
+
if (inFlightUsageQuery?.promise === promise)
|
|
292
364
|
inFlightUsageQuery = undefined;
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
return
|
|
365
|
+
});
|
|
366
|
+
inFlightUsageQuery = { limitId, promise };
|
|
367
|
+
return promise;
|
|
296
368
|
};
|
|
297
369
|
|
|
298
370
|
const refreshCurrentCodexUsageStatusline = async (
|
|
299
371
|
ctx: ExtensionContext,
|
|
300
372
|
force: boolean,
|
|
301
|
-
model
|
|
373
|
+
model?: CodexUsageModel,
|
|
302
374
|
) => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
375
|
+
try {
|
|
376
|
+
const activeModel = model ?? ctx.model;
|
|
377
|
+
if (!isOpenAICodexModel(activeModel)) {
|
|
378
|
+
clearUsageStatusline(ctx);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
307
381
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
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
|
+
}
|
|
395
|
+
const requestId = statuslineRequestId + 1;
|
|
396
|
+
statuslineRequestId = requestId;
|
|
397
|
+
const freshCache =
|
|
398
|
+
usableCache && Date.now() - usableCache.createdAt < REFRESH_INTERVAL_MS
|
|
399
|
+
? usableCache
|
|
400
|
+
: undefined;
|
|
401
|
+
if (freshCache && !force) {
|
|
402
|
+
setUsageStatusline(ctx, freshCache.report, {
|
|
403
|
+
autoRefresh: true,
|
|
404
|
+
blink: false,
|
|
405
|
+
model: activeModel,
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
323
409
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
410
|
+
const result = await queryCurrentUsage(ctx, activeModel);
|
|
411
|
+
if (requestId !== statuslineRequestId) return;
|
|
412
|
+
if (!isOpenAICodexModel(ctx.model)) {
|
|
413
|
+
clearUsageStatusline(ctx);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
330
416
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
)
|
|
417
|
+
if (!result.ok) {
|
|
418
|
+
failedRefreshes += 1;
|
|
419
|
+
const activeCache =
|
|
420
|
+
cache && canReuseCachedReport(cache.report, activeModel)
|
|
421
|
+
? cache
|
|
422
|
+
: undefined;
|
|
423
|
+
if (!activeCache || failedRefreshes >= 5) {
|
|
424
|
+
stopStatuslineLoading();
|
|
425
|
+
ctx.ui.setStatus(
|
|
426
|
+
STATUS_KEY,
|
|
427
|
+
formatStatuslineProblem(ctx, result.errors, activeModel),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
scheduleStatuslineRefresh(ctx);
|
|
431
|
+
return;
|
|
338
432
|
}
|
|
339
|
-
scheduleStatuslineRefresh(ctx);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
433
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
}
|
|
457
|
+
const blink = previousReport
|
|
458
|
+
? formatReportBar(previousReport, activeModel) !==
|
|
459
|
+
formatReportBar(result.report, activeModel)
|
|
460
|
+
: false;
|
|
461
|
+
failedRefreshes = 0;
|
|
462
|
+
provisionalFullReport = undefined;
|
|
463
|
+
cache = { createdAt: Date.now(), report: result.report };
|
|
464
|
+
setUsageStatusline(ctx, result.report, {
|
|
465
|
+
autoRefresh: true,
|
|
466
|
+
blink,
|
|
467
|
+
model: activeModel,
|
|
468
|
+
});
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (isStaleExtensionContextError(error)) {
|
|
471
|
+
clearStatuslineTimers();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
354
476
|
};
|
|
355
477
|
|
|
356
478
|
ensureTelegramStatusLineRegistered();
|
|
@@ -358,19 +480,25 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
358
480
|
pi.on("session_start", (_event, ctx) => {
|
|
359
481
|
ensureTelegramStatusLineRegistered();
|
|
360
482
|
if (isOpenAICodexModel(ctx.model))
|
|
361
|
-
void refreshCurrentCodexUsageStatusline(ctx, false)
|
|
483
|
+
void refreshCurrentCodexUsageStatusline(ctx, false).catch(
|
|
484
|
+
handleAsyncTimerError,
|
|
485
|
+
);
|
|
362
486
|
else clearUsageStatusline(ctx);
|
|
363
487
|
});
|
|
364
488
|
|
|
365
489
|
pi.on("session_tree", (_event, ctx) => {
|
|
366
490
|
if (isOpenAICodexModel(ctx.model))
|
|
367
|
-
void refreshCurrentCodexUsageStatusline(ctx, false)
|
|
491
|
+
void refreshCurrentCodexUsageStatusline(ctx, false).catch(
|
|
492
|
+
handleAsyncTimerError,
|
|
493
|
+
);
|
|
368
494
|
else clearUsageStatusline(ctx);
|
|
369
495
|
});
|
|
370
496
|
|
|
371
497
|
pi.on("model_select", (event, ctx) => {
|
|
372
498
|
if (isOpenAICodexModel(event.model)) {
|
|
373
|
-
void refreshCurrentCodexUsageStatusline(ctx, false, event.model)
|
|
499
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, event.model).catch(
|
|
500
|
+
handleAsyncTimerError,
|
|
501
|
+
);
|
|
374
502
|
} else {
|
|
375
503
|
clearUsageStatusline(ctx);
|
|
376
504
|
}
|
|
@@ -383,6 +511,19 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
383
511
|
});
|
|
384
512
|
}
|
|
385
513
|
|
|
514
|
+
function handleAsyncTimerError(error: unknown): void {
|
|
515
|
+
handleTimerError(error);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function handleTimerError(error: unknown): void {
|
|
519
|
+
if (isStaleExtensionContextError(error)) return;
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
524
|
+
return error instanceof Error && error.message.includes("ctx is stale");
|
|
525
|
+
}
|
|
526
|
+
|
|
386
527
|
function isOpenAICodexModel(
|
|
387
528
|
model: Pick<PiModel, "provider"> | undefined,
|
|
388
529
|
): boolean {
|
|
@@ -406,7 +547,9 @@ function activeUsageLimitId(
|
|
|
406
547
|
function activeUsageLabel(
|
|
407
548
|
model: Pick<PiModel, "id" | "name" | "provider"> | undefined,
|
|
408
549
|
): string {
|
|
409
|
-
return isSparkCodexModel(model)
|
|
550
|
+
return isSparkCodexModel(model)
|
|
551
|
+
? SPARK_STATUS_LABEL_TEXT
|
|
552
|
+
: DEFAULT_STATUS_LABEL_TEXT;
|
|
410
553
|
}
|
|
411
554
|
|
|
412
555
|
async function importTelegramStatusLineModule(): Promise<
|
|
@@ -786,7 +929,7 @@ function backendAdditionalLimitId(limit: BackendAdditionalRateLimit): string {
|
|
|
786
929
|
const raw = `${asString(limit.limit_name) ?? ""} ${asString(limit.metered_feature) ?? ""}`;
|
|
787
930
|
return raw.toLowerCase().includes("spark")
|
|
788
931
|
? SPARK_USAGE_LIMIT_ID
|
|
789
|
-
: normalizedUsageKey(raw) ?? CODEX_USAGE_LIMIT_ID;
|
|
932
|
+
: (normalizedUsageKey(raw) ?? CODEX_USAGE_LIMIT_ID);
|
|
790
933
|
}
|
|
791
934
|
|
|
792
935
|
function normalizeBackendSnapshot(
|
|
@@ -1077,15 +1220,41 @@ function formatStatuslineBarText(
|
|
|
1077
1220
|
return `${label} ${value}`;
|
|
1078
1221
|
}
|
|
1079
1222
|
|
|
1080
|
-
function
|
|
1081
|
-
|
|
1223
|
+
export function formatCodexUsageLoadingBar(frame: number): string {
|
|
1224
|
+
const totalParts = DUAL_BAR_WIDTH * 2;
|
|
1225
|
+
const cycleFrames = (totalParts - 1) * 2;
|
|
1226
|
+
const finiteFrame = Number.isFinite(frame) ? frame : 0;
|
|
1227
|
+
const cycleFrame = Math.abs(Math.trunc(finiteFrame)) % cycleFrames;
|
|
1228
|
+
const primaryPart =
|
|
1229
|
+
cycleFrame < totalParts ? cycleFrame : cycleFrames - cycleFrame;
|
|
1230
|
+
const secondaryPart = totalParts - primaryPart - 1;
|
|
1231
|
+
const masks = Array<number>(DUAL_BAR_WIDTH).fill(0);
|
|
1232
|
+
const primaryCell = Math.floor(primaryPart / 2);
|
|
1233
|
+
const secondaryCell = Math.floor(secondaryPart / 2);
|
|
1234
|
+
masks[primaryCell] |= primaryPart % 2 === 0 ? 1 : 2;
|
|
1235
|
+
masks[secondaryCell] |= secondaryPart % 2 === 0 ? 4 : 8;
|
|
1236
|
+
return masks.map((mask) => DUAL_BAR_CHARS[mask]).join("");
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function formatStatuslineLoading(
|
|
1240
|
+
ctx: ExtensionContext,
|
|
1241
|
+
frame: number,
|
|
1242
|
+
model?: CodexUsageModel,
|
|
1243
|
+
): string {
|
|
1244
|
+
return formatStatuslineBarText(
|
|
1245
|
+
ctx,
|
|
1246
|
+
formatCodexUsageLoadingBar(frame),
|
|
1247
|
+
"selectedBg",
|
|
1248
|
+
model,
|
|
1249
|
+
);
|
|
1082
1250
|
}
|
|
1083
1251
|
|
|
1084
1252
|
function formatStatuslineProblem(
|
|
1085
1253
|
ctx: ExtensionContext,
|
|
1086
1254
|
errors: UsageQueryError[],
|
|
1255
|
+
model?: CodexUsageModel,
|
|
1087
1256
|
): string {
|
|
1088
|
-
const label = ctx.ui.theme.fg("accent",
|
|
1257
|
+
const label = ctx.ui.theme.fg("accent", activeUsageLabel(model));
|
|
1089
1258
|
const value = isUsageUnavailable(errors)
|
|
1090
1259
|
? ctx.ui.theme.fg("muted", "n/a")
|
|
1091
1260
|
: ctx.ui.theme.fg("error", "error");
|
|
@@ -1179,6 +1348,26 @@ function hasExhaustedQuotaWindow(
|
|
|
1179
1348
|
);
|
|
1180
1349
|
}
|
|
1181
1350
|
|
|
1351
|
+
export function canReuseCachedReport(
|
|
1352
|
+
report: CodexUsageReport,
|
|
1353
|
+
model: CodexUsageModel | undefined,
|
|
1354
|
+
): boolean {
|
|
1355
|
+
return selectActiveUsageSnapshot(report, model) !== undefined;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
export function isFullyAvailableReport(
|
|
1359
|
+
report: CodexUsageReport,
|
|
1360
|
+
model?: CodexUsageModel,
|
|
1361
|
+
): boolean {
|
|
1362
|
+
const snapshot = selectActiveUsageSnapshot(report, model);
|
|
1363
|
+
return (
|
|
1364
|
+
snapshot?.primary !== undefined &&
|
|
1365
|
+
snapshot.secondary !== undefined &&
|
|
1366
|
+
clampPercent(snapshot.primary.usedPercent) === 0 &&
|
|
1367
|
+
clampPercent(snapshot.secondary.usedPercent) === 0
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1182
1371
|
function selectActiveUsageSnapshot(
|
|
1183
1372
|
report: CodexUsageReport,
|
|
1184
1373
|
model: CodexUsageModel | undefined,
|