diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.test.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.test.tsx new file mode 100644 index 0000000000..ecc01c6dcc --- /dev/null +++ b/apps/mobile/src/features/usage/UsageLimitsPooled.test.tsx @@ -0,0 +1,133 @@ +import { + EnvironmentId, + ProviderDriverKind, + ProviderInstanceId, + type ServerProvider, + type ServerProviderUsageWindow, +} from "@t3tools/contracts"; +import { act, create, type ReactTestRenderer } from "react-test-renderer"; +import { describe, expect, it, vi } from "vite-plus/test"; + +const state = vi.hoisted(() => ({ presentations: new Map(), navigate: vi.fn() })); +vi.mock("@effect/atom-react", () => ({ useAtomValue: () => state.presentations })); +vi.mock("@react-navigation/native", () => ({ + useNavigation: () => ({ navigate: state.navigate }), +})); +vi.mock("react-native", () => ({ + Platform: { OS: "ios" }, + Pressable: "Pressable", + ScrollView: "ScrollView", + View: "View", +})); +vi.mock("react-native-svg", () => ({ + Svg: "Svg", + Defs: "Defs", + Path: "Path", + Pattern: "Pattern", + Rect: "Rect", +})); +vi.mock("react-native-safe-area-context", () => ({ + useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), +})); +vi.mock("../../components/AndroidScreenHeader", () => ({ AndroidScreenHeader: () => null })); +vi.mock("../../components/AppSymbol", () => ({ SymbolView: () => null })); +vi.mock("../../components/AppText", () => ({ AppText: "Text" })); +vi.mock("../../components/ProviderIcon", () => ({ ProviderIcon: () => null })); +vi.mock("../../native/StackHeader", () => ({ NativeStackScreenOptions: {} })); +vi.mock("../../state/presentation", () => ({ + environmentPresentations: { presentationsAtom: null }, +})); +vi.mock("./UsageLimitsSection", () => ({ ResetCredits: () => null })); +vi.mock("./usageProviders", () => ({ + useProviderColors: () => ({ codex: "#000", claude: "#fff" }), +})); + +import { UsageLimitsSection } from "./UsageLimitsPooled"; + +Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); +const now = Date.parse("2026-09-03T12:00:00Z"); +const home = EnvironmentId.make("home"); +const windows: readonly ServerProviderUsageWindow[] = [ + { id: "totalPercentUsed", kind: "monthly", label: "Monthly", usedPercent: 15 }, + { id: "apiPercentUsed", kind: "monthly", label: "Monthly · API", usedPercent: 49 }, + { id: "autoPercentUsed", kind: "monthly", label: "Monthly · Auto", usedPercent: 9 }, +]; +function presentation(readings: readonly ServerProviderUsageWindow[]) { + const provider = { + instanceId: ProviderInstanceId.make("cursor"), + driver: ProviderDriverKind.make("cursor"), + enabled: true, + installed: true, + version: null, + status: "ready", + auth: { status: "authenticated", email: "cursor@example.com" }, + checkedAt: "2026-09-03T11:00:00Z", + models: [], + slashCommands: [], + skills: [], + usageLimits: { checkedAt: "2026-09-03T11:00:00Z", windows: readings }, + } satisfies ServerProvider; + return { entry: { target: { label: "Home" } }, serverConfig: { providers: [provider] } }; +} + +describe("mobile Cursor Limits", () => { + it("keeps pool labels, older readings and account navigation correct when the selection changes", async () => { + state.presentations = new Map([[home, presentation(windows)]]); + state.navigate.mockReset(); + let renderer: ReactTestRenderer | undefined; + try { + await act(async () => { + renderer = create( + , + ); + }); + let tree = JSON.stringify(renderer?.toJSON()); + expect(tree).toContain("Cursor Models"); + expect(tree).toContain("Other Models"); + expect(tree).toContain("Auto can use either pool."); + expect(tree).not.toContain("Overall"); + const open = renderer?.root.findAll( + (node) => node.props.accessibilityHint === "Show account details", + )[0]; + await act(async () => open?.props.onPress()); + expect(state.navigate).toHaveBeenCalledWith( + "SettingsSheet", + expect.objectContaining({ + params: expect.objectContaining({ + params: expect.objectContaining({ + windowId: "autoPercentUsed", + environmentIds: [home], + }), + }), + }), + ); + state.presentations = new Map([[home, presentation([windows[0]!])]]); + await act(async () => + renderer?.update( + , + ), + ); + tree = JSON.stringify(renderer?.toJSON()); + expect(tree).toContain("Overall"); + expect(tree).not.toContain("Cursor Models"); + await act(async () => + renderer?.update( + , + ), + ); + tree = JSON.stringify(renderer?.toJSON()); + expect(tree).not.toContain("Overall"); + expect(tree).toContain("Select an environment to see limits."); + } finally { + await act(async () => renderer?.unmount()); + } + }); +}); diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx index 40bfe653d3..6cb945027a 100644 --- a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx +++ b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx @@ -5,6 +5,8 @@ import { collectLimitAccounts, collectLimitNotices, collectLimitPools, + cursorUsageWindowDetails, + displayLimitWindows, formatDuration, formatResetsIn, remainingPercent, @@ -79,11 +81,15 @@ function PoolWindowCard({ color, now, environmentIds, + label, + description, }: { readonly pool: LimitPoolWindow; readonly color: string; readonly now: number; readonly environmentIds: readonly string[] | null; + readonly label?: string | undefined; + readonly description?: string | undefined; }) { const navigation = useNavigation(); const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0); @@ -105,7 +111,7 @@ function PoolWindowCard({ - {pool.label} + {label ?? pool.label} {pool.remainingPercent}% @@ -117,6 +123,7 @@ function PoolWindowCard({ {PACE_LABEL[pool.pace]} ) : null} + {description ? {description} : null} {nextRefill ? ( ↻ +{nextRefill.restoresPercent}%{" "} @@ -235,15 +242,23 @@ export function UsageLimitsSection({ {DRIVER_LABEL[pool.driver] ?? pool.driver} - {pool.windows.map((window) => ( - - ))} + {displayLimitWindows(pool).map((window) => { + const details = + pool.driver === "cursor" ? cursorUsageWindowDetails(window.id) : undefined; + return ( + + ); + })} ))} {notices.length > 0 || failedLabels.length > 0 ? ( diff --git a/apps/server/src/provider/Layers/CursorProvider.test.ts b/apps/server/src/provider/Layers/CursorProvider.test.ts index 835579c4e9..28c2f7210f 100644 --- a/apps/server/src/provider/Layers/CursorProvider.test.ts +++ b/apps/server/src/provider/Layers/CursorProvider.test.ts @@ -893,21 +893,21 @@ describe("Cursor usage limits", () => { { id: "totalPercentUsed", kind: "monthly", - label: "Monthly", + label: "Overall", usedPercent: 72.4, resetsAt: "2026-09-20T03:53:06.000Z", }, { id: "autoPercentUsed", kind: "monthly", - label: "Monthly · Auto", + label: "Cursor Models", usedPercent: 69.5, resetsAt: "2026-09-20T03:53:06.000Z", }, { id: "apiPercentUsed", kind: "monthly", - label: "Monthly · API", + label: "Other Models", usedPercent: 100, resetsAt: "2026-09-20T03:53:06.000Z", }, @@ -921,10 +921,10 @@ describe("Cursor usage limits", () => { ); expect( cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 0 } }, checkedAt).windows, - ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Monthly", usedPercent: 0 }]); + ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Overall", usedPercent: 0 }]); expect( cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 150 } }, checkedAt).windows, - ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Monthly", usedPercent: 100 }]); + ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Overall", usedPercent: 100 }]); }); it("reads the instance's credentials and endpoint even when usage enabled is false", async () => { diff --git a/apps/server/src/provider/Layers/cursorUsageLimits.ts b/apps/server/src/provider/Layers/cursorUsageLimits.ts index 238809edeb..a525478854 100644 --- a/apps/server/src/provider/Layers/cursorUsageLimits.ts +++ b/apps/server/src/provider/Layers/cursorUsageLimits.ts @@ -1,6 +1,7 @@ import * as NodeOS from "node:os"; import type { CursorSettings, ServerProviderUsageWindow } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -39,15 +40,11 @@ export function cursorUsageResponseToLimits( : undefined; const windows: ServerProviderUsageWindow[] = []; if (response.planUsage) { - for (const [key, label] of [ - ["totalPercentUsed", "Monthly"], - ["autoPercentUsed", "Monthly · Auto"], - ["apiPercentUsed", "Monthly · API"], - ] as const) { - const usedPercent = response.planUsage[key]; + for (const { id, label } of CURSOR_USAGE_WINDOWS) { + const usedPercent = response.planUsage[id]; if (usedPercent === undefined || !Number.isFinite(usedPercent)) continue; windows.push({ - id: key, + id, kind: "monthly", label, usedPercent: clampPercent(usedPercent), diff --git a/apps/web/src/components/usage/UsageLimitsPooled.test.tsx b/apps/web/src/components/usage/UsageLimitsPooled.test.tsx new file mode 100644 index 0000000000..66344c0699 --- /dev/null +++ b/apps/web/src/components/usage/UsageLimitsPooled.test.tsx @@ -0,0 +1,84 @@ +import { + EnvironmentId, + ProviderDriverKind, + ProviderInstanceId, + type ServerProvider, + type ServerProviderUsageWindow, +} from "@t3tools/contracts"; +import type { ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vite-plus/test"; + +vi.mock("../../hooks/useSettings", () => ({ usePrimarySettings: () => ({}) })); +vi.mock("../chat/ProviderInstanceIcon", () => ({ ProviderInstanceIcon: () => null })); +vi.mock("../settings/providerDriverMeta", () => ({ getDriverOption: () => ({ label: "Cursor" }) })); +vi.mock("./UsageLimits", () => ({ + PaceIcon: () => null, + ResetCreditDialog: () => null, + barColor: () => "#000", + resetCreditsSummary: () => null, + useResetCredit: () => ({}), +})); +vi.mock("../ui/popover", () => ({ + Popover: ({ children }: { children: ReactNode }) => children, + PopoverTrigger: ({ children }: { children: ReactNode }) => , + PopoverPopup: () => null, +})); + +import { UsageLimitsPooled } from "./UsageLimitsPooled"; + +const now = Date.parse("2026-09-03T12:00:00Z"); +const windows: readonly ServerProviderUsageWindow[] = [ + { id: "totalPercentUsed", kind: "monthly", label: "Monthly", usedPercent: 15 }, + { id: "apiPercentUsed", kind: "monthly", label: "Monthly · API", usedPercent: 49 }, + { id: "autoPercentUsed", kind: "monthly", label: "Monthly · Auto", usedPercent: 9 }, +]; +function provider(readings = windows, id = "cursor") { + return { + instanceId: ProviderInstanceId.make(id), + driver: ProviderDriverKind.make("cursor"), + enabled: true, + installed: true, + version: null, + status: "ready", + auth: { status: "authenticated", email: `${id}@example.com` }, + checkedAt: "2026-09-03T11:00:00Z", + models: [], + slashCommands: [], + skills: [], + usageLimits: { checkedAt: "2026-09-03T11:00:00Z", windows: readings }, + } satisfies ServerProvider; +} +function render(providers: readonly ServerProvider[]) { + const presentations = new Map([ + [ + EnvironmentId.make("desktop"), + { entry: { target: { label: "Desktop" } }, serverConfig: { providers } }, + ], + ]); + return renderToStaticMarkup(); +} + +describe("Cursor Limits cards", () => { + it("uses existing readings to explain both usable pools without showing a third quota", () => { + const markup = render([provider()]); + expect(markup).toContain("Cursor Models"); + expect(markup).toContain("Other Models"); + expect(markup).toContain("Auto can use either pool."); + expect(markup).not.toContain(">Overall<"); + expect(markup).not.toContain(">Monthly<"); + expect(markup.indexOf("Cursor Models")).toBeLessThan(markup.indexOf("Other Models")); + }); + it("keeps combined-only accounts and unfamiliar readings visible", () => { + const markup = render([ + provider(), + provider( + [windows[0]!, { id: "future", kind: "other", label: "Future allowance", usedPercent: 20 }], + "older", + ), + ]); + expect(markup).toContain(">Overall<"); + expect(markup).toContain("Combined usage across both allowances, not a third quota."); + expect(markup).toContain("Future allowance"); + }); +}); diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx index fd0471cea4..94cd1886ca 100644 --- a/apps/web/src/components/usage/UsageLimitsPooled.tsx +++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx @@ -2,7 +2,8 @@ import { collectLimitAccounts, collectLimitNotices, collectLimitPools, - formatDuration, + cursorUsageWindowDetails, + displayLimitWindows, formatResetsIn, type LimitAccount, type LimitPool, @@ -224,6 +225,7 @@ function PoolSegment({ color, now, index, + showAccountName, }: { readonly account: LimitAccount; readonly window: LimitPoolMember["window"]; @@ -232,6 +234,7 @@ function PoolSegment({ readonly now: number; /** 1-based position in the bar, shown on the strip and its legend row to tie them together. */ readonly index: number; + readonly showAccountName: boolean; }) { const [open, setOpen] = useState(false); const remaining = remainingPercent(window); @@ -274,7 +277,12 @@ function PoolSegment({ {index}
- + {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,