@narumitw/pi-usage 0.65.0 → 0.65.1

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
@@ -147,7 +147,7 @@ This preference changes only Codex statusline formatting, not provider data or t
147
147
  ### Codex statusline reset countdown
148
148
 
149
149
  The `codexStatusResetCountdown` preference defaults to `true`. It replaces the window labels with the time remaining until each returned limit resets.
150
- Turn **Codex reset countdown** Off in the TUI Settings screen, or set it to `false` in `pi-usage.json` and run `/reload`, to restore the legacy `5h` and `wk` labels:
150
+ Turn **Codex reset countdown** Off in the TUI Settings screen, or set it to `false` in `pi-usage.json` and run `/reload`, to show static labels such as `codex 5h:59% · wk:61%`:
151
151
 
152
152
  ```json
153
153
  {
@@ -197,6 +197,16 @@ After the active runtime credential changes, the next command, turn, or schedule
197
197
  ## 📊 Statusline behavior
198
198
 
199
199
  The `usage` status item is active only for selected providers that publish statusline usage.
200
+ Codex, Command Code, Kimi, MiniMax Token Plan, Z.AI, and OpenCode Go separate quota windows with ` · ` and place static labels before values, for example `cmd 5h:100% · wk:97% · mo:98%`.
201
+ Countdown segments retain their spacing without window labels, for example `codex 89% ↻ 2h30m · 97% ↻ 5d17h`; a missing reset time falls back to a static label, such as `codex 89% ↻ 2h30m · wk:97%`.
202
+ Single windows have no separator. These presentation changes do not alter the reported values; consumers parsing the old status text must adapt to the new format. ChatGPT companion formatting is unchanged.
203
+
204
+ Kimi, MiniMax Token Plan Global/China, Z.AI Global/China, Command Code, OpenCode Go, and finite GitHub Copilot quotas show countdowns automatically when a displayed quota has a valid future reset time.
205
+ For example, `kimi 77% ↻ 2h52m · mo:87%` mixes a countdown with a static fallback; Copilot preserves its counts, percentage, and any overage, such as `copilot 245/300 82% ↻ 6d`.
206
+ These providers fall back to their existing static output when reset times are missing, invalid, or expired; unlimited quotas and balance/spend reports do not show countdowns.
207
+ Their countdowns are not controlled by the Codex-only settings. Codex and ChatGPT companion retain their existing reset behavior, including `↻ 0m` for expired returned resets.
208
+ Countdowns repaint locally every minute while a displayed eligible reset remains in the future, without additional provider requests; hidden windows and unselected model groups do not activate this repaint for the newly supported providers.
209
+
200
210
  It refreshes every five minutes while the session remains on such a provider and is cleared when the model changes to an unsupported or menu-only provider.
201
211
  DeepSeek publishes each returned currency as a separate exact balance segment and reports when the API is unavailable.
202
212
  Fireworks publishes exact per-currency rated spend totals and reports when no rated usage exists.
package/dist/index.ts CHANGED
@@ -421,6 +421,14 @@ function formatUsageReport(report, displayState) {
421
421
  return lines.join("\n").trimEnd();
422
422
  }
423
423
  function formatUsageStatusline(report, model, now = Date.now(), showCodexResetCountdown = true, codexStatusPercentage = "remaining") {
424
+ return formatUsageStatuslineWithCountdown(report, model, now, showCodexResetCountdown, codexStatusPercentage).value;
425
+ }
426
+ function formatUsageStatuslineWithCountdown(report, model, now = Date.now(), showCodexResetCountdown = true, codexStatusPercentage = "remaining") {
427
+ const countdown = { now, active: false };
428
+ const value = formatStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage, countdown);
429
+ return { value, hasCountdown: countdown.active };
430
+ }
431
+ function formatStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage, countdown) {
424
432
  if (report.providerId === "openai" && report.source === "openai-chatgpt-auth") return "chatgpt usage: web only";
425
433
  if (report.source === "openai-chatgpt-companion") {
426
434
  const windows = report.buckets.filter((bucket) => bucket.groupId === "chatgpt-plan").map((bucket) => {
@@ -437,24 +445,24 @@ function formatUsageStatusline(report, model, now = Date.now(), showCodexResetCo
437
445
  if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
438
446
  if (report.providerId === "fireworks") return formatFireworksStatusline(report);
439
447
  if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
440
- if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
448
+ if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report, countdown);
441
449
  if (report.providerId === "openrouter") {
442
450
  const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
443
451
  if (limit?.remaining !== void 0) return `openrouter ${formatUsd(limit.remaining)} left`;
444
452
  const total = report.metrics.find((metric) => metric.id === "usage-total");
445
453
  if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
446
454
  }
447
- if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
448
- if (report.providerId === "command-code") return formatCommandCodeStatusline(report);
449
- if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
455
+ if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report, countdown);
456
+ if (report.providerId === "command-code") return formatCommandCodeStatusline(report, countdown);
457
+ if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report, countdown);
450
458
  if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
451
459
  return formatMoonshotStatusline(report);
452
460
  }
453
461
  if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
454
- return formatMiniMaxStatusline(report, model);
462
+ return formatMiniMaxStatusline(report, countdown, model);
455
463
  }
456
464
  if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
457
- return formatZaiStatusline(report);
465
+ return formatZaiStatusline(report, countdown);
458
466
  }
459
467
  return void 0;
460
468
  }
@@ -572,7 +580,7 @@ function formatGitHubCopilotReport(lines, report) {
572
580
  lines.push(`${"Additional usage:".padEnd(VALUE_COLUMN)}${overage.value} ${quota.label}`);
573
581
  }
574
582
  }
575
- function formatGitHubCopilotStatusline(report) {
583
+ function formatGitHubCopilotStatusline(report, countdown) {
576
584
  const quota = findGitHubCopilotQuota(report);
577
585
  const kind = compactGitHubCopilotQuotaKind(quota);
578
586
  if (!quota || quota.limit === void 0 || quota.remaining === void 0) {
@@ -580,7 +588,8 @@ function formatGitHubCopilotStatusline(report) {
580
588
  }
581
589
  const overage = report.metrics.find((metric) => metric.id === "overage-used");
582
590
  const overageSuffix = typeof overage?.value === "number" && overage.value > 0 ? ` +${overage.value} over` : "";
583
- return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}`;
591
+ const reset = formatQuotaCountdown(quota, countdown);
592
+ return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}${reset ? ` \u21BB ${reset}` : ""}`;
584
593
  }
585
594
  function findGitHubCopilotQuota(report) {
586
595
  return report.buckets.find((bucket) => ["ai-credits", "premium-requests", "chat-requests"].includes(bucket.id));
@@ -616,14 +625,14 @@ function formatOpenCodeZenReport(lines, report) {
616
625
  lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
617
626
  }
618
627
  }
619
- function formatOpenCodeZenStatusline(report) {
620
- const parts = ["zen"];
628
+ function formatOpenCodeZenStatusline(report, countdown) {
629
+ const parts = [];
621
630
  for (const bucket of report.buckets) {
622
631
  if (bucket.used === void 0) continue;
623
632
  const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
624
- parts.push(`${clampPercent(bucket.used).toFixed(0)}% ${compact}`);
633
+ parts.push(formatQuotaWindow(bucket, `${clampPercent(bucket.used).toFixed(0)}%`, compact, countdown));
625
634
  }
626
- return parts.length > 1 ? parts.join(" ") : void 0;
635
+ return parts.length > 0 ? `zen ${parts.join(" \xB7 ")}` : void 0;
627
636
  }
628
637
  function formatCommandCodeReport(lines, report) {
629
638
  for (const bucket of report.buckets) {
@@ -640,8 +649,8 @@ function formatCommandCodeReport(lines, report) {
640
649
  lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
641
650
  }
642
651
  }
643
- function formatCommandCodeStatusline(report) {
644
- const parts = ["cmd"];
652
+ function formatCommandCodeStatusline(report, countdown) {
653
+ const parts = [];
645
654
  for (const [id, label] of [
646
655
  ["five-hour", "5h"],
647
656
  ["weekly", "wk"],
@@ -649,9 +658,9 @@ function formatCommandCodeStatusline(report) {
649
658
  ]) {
650
659
  const bucket = report.buckets.find((candidate) => candidate.id === id);
651
660
  if (bucket?.unit !== "usd" || bucket.limit === void 0 || bucket.remaining === void 0) continue;
652
- parts.push(`${percentRemaining(bucket)}% ${label}`);
661
+ parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, label, countdown));
653
662
  }
654
- return parts.length > 1 ? parts.join(" ") : void 0;
663
+ return parts.length > 0 ? `cmd ${parts.join(" \xB7 ")}` : void 0;
655
664
  }
656
665
  function formatKimiCodingReport(lines, report) {
657
666
  for (const bucket of report.buckets) {
@@ -687,7 +696,7 @@ function formatKimiCodingReport(lines, report) {
687
696
  lines.push(`${"Monthly limit:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyLimit)}`);
688
697
  }
