@narumitw/pi-usage 0.60.10 → 0.61.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/README.md +20 -5
- package/dist/index.ts +137 -23
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +11 -6
- package/package.json +1 -1
- package/src/codex-fast.ts +1 -0
- package/src/codex-resets.ts +7 -4
- package/src/format.ts +22 -6
- package/src/index.ts +1 -0
- package/src/oauth-credential-source.ts +39 -2
- package/src/providers/kimi-coding.ts +41 -2
- package/src/query.ts +9 -2
- package/src/settings.ts +13 -0
- package/src/types.ts +2 -0
- package/src/usage-settings-ui.ts +27 -7
- package/src/usage.ts +10 -3
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ xAI OAuth subscription reporting follows the reviewed Grok Build contract and ru
|
|
|
14
14
|
- Redeems eligible Codex resets only after fresh account matching and explicit confirmation.
|
|
15
15
|
- Refreshes one or all configured providers with bounded concurrency while preserving partial results.
|
|
16
16
|
- Scopes statusline and cache data to the active provider and runtime account.
|
|
17
|
-
- Resolves credentials through Pi or the process-local OAuth credential-source protocol and validates the effective provider endpoint before sending them.
|
|
17
|
+
- Resolves credentials through Pi or the process-local OAuth credential-source protocol, waits for compatible pending account activation, and validates the effective provider endpoint before sending them.
|
|
18
18
|
|
|
19
19
|
## 📦 Install
|
|
20
20
|
|
|
@@ -66,7 +66,7 @@ Read the [query and reset guide](./docs/operations.md) for target selection, can
|
|
|
66
66
|
|
|
67
67
|
## ⚙️ Settings
|
|
68
68
|
|
|
69
|
-
Choose **Settings** in `/usage` to edit Codex Fast mode and the
|
|
69
|
+
Choose **Settings** in `/usage` to edit Codex Fast mode, percentage display, and the reset countdown through Pi's settings-list interaction in TUI mode.
|
|
70
70
|
RPC mode reports the active manual settings path instead of opening terminal UI.
|
|
71
71
|
|
|
72
72
|
These preferences live in `pi-usage.json` under Pi's user agent directory, normally `~/.pi/agent/pi-usage.json`.
|
|
@@ -87,7 +87,7 @@ Run `/fast` without arguments to toggle Fast for the active supported Codex mode
|
|
|
87
87
|
Fast is about 1.5× faster and uses more of your plan allowance.
|
|
88
88
|
The `codexFastMode` preference defaults to Off.
|
|
89
89
|
|
|
90
|
-
Fast currently applies only to official `openai-codex-responses` requests for `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`,
|
|
90
|
+
Fast currently applies only to official `openai-codex-responses` requests for `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, and `gpt-6-sol` at `https://chatgpt.com`.
|
|
91
91
|
It sends `service_tier: "priority"` while enabled and explicit `service_tier: "default"` otherwise.
|
|
92
92
|
The statusline adds `fast` only while the preference is effective, for example `codex fast 59% ↻ 2h30m` with the default reset countdown.
|
|
93
93
|
Unsupported models and custom or proxy origins are left unchanged.
|
|
@@ -95,6 +95,20 @@ Unsupported models and custom or proxy origins are left unchanged.
|
|
|
95
95
|
A toggle affects provider requests whose payload hook starts after the save; a request already sent is unchanged.
|
|
96
96
|
Repair or remove an invalid file, then run `/reload` before trying the toggle again.
|
|
97
97
|
|
|
98
|
+
### Codex statusline percentage
|
|
99
|
+
|
|
100
|
+
The `codexStatusPercentage` preference accepts `"remaining"` or `"used"` and defaults to `"remaining"` to preserve existing output.
|
|
101
|
+
Choose **Codex percentage** in the TUI Settings screen, or set the field manually and run `/reload`:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"codexStatusPercentage": "used"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
With `"used"`, a limit with 70% remaining appears as `30%`; its reset countdown or window label stays unchanged.
|
|
110
|
+
This preference changes only Codex statusline formatting, not provider data or the detailed `/usage` report.
|
|
111
|
+
|
|
98
112
|
### Codex statusline reset countdown
|
|
99
113
|
|
|
100
114
|
The `codexStatusResetCountdown` preference defaults to `true`. It replaces the window labels with the time remaining until each returned limit resets.
|
|
@@ -138,6 +152,7 @@ Codex reset redemption requires a freshly matched current OAuth account and expl
|
|
|
138
152
|
The extension selects one provider target for one query and never flattens targets into provider rows or aggregates every visible target.
|
|
139
153
|
Provider adapters own target discovery and validation; core owns one-target selection, persistence, cache identity, cancellation, and UI.
|
|
140
154
|
A compatible credential owner may offer the verified active named account through the versioned process-local protocol without exposing its account label or storage.
|
|
155
|
+
When that owner is still activating the requested provider in the current session, `/usage` waits through the extension-neutral `oauth:credential-readiness:v1` protocol before collecting the synchronous credential offer.
|
|
141
156
|
Without such an owner, `pi-usage` retains its standalone Pi `auth.json` behavior.
|
|
142
157
|
An older or incompatible owner degrades to the existing authentication-unavailable result when the stored login does not match runtime auth.
|
|
143
158
|
After the active runtime credential changes, the next command, turn, or scheduled refresh resolves auth again and cannot reuse another account's cached report.
|
|
@@ -179,8 +194,8 @@ Behavior changes:
|
|
|
179
194
|
|
|
180
195
|
## 🔒 Security and privacy
|
|
181
196
|
|
|
182
|
-
Credential candidates are collected synchronously in memory and are not cached, persisted, logged, formatted, or appended to the Pi session.
|
|
183
|
-
The
|
|
197
|
+
Credential candidates are collected synchronously in memory after any compatible readiness promises settle, and are not cached, persisted, logged, formatted, or appended to the Pi session.
|
|
198
|
+
The protocols carry no account name or extension identity.
|
|
184
199
|
Only the selected provider's exact runtime match is used, and secrets are sent only to its validated official origin.
|
|
185
200
|
DeepSeek balance requests require Bearer authentication, send only that resolved credential from Pi's runtime auth to `https://api.deepseek.com/user/balance`, and refuse redirects.
|
|
186
201
|
Fireworks spend requests send only that resolved credential to the official `https://api.fireworks.ai` account-listing and billing-summary endpoints and refuse redirects.
|
package/dist/index.ts
CHANGED
|
@@ -9,7 +9,8 @@ var CODEX_FAST_MODEL_IDS = /* @__PURE__ */ new Set([
|
|
|
9
9
|
"gpt-5.5",
|
|
10
10
|
"gpt-5.6-luna",
|
|
11
11
|
"gpt-5.6-sol",
|
|
12
|
-
"gpt-5.6-terra"
|
|
12
|
+
"gpt-5.6-terra",
|
|
13
|
+
"gpt-6-sol"
|
|
13
14
|
]);
|
|
14
15
|
function codexFastAvailability(model, enabled) {
|
|
15
16
|
if (model?.provider !== "openai-codex") return { kind: "not-codex" };
|
|
@@ -280,8 +281,11 @@ function truncate(value, maxChars) {
|
|
|
280
281
|
// src/oauth-credential-source.ts
|
|
281
282
|
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
282
283
|
var OAUTH_CREDENTIAL_SOURCE_CHANNEL = "oauth:credential-source:v1";
|
|
284
|
+
var OAUTH_CREDENTIAL_READINESS_CHANNEL = "oauth:credential-readiness:v1";
|
|
283
285
|
function createOAuthCredentialCandidateReader(pi, credentialReader = readStoredCredential) {
|
|
284
|
-
|
|
286
|
+
const reader = (ctx, providerId) => collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader);
|
|
287
|
+
reader.waitUntilReady = (ctx, providerId) => waitForOAuthCredentialSources(pi, ctx, providerId);
|
|
288
|
+
return reader;
|
|
285
289
|
}
|
|
286
290
|
function collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader = readStoredCredential) {
|
|
287
291
|
const candidates = [];
|
|
@@ -310,6 +314,31 @@ function collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader
|
|
|
310
314
|
}
|
|
311
315
|
return { ok: true, candidates, offeredCount };
|
|
312
316
|
}
|
|
317
|
+
async function waitForOAuthCredentialSources(pi, ctx, providerId) {
|
|
318
|
+
const pending = [];
|
|
319
|
+
let collecting = true;
|
|
320
|
+
const request = Object.freeze({
|
|
321
|
+
session: ctx.sessionManager,
|
|
322
|
+
provider: providerId,
|
|
323
|
+
waitUntil(value) {
|
|
324
|
+
if (!collecting || !(value instanceof Promise)) return;
|
|
325
|
+
pending.push(value);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
try {
|
|
329
|
+
pi.events.emit(OAUTH_CREDENTIAL_READINESS_CHANNEL, request);
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
} finally {
|
|
333
|
+
collecting = false;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
await Promise.all(pending);
|
|
337
|
+
return true;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
313
342
|
function fallbackOAuthCredentialCandidates(providerId, credentialReader) {
|
|
314
343
|
try {
|
|
315
344
|
const credential = cloneOAuthCredential(credentialReader(providerId));
|
|
@@ -908,11 +937,18 @@ var FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
|
908
937
|
var DAILY_WINDOW_MINUTES = 1440;
|
|
909
938
|
var WEEKLY_WINDOW_MINUTES = 10080;
|
|
910
939
|
var FIXED_POINT_UNITS_PER_CENT = 1e6;
|
|
940
|
+
var RATIO_WINDOWS = [
|
|
941
|
+
{ key: "limit_5h", id: "five-hour", label: "Five-hour window", windowMinutes: FIVE_HOUR_WINDOW_MINUTES },
|
|
942
|
+
{ key: "limit_week", id: "weekly", label: "Weekly window", windowMinutes: WEEKLY_WINDOW_MINUTES },
|
|
943
|
+
{ key: "limit_month_total", id: "monthly", label: "Monthly window" }
|
|
944
|
+
];
|
|
911
945
|
function normalizeKimiCodingUsagePayload(payload, capturedAt) {
|
|
912
946
|
const root = asObject6(payload);
|
|
913
947
|
if (!root) throw new Error("Kimi Coding usage response was not an object.");
|
|
914
948
|
const candidates = [];
|
|
949
|
+
const explicitWindows = /* @__PURE__ */ new Set();
|
|
915
950
|
let omittedWindow = false;
|
|
951
|
+
if (root.usage !== void 0) explicitWindows.add(WEEKLY_WINDOW_MINUTES);
|
|
916
952
|
const summary = parseUsageRow(root.usage, WEEKLY_WINDOW_MINUTES, "Weekly window");
|
|
917
953
|
if (summary) candidates.push(summary);
|
|
918
954
|
else if (root.usage !== void 0) omittedWindow = true;
|
|
@@ -920,6 +956,7 @@ function normalizeKimiCodingUsagePayload(payload, capturedAt) {
|
|
|
920
956
|
for (const raw of root.limits) {
|
|
921
957
|
const item = asObject6(raw);
|
|
922
958
|
const windowMinutes = parseWindowMinutes(item?.window);
|
|
959
|
+
if (windowMinutes !== void 0) explicitWindows.add(windowMinutes);
|
|
923
960
|
const label = sanitizedLabel(item?.name);
|
|
924
961
|
const bucket = windowMinutes === void 0 ? void 0 : parseUsageRow(item?.detail, windowMinutes, label ?? defaultWindowLabel(windowMinutes));
|
|
925
962
|
if (bucket) candidates.push(bucket);
|
|
@@ -938,8 +975,23 @@ function normalizeKimiCodingUsagePayload(payload, capturedAt) {
|
|
|
938
975
|
if (rows.length === 1) buckets.push(rows[0]);
|
|
939
976
|
else omittedWindow = true;
|
|
940
977
|
}
|
|
941
|
-
|
|
942
|
-
|
|
978
|
+
const usages = asObject6(root.usages);
|
|
979
|
+
if (root.usages !== void 0 && !usages) omittedWindow = true;
|
|
980
|
+
if (usages) {
|
|
981
|
+
for (const window of RATIO_WINDOWS) {
|
|
982
|
+
const raw = usages[window.key];
|
|
983
|
+
if (raw === void 0 || "windowMinutes" in window && explicitWindows.has(window.windowMinutes)) continue;
|
|
984
|
+
const bucket = parseRatioWindow(raw, window.id, window.label);
|
|
985
|
+
if (bucket) {
|
|
986
|
+
buckets.push({
|
|
987
|
+
...bucket,
|
|
988
|
+
..."windowMinutes" in window ? { windowMinutes: window.windowMinutes } : {}
|
|
989
|
+
});
|
|
990
|
+
} else omittedWindow = true;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
buckets.sort((left, right) => (left.windowMinutes ?? Infinity) - (right.windowMinutes ?? Infinity));
|
|
994
|
+
const metrics = parseBoosterWallet(root.boosterWallet === void 0 ? root.booster_wallet : root.boosterWallet);
|
|
943
995
|
if (buckets.length === 0 && metrics.length === 0) {
|
|
944
996
|
throw new Error("Kimi Coding usage endpoint returned no displayable usage data.");
|
|
945
997
|
}
|
|
@@ -978,6 +1030,21 @@ function parseUsageRow(value, windowMinutes, label) {
|
|
|
978
1030
|
...resetsAt !== void 0 ? { resetsAt } : {}
|
|
979
1031
|
};
|
|
980
1032
|
}
|
|
1033
|
+
function parseRatioWindow(value, id, label) {
|
|
1034
|
+
const row = asObject6(value);
|
|
1035
|
+
const ratio = row?.used_ratio;
|
|
1036
|
+
if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio < 0 || ratio > 1) return void 0;
|
|
1037
|
+
const resetsAt = asIsoEpochSeconds(row?.reset_time);
|
|
1038
|
+
const used = ratio * 100;
|
|
1039
|
+
return {
|
|
1040
|
+
id,
|
|
1041
|
+
label,
|
|
1042
|
+
used,
|
|
1043
|
+
remaining: 100 - used,
|
|
1044
|
+
unit: "percent",
|
|
1045
|
+
...resetsAt !== void 0 ? { resetsAt } : {}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
981
1048
|
function parseWindowMinutes(value) {
|
|
982
1049
|
const window = asObject6(value);
|
|
983
1050
|
if (!window) return void 0;
|
|
@@ -2301,6 +2368,9 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
|
|
|
2301
2368
|
}
|
|
2302
2369
|
const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
|
|
2303
2370
|
if (!model) return void 0;
|
|
2371
|
+
if (["github-copilot", "xai"].includes(adapter.id) && candidateReader?.waitUntilReady && !await candidateReader.waitUntilReady(ctx, adapter.id)) {
|
|
2372
|
+
throw new Error(`${adapter.displayName} OAuth credential readiness failed closed.`);
|
|
2373
|
+
}
|
|
2304
2374
|
const registry = ctx.modelRegistry;
|
|
2305
2375
|
const provider = registry.getProvider?.(adapter.id);
|
|
2306
2376
|
if (provider?.baseUrl && !hasOfficialUrlOrigin(provider.baseUrl, adapter.id)) {
|
|
@@ -2370,14 +2440,14 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
|
|
|
2370
2440
|
};
|
|
2371
2441
|
};
|
|
2372
2442
|
if (adapter.id === "github-copilot") {
|
|
2373
|
-
const offered = candidateReader ? candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
2443
|
+
const offered = candidateReader ? await candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
2374
2444
|
if (!offered.ok) {
|
|
2375
2445
|
throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
|
|
2376
2446
|
}
|
|
2377
2447
|
return finalize(resolveGitHubCopilotUsageAuth(auth, model, salt, offered.candidates, offered.offeredCount === 0));
|
|
2378
2448
|
}
|
|
2379
2449
|
if (adapter.id === "xai") {
|
|
2380
|
-
const offered = candidateReader ? candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
2450
|
+
const offered = candidateReader ? await candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
2381
2451
|
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
2382
2452
|
return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
|
|
2383
2453
|
}
|
|
@@ -2929,19 +2999,22 @@ async function resolveCodexResetAuth(ctx, salt = AUTH_FINGERPRINT_SALT, credenti
|
|
|
2929
2999
|
const expectedModel = `${model.provider}/${model.id}`;
|
|
2930
3000
|
const adapter = adapterForProvider("openai-codex");
|
|
2931
3001
|
if (!adapter) throw new Error("OpenAI Codex usage support is unavailable.");
|
|
3002
|
+
if (candidateReader?.waitUntilReady && !await candidateReader.waitUntilReady(ctx, "openai-codex")) {
|
|
3003
|
+
throw new Error("OpenAI Codex OAuth credential readiness failed closed.");
|
|
3004
|
+
}
|
|
2932
3005
|
const auth = await resolveUsageAuth(ctx, adapter, salt, credentialReader);
|
|
2933
3006
|
if (`${ctx.model?.provider}/${ctx.model?.id}` !== expectedModel) {
|
|
2934
3007
|
throw new Error("The current model changed while resolving Codex reset authentication.");
|
|
2935
3008
|
}
|
|
2936
3009
|
if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
|
|
3010
|
+
const offered = candidateReader ? await candidateReader(ctx, "openai-codex") : fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
|
|
3011
|
+
if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
|
|
2937
3012
|
const resolvedAccess = bearerToken2(headerValue2(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
2938
3013
|
if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
2939
3014
|
const resolvedAccountId = codexAccountIdFromAccessToken(resolvedAccess);
|
|
2940
3015
|
if (!resolvedAccountId) {
|
|
2941
3016
|
throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
|
|
2942
3017
|
}
|
|
2943
|
-
const offered = candidateReader ? candidateReader(ctx, "openai-codex") : fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
|
|
2944
|
-
if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
|
|
2945
3018
|
const { accountId, storedAccess } = selectCodexResetCredential(
|
|
2946
3019
|
offered.candidates,
|
|
2947
3020
|
resolvedAccess,
|
|
@@ -3155,10 +3228,10 @@ function formatUsageReport(report2, displayState) {
|
|
|
3155
3228
|
}
|
|
3156
3229
|
return lines.join("\n").trimEnd();
|
|
3157
3230
|
}
|
|
3158
|
-
function formatUsageStatusline(report2, model, now = Date.now(), showCodexResetCountdown = true) {
|
|
3231
|
+
function formatUsageStatusline(report2, model, now = Date.now(), showCodexResetCountdown = true, codexStatusPercentage = "remaining") {
|
|
3159
3232
|
if (report2.providerId === "baseten") return formatBasetenStatusline(report2);
|
|
3160
3233
|
if (report2.providerId === "openai-codex") {
|
|
3161
|
-
return formatCodexStatusline(report2, model, now, showCodexResetCountdown);
|
|
3234
|
+
return formatCodexStatusline(report2, model, now, showCodexResetCountdown, codexStatusPercentage);
|
|
3162
3235
|
}
|
|
3163
3236
|
if (report2.providerId === "deepseek") return formatDeepSeekStatusline(report2);
|
|
3164
3237
|
if (report2.providerId === "fireworks") return formatFireworksStatusline(report2);
|
|
@@ -3353,6 +3426,12 @@ function formatOpenCodeZenStatusline(report2) {
|
|
|
3353
3426
|
function formatKimiCodingReport(lines, report2) {
|
|
3354
3427
|
for (const bucket of report2.buckets) {
|
|
3355
3428
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
3429
|
+
if (bucket.unit === "percent") {
|
|
3430
|
+
const remaining = bucket.remaining === void 0 ? void 0 : Math.round(clampPercent4(bucket.remaining));
|
|
3431
|
+
const value = bucket.used === void 0 || remaining === void 0 ? "unavailable" : `${100 - remaining}% used \xB7 ${remaining}% left`;
|
|
3432
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
|
|
3433
|
+
continue;
|
|
3434
|
+
}
|
|
3356
3435
|
if (bucket.used === void 0 || bucket.limit === void 0) {
|
|
3357
3436
|
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
|
|
3358
3437
|
continue;
|
|
@@ -3381,15 +3460,18 @@ function formatKimiCodingReport(lines, report2) {
|
|
|
3381
3460
|
function formatKimiCodingStatusline(report2) {
|
|
3382
3461
|
const fiveHour = report2.buckets.find((bucket) => bucket.id === "five-hour");
|
|
3383
3462
|
const weekly = report2.buckets.find((bucket) => bucket.id === "weekly");
|
|
3384
|
-
const
|
|
3385
|
-
const
|
|
3463
|
+
const monthly = report2.buckets.find((bucket) => bucket.id === "monthly");
|
|
3464
|
+
const subWindow = fiveHour ?? report2.buckets.find((bucket) => bucket.id !== "weekly" && bucket.id !== "monthly");
|
|
3465
|
+
const selected = [subWindow, weekly, monthly].filter(
|
|
3386
3466
|
(bucket, index, buckets) => bucket !== void 0 && buckets.indexOf(bucket) === index
|
|
3387
3467
|
);
|
|
3388
3468
|
const parts = ["kimi"];
|
|
3389
3469
|
for (const bucket of selected) {
|
|
3390
|
-
if (
|
|
3470
|
+
if (bucket.remaining === void 0 || bucket.unit !== "percent" && !bucket.limit) continue;
|
|
3391
3471
|
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
3392
|
-
|
|
3472
|
+
const window = bucket.id === "monthly" ? "mo" : formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
3473
|
+
const remaining = bucket.unit === "percent" ? Math.round(clampPercent4(bucket.remaining)) : percentRemaining(bucket);
|
|
3474
|
+
parts.push(`${remaining}% ${window}`);
|
|
3393
3475
|
}
|
|
3394
3476
|
return parts.length > 1 ? parts.join(" ") : void 0;
|
|
3395
3477
|
}
|
|
@@ -3563,7 +3645,7 @@ function formatGenericReport(lines, report2) {
|
|
|
3563
3645
|
lines.push(`${`${metric2.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric2.value, metric2.unit)}`);
|
|
3564
3646
|
}
|
|
3565
3647
|
}
|
|
3566
|
-
function formatCodexStatusline(report2, model, now = Date.now(), showResetCountdown = true) {
|
|
3648
|
+
function formatCodexStatusline(report2, model, now = Date.now(), showResetCountdown = true, percentage = "remaining") {
|
|
3567
3649
|
const group = selectCodexGroup(report2, model);
|
|
3568
3650
|
if (!group) return formatCodexCreditsStatus(report2);
|
|
3569
3651
|
const buckets = report2.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
|
|
@@ -3571,7 +3653,8 @@ function formatCodexStatusline(report2, model, now = Date.now(), showResetCountd
|
|
|
3571
3653
|
const parts = [group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`];
|
|
3572
3654
|
for (const bucket of buckets) {
|
|
3573
3655
|
if (bucket.remaining === void 0) continue;
|
|
3574
|
-
const
|
|
3656
|
+
const displayedPercentage = percentage === "used" ? bucket.used ?? 100 - bucket.remaining : bucket.remaining;
|
|
3657
|
+
const percent = `${clampPercent4(displayedPercentage).toFixed(0)}%`;
|
|
3575
3658
|
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
3576
3659
|
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
3577
3660
|
if (!showResetCountdown) {
|
|
@@ -3705,6 +3788,7 @@ var MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
|
3705
3788
|
var DEFAULT_USAGE_SETTINGS = Object.freeze({
|
|
3706
3789
|
codexFastMode: false,
|
|
3707
3790
|
codexStatusResetCountdown: true,
|
|
3791
|
+
codexStatusPercentage: "remaining",
|
|
3708
3792
|
selectedTargets: Object.freeze({})
|
|
3709
3793
|
});
|
|
3710
3794
|
function usageSettingsPath() {
|
|
@@ -3718,6 +3802,9 @@ function normalizeUsageSettings(value) {
|
|
|
3718
3802
|
if (Object.hasOwn(value, "codexStatusResetCountdown") && typeof value.codexStatusResetCountdown !== "boolean") {
|
|
3719
3803
|
return void 0;
|
|
3720
3804
|
}
|
|
3805
|
+
if (Object.hasOwn(value, "codexStatusPercentage") && value.codexStatusPercentage !== "remaining" && value.codexStatusPercentage !== "used") {
|
|
3806
|
+
return void 0;
|
|
3807
|
+
}
|
|
3721
3808
|
if (Object.hasOwn(value, "fireworksAccountId") && !isFireworksAccountId(value.fireworksAccountId)) {
|
|
3722
3809
|
return void 0;
|
|
3723
3810
|
}
|
|
@@ -3730,6 +3817,7 @@ function normalizeUsageSettings(value) {
|
|
|
3730
3817
|
return {
|
|
3731
3818
|
codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode,
|
|
3732
3819
|
codexStatusResetCountdown: typeof value.codexStatusResetCountdown === "boolean" ? value.codexStatusResetCountdown : DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
|
|
3820
|
+
codexStatusPercentage: value.codexStatusPercentage === "used" ? "used" : DEFAULT_USAGE_SETTINGS.codexStatusPercentage,
|
|
3733
3821
|
selectedTargets: effectiveTargets
|
|
3734
3822
|
};
|
|
3735
3823
|
}
|
|
@@ -4132,6 +4220,12 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
|
4132
4220
|
import { Key, matchesKey, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
4133
4221
|
var OFF = "Off";
|
|
4134
4222
|
var ON = "On";
|
|
4223
|
+
var REMAINING = "Remaining";
|
|
4224
|
+
var USED = "Used";
|
|
4225
|
+
function settingValueLabel(id, value) {
|
|
4226
|
+
if (id === "codexStatusPercentage") return value === "used" ? USED : REMAINING;
|
|
4227
|
+
return value === true ? ON : OFF;
|
|
4228
|
+
}
|
|
4135
4229
|
async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent, onApplied) {
|
|
4136
4230
|
if (ctx.mode !== "tui") {
|
|
4137
4231
|
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
|
|
@@ -4160,6 +4254,13 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
|
|
|
4160
4254
|
description: "Show time remaining until each Codex usage limit resets.",
|
|
4161
4255
|
currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
|
|
4162
4256
|
values: [OFF, ON]
|
|
4257
|
+
},
|
|
4258
|
+
{
|
|
4259
|
+
id: "codexStatusPercentage",
|
|
4260
|
+
label: "Codex percentage",
|
|
4261
|
+
description: "Show remaining or used Codex quota in the statusline.",
|
|
4262
|
+
currentValue: settingValueLabel("codexStatusPercentage", state.settings.codexStatusPercentage),
|
|
4263
|
+
values: [REMAINING, USED]
|
|
4163
4264
|
}
|
|
4164
4265
|
];
|
|
4165
4266
|
const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
|
|
@@ -4174,7 +4275,7 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
|
|
|
4174
4275
|
saveQueue = saveQueue.then(async () => {
|
|
4175
4276
|
const previous = settingsRuntime.get().settings[id];
|
|
4176
4277
|
if (settingsRuntime.get().kind === "invalid") {
|
|
4177
|
-
settingsList.updateValue(id, previous
|
|
4278
|
+
settingsList.updateValue(id, settingValueLabel(id, previous));
|
|
4178
4279
|
if (!signal.aborted && isCurrent()) {
|
|
4179
4280
|
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
4180
4281
|
tui.requestRender();
|
|
@@ -4182,10 +4283,12 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
|
|
|
4182
4283
|
return;
|
|
4183
4284
|
}
|
|
4184
4285
|
try {
|
|
4185
|
-
|
|
4286
|
+
const patch = {};
|
|
4287
|
+
patch[id] = requested;
|
|
4288
|
+
await settingsRuntime.update(patch, signal);
|
|
4186
4289
|
} catch (error) {
|
|
4187
4290
|
if (signal.aborted || !isCurrent()) return;
|
|
4188
|
-
settingsList.updateValue(id, previous
|
|
4291
|
+
settingsList.updateValue(id, settingValueLabel(id, previous));
|
|
4189
4292
|
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
4190
4293
|
tui.requestRender();
|
|
4191
4294
|
return;
|
|
@@ -4205,7 +4308,11 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
|
|
|
4205
4308
|
getSettingsListTheme(),
|
|
4206
4309
|
(id, value) => {
|
|
4207
4310
|
if (closing || signal.aborted || !isCurrent()) return;
|
|
4208
|
-
|
|
4311
|
+
if (id === "codexStatusPercentage") {
|
|
4312
|
+
queueUpdate(id, value === USED ? "used" : "remaining", value);
|
|
4313
|
+
} else if (id === "codexFastMode" || id === "codexStatusResetCountdown") {
|
|
4314
|
+
queueUpdate(id, value === ON, value);
|
|
4315
|
+
}
|
|
4209
4316
|
},
|
|
4210
4317
|
cancel
|
|
4211
4318
|
);
|
|
@@ -4334,9 +4441,16 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
4334
4441
|
}
|
|
4335
4442
|
return;
|
|
4336
4443
|
}
|
|
4337
|
-
const
|
|
4444
|
+
const settings = settingsRuntime.get().settings;
|
|
4445
|
+
const showCodexResetCountdown = outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
|
|
4338
4446
|
const now = Date.now();
|
|
4339
|
-
const rawValue = formatUsageStatusline(
|
|
4447
|
+
const rawValue = formatUsageStatusline(
|
|
4448
|
+
outcome.state.report,
|
|
4449
|
+
model,
|
|
4450
|
+
now,
|
|
4451
|
+
showCodexResetCountdown,
|
|
4452
|
+
settings.codexStatusPercentage
|
|
4453
|
+
);
|
|
4340
4454
|
const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : void 0;
|
|
4341
4455
|
if (!safeSetStatus(ctx, value)) return;
|
|
4342
4456
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
@@ -5029,7 +5143,7 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
5029
5143
|
controller.signal,
|
|
5030
5144
|
() => statusGeneration === menuGeneration && !controller.signal.aborted,
|
|
5031
5145
|
(id) => {
|
|
5032
|
-
if (id === "codexStatusResetCountdown" && stableCurrent && statusGeneration === menuGeneration && !controller.signal.aborted) {
|
|
5146
|
+
if ((id === "codexStatusResetCountdown" || id === "codexStatusPercentage") && stableCurrent && statusGeneration === menuGeneration && !controller.signal.aborted) {
|
|
5033
5147
|
publishStableCurrent(ctx, stableCurrent);
|
|
5034
5148
|
}
|
|
5035
5149
|
}
|