@llblab/pi-codex-usage 0.11.0 → 0.12.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.
@@ -0,0 +1,347 @@
1
+ /** Domain: quota presentation. Owns: terminal formatting and countdowns. Excludes: polling and request transport. */
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { clampPercent, selectActiveUsageSnapshot, type CodexUsageModel, type CodexUsageReport, type NormalizedCreditUsage, type NormalizedRateLimitSnapshot, type NormalizedRateLimitWindow } from "./usage.ts";
4
+ const SECOND_MS = 1000;
5
+ const MINUTE_MS = 60 * SECOND_MS;
6
+ const HOUR_MS = 60 * MINUTE_MS;
7
+ const HOUR_TENTH_MS = 6 * MINUTE_MS;
8
+ const DAY_MS = 24 * HOUR_MS;
9
+ const DAY_TENTH_MS = 144 * MINUTE_MS;
10
+ const DEFAULT_STATUS_LABEL_TEXT = "codex";
11
+ const DUAL_BAR_WIDTH = 10;
12
+ const DUAL_BAR_CHARS = [
13
+ "⠀",
14
+ "▘",
15
+ "▝",
16
+ "▀",
17
+ "▖",
18
+ "▌",
19
+ "▞",
20
+ "▛",
21
+ "▗",
22
+ "▚",
23
+ "▐",
24
+ "▜",
25
+ "▄",
26
+ "▙",
27
+ "▟",
28
+ "█",
29
+ ];
30
+
31
+ export function appendFastStatus(ctx: ExtensionContext, text: string, enabled: boolean): string {
32
+ return enabled ? `${text} ${ctx.ui.theme.fg("dim", "fast")}` : text;
33
+ }
34
+
35
+ export function formatCodexUsageStatusline(
36
+ report: CodexUsageReport,
37
+ ctx: ExtensionContext,
38
+ model?: CodexUsageModel,
39
+ ): string {
40
+ const value = formatCodexUsageStatusValue(report, model);
41
+ if (!value) return formatStatuslineText(ctx, "n/a", model);
42
+ const snapshot = selectActiveUsageSnapshot(report, model);
43
+ if (!snapshot?.primary || !snapshot.secondary)
44
+ return formatStatuslineText(ctx, value, model);
45
+ const [bar, countdown] = value.split(" ", 2);
46
+ const barText = formatStatuslineBarText(
47
+ ctx,
48
+ bar ?? "",
49
+ hasExhaustedQuotaWindow(report, model) ? "toolErrorBg" : "userMessageBg",
50
+ model,
51
+ );
52
+ return countdown
53
+ ? `${barText} ${ctx.ui.theme.fg("dim", countdown)}`
54
+ : barText;
55
+ }
56
+
57
+ export function formatCodexUsageBar(
58
+ report: CodexUsageReport,
59
+ ): string | undefined {
60
+ return formatReportBar(report);
61
+ }
62
+
63
+ export function formatCodexUsageStatusValue(
64
+ report: CodexUsageReport,
65
+ modelOrNow?: CodexUsageModel | number,
66
+ now = Date.now(),
67
+ ): string | undefined {
68
+ const model = typeof modelOrNow === "number" ? undefined : modelOrNow;
69
+ const capturedNow = typeof modelOrNow === "number" ? modelOrNow : now;
70
+ const snapshot = selectActiveUsageSnapshot(report, model);
71
+ if (!snapshot || (!snapshot.primary && !snapshot.secondary)) {
72
+ return formatCreditUsage(report.credits, capturedNow);
73
+ }
74
+ if (!snapshot.primary || !snapshot.secondary) {
75
+ const window = snapshot.primary ?? snapshot.secondary;
76
+ if (!window) return undefined;
77
+ const percentage = `${Math.round(remainingPercent(window))}%`;
78
+ const countdown = window.resetAt
79
+ ? formatResetCountdown(window.resetAt, capturedNow)
80
+ : undefined;
81
+ return countdown ? `${percentage} ${countdown}` : percentage;
82
+ }
83
+ const bar = formatDualLimitBar(snapshot.primary, snapshot.secondary);
84
+ if (isQuotaWindowExhausted(snapshot.primary) && snapshot.primary?.resetAt) {
85
+ const primaryCountdown = formatResetCountdown(
86
+ snapshot.primary.resetAt,
87
+ capturedNow,
88
+ );
89
+ const weeklyCountdown = snapshot.secondary?.resetAt
90
+ ? formatResetCountdown(snapshot.secondary.resetAt, capturedNow)
91
+ : undefined;
92
+ return weeklyCountdown
93
+ ? `${bar} ${primaryCountdown}/${weeklyCountdown}`
94
+ : `${bar} ${primaryCountdown}`;
95
+ }
96
+ const countdown = formatWeeklyResetCountdown(report, model, capturedNow);
97
+ return countdown ? `${bar} ${countdown}` : bar;
98
+ }
99
+
100
+ function formatCreditUsage(
101
+ credits: NormalizedCreditUsage | undefined,
102
+ now: number,
103
+ ): string | undefined {
104
+ if (!credits) return undefined;
105
+ const percentage = `${Math.round(credits.remainingPercent)}%`;
106
+ return credits.resetAt
107
+ ? `${percentage} ${formatResetCountdown(credits.resetAt, now)}`
108
+ : percentage;
109
+ }
110
+
111
+ export function formatWeeklyResetCountdown(
112
+ report: CodexUsageReport,
113
+ modelOrNow?: CodexUsageModel | number,
114
+ now = Date.now(),
115
+ ): string | undefined {
116
+ const model = typeof modelOrNow === "number" ? undefined : modelOrNow;
117
+ const capturedNow = typeof modelOrNow === "number" ? modelOrNow : now;
118
+ const snapshot = selectActiveUsageSnapshot(report, model);
119
+ const resetAt = weeklyWindow(snapshot)?.resetAt;
120
+ if (resetAt === undefined) return undefined;
121
+ return formatResetCountdown(resetAt, capturedNow);
122
+ }
123
+
124
+ export function formatResetCountdown(
125
+ resetAt: number,
126
+ now = Date.now(),
127
+ ): string {
128
+ const remainingMs = Math.max(0, resetAt - now);
129
+ if (remainingMs > DAY_MS) {
130
+ const dayTenths = Math.max(10, Math.ceil(remainingMs / DAY_TENTH_MS));
131
+ return `${formatTenths(dayTenths)}d`;
132
+ }
133
+ if (remainingMs >= HOUR_MS) {
134
+ const hourTenths = Math.max(10, Math.ceil(remainingMs / HOUR_TENTH_MS));
135
+ return `${formatTenths(hourTenths)}h`;
136
+ }
137
+ if (remainingMs >= MINUTE_MS)
138
+ return `${Math.floor(remainingMs / MINUTE_MS)}m`;
139
+ return `${Math.floor(remainingMs / SECOND_MS)}s`;
140
+ }
141
+
142
+ export function nextResetCountdownDelayMs(
143
+ report: CodexUsageReport,
144
+ now = Date.now(),
145
+ model?: CodexUsageModel,
146
+ ): number | undefined {
147
+ const snapshot = selectActiveUsageSnapshot(report, model);
148
+ const hasRateLimitWindow = Boolean(snapshot?.primary || snapshot?.secondary);
149
+ const resetTimes = [
150
+ hasRateLimitWindow ? weeklyWindow(snapshot)?.resetAt : report.credits?.resetAt,
151
+ ];
152
+ if (snapshot?.secondary && isQuotaWindowExhausted(snapshot.primary)) {
153
+ resetTimes.push(snapshot.primary?.resetAt);
154
+ }
155
+ const delays = resetTimes
156
+ .filter((resetAt): resetAt is number => resetAt !== undefined)
157
+ .map((resetAt) => nextResetCountdownDelayForRemainingMs(resetAt - now))
158
+ .filter((delay): delay is number => delay !== undefined);
159
+ return delays.length > 0 ? Math.min(...delays) : undefined;
160
+ }
161
+
162
+ export function nextResetCountdownDelayForRemainingMs(
163
+ remainingMs: number,
164
+ ): number | undefined {
165
+ if (remainingMs <= 0) return undefined;
166
+ if (remainingMs > DAY_MS) {
167
+ const dayTenths = Math.max(10, Math.ceil(remainingMs / DAY_TENTH_MS));
168
+ return Math.max(1, remainingMs - (dayTenths - 1) * DAY_TENTH_MS);
169
+ }
170
+ if (remainingMs >= HOUR_MS) {
171
+ const hourTenths = Math.max(10, Math.ceil(remainingMs / HOUR_TENTH_MS));
172
+ if (hourTenths === 10) return Math.max(1, remainingMs - HOUR_MS + 1);
173
+ return Math.max(1, remainingMs - (hourTenths - 1) * HOUR_TENTH_MS);
174
+ }
175
+ if (remainingMs >= MINUTE_MS) {
176
+ return Math.max(
177
+ 1,
178
+ remainingMs - Math.floor(remainingMs / MINUTE_MS) * MINUTE_MS + 1,
179
+ );
180
+ }
181
+ return Math.max(
182
+ 1,
183
+ remainingMs - Math.floor(remainingMs / SECOND_MS) * SECOND_MS + 1,
184
+ );
185
+ }
186
+
187
+ function formatTenths(value: number): string {
188
+ return value % 10 === 0 ? String(value / 10) : (value / 10).toFixed(1);
189
+ }
190
+
191
+ export function formatReportBar(
192
+ report: CodexUsageReport,
193
+ model?: CodexUsageModel,
194
+ ): string | undefined {
195
+ const snapshot = selectActiveUsageSnapshot(report, model);
196
+ if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
197
+ return formatAdaptiveLimitBar(snapshot.primary, snapshot.secondary);
198
+ }
199
+
200
+ function formatStatuslineText(
201
+ ctx: ExtensionContext,
202
+ value: string,
203
+ _model?: CodexUsageModel,
204
+ ): string {
205
+ const label = ctx.ui.theme.fg("accent", DEFAULT_STATUS_LABEL_TEXT);
206
+ return `${label} ${ctx.ui.theme.fg("dim", value)}`;
207
+ }
208
+
209
+ function formatStatuslineBarText(
210
+ ctx: ExtensionContext,
211
+ bar: string,
212
+ background: "userMessageBg" | "toolErrorBg",
213
+ _model?: CodexUsageModel,
214
+ ): string {
215
+ const label = ctx.ui.theme.fg("accent", DEFAULT_STATUS_LABEL_TEXT);
216
+ const value = ctx.ui.theme.bg(background, ctx.ui.theme.fg("dim", bar));
217
+ return `${label} ${value}`;
218
+ }
219
+
220
+ export function formatCodexUsageLoadingBar(frame: number): string {
221
+ const totalParts = DUAL_BAR_WIDTH * 2;
222
+ const cycleFrames = (totalParts - 1) * 2;
223
+ const finiteFrame = Number.isFinite(frame) ? frame : 0;
224
+ const cycleFrame = Math.abs(Math.trunc(finiteFrame)) % cycleFrames;
225
+ const primaryPart =
226
+ cycleFrame < totalParts ? cycleFrame : cycleFrames - cycleFrame;
227
+ const secondaryPart = totalParts - primaryPart - 1;
228
+ const masks = Array<number>(DUAL_BAR_WIDTH).fill(0);
229
+ const primaryCell = Math.floor(primaryPart / 2);
230
+ const secondaryCell = Math.floor(secondaryPart / 2);
231
+ masks[primaryCell] |= primaryPart % 2 === 0 ? 1 : 2;
232
+ masks[secondaryCell] |= secondaryPart % 2 === 0 ? 4 : 8;
233
+ return masks.map((mask) => DUAL_BAR_CHARS[mask]).join("");
234
+ }
235
+
236
+ export function formatStatuslineLoading(
237
+ ctx: ExtensionContext,
238
+ frame: number,
239
+ model?: CodexUsageModel,
240
+ ): string {
241
+ return formatStatuslineBarText(
242
+ ctx,
243
+ formatCodexUsageLoadingBar(frame),
244
+ "userMessageBg",
245
+ model,
246
+ );
247
+ }
248
+
249
+ export function formatStatuslineProblem(
250
+ ctx: ExtensionContext,
251
+ unavailable: boolean,
252
+ _model?: CodexUsageModel,
253
+ ): string {
254
+ const label = ctx.ui.theme.fg("accent", DEFAULT_STATUS_LABEL_TEXT);
255
+ const value = unavailable
256
+ ? ctx.ui.theme.fg("muted", "n/a")
257
+ : ctx.ui.theme.fg("error", "error");
258
+ return `${label} ${value}`;
259
+ }
260
+
261
+
262
+ function formatAdaptiveLimitBar(
263
+ primary: NormalizedRateLimitWindow | undefined,
264
+ secondary: NormalizedRateLimitWindow | undefined,
265
+ ): string {
266
+ if (!primary || !secondary) return formatSingleLimitBar(primary ?? secondary);
267
+ return formatDualLimitBar(primary, secondary);
268
+ }
269
+
270
+ function formatSingleLimitBar(
271
+ window: NormalizedRateLimitWindow | undefined,
272
+ ): string {
273
+ const filled = filledParts(window, DUAL_BAR_WIDTH * 4);
274
+ const partMasks = [1, 4, 2, 8];
275
+ let value = "";
276
+ for (let index = 0; index < DUAL_BAR_WIDTH; index++) {
277
+ const cellParts = Math.min(4, Math.max(0, filled - index * 4));
278
+ let mask = 0;
279
+ for (let part = 0; part < cellParts; part++) mask |= partMasks[part] ?? 0;
280
+ value += DUAL_BAR_CHARS[mask];
281
+ }
282
+ return value;
283
+ }
284
+
285
+ function formatDualLimitBar(
286
+ primary: NormalizedRateLimitWindow,
287
+ secondary: NormalizedRateLimitWindow,
288
+ ): string {
289
+ const primaryParts = filledTwentieths(primary);
290
+ const secondaryParts = filledTwentieths(secondary);
291
+ let value = "";
292
+ for (let index = 0; index < DUAL_BAR_WIDTH; index++) {
293
+ const leftPart = index * 2 + 1;
294
+ const rightPart = leftPart + 1;
295
+ let mask = 0;
296
+ if (primaryParts >= leftPart) mask |= 1;
297
+ if (primaryParts >= rightPart) mask |= 2;
298
+ if (secondaryParts >= leftPart) mask |= 4;
299
+ if (secondaryParts >= rightPart) mask |= 8;
300
+ value += DUAL_BAR_CHARS[mask];
301
+ }
302
+ return value;
303
+ }
304
+
305
+ function filledTwentieths(
306
+ window: NormalizedRateLimitWindow | undefined,
307
+ ): number {
308
+ return filledParts(window, 20);
309
+ }
310
+
311
+ function filledParts(
312
+ window: NormalizedRateLimitWindow | undefined,
313
+ totalParts: number,
314
+ ): number {
315
+ if (!window) return 0;
316
+ const remaining = remainingPercent(window);
317
+ if (remaining <= 0) return 0;
318
+ return Math.max(1, Math.round(remaining / (100 / totalParts)));
319
+ }
320
+
321
+ function remainingPercent(window: NormalizedRateLimitWindow): number {
322
+ return 100 - clampPercent(window.usedPercent);
323
+ }
324
+
325
+ function isQuotaWindowExhausted(
326
+ window: NormalizedRateLimitWindow | undefined,
327
+ ): boolean {
328
+ return window !== undefined && remainingPercent(window) <= 0;
329
+ }
330
+
331
+ function hasExhaustedQuotaWindow(
332
+ report: CodexUsageReport,
333
+ model?: CodexUsageModel,
334
+ ): boolean {
335
+ const snapshot = selectActiveUsageSnapshot(report, model);
336
+ return (
337
+ isQuotaWindowExhausted(snapshot?.primary) ||
338
+ isQuotaWindowExhausted(snapshot?.secondary)
339
+ );
340
+ }
341
+
342
+
343
+ function weeklyWindow(
344
+ snapshot: NormalizedRateLimitSnapshot | undefined,
345
+ ): NormalizedRateLimitWindow | undefined {
346
+ return snapshot?.secondary ?? snapshot?.primary;
347
+ }