@latentminds/pi-quotas 0.2.4 → 0.3.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
@@ -1,12 +1,14 @@
1
1
  # @latentminds/pi-quotas
2
2
 
3
- Quota monitoring for the [Pi coding agent](https://github.com/mariozechner/pi). Shows remaining usage and rate limits for Anthropic, OpenAI Codex, GitHub Copilot, OpenRouter, and Synthetic — directly in your Pi session.
3
+ Quota monitoring for Pi. Shows remaining usage and rate limits for Anthropic, OpenAI Codex, GitHub Copilot, OpenRouter, Synthetic, Z.ai, and OpenCode Go — directly in your Pi session.
4
4
 
5
5
  ## Screenshots
6
6
 
7
+
7
8
  | `/quotas` dashboard | Footer status |
8
- |---|---|
9
- | ![Quotas dashboard](docs/quotas-dashboard.png) | ![Footer status](docs/footer-status.png) |
9
+ | ------------------- | ------------- |
10
+ | Quotas dashboard | Footer status |
11
+
10
12
 
11
13
  ## Install
12
14
 
@@ -31,15 +33,20 @@ pi -e npm:@latentminds/pi-quotas
31
33
 
32
34
  ## Commands
33
35
 
34
- | Command | Description |
35
- |---------|-------------|
36
- | `/quotas` | Combined quota dashboard for all providers |
37
- | `/anthropic:quotas` | Anthropic quotas only |
38
- | `/codex:quotas` | OpenAI Codex quotas only |
39
- | `/github:quotas` | GitHub Copilot quotas only |
40
- | `/openrouter:quotas` | OpenRouter quotas only |
41
- | `/synthetic:quotas` | Synthetic quotas only |
42
- | `/quotas:settings` | Toggle individual features on or off |
36
+
37
+ | Command | Description |
38
+ | -------------------- | ------------------------------------------ |
39
+ | `/quotas` | Combined quota dashboard for all providers |
40
+ | `/anthropic:quotas` | Anthropic quotas only |
41
+ | `/codex:quotas` | OpenAI Codex quotas only |
42
+ | `/github:quotas` | GitHub Copilot quotas only |
43
+ | `/openrouter:quotas` | OpenRouter quotas only |
44
+ | `/synthetic:quotas` | Synthetic quotas only |
45
+ | `/zai:quotas` | Z.ai quotas only |
46
+ | `/opencode-go:quotas`| OpenCode Go quotas only |
47
+ | `/tokens` | Cross-session token/cost usage |
48
+ | `/quotas:settings` | Toggle individual features on or off |
49
+
43
50
 
44
51
  ## Features
45
52
 
@@ -58,8 +65,9 @@ Automatic notifications when projected usage is on track to exceed limits before
58
65
  ### Per-feature toggles
59
66
 
60
67
  Use `/quotas:settings` to enable or disable:
68
+
61
69
  - Combined `/quotas` command
62
- - Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, `/synthetic:quotas`)
70
+ - Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, `/synthetic:quotas`, `/zai:quotas`, `/opencode-go:quotas`)
63
71
  - Footer status widget
64
72
  - Quota warning notifications
