@llblab/pi-codex-usage 0.8.1 → 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 +1 -0
- package/CHANGELOG.md +5 -0
- package/README.md +4 -1
- package/index.ts +168 -27
- package/package.json +1 -1
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,11 @@
|
|
|
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
|
+
|
|
5
10
|
## 0.8.1: Stale Context Hotfix
|
|
6
11
|
|
|
7
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.
|
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;
|
|
@@ -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(
|
|
@@ -1117,8 +1220,33 @@ function formatStatuslineBarText(
|
|
|
1117
1220
|
return `${label} ${value}`;
|
|
1118
1221
|
}
|
|
1119
1222
|
|
|
1120
|
-
function
|
|
1121
|
-
|
|
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
|
+
);
|
|
1122
1250
|
}
|
|
1123
1251
|
|
|
1124
1252
|
function formatStatuslineProblem(
|
|
@@ -1227,6 +1355,19 @@ export function canReuseCachedReport(
|
|
|
1227
1355
|
return selectActiveUsageSnapshot(report, model) !== undefined;
|
|
1228
1356
|
}
|
|
1229
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
|
+
|
|
1230
1371
|
function selectActiveUsageSnapshot(
|
|
1231
1372
|
report: CodexUsageReport,
|
|
1232
1373
|
model: CodexUsageModel | undefined,
|