689
698
  }
690
- function formatKimiCodingStatusline(report) {
699
+ function formatKimiCodingStatusline(report, countdown) {
691
700
  const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
692
701
  const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
693
702
  const monthly = report.buckets.find((bucket) => bucket.id === "monthly");
@@ -695,15 +704,15 @@ function formatKimiCodingStatusline(report) {
695
704
  const selected = [subWindow, weekly, monthly].filter(
696
705
  (bucket, index, buckets) => bucket !== void 0 && buckets.indexOf(bucket) === index
697
706
  );
698
- const parts = ["kimi"];
707
+ const parts = [];
699
708
  for (const bucket of selected) {
700
709
  if (bucket.remaining === void 0 || bucket.unit !== "percent" && !bucket.limit) continue;
701
710
  const fallback = bucket.id === "weekly" ? "weekly" : "5h";
702
711
  const window = bucket.id === "monthly" ? "mo" : formatWindowLabel(bucket.windowMinutes, fallback, true);
703
712
  const remaining = bucket.unit === "percent" ? Math.round(clampPercent(bucket.remaining)) : percentRemaining(bucket);
704
- parts.push(`${remaining}% ${window}`);
713
+ parts.push(formatQuotaWindow(bucket, `${remaining}%`, window, countdown));
705
714
  }
706
- return parts.length > 1 ? parts.join(" ") : void 0;
715
+ return parts.length > 0 ? `kimi ${parts.join(" \xB7 ")}` : void 0;
707
716
  }
708
717
  function formatMoonshotReport(lines, report) {
709
718
  for (const metric of report.metrics) {
@@ -731,7 +740,7 @@ function formatMiniMaxReport(lines, report) {
731
740
  lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}`);
732
741
  }
733
742
  }
734
- function formatMiniMaxStatusline(report, model) {
743
+ function formatMiniMaxStatusline(report, countdown, model) {
735
744
  const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
736
745
  if (report.source === "minimax-account-balance") {
737
746
  const available = report.metrics.find((metric) => metric.id === "available-balance");
@@ -740,22 +749,22 @@ function formatMiniMaxStatusline(report, model) {
740
749
  const selectedGroup = selectMiniMaxGroup(report, model);
741
750
  if (!selectedGroup) return void 0;
742
751
  const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
743
- const parts = [prefix];
752
+ const parts = [];
744
753
  for (const bucket of selected) {
745
754
  const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
746
755
  const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
747
756
  if (bucket.period === "unlimited") {
748
- parts.push(`unlimited ${window}`);
757
+ parts.push(`${window}:unlimited`);
749
758
  continue;
750
759
  }
751
760
  if (bucket.unit === "percent" && bucket.remaining !== void 0) {
752
- parts.push(`${bucket.remaining}% ${window}`);
761
+ parts.push(formatQuotaWindow(bucket, `${bucket.remaining}%`, window, countdown));
753
762
  continue;
754
763
  }
755
764
  if (bucket.limit === void 0 || bucket.remaining === void 0) continue;
756
- parts.push(`${percentRemaining(bucket)}% ${window}`);
765
+ parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, window, countdown));
757
766
  }
758
- return parts.length > 1 ? parts.join(" ") : void 0;
767
+ return parts.length > 0 ? `${prefix} ${parts.join(" \xB7 ")}` : void 0;
759
768
  }
760
769
  function selectMiniMaxGroup(report, model) {
761
770
  const groups = [
@@ -797,18 +806,25 @@ function wildcardKeyMatches(pattern, value) {
797
806
  const last = segments.at(-1);
798
807
  return pattern.endsWith("*") || last !== void 0 && value.endsWith(last);
799
808
  }
800
- function formatZaiStatusline(report) {
809
+ function formatZaiStatusline(report, countdown) {
801
810
  const selected = [
802
811
  report.buckets.find((bucket) => bucket.id === "five-hour"),
803
812
  report.buckets.find((bucket) => bucket.id === "weekly")
804
813
  ];
805
- const parts = ["zai"];
814
+ const parts = [];
806
815
  for (const bucket of selected) {
807
816
  if (!bucket?.limit || bucket.remaining === void 0) continue;
808
817
  const fallback = bucket.id === "weekly" ? "weekly" : "5h";
809
- parts.push(`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`);
818
+ parts.push(
819
+ formatQuotaWindow(
820
+ bucket,
821
+ `${percentRemaining(bucket)}%`,
822
+ formatWindowLabel(bucket.windowMinutes, fallback, true),
823
+ countdown
824
+ )
825
+ );
810
826
  }
811
- return parts.length > 1 ? parts.join(" ") : void 0;
827
+ return parts.length > 0 ? `zai ${parts.join(" \xB7 ")}` : void 0;
812
828
  }
813
829
  function formatCurrencyMetric(metric) {
814
830
  if (typeof metric.value !== "number") return String(metric.value);
@@ -891,7 +907,8 @@ function formatCodexStatusline(report, model, now = Date.now(), showResetCountdo
891
907
  if (!group) return formatCodexCreditsStatus(report);
892
908
  const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
893
909
  const labelBucket = buckets[0];
894
- const parts = [group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`];
910
+ const prefix = group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`;
911
+ const parts = [];
895
912
  for (const bucket of buckets) {
896
913
  if (bucket.remaining === void 0) continue;
897
914
  const displayedPercentage = percentage === "used" ? bucket.used ?? 100 - bucket.remaining : bucket.remaining;
@@ -899,13 +916,13 @@ function formatCodexStatusline(report, model, now = Date.now(), showResetCountdo
899
916
  const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
900
917
  const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
901
918
  if (!showResetCountdown) {
902
- parts.push(`${percent} ${window}`);
919
+ parts.push(`${window}:${percent}`);
903
920
  continue;
904
921
  }
905
922
  const reset = formatResetCountdown(bucket.resetsAt, now);
906
- parts.push(`${percent} ${reset ? `\u21BB ${reset}` : window}`);
923
+ parts.push(reset ? `${percent} \u21BB ${reset}` : `${window}:${percent}`);
907
924
  }
908
- return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
925
+ return parts.length > 0 ? `${prefix} ${parts.join(" \xB7 ")}` : formatCodexCreditsStatus(report);
909
926
  }
910
927
  function formatCodexCreditsStatus(report) {
911
928
  const credits = report.metrics.find((metric) => metric.id === "credits");
@@ -984,6 +1001,17 @@ function formatWindowLabel(minutes, fallback, compact) {
984
1001
  if (minutes % 60 === 0) return `${minutes / 60}h`;
985
1002
  return `${minutes}m`;
986
1003
  }
1004
+ function formatQuotaWindow(bucket, percent, window, countdown) {
1005
+ const reset = formatQuotaCountdown(bucket, countdown);
1006
+ return reset ? `${percent} \u21BB ${reset}` : `${window}:${percent}`;
1007
+ }
1008
+ function formatQuotaCountdown(bucket, countdown) {
1009
+ if (bucket.period === "unlimited" || bucket.resetsAt === void 0 || !Number.isFinite(bucket.resetsAt * 1e3) || !Number.isFinite(countdown.now) || bucket.resetsAt * 1e3 <= countdown.now)
1010
+ return void 0;
1011
+ const reset = formatResetCountdown(bucket.resetsAt, countdown.now);
1012
+ if (reset !== void 0) countdown.active = true;
1013
+ return reset;
1014
+ }
987
1015
  function formatResetCountdown(resetsAt, now) {
988
1016
  if (resetsAt === void 0 || !Number.isFinite(resetsAt) || !Number.isFinite(now)) return void 0;
989
1017
  const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1e3 - now) / 6e4));
@@ -1942,23 +1970,23 @@ function usageExtension(pi, dependencies = {}) {
1942
1970
  const showCodexResetCountdown = outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
1943
1971
  const isCompanionReport = outcome.state.report.source === "openai-chatgpt-companion";
1944
1972
  const now = Date.now();
1945
- const rawValue = formatUsageStatusline(
1973
+ const status = formatUsageStatuslineWithCountdown(
1946
1974
  outcome.state.report,
1947
1975
  model,
1948
1976
  now,
1949
1977
  showCodexResetCountdown,
1950
1978
  settings.codexStatusPercentage
1951
1979
  );
1952
- const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : void 0;
1980
+ const value = status.value ? fastRuntime.decorateStatus(model, status.value) : void 0;
1953
1981
  if (!safeSetStatus(ctx, value)) return;
1954
1982
  if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
1955
- if (sessionActive && (showCodexResetCountdown || isCompanionReport) && outcome.state.report.buckets.some(
1983
+ if (sessionActive && (status.hasCountdown || (showCodexResetCountdown || isCompanionReport) && outcome.state.report.buckets.some(
1956
1984
  (bucket) => (!isCompanionReport || bucket.groupId === "chatgpt-plan") && bucket.resetsAt !== void 0 && Number.isFinite(bucket.resetsAt) && bucket.resetsAt * 1e3 > now
1957
- )) {
1985
+ ))) {
1958
1986
  const generation = statusGeneration;
1959
1987
  statusCountdownTimer = setTimeout(() => {
1960
- statusCountdownTimer = void 0;
1961
1988
  if (!sessionActive || generation !== statusGeneration) return;
1989
+ statusCountdownTimer = void 0;
1962
1990
  publishStatus(ctx, outcome, model, false);
1963
1991
  }, STATUS_COUNTDOWN_REFRESH_MS);
1964
1992
  statusCountdownTimer.unref?.();