65
73
  - **Defer to Synthetic** — when both pi-quotas and [pi-synthetic](https://www.npmjs.com/package/@aliou/pi-synthetic) are loaded, pi-quotas hides its own Synthetic footer to avoid showing duplicate quota information. Enabled by default; disable if you prefer to see both footers.
@@ -68,13 +76,17 @@ Settings can be saved globally (`~/.pi/agent/extensions/quotas.json`) or per-pro
68
76
 
69
77
  ## Supported providers
70
78
 
71
- | Provider | Windows | Details |
72
- |----------|---------|---------|
73
- | Anthropic | 5h, 7d, per-model 7d, extra usage | Utilization percentages; optional overage budget in local currency |
74
- | OpenAI Codex | 5h, 7d, credits, spend cap | Rate-limit percentages; credit balance; spend-cap reached/OK |
75
- | GitHub Copilot | Premium/chat/completions per month | Remaining/entitlement counts with overage indicators |
76
- | OpenRouter | Monthly budget, daily/weekly/monthly usage | USD spending tracking with cents precision; optional per-key budget limits; UTC-based period resets |
77
- | Synthetic | Subscription, search/hour, free tools, weekly tokens, 5h limit | Request counts and token budgets; rolling five-hour rate limit; weekly token regen |
79
+
80
+ | Provider | Windows | Details |
81
+ | -------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
82
+ | Anthropic | 5h, 7d, per-model 7d, extra usage | Utilization percentages; optional overage budget in local currency |
83
+ | OpenAI Codex | 5h, 7d, credits, spend cap | Rate-limit percentages; credit balance; spend-cap reached/OK |
84
+ | GitHub Copilot | Premium/chat/completions per month | Remaining/entitlement counts with overage indicators |
85
+ | OpenRouter | Monthly budget, daily/weekly/monthly usage | USD spending tracking with cents precision; optional per-key budget limits; UTC-based period resets |
86
+ | Synthetic | Subscription, search/hour, free tools, weekly tokens, 5h limit | Request counts and token budgets; rolling five-hour rate limit; weekly token regen |
87
+ | Z.ai | 5h, 7d, monthly web searches | Token utilisation percentages (rolling 5h/7d windows); monthly web-search count limit |
88
+ | OpenCode Go | Rolling 5h, weekly, monthly USD | USD spend tracking against tier limits; cross-session token/cost aggregation via the `/tokens` command |
89
+
78
90
 
79
91
  ## Credentials
80
92
 
@@ -85,6 +97,8 @@ pi-quotas reads existing Pi auth entries from `~/.pi/agent/auth.json`:
85
97
  - `github-copilot` — GitHub Copilot OAuth token (falls back to `gh auth token` if needed)
86
98
  - `openrouter` — OpenRouter API key (Bearer token)
87
99
  - `synthetic` — Synthetic API key (set the `SYNTHETIC_API_KEY` environment variable)
100
+ - `zai` — Z.ai (Zhipu AI / GLM Coding Plan) API key
101
+ - `opencode-go` — OpenCode Go workspace ID and auth cookie (set the `OPENCODE_GO_WORKSPACE_ID` and `OPENCODE_GO_AUTH_COOKIE` environment variables, or configure them in the OpenCode Go config file)
88
102
 
89
103
  No additional setup is required - if Pi can use the provider, pi-quotas can check its quotas. For Synthetic, export `SYNTHETIC_API_KEY` in your shell or Pi environment.
90
104
 
@@ -104,6 +118,3 @@ See [CHANGELOG.md](CHANGELOG.md) for release notes and recent changes.
104
118
 
105
119
  This project was inspired by [@aliou/pi-synthetic](https://www.npmjs.com/package/@aliou/pi-synthetic).
106
120
 
107
- <p align="center">
108
- <img src="docs/latent-minds@2x.png" alt="Latent Minds" width="320" />
109
- </p>
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@latentminds/pi-quotas",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "author": "Markus Strazds <markus@latentminds.ai>",
8
- "keywords": ["pi-package", "pi-extension", "pi"],
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension",
11
+ "pi"
12
+ ],
9
13
  "repository": {
10
14
  "type": "git",
11
15
  "url": "https://github.com/latentminds-ai/pi-quotas"
@@ -14,30 +18,46 @@
14
18
  "extensions": [
15
19
  "./src/extensions/core/index.ts",
16
20
  "./src/extensions/command-quotas/index.ts",
21
+ "./src/extensions/command-tokens/index.ts",
17
22
  "./src/extensions/usage-status/index.ts",
23
+ "./src/extensions/token-status/index.ts",
18
24
  "./src/extensions/quota-warnings/index.ts"
19
25
  ],
20
26
  "image": "https://raw.githubusercontent.com/latentminds-ai/pi-quotas/main/docs/quotas-dashboard.png"
21
27
  },
22
- "files": ["src", "README.md", "LICENSE"],
28
+ "files": [
29
+ "src",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
23
33
  "peerDependencies": {
24
34
  "@mariozechner/pi-coding-agent": "*",
25
35
  "@mariozechner/pi-tui": "*"
26
36
  },
27
37
  "peerDependenciesMeta": {
28
- "@mariozechner/pi-coding-agent": { "optional": true },
29
- "@mariozechner/pi-tui": { "optional": true }
38
+ "@mariozechner/pi-coding-agent": {
39
+ "optional": true
40
+ },
41
+ "@mariozechner/pi-tui": {
42
+ "optional": true
43
+ }
30
44
  },
31
45
  "devDependencies": {
46
+ "@eslint/js": "^10.0.1",
32
47
  "@mariozechner/pi-coding-agent": "0.61.0",
33
48
  "@mariozechner/pi-tui": "0.61.0",
34
49
  "@sinclair/typebox": "^0.34.48",
35
50
  "@types/node": "^25.0.10",
51
+ "eslint": "^10.6.0",
52
+ "globals": "^17.7.0",
36
53
  "typescript": "^5.9.3",
54
+ "typescript-eslint": "^8.63.0",
37
55
  "vitest": "^4.0.18"
38
56
  },
39
57
  "scripts": {
40
58
  "typecheck": "tsc --noEmit",
59
+ "lint": "eslint src",
60
+ "lint:fix": "eslint src --fix",
41
61
  "test": "vitest run",
42
62
  "test:watch": "vitest"
43
63
  }
package/src/config.ts CHANGED
@@ -9,6 +9,7 @@ export type QuotasFeatureId =
9
9
  | "quotasCommand"
10
10
  | "providerCommands"
11
11
  | "usageStatus"
12
+ | "tokenStatus"
12
13
  | "quotaWarnings"
13
14
  | "deferToSynthetic";
14
15
 
@@ -16,8 +17,7 @@ export const QUOTAS_EXTENSIONS_REQUEST_EVENT =
16
17
  "quotas:extensions:request" as const;
17
18
  export const QUOTAS_EXTENSIONS_REGISTER_EVENT =
18
19
  "quotas:extensions:register" as const;
19
- export const QUOTAS_CONFIG_UPDATED_EVENT =
20
- "quotas:config:updated" as const;
20
+ export const QUOTAS_CONFIG_UPDATED_EVENT = "quotas:config:updated" as const;
21
21
 
22
22
  export interface QuotasExtensionsRegisterPayload {
23
23
  feature: QuotasFeatureId;
@@ -28,6 +28,7 @@ export interface QuotasConfig {
28
28
  quotasCommand?: boolean;
29
29
  providerCommands?: boolean;
30
30
  usageStatus?: boolean;
31
+ tokenStatus?: boolean;
31
32
  quotaWarnings?: boolean;
32
33
  /** When true and pi-synthetic's usage footer is active, hide pi-quotas' Synthetic footer. */
33
34
  deferToSynthetic?: boolean;
@@ -38,6 +39,7 @@ export interface ResolvedQuotasConfig {
38
39
  quotasCommand: boolean;
39
40
  providerCommands: boolean;
40
41
  usageStatus: boolean;
42
+ tokenStatus: boolean;
41
43
  quotaWarnings: boolean;
42
44
  deferToSynthetic: boolean;
43
45
  }
@@ -47,6 +49,7 @@ const DEFAULT_CONFIG: ResolvedQuotasConfig = {
47
49
  quotasCommand: true,
48
50
  providerCommands: true,
49
51
  usageStatus: true,
52
+ tokenStatus: true,
50
53
  quotaWarnings: true,
51
54
  deferToSynthetic: true,
52
55
  };
@@ -81,10 +84,13 @@ class QuotasConfigStore {
81
84
  return {
82
85
  configVersion: input?.configVersion ?? DEFAULT_CONFIG.configVersion,
83
86
  quotasCommand: input?.quotasCommand ?? DEFAULT_CONFIG.quotasCommand,
84
- providerCommands: input?.providerCommands ?? DEFAULT_CONFIG.providerCommands,
87
+ providerCommands:
88
+ input?.providerCommands ?? DEFAULT_CONFIG.providerCommands,
85
89
  usageStatus: input?.usageStatus ?? DEFAULT_CONFIG.usageStatus,
90
+ tokenStatus: input?.tokenStatus ?? DEFAULT_CONFIG.tokenStatus,
86
91
  quotaWarnings: input?.quotaWarnings ?? DEFAULT_CONFIG.quotaWarnings,
87
- deferToSynthetic: input?.deferToSynthetic ?? DEFAULT_CONFIG.deferToSynthetic,
92
+ deferToSynthetic:
93
+ input?.deferToSynthetic ?? DEFAULT_CONFIG.deferToSynthetic,
88
94
  };
89
95
  }
90
96
 
@@ -118,7 +124,11 @@ class QuotasConfigStore {
118
124
  async save(scope: "global" | "local", config: QuotasConfig): Promise<void> {
119
125
  const path = scope === "global" ? this.globalPath() : this.localPath();
120
126
  await mkdir(dirname(path), { recursive: true });
121
- await writeFile(path, JSON.stringify(this.resolve(config), null, 2) + "\n", "utf8");
127
+ await writeFile(
128
+ path,
129
+ JSON.stringify(this.resolve(config), null, 2) + "\n",
130
+ "utf8",
131
+ );
122
132
  await this.load(this.cwd);
123
133
  }
124
134
  }
@@ -126,7 +136,8 @@ class QuotasConfigStore {
126
136
  export const configLoader = new QuotasConfigStore();
127
137
 
128
138
  export async function seedQuotasConfigIfMissing(): Promise<void> {
129
- if (configLoader.hasConfig("global") || configLoader.hasConfig("local")) return;
139
+ if (configLoader.hasConfig("global") || configLoader.hasConfig("local"))
140
+ return;
130
141
  markMigrationNoticePending();
131
142
  try {
132
143
  await configLoader.save("global", DEFAULT_CONFIG);
@@ -158,13 +169,19 @@ const FEATURE_META: Array<{
158
169
  {
159
170
  id: "providerCommands",
160
171
  label: "Provider quota commands",
161
- description: "Toggle `/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, and `/synthetic:quotas`",
172
+ description:
173
+ "Toggle `/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, and `/synthetic:quotas`",
162
174
  },
163
175
  {
164
176
  id: "usageStatus",
165
177
  label: "Usage status",
166
178
  description: "Toggle footer quota status for the active provider",
167
179
  },
180
+ {
181
+ id: "tokenStatus",
182
+ label: "Token usage status",
183
+ description: "Toggle the footer token-usage status and /tokens command",
184
+ },
168
185
  {
169
186
  id: "quotaWarnings",
170
187
  label: "Quota warnings",
@@ -173,7 +190,8 @@ const FEATURE_META: Array<{
173
190
  {
174
191
  id: "deferToSynthetic",
175
192
  label: "Defer to Synthetic",
176
- description: "When pi-synthetic is loaded, hide pi-quotas' Synthetic footer to avoid duplicates",
193
+ description:
194
+ "When pi-synthetic is loaded, hide pi-quotas' Synthetic footer to avoid duplicates",
177
195
  },
178
196
  ];
179
197
 
@@ -196,7 +214,9 @@ export function registerQuotasSettings(
196
214
 
197
215
  while (true) {
198
216
  const choices = FEATURE_META.map((feature) => {
199
- const loaded = getLoadedFeatures().has(feature.id) ? "" : " (not loaded)";
217
+ const loaded = getLoadedFeatures().has(feature.id)
218
+ ? ""
219
+ : " (not loaded)";
200
220
  const enabled = draft[feature.id] ? "enabled" : "disabled";
201
221
  return `${feature.label}: ${enabled}${loaded}`;
202
222
  });
@@ -214,13 +234,23 @@ export function registerQuotasSettings(
214
234
  return;
215
235
  }
216
236
 
217
- const feature = FEATURE_META.find((item) => selected.startsWith(item.label));
237
+ const feature = FEATURE_META.find((item) =>
238
+ selected.startsWith(item.label),
239
+ );
218
240
  if (!feature) continue;
219
- if (feature.id !== "deferToSynthetic" && !getLoadedFeatures().has(feature.id)) {
220
- ctx.ui.notify(`${feature.label} is not loaded by Pi in this session.`, "warning");
241
+ if (
242
+ feature.id !== "deferToSynthetic" &&
243
+ !getLoadedFeatures().has(feature.id)
244
+ ) {
245
+ ctx.ui.notify(
246
+ `${feature.label} is not loaded by Pi in this session.`,
247
+ "warning",
248
+ );
221
249
  continue;
222
250
  }
223
- (draft as unknown as Record<string, boolean>)[feature.id] = !(draft as unknown as Record<string, boolean>)[feature.id];
251
+ (draft as unknown as Record<string, boolean>)[feature.id] = !(
252
+ draft as unknown as Record<string, boolean>
253
+ )[feature.id];
224
254
  }
225
255
  },
226
256
  });
@@ -59,10 +59,32 @@ async function openQuotaView(
59
59
 
60
60
  if (result === undefined) {
61
61
  const snapshots = await loadSnapshots(true);
62
- ctx.ui.notify(JSON.stringify(snapshots, null, 2), "info");
62
+ ctx.ui.notify(formatSnapshotsForNotify(snapshots), "info");
63
63
  }
64
64
  }
65
65
 
66
+ /**
67
+ * Render quota snapshots as a readable multi-line summary for the
68
+ * non-interactive fallback (when `ctx.ui.custom` returns undefined). Avoids
69
+ * dumping raw JSON — which previously leaked raw HTTP error bodies — and
70
+ * skips "not_applicable" providers since they have nothing to report.
71
+ */
72
+ function formatSnapshotsForNotify(snapshots: Snapshot[]): string {
73
+ const lines: string[] = [];
74
+ for (const { provider, result } of snapshots) {
75
+ if (!result.success) {
76
+ if (result.error.kind === "not_applicable") continue;
77
+ lines.push(`${provider}: ${result.error.message}`);
78
+ continue;
79
+ }
80
+ const summary = result.data.windows
81
+ .map((w) => `${w.label} ${w.usedPercent}%`)
82
+ .join(", ");
83
+ lines.push(`${provider}: ${summary || "no windows"}`);
84
+ }
85
+ return lines.join("\n") || "No quota data available";
86
+ }
87
+
66
88
  export function registerQuotasCommands(pi: ExtensionAPI): void {
67
89
  pi.registerCommand("quotas", {
68
90
  description: "Display remaining quotas for Anthropic, Codex, GitHub Copilot, and OpenRouter",
@@ -0,0 +1,154 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import pkg from "../../../../package.json" with { type: "json" };
3
+ import { QuotasComponent } from "./quotas-display.js";
4
+
5
+ const ansi = {
6
+ accent: "\x1b[32m",
7
+ border: "\x1b[36m",
8
+ dim: "\x1b[2m",
9
+ error: "\x1b[31m",
10
+ muted: "\x1b[90m",
11
+ success: "\x1b[32m",
12
+ warning: "\x1b[33m",
13
+ } as const;
14
+
15
+ function fakeTheme() {
16
+ return {
17
+ bold: (text: string) => text,
18
+ fg: (color: keyof typeof ansi, text: string) => `${ansi[color] ?? ""}${text}\x1b[0m`,
19
+ getFgAnsi: (color: keyof typeof ansi) => ansi[color] ?? "",
20
+ };
21
+ }
22
+
23
+ function makeComponent(): QuotasComponent {
24
+ return new QuotasComponent(
25
+ fakeTheme() as any,
26
+ { requestRender: () => {} } as any,
27
+ "Quotas",
28
+ () => {},
29
+ () => {},
30
+ );
31
+ }
32
+
33
+ describe("QuotasComponent", () => {
34
+ it("renders the pi-quotas package version in the dashboard footer", () => {
35
+ const component = makeComponent();
36
+ component.setState({ type: "loaded", snapshots: [] });
37
+
38
+ expect(component.render(70).join("\n")).toContain(`pi-quotas v${pkg.version}`);
39
+ });
40
+
41
+ it("does not render an accent-colored pace marker inside filled warning bars", () => {
42
+ vi.useFakeTimers();
43
+ vi.setSystemTime(new Date("2026-05-14T13:42:11Z"));
44
+
45
+ const component = makeComponent();
46
+ component.setState({
47
+ type: "loaded",
48
+ snapshots: [
49
+ {
50
+ provider: "github-copilot",
51
+ result: {
52
+ success: true,
53
+ data: {
54
+ provider: "github-copilot",
55
+ windows: [
56
+ {
57
+ provider: "github-copilot",
58
+ label: "Premium / month",
59
+ usedPercent: 83,
60
+ resetsAt: new Date("2026-06-01T10:00:00Z"),
61
+ windowSeconds: 31 * 24 * 3600,
62
+ usedValue: 249,
63
+ limitValue: 300,
64
+ showPace: true,
65
+ nextLabel: "Resets",
66
+ nextAmount: "overage allowed",
67
+ },
68
+ ],
69
+ },
70
+ },
71
+ },
72
+ ],
73
+ });
74
+
75
+ const output = component.render(70).join("\n");
76
+
77
+ expect(output).toContain("51/300 left");
78
+ expect(output).not.toContain(`${ansi.accent}|`);
79
+
80
+ vi.useRealTimers();
81
+ });
82
+
83
+ it("does not render a pace marker for zero-usage windows", () => {
84
+ vi.useFakeTimers();
85
+ vi.setSystemTime(new Date("2026-05-14T13:49:29Z"));
86
+
87
+ const component = makeComponent();
88
+ component.setState({
89
+ type: "loaded",
90
+ snapshots: [
91
+ {
92
+ provider: "synthetic",
93
+ result: {
94
+ success: true,
95
+ data: {
96
+ provider: "synthetic",
97
+ windows: [
98
+ {
99
+ provider: "synthetic",
100
+ label: "Credits / week",
101
+ usedPercent: 0,
102
+ resetsAt: new Date("2026-05-14T16:40:29Z"),
103
+ windowSeconds: 7 * 24 * 3600,
104
+ usedValue: 0,
105
+ limitValue: 24,
106
+ isCurrency: true,
107
+ showPace: true,
108
+ nextLabel: "Next regen",
109
+ nextAmount: "+$0.48",
110
+ },
111
+ ],
112
+ },
113
+ },
114
+ },
115
+ ],
116
+ });
117
+
118
+ const output = component.render(70).join("\n");
119
+
120
+ expect(output).toContain("$0.00 / $24.00");
121
+ expect(output).not.toContain("|");
122
+
123
+ vi.useRealTimers();
124
+ });
125
+
126
+ it("renders a not_applicable provider silently with a dim note, not a warning", () => {
127
+ const component = makeComponent();
128
+ component.setState({
129
+ type: "loaded",
130
+ snapshots: [
131
+ {
132
+ provider: "anthropic",
133
+ result: {
134
+ success: false,
135
+ error: {
136
+ kind: "not_applicable",
137
+ message: "Direct API key — no subscription usage to report",
138
+ },
139
+ },
140
+ },
141
+ ],
142
+ });
143
+
144
+ const output = component.render(70).join("\n");
145
+
146
+ expect(output).toContain("Anthropic");
147
+ expect(output).toContain("Direct API key");
148
+ // Rendered as a dim informational note, not a warning.
149
+ expect(output).toContain("\x1b[2m");
150
+ expect(output).not.toContain("\x1b[33m");
151
+ expect(output).not.toContain("usage unavailable");
152
+ expect(output).not.toContain("{");
153
+ });
154
+ });
@@ -2,6 +2,7 @@ import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { DynamicBorder } from "@mariozechner/pi-coding-agent";
3
3
  import type { Component } from "@mariozechner/pi-tui";
4
4
  import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
5
+ import pkg from "../../../../package.json" with { type: "json" };
5
6
  import { PROVIDER_LABELS } from "../../../lib/quotas.js";
6
7
  import type { QuotasResult, SupportedQuotaProvider } from "../../../types/quotas.js";
7
8
  import {
@@ -19,10 +20,6 @@ type QuotasState =
19
20
  | { type: "loading" }
20
21
  | { type: "loaded"; snapshots: Snapshot[] };
21
22
 
22
- function fgAnsiToBg(fgAnsi: string): string {
23
- return fgAnsi.split("[38;").join("[48;").replace(/\[3([0-9])m/g, "[4$1m");
24
- }
25
-
26
23
  function renderProgressBar(
27
24
  percent: number,
28
25
  width: number,
@@ -33,6 +30,7 @@ function renderProgressBar(
33
30
  const clamped = Math.max(0, Math.min(100, Math.round(percent)));
34
31
  const filled = Math.round((clamped / 100) * width);
35
32
  const showPace =
33
+ percent > 0 &&
36
34
  pacePercent !== null &&
37
35
  pacePercent !== undefined &&
38
36
  pacePercent >= 5 &&
@@ -40,15 +38,13 @@ function renderProgressBar(
40
38
  const paceIndex = showPace
41
39
  ? Math.min(width - 1, Math.round((Math.max(0, Math.min(100, pacePercent ?? 0)) / 100) * width))
42
40
  : null;
43
- const reset = "\x1b[0m";
44
41
  const parts: string[] = [];
45
42
  for (let idx = 0; idx < width; idx++) {
46
43
  if (paceIndex !== null && idx === paceIndex) {
47
- const markerColor = idx < filled ? "accent" : fillColor;
48
44
  if (idx < filled) {
49
- parts.push(`${fgAnsiToBg(theme.getFgAnsi(fillColor))}${theme.getFgAnsi(markerColor)}|${reset}`);
45
+ parts.push(theme.fg(fillColor, "█"));
50
46
  } else {
51
- parts.push(theme.fg(markerColor, "|"));
47
+ parts.push(theme.fg(fillColor, "|"));
52
48
  }
53
49
  } else if (idx < filled) {
54
50
  parts.push(theme.fg(fillColor, "█"));
@@ -123,7 +119,7 @@ export class QuotasComponent implements Component {
123
119
  }
124
120
 
125
121
  lines.push("");
126
- lines.push(this.theme.fg("dim", " r to refresh q/Esc to close"));
122
+ lines.push(this.theme.fg("dim", ` pi-quotas v${pkg.version} · r to refresh q/Esc to close`));
127
123
  lines.push(...border.render(width));
128
124
  return lines;
129
125
  }
@@ -144,7 +140,14 @@ export class QuotasComponent implements Component {
144
140
  lines.push(truncateToWidth(` ${this.theme.fg("accent", title)}`, maxWidth));
145
141
 
146
142
  if (!snapshot.result.success) {
147
- lines.push(truncateToWidth(` ${this.theme.fg("warning", snapshot.result.error.message)}`, maxWidth));
143
+ // "not applicable" is an expected, non-error state (e.g. a direct
144
+ // Anthropic API key with no subscription usage) — show a dim note,
145
+ // not a warning-coloured error.
146
+ const { error } = snapshot.result;
147
+ const tone = error.kind === "not_applicable" ? "dim" : "warning";
148
+ lines.push(
149
+ truncateToWidth(` ${this.theme.fg(tone, error.message)}`, maxWidth),
150
+ );
148
151
  return lines;
149
152
  }
150
153
 
@@ -40,4 +40,13 @@ describe("getProviderCommandInfo", () => {
40
40
  title: "OpenRouter Quotas",
41
41
  });
42
42
  });
43
+
44
+ it("maps zai to zai:quotas", () => {
45
+ const info = getProviderCommandInfo("zai");
46
+ expect(info).toMatchObject<Partial<ProviderCommandInfo>>({
47
+ provider: "zai",
48
+ commandName: "zai:quotas",
49
+ title: "Z.ai Quotas",
50
+ });
51
+ });
43
52
  });
@@ -40,5 +40,17 @@ export function getProviderCommandInfo(
40
40
  commandName: "synthetic:quotas",
41
41
  title: "Synthetic Quotas",
42
42
  };
43
+ case "zai":
44
+ return {
45
+ provider,
46
+ commandName: "zai:quotas",
47
+ title: "Z.ai Quotas",
48
+ };
49
+ case "opencode-go":
50
+ return {
51
+ provider,
52
+ commandName: "opencode-go:quotas",
53
+ title: "OpenCode Go Quotas",
54
+ };
43
55
  }
44
56
  }