-
+ {showAccountName ? (
+
+ ) : null}
{remaining}%
{/* Countdown and badge get their own plate: fill and hatching run under them otherwise. */}
@@ -462,6 +470,7 @@ function PoolBar({
color={color}
now={now}
index={position + 1}
+ showAccountName={pool.columns.length > 1}
/>
) : null,
)}
@@ -478,17 +487,21 @@ function PoolWindowCard({
pool,
color,
now,
+ label,
+ description,
}: {
readonly pool: LimitPoolWindow;
readonly color: string;
readonly now: number;
+ readonly label?: string | undefined;
+ readonly description?: string | undefined;
}) {
// The soonest reset that hands anything back; an untouched account resets to no effect.
const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0);
return (
-
{pool.label}
+
{label ?? pool.label}
{pool.remainingPercent}%
@@ -496,14 +509,16 @@ function PoolWindowCard({
left
{pool.pace ? : null}
- {nextRefill ? (
-
- ↻ +{nextRefill.restoresPercent}%{" "}
- {nextRefill.at <= now ? "now" : `in ${formatDuration(nextRefill.at - now)}`}
+ {nextRefill && pool.columns.length > 1 ? (
+
+ ↻ +{nextRefill.restoresPercent}%
) : null}
+ {description ? (
+
{description}
+ ) : null}
);
}
@@ -523,9 +538,19 @@ function PoolSection({ pool, now }: { readonly pool: LimitPool; readonly now: nu
/>
{label}
- {pool.windows.map((window) => (
-
- ))}
+ {displayLimitWindows(pool).map((window) => {
+ const details = pool.driver === "cursor" ? cursorUsageWindowDetails(window.id) : undefined;
+ return (
+
+ );
+ })}
);
}
diff --git a/docs/user/usage.md b/docs/user/usage.md
index 6d28ceae7a..74813b8846 100644
--- a/docs/user/usage.md
+++ b/docs/user/usage.md
@@ -100,10 +100,12 @@ proxy through `ANTHROPIC_AUTH_TOKEN`.
OpenCode Go reports its session, weekly, and monthly allowance when OpenCode runs locally in
the environment. Pylon cannot report limits for external OpenCode servers because their credentials
belong to the remote server. Cursor reports
-its monthly allowance, including separate Auto and API usage, using a file-based CLI login or
+its monthly allowance as **Cursor Models** and **Other Models** pools, using a file-based CLI login or
`CURSOR_AUTH_TOKEN`. Cursor's default macOS keychain login does not currently report limits.
On macOS, use `AGENT_CLI_CREDENTIAL_STORE=file` when signing in and in the provider's environment
to use a file-based login.
+When both pools are reported, Pylon shows them separately; their combined percentage is not a
+third allowance. Accounts that report only a combined total keep an **Overall** reading.
Grok reports the remaining subscription allowance and reset time for its current billing period
after signing in with `grok login`. Explicit `XAI_API_KEY` connections and custom authentication
diff --git a/packages/shared/src/usageFormat.test.ts b/packages/shared/src/usageFormat.test.ts
index 0ebce8fa53..48998fcc5d 100644
--- a/packages/shared/src/usageFormat.test.ts
+++ b/packages/shared/src/usageFormat.test.ts
@@ -5,11 +5,23 @@ import {
enumerateHourStarts,
formatDateTimeShort,
formatHourShort,
+ formatPercent,
formatRelativeHourShort,
formatUsageContractMismatch,
makeWindow,
} from "./usageFormat.ts";
+describe("formatPercent", () => {
+ it("distinguishes small positive usage from zero at the requested precision", () => {
+ expect(formatPercent(0)).toBe("0.0%");
+ expect(formatPercent(0.0004)).toBe("<0.1%");
+ expect(formatPercent(0.0009)).toBe("<0.1%");
+ expect(formatPercent(0.001)).toBe("0.1%");
+ expect(formatPercent(0.023)).toBe("2.3%");
+ expect(formatPercent(0.00004, 2)).toBe("<0.01%");
+ });
+});
+
describe("usage contract mismatch wording", () => {
it("identifies which side needs updating", () => {
expect(formatUsageContractMismatch("Office", { direction: "serverBehind" })).toBe(
diff --git a/packages/shared/src/usageFormat.ts b/packages/shared/src/usageFormat.ts
index 2b2775d045..c9d9da813f 100644
--- a/packages/shared/src/usageFormat.ts
+++ b/packages/shared/src/usageFormat.ts
@@ -44,7 +44,10 @@ function trim(value: number): string {
}
export function formatPercent(share: number, digits = 1): string {
- return `${(share * 100).toFixed(digits)}%`;
+ const percent = share * 100;
+ const smallest = 10 ** -digits;
+ if (percent > 0 && percent < smallest) return `<${smallest.toFixed(digits)}%`;
+ return `${percent.toFixed(digits)}%`;
}
export function formatUsageContractMismatch(
diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts
index 583d12a59c..150a676d7e 100644
--- a/packages/shared/src/usageLimits.test.ts
+++ b/packages/shared/src/usageLimits.test.ts
@@ -19,6 +19,8 @@ import {
collectLimitAccounts,
collectLimitNotices,
collectLimitPools,
+ cursorUsageWindowDetails,
+ displayLimitWindows,
collectLimitSources,
collectLimitsGroups,
elapsedShare,
@@ -1127,6 +1129,100 @@ describe("pooled account columns", () => {
});
});
+describe("Cursor limit presentation", () => {
+ const readings = [
+ { id: "apiPercentUsed", kind: "monthly", label: "Monthly · API", usedPercent: 49 },
+ { id: "autoPercentUsed", kind: "monthly", label: "Monthly · Auto", usedPercent: 9 },
+ { id: "totalPercentUsed", kind: "monthly", label: "Monthly", usedPercent: 15 },
+ ] as const;
+ function pool(
+ providers = [
+ provider({
+ driver: ProviderDriverKind.make("cursor"),
+ usageLimits: { checkedAt: "2026-09-03T11:00:00Z", windows: readings },
+ }),
+ ],
+ ) {
+ const accounts = collectLimitAccounts(
+ new Map([
+ [
+ EnvironmentId.make("home"),
+ { entry: { target: { label: "Home" } }, serverConfig: { providers } },
+ ],
+ ]),
+ );
+ return collectLimitPools(accounts, now)[0]!;
+ }
+ it("orders the two allowances and hides their combined percentage", () => {
+ const original = pool();
+ const raw = [...original.windows];
+ Object.freeze(original.windows);
+ expect(displayLimitWindows(original).map((window) => window.id)).toEqual([
+ "autoPercentUsed",
+ "apiPercentUsed",
+ ]);
+ expect(original.windows).toEqual(raw);
+ expect(cursorUsageWindowDetails("totalPercentUsed")?.description).toContain(
+ "not a third quota",
+ );
+ expect(cursorUsageWindowDetails("future")).toBeUndefined();
+ });
+ it.each(["autoPercentUsed", "apiPercentUsed"])("retains Overall when %s is absent", (missing) => {
+ const original = pool([
+ provider({
+ driver: ProviderDriverKind.make("cursor"),
+ usageLimits: {
+ checkedAt: "2026-09-03T11:00:00Z",
+ windows: readings.filter((window) => window.id !== missing),
+ },
+ }),
+ ]);
+ expect(displayLimitWindows(original)[0]?.id).toBe("totalPercentUsed");
+ });
+ it("keeps a combined-only account across mixed-version hosts", () => {
+ const original = pool([
+ provider({
+ instanceId: ProviderInstanceId.make("new"),
+ driver: ProviderDriverKind.make("cursor"),
+ auth: { status: "authenticated", email: "new@example.com" },
+ usageLimits: { checkedAt: "2026-09-03T11:00:00Z", windows: readings },
+ }),
+ provider({
+ instanceId: ProviderInstanceId.make("old"),
+ driver: ProviderDriverKind.make("cursor"),
+ auth: { status: "authenticated", email: "old@example.com" },
+ usageLimits: { checkedAt: "2026-09-03T11:00:00Z", windows: [readings[2]] },
+ }),
+ ]);
+ expect(displayLimitWindows(original).map((window) => window.id)).toEqual([
+ "totalPercentUsed",
+ "autoPercentUsed",
+ "apiPercentUsed",
+ ]);
+ expect(original.accounts).toHaveLength(2);
+ });
+ it("keeps unfamiliar windows and other providers unchanged", () => {
+ const original = pool([
+ provider({
+ driver: ProviderDriverKind.make("cursor"),
+ usageLimits: {
+ checkedAt: "2026-09-03T11:00:00Z",
+ windows: [...readings, { id: "future", kind: "other", label: "Future", usedPercent: 20 }],
+ },
+ }),
+ ]);
+ expect(displayLimitWindows(original).map((window) => window.id)).toEqual([
+ "autoPercentUsed",
+ "apiPercentUsed",
+ "future",
+ ]);
+ const other = pool([
+ provider({ usageLimits: { checkedAt: "2026-09-03T11:00:00Z", windows: readings } }),
+ ]);
+ expect(displayLimitWindows(other)).toBe(other.windows);
+ });
+});
+
describe("collectLimitNotices", () => {
const checkedAt = "2026-09-03T11:00:00.000Z";
const claude = ProviderDriverKind.make("claudeAgent");
diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts
index e2395e8998..6d5a636a23 100644
--- a/packages/shared/src/usageLimits.ts
+++ b/packages/shared/src/usageLimits.ts
@@ -26,6 +26,33 @@ const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
+export const CURSOR_USAGE_WINDOWS = [
+ {
+ id: "totalPercentUsed",
+ label: "Overall",
+ description: "Combined usage across both allowances, not a third quota.",
+ },
+ {
+ id: "autoPercentUsed",
+ label: "Cursor Models",
+ description: "Grok and Composer use this first. Auto can use either pool.",
+ },
+ {
+ id: "apiPercentUsed",
+ label: "Other Models",
+ description: "Claude, GPT, and Gemini use this pool. Grok and Composer fall back here.",
+ },
+] as const;
+
+export function cursorUsageWindowDetails(id: string) {
+ return CURSOR_USAGE_WINDOWS.find((window) => window.id === id);
+}
+
+function cursorUsageWindowRank(id: string): number {
+ const rank = CURSOR_USAGE_WINDOWS.findIndex((window) => window.id === id);
+ return rank < 0 ? CURSOR_USAGE_WINDOWS.length : rank;
+}
+
/** Keep an uncertain native redemption's identity across retries and account-view remounts. */
export function createResetCreditAttempts(createRequestId: () => string) {
const pending = new Map();
@@ -475,6 +502,26 @@ export interface LimitPool {
readonly windows: readonly LimitPoolWindow[];
}
+/** Show Cursor's usable pools without treating their combined percentage as another quota. */
+export function displayLimitWindows(pool: LimitPool) {
+ if (pool.driver !== "cursor") return pool.windows;
+ const hasAuto = pool.windows.some((window) => window.id === "autoPercentUsed");
+ const hasApi = pool.windows.some((window) => window.id === "apiPercentUsed");
+ // Mixed-version accounts can report only Overall. Keep that card unless
+ // every account it represents also reports both individual allowances.
+ const combinedAccountsComplete = pool.accounts.every(
+ (account) =>
+ !account.limits.windows.some((window) => window.id === "totalPercentUsed") ||
+ ["autoPercentUsed", "apiPercentUsed"].every((id) =>
+ account.limits.windows.some((window) => window.id === id),
+ ),
+ );
+ const hasBothPools = hasAuto && hasApi && combinedAccountsComplete;
+ return pool.windows
+ .filter((window) => !hasBothPools || window.id !== "totalPercentUsed")
+ .sort((left, right) => cursorUsageWindowRank(left.id) - cursorUsageWindowRank(right.id));
+}
+
const WINDOW_KIND_ORDER: Record = {
session: 0,
weekly: 1,