@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 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 Codex reset countdown through Pi's settings-list interaction in TUI mode.
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`, and `gpt-5.6-luna` at `https://chatgpt.com`.
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 protocol carries no account name or extension identity.
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
- return (ctx, providerId) => collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader);
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
- buckets.sort((left, right) => (left.windowMinutes ?? 0) - (right.windowMinutes ?? 0));
942
- const metrics = parseBoosterWallet(root.boosterWallet);
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 subWindow = fiveHour ?? report2.buckets.find((bucket) => bucket.id !== "weekly");
3385
- const selected = [subWindow, weekly].filter(
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 (!bucket.limit || bucket.remaining === void 0) continue;
3470
+ if (bucket.remaining === void 0 || bucket.unit !== "percent" && !bucket.limit) continue;
3391
3471
  const fallback = bucket.id === "weekly" ? "weekly" : "5h";
3392
- parts.push(`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`);
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 percent = `${clampPercent4(bucket.remaining).toFixed(0)}%`;
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 ? ON : OFF);
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
- await settingsRuntime.update({ [id]: requested }, signal);
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 ? ON : OFF);
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
- queueUpdate(id, value !== OFF, value);
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 showCodexResetCountdown = outcome.state.report.providerId === "openai-codex" && settingsRuntime.get().settings.codexStatusResetCountdown;
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(outcome.state.report, model, now, showCodexResetCountdown);
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
  }