@monotykamary/pi-better-openai 0.1.42 → 0.1.44
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/index.ts +80 -35
- package/package.json +1 -1
- package/src/reset-controller.ts +97 -0
- package/src/usage-controller.ts +15 -3
- package/src/usage.ts +53 -18
package/index.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Adds `service_tier: "priority"` to OpenAI provider payloads while fast mode is
|
|
5
5
|
* enabled and the selected model is in the configured allow-list.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type ExtensionAPI,
|
|
9
|
+
type ExtensionContext,
|
|
10
|
+
type Theme,
|
|
11
|
+
type ThemeColor,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
13
|
import {
|
|
9
14
|
Container,
|
|
10
15
|
Key,
|
|
@@ -58,12 +63,12 @@ import {
|
|
|
58
63
|
applySettingToRawConfig,
|
|
59
64
|
} from "./src/config.ts";
|
|
60
65
|
import {
|
|
66
|
+
type UsageSegment,
|
|
67
|
+
type UsageSeverity,
|
|
61
68
|
formatPercent,
|
|
62
69
|
formatUsageSnapshot,
|
|
63
70
|
parseUsageSnapshot,
|
|
64
71
|
readCodexAuth,
|
|
65
|
-
requestCodexUsage,
|
|
66
|
-
type UsageSnapshot,
|
|
67
72
|
} from "./src/usage.ts";
|
|
68
73
|
import {
|
|
69
74
|
buildBankedResetConfirmation,
|
|
@@ -71,10 +76,9 @@ import {
|
|
|
71
76
|
formatBankedResetChoice,
|
|
72
77
|
formatConsumeOutcome,
|
|
73
78
|
newRedeemRequestId,
|
|
74
|
-
requestBankedResetCredits,
|
|
75
79
|
selectBankedResetCredit,
|
|
76
|
-
type BankedResetCredits,
|
|
77
80
|
} from "./src/resets.ts";
|
|
81
|
+
import { ResetController } from "./src/reset-controller.ts";
|
|
78
82
|
import { registerOpenAIImage, _imageTest } from "./src/image.ts";
|
|
79
83
|
import { registerOpenAIWebSearch, _websearchTest } from "./src/websearch.ts";
|
|
80
84
|
import { registerOpenAILive } from "./src/live/index.ts";
|
|
@@ -90,7 +94,7 @@ import {
|
|
|
90
94
|
_petsTest,
|
|
91
95
|
} from "./src/pets.ts";
|
|
92
96
|
import { FastController, modelList, supportsFast } from "./src/fast-controller.ts";
|
|
93
|
-
import { UsageController } from "./src/usage-controller.ts";
|
|
97
|
+
import { isOpenAISubscriptionModel, UsageController } from "./src/usage-controller.ts";
|
|
94
98
|
import { PetFooterController } from "./src/pet-footer-controller.ts";
|
|
95
99
|
import {
|
|
96
100
|
abbreviateHomePath,
|
|
@@ -117,6 +121,46 @@ const requireSettingsListTheme = (): SettingsListTheme => {
|
|
|
117
121
|
return loadedSettingsListTheme;
|
|
118
122
|
};
|
|
119
123
|
|
|
124
|
+
const USAGE_SEVERITY_COLORS: Record<UsageSeverity, ThemeColor> = {
|
|
125
|
+
ok: "success",
|
|
126
|
+
warning: "warning",
|
|
127
|
+
critical: "error",
|
|
128
|
+
muted: "dim",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Keep a coloured segment on one line. Unlike sanitizeStatusText this leaves the
|
|
133
|
+
* spacing shared by neighbouring segments ("Usage: " + "5h: " + "90%") intact.
|
|
134
|
+
*/
|
|
135
|
+
function flattenSegment(text: string): string {
|
|
136
|
+
return text.replace(/[\r\n\t]+/g, " ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Colour a usage line segment by segment: the percentages carry the severity
|
|
141
|
+
* colour while the labels, countdowns and banked resets stay dim.
|
|
142
|
+
*/
|
|
143
|
+
function colorizeUsageSegments(segments: UsageSegment[], theme: Theme): string {
|
|
144
|
+
return segments
|
|
145
|
+
.map((segment) =>
|
|
146
|
+
theme.fg(USAGE_SEVERITY_COLORS[segment.severity], flattenSegment(segment.text)),
|
|
147
|
+
)
|
|
148
|
+
.join("");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Pair the fast-mode segment with the usage segments for the status widget. */
|
|
152
|
+
function statusWidgetParts(
|
|
153
|
+
fast: string | undefined,
|
|
154
|
+
usage: UsageSegment[] | undefined,
|
|
155
|
+
): UsageSegment[] | undefined {
|
|
156
|
+
const parts: UsageSegment[] = fast ? [{ text: fast, severity: "muted" }] : [];
|
|
157
|
+
if (usage?.length) {
|
|
158
|
+
if (parts.length > 0) parts.push({ text: " · ", severity: "muted" });
|
|
159
|
+
parts.push(...usage);
|
|
160
|
+
}
|
|
161
|
+
return parts.length > 0 ? parts : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
120
164
|
class DynamicBorder {
|
|
121
165
|
readonly #color: (text: string) => string;
|
|
122
166
|
|
|
@@ -247,6 +291,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
247
291
|
let cachedSessionNameLeafId: string | null | undefined;
|
|
248
292
|
let cachedSessionName: string | undefined;
|
|
249
293
|
const usageController = new UsageController(config, updateFooter);
|
|
294
|
+
const resetController = new ResetController();
|
|
250
295
|
const petController = new PetFooterController(config, updateFooter, () => footerInstalled);
|
|
251
296
|
let multiproviderService: MultiproviderService | undefined;
|
|
252
297
|
let unsubscribeMultiprovider: (() => void) | undefined;
|
|
@@ -265,6 +310,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
265
310
|
setActiveMultiproviderService(value);
|
|
266
311
|
unsubscribeMultiprovider = value.onActiveAccountChanged(CODEX_PROVIDER_ID, (event) => {
|
|
267
312
|
void usageController.refresh(event.ctx, undefined, { force: true });
|
|
313
|
+
void resetController.refresh(event.ctx, { force: true }).catch(() => {});
|
|
268
314
|
updateFooter(event.ctx);
|
|
269
315
|
});
|
|
270
316
|
const ctx = multiproviderRefreshCtx;
|
|
@@ -402,27 +448,18 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
402
448
|
ctx.ui.notify("/openai-resets requires an interactive TUI session.", "warning");
|
|
403
449
|
return;
|
|
404
450
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
]);
|
|
414
|
-
credits = creditResult;
|
|
415
|
-
snapshot = usageResult ? parseUsageSnapshot(usageResult, ctx.model?.id) : undefined;
|
|
416
|
-
} catch (error) {
|
|
417
|
-
ctx.ui.notify(
|
|
418
|
-
`Banked reset lookup failed: ${sanitizeDiagnosticError(
|
|
419
|
-
error instanceof Error ? error.message : String(error),
|
|
420
|
-
)}`,
|
|
421
|
-
"error",
|
|
422
|
-
);
|
|
451
|
+
let credits = resetController.snapshot?.credits;
|
|
452
|
+
if (!credits) {
|
|
453
|
+
await resetController.refresh(ctx, { force: true }).catch(() => {});
|
|
454
|
+
credits = resetController.snapshot?.credits;
|
|
455
|
+
}
|
|
456
|
+
if (!credits) {
|
|
457
|
+
const reason = resetController.lastError;
|
|
458
|
+
ctx.ui.notify(`Banked reset lookup failed${reason ? `: ${reason}` : "."}`, "error");
|
|
423
459
|
return;
|
|
424
460
|
}
|
|
425
|
-
|
|
461
|
+
void resetController.refresh(ctx).catch(() => {});
|
|
462
|
+
if (credits.availableCount <= 0) {
|
|
426
463
|
ctx.ui.notify("No banked Codex resets are available for this account.", "info");
|
|
427
464
|
return;
|
|
428
465
|
}
|
|
@@ -440,17 +477,20 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
440
477
|
const confirmation = buildBankedResetConfirmation({
|
|
441
478
|
credit: selected,
|
|
442
479
|
availableCount: credits.availableCount,
|
|
443
|
-
snapshot,
|
|
480
|
+
snapshot: usageController.snapshot,
|
|
444
481
|
});
|
|
445
482
|
if (!(await ctx.ui.confirm(confirmation.title, confirmation.message))) {
|
|
446
483
|
ctx.ui.notify("Banked reset redemption cancelled.", "info");
|
|
447
484
|
return;
|
|
448
485
|
}
|
|
449
486
|
try {
|
|
487
|
+
const timeoutSignal = AbortSignal.timeout(15_000);
|
|
488
|
+
const signal = ctx.signal ? AbortSignal.any([ctx.signal, timeoutSignal]) : timeoutSignal;
|
|
450
489
|
const result = await consumeBankedReset(ctx, selected?.id, newRedeemRequestId(), signal);
|
|
451
490
|
const outcome = formatConsumeOutcome(result);
|
|
452
491
|
ctx.ui.notify(outcome.message, outcome.level);
|
|
453
492
|
void usageController.refresh(ctx, ctx.model?.id, { force: true });
|
|
493
|
+
void resetController.refresh(ctx, { force: true }).catch(() => {});
|
|
454
494
|
updateFooter(ctx);
|
|
455
495
|
} catch (error) {
|
|
456
496
|
ctx.ui.notify(
|
|
@@ -1195,8 +1235,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1195
1235
|
const petColumnWidth = Math.min(petRenderSizeCells, Math.max(1, width - 1));
|
|
1196
1236
|
const footerTextWidth = inlinePet ? Math.max(1, width - petColumnWidth - 2) : width;
|
|
1197
1237
|
|
|
1198
|
-
const
|
|
1199
|
-
const usageLine =
|
|
1238
|
+
const usageStatusSegments = usageController.statusSegments(ctx, cfg, usingSubscription);
|
|
1239
|
+
const usageLine = usageStatusSegments
|
|
1240
|
+
? colorizeUsageSegments(usageStatusSegments, theme)
|
|
1241
|
+
: undefined;
|
|
1200
1242
|
|
|
1201
1243
|
let statsLeft = parts.join(" ");
|
|
1202
1244
|
let statsLeftWidth = visibleWidth(statsLeft);
|
|
@@ -1322,22 +1364,22 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1322
1364
|
statusInstalled = text !== undefined;
|
|
1323
1365
|
}
|
|
1324
1366
|
|
|
1325
|
-
function setStatusWidget(ctx: ExtensionContext,
|
|
1326
|
-
if (!
|
|
1367
|
+
function setStatusWidget(ctx: ExtensionContext, parts: UsageSegment[] | undefined): void {
|
|
1368
|
+
if (!parts && !statusWidgetInstalled) return;
|
|
1327
1369
|
ctx.ui.setWidget(
|
|
1328
1370
|
STATUS_KEY,
|
|
1329
|
-
|
|
1371
|
+
parts
|
|
1330
1372
|
? (_tui, theme) => ({
|
|
1331
1373
|
invalidate() {},
|
|
1332
1374
|
render(width: number): string[] {
|
|
1333
|
-
const line =
|
|
1375
|
+
const line = colorizeUsageSegments(parts, theme);
|
|
1334
1376
|
return [truncateToWidth(line, width, theme.fg("dim", "..."))];
|
|
1335
1377
|
},
|
|
1336
1378
|
})
|
|
1337
1379
|
: undefined,
|
|
1338
1380
|
{ placement: "belowEditor" },
|
|
1339
1381
|
);
|
|
1340
|
-
statusWidgetInstalled =
|
|
1382
|
+
statusWidgetInstalled = parts !== undefined;
|
|
1341
1383
|
}
|
|
1342
1384
|
|
|
1343
1385
|
function updateFooter(ctx: ExtensionContext): void {
|
|
@@ -1373,8 +1415,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1373
1415
|
}
|
|
1374
1416
|
|
|
1375
1417
|
const fast = fastController.statusSegment(ctx, cfg);
|
|
1376
|
-
const usage = usageController.
|
|
1377
|
-
setStatusWidget(ctx,
|
|
1418
|
+
const usage = usageController.statusSegments(ctx, cfg);
|
|
1419
|
+
setStatusWidget(ctx, statusWidgetParts(fast, usage));
|
|
1378
1420
|
}
|
|
1379
1421
|
|
|
1380
1422
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -1393,6 +1435,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1393
1435
|
updateFooter(ctx);
|
|
1394
1436
|
if (hasTerminalUI(ctx) && nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
|
|
1395
1437
|
usageController.start(ctx);
|
|
1438
|
+
if (nextConfig.usage.enabled && isOpenAISubscriptionModel(ctx, nextConfig))
|
|
1439
|
+
resetController.start(ctx);
|
|
1396
1440
|
if (fastController.active) ctx.ui.notify(fastController.stateText(ctx, nextConfig), "info");
|
|
1397
1441
|
});
|
|
1398
1442
|
|
|
@@ -1469,6 +1513,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1469
1513
|
invalidateSessionName();
|
|
1470
1514
|
multiproviderRefreshCtx = undefined;
|
|
1471
1515
|
usageController.shutdown();
|
|
1516
|
+
resetController.stop();
|
|
1472
1517
|
petController.shutdown();
|
|
1473
1518
|
});
|
|
1474
1519
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { sanitizeDiagnosticError } from "./format.ts";
|
|
3
|
+
import { type BankedResetCredits, requestBankedResetCredits } from "./resets.ts";
|
|
4
|
+
|
|
5
|
+
export const BANKED_RESET_CACHE_TTL_MS = 5 * 60_000;
|
|
6
|
+
const BANKED_RESET_REQUEST_TIMEOUT_MS = 10_000;
|
|
7
|
+
|
|
8
|
+
export type BankedResetCache = {
|
|
9
|
+
credits: BankedResetCredits;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Keeps the banked-reset credit list warm so /openai-resets opens without
|
|
14
|
+
// paying the credential-resolution + wham round trip. The cache is prefetched
|
|
15
|
+
// at session start, refreshed on a TTL, and served even when stale (a stale
|
|
16
|
+
// open still kicks off a background refresh; the backend re-validates every
|
|
17
|
+
// redemption server-side, so a stale offer can never be consumed twice).
|
|
18
|
+
export class ResetController {
|
|
19
|
+
private cache: BankedResetCache | undefined;
|
|
20
|
+
private error: string | undefined;
|
|
21
|
+
private lastFetchAt: number | undefined;
|
|
22
|
+
private inFlight: Promise<void> | undefined;
|
|
23
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
24
|
+
private sessionSignal: AbortSignal | undefined;
|
|
25
|
+
private sessionAbortHandler: (() => void) | undefined;
|
|
26
|
+
|
|
27
|
+
get snapshot(): BankedResetCache | undefined {
|
|
28
|
+
return this.cache;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get lastError(): string | undefined {
|
|
32
|
+
return this.error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isFresh(now = Date.now()): boolean {
|
|
36
|
+
return this.cache !== undefined && now - this.cache.updatedAt < BANKED_RESET_CACHE_TTL_MS;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
refresh(ctx: ExtensionContext, options?: { force?: boolean }): Promise<void> {
|
|
40
|
+
if (this.inFlight) return this.inFlight;
|
|
41
|
+
if (
|
|
42
|
+
!options?.force &&
|
|
43
|
+
this.lastFetchAt !== undefined &&
|
|
44
|
+
Date.now() - this.lastFetchAt < BANKED_RESET_CACHE_TTL_MS
|
|
45
|
+
)
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
this.lastFetchAt = Date.now();
|
|
48
|
+
const timeoutSignal = AbortSignal.timeout(BANKED_RESET_REQUEST_TIMEOUT_MS);
|
|
49
|
+
const signal = ctx.signal ? AbortSignal.any([ctx.signal, timeoutSignal]) : timeoutSignal;
|
|
50
|
+
const task = (async () => {
|
|
51
|
+
try {
|
|
52
|
+
const credits = await requestBankedResetCredits(ctx, signal);
|
|
53
|
+
if (credits) {
|
|
54
|
+
this.cache = { credits, updatedAt: Date.now() };
|
|
55
|
+
this.error = undefined;
|
|
56
|
+
} else {
|
|
57
|
+
this.error = "OpenAI Codex credentials unavailable.";
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.error = sanitizeDiagnosticError(
|
|
61
|
+
error instanceof Error ? error.message : String(error),
|
|
62
|
+
);
|
|
63
|
+
} finally {
|
|
64
|
+
this.inFlight = undefined;
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
this.inFlight = task;
|
|
68
|
+
return task;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
start(ctx: ExtensionContext): void {
|
|
72
|
+
this.stop();
|
|
73
|
+
if (ctx.signal?.aborted) return;
|
|
74
|
+
this.sessionSignal = ctx.signal;
|
|
75
|
+
this.sessionAbortHandler = () => this.stop();
|
|
76
|
+
ctx.signal?.addEventListener("abort", this.sessionAbortHandler, { once: true });
|
|
77
|
+
void this.refresh(ctx).catch(() => {});
|
|
78
|
+
this.timer = setInterval(() => {
|
|
79
|
+
if (ctx.signal?.aborted) {
|
|
80
|
+
this.stop();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
void this.refresh(ctx).catch(() => {});
|
|
84
|
+
}, BANKED_RESET_CACHE_TTL_MS);
|
|
85
|
+
this.timer.unref?.();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
stop(): void {
|
|
89
|
+
if (this.timer) clearInterval(this.timer);
|
|
90
|
+
this.timer = undefined;
|
|
91
|
+
if (this.sessionSignal && this.sessionAbortHandler) {
|
|
92
|
+
this.sessionSignal.removeEventListener("abort", this.sessionAbortHandler);
|
|
93
|
+
}
|
|
94
|
+
this.sessionSignal = undefined;
|
|
95
|
+
this.sessionAbortHandler = undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/usage-controller.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ResolvedConfig } from "./config.ts";
|
|
|
3
3
|
import { maskIdentifier, sanitizeDiagnosticError } from "./format.ts";
|
|
4
4
|
import {
|
|
5
5
|
AUTH_FILE,
|
|
6
|
+
type UsageSegment,
|
|
6
7
|
type UsageSnapshot,
|
|
7
8
|
formatResetCountdown,
|
|
8
9
|
formatUsageSnapshot,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
readCodexAuth,
|
|
11
12
|
requestCodexUsage,
|
|
12
13
|
usageScopeForModel,
|
|
14
|
+
usageSegments,
|
|
13
15
|
} from "./usage.ts";
|
|
14
16
|
import { currentModelKey } from "./fast-controller.ts";
|
|
15
17
|
|
|
@@ -70,20 +72,30 @@ export class UsageController {
|
|
|
70
72
|
return this.usageSnapshot;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
statusSegments(
|
|
74
76
|
ctx: ExtensionContext,
|
|
75
77
|
cfg = this.getConfig(ctx),
|
|
76
78
|
isUsingOAuth?: boolean,
|
|
77
|
-
):
|
|
79
|
+
): UsageSegment[] | undefined {
|
|
78
80
|
return this.usageSnapshot &&
|
|
79
81
|
!this.usageError &&
|
|
80
82
|
this.usageSnapshot.scope === usageScopeForModel(ctx.model?.id) &&
|
|
81
83
|
cfg.usage.enabled &&
|
|
82
84
|
isOpenAISubscriptionModel(ctx, cfg, isUsingOAuth)
|
|
83
|
-
?
|
|
85
|
+
? usageSegments(this.usageSnapshot, cfg.usage)
|
|
84
86
|
: undefined;
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
statusLine(
|
|
90
|
+
ctx: ExtensionContext,
|
|
91
|
+
cfg = this.getConfig(ctx),
|
|
92
|
+
isUsingOAuth?: boolean,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
return this.statusSegments(ctx, cfg, isUsingOAuth)
|
|
95
|
+
?.map((segment) => segment.text)
|
|
96
|
+
.join("");
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
formatStatus(ctx: ExtensionContext): string {
|
|
88
100
|
const cfg = this.getConfig(ctx);
|
|
89
101
|
if (!cfg.usage.enabled) return "Usage display is disabled.";
|
package/src/usage.ts
CHANGED
|
@@ -253,11 +253,29 @@ export function formatPercent(value: number | null): string {
|
|
|
253
253
|
: "--";
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
export
|
|
256
|
+
export type UsageSeverity = "ok" | "warning" | "critical" | "muted";
|
|
257
|
+
|
|
258
|
+
export type UsageSegment = {
|
|
259
|
+
text: string;
|
|
260
|
+
severity: UsageSeverity;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** Remaining budget at or below these thresholds turns the percentage amber/red. */
|
|
264
|
+
const WARNING_LEFT_PERCENT = 30;
|
|
265
|
+
const CRITICAL_LEFT_PERCENT = 10;
|
|
266
|
+
|
|
267
|
+
export function severityForLeftPercent(percent: number | null): UsageSeverity {
|
|
268
|
+
if (percent === null) return "muted";
|
|
269
|
+
if (percent <= CRITICAL_LEFT_PERCENT) return "critical";
|
|
270
|
+
if (percent <= WARNING_LEFT_PERCENT) return "warning";
|
|
271
|
+
return "ok";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function usageSegments(
|
|
257
275
|
snapshot: UsageSnapshot,
|
|
258
276
|
options: { showResetTimes: boolean; showBankedResets?: boolean },
|
|
259
277
|
now = Date.now(),
|
|
260
|
-
):
|
|
278
|
+
): UsageSegment[] {
|
|
261
279
|
const windows = [
|
|
262
280
|
{
|
|
263
281
|
label: "5h",
|
|
@@ -276,24 +294,41 @@ export function formatUsageSnapshot(
|
|
|
276
294
|
(window) => window.percent !== null || window.resetSeconds !== null,
|
|
277
295
|
);
|
|
278
296
|
const displayedWindows = availableWindows.length > 0 ? availableWindows : windows;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
.
|
|
293
|
-
|
|
297
|
+
const segments: UsageSegment[] = [{ text: "Usage: ", severity: "muted" }];
|
|
298
|
+
displayedWindows.forEach((window, index) => {
|
|
299
|
+
if (index > 0) segments.push({ text: " · ", severity: "muted" });
|
|
300
|
+
segments.push({ text: `${window.label}: `, severity: "muted" });
|
|
301
|
+
segments.push({
|
|
302
|
+
text: formatPercent(window.percent),
|
|
303
|
+
severity: severityForLeftPercent(window.percent),
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
if (options.showResetTimes) {
|
|
307
|
+
for (const window of displayedWindows) {
|
|
308
|
+
const reset = formatCompactReset(
|
|
309
|
+
displayedWindows.length > 1 ? window.label : undefined,
|
|
310
|
+
remainingResetSeconds(window.resetSeconds, snapshot.capturedAt, now),
|
|
311
|
+
window.includeDate ? { includeDate: true } : undefined,
|
|
312
|
+
now,
|
|
313
|
+
);
|
|
314
|
+
if (reset) segments.push({ text: ` · ${reset}`, severity: "muted" });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
294
317
|
const banked =
|
|
295
318
|
options.showBankedResets === false ? null : formatBankedResetsSuffix(snapshot.bankedResets);
|
|
296
|
-
|
|
319
|
+
if (banked) segments.push({ text: ` · ${banked}`, severity: "muted" });
|
|
320
|
+
return segments;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Flat text form of {@link usageSegments}, used for status lines and notifications. */
|
|
324
|
+
export function formatUsageSnapshot(
|
|
325
|
+
snapshot: UsageSnapshot,
|
|
326
|
+
options: { showResetTimes: boolean; showBankedResets?: boolean },
|
|
327
|
+
now = Date.now(),
|
|
328
|
+
): string {
|
|
329
|
+
return usageSegments(snapshot, options, now)
|
|
330
|
+
.map((segment) => segment.text)
|
|
331
|
+
.join("");
|
|
297
332
|
}
|
|
298
333
|
|
|
299
334
|
export function formatBankedResetsSuffix(count: number | null): string | null {
|