diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx
index 218a7f21feb1..b07f65d739e2 100644
--- a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx
+++ b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx
@@ -182,7 +182,7 @@ function PoolWindowCard({
const inlineLabels = wide && segmentWidth >= 96;
const inlineReset = wide && segmentWidth >= 150;
const inlineName = wide && segmentWidth >= 220;
- const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0);
+ const nextReset = pool.resets[0];
const openAccount = (account: LimitAccount) =>
navigation.navigate("SettingsUsageAccount", {
accountKey: account.key,
@@ -202,18 +202,25 @@ function PoolWindowCard({
{pool.remainingPercent}%
- left
+
+ {pool.members.length > 1 ? "average left" : "left"}
+
+ {pool.members.length > 1 ? (
+
+ Across {pool.members.length} accounts
+
+ ) : null}
{pool.pace ? (
{PACE_LABEL[pool.pace]}
) : null}
{description ? {description} : null}
- {nextRefill ? (
+ {nextReset ? (
- ↻ +{nextRefill.restoresPercent}%{" "}
- {formatResetsIn(nextRefill.member.window, now)
+ Next account reset{" "}
+ {formatResetsIn(nextReset.member.window, now)
?.replace("resets in ", "in ")
.replace("resets now", "now")}
@@ -598,9 +605,11 @@ export function UsageLimitAccountScreen({ route }: AccountScreenProps) {
Even pace: {evenPace}% left by now
) : null}
- {reset && reset.restoresPercent > 0 ? (
+ {reset && reset.member.refillPercent > 0 ? (
- Restores {reset.restoresPercent}% of the pool
+ After reset:{" "}
+ {Math.round(Math.min(100, remainingPercent(window) + reset.member.refillPercent))}
+ % left on this account
) : null}
diff --git a/apps/web/src/components/usage/UsageLimitsPooled.browser.test.tsx b/apps/web/src/components/usage/UsageLimitsPooled.browser.test.tsx
index ab114fbbd6f2..8cca10b9e70c 100644
--- a/apps/web/src/components/usage/UsageLimitsPooled.browser.test.tsx
+++ b/apps/web/src/components/usage/UsageLimitsPooled.browser.test.tsx
@@ -2,12 +2,16 @@ import {
EnvironmentId,
ProviderDriverKind,
ProviderInstanceId,
+ UsageLimitSourceId,
type ServerProvider,
} from "@t3tools/contracts";
-import { CHATGPT_USAGE_URL } from "@t3tools/shared/usageLimits";
+import { type LimitPresentations, CHATGPT_USAGE_URL } from "@t3tools/shared/usageLimits";
import { describe, expect, it } from "vite-plus/test";
+import { page } from "vite-plus/test/browser";
import { render } from "../../../test/render";
+import { AppAtomRegistryProvider } from "../../rpc/atomRegistry";
+import "../../index.css";
import { UsageLimitsPooled } from "./UsageLimitsPooled";
const checkedAt = "2026-10-25T16:00:00.000Z";
@@ -63,3 +67,93 @@ describe("UsageLimitsPooled ChatGPT usage card", () => {
.toBeVisible();
});
});
+
+it("keeps the account label and reset summary on a one-account row of a multi-account provider", async () => {
+ const now = Date.parse("2026-10-28T01:00:00.000Z");
+ const checkedAt = new Date(now).toISOString();
+ const driver = ProviderDriverKind.make("codex");
+ const presentations: LimitPresentations = new Map([
+ [
+ EnvironmentId.make("demo"),
+ {
+ entry: { target: { label: "Demo" } },
+ serverConfig: {
+ providers: [],
+ usageLimitSources: [
+ {
+ id: UsageLimitSourceId.make("demo"),
+ kind: "cliproxy",
+ label: "Demo",
+ checkedAt,
+ accounts: [
+ {
+ id: "session-only",
+ driver,
+ usageLimits: {
+ checkedAt,
+ windows: [
+ {
+ id: "primary",
+ kind: "session",
+ label: "Session",
+ usedPercent: 0,
+ resetsAt: "2026-10-28T03:55:00.000Z",
+ },
+ ],
+ },
+ },
+ {
+ id: "later-session",
+ driver,
+ usageLimits: {
+ checkedAt,
+ windows: [
+ {
+ id: "primary",
+ kind: "session",
+ label: "Session",
+ usedPercent: 0.6,
+ resetsAt: "2026-10-28T05:00:00.000Z",
+ },
+ ],
+ },
+ },
+ {
+ id: "monthly-only",
+ driver,
+ usageLimits: {
+ checkedAt,
+ windows: [
+ {
+ id: "primary",
+ kind: "monthly",
+ label: "Monthly",
+ usedPercent: 2,
+ resetsAt: "2026-11-26T18:00:00.000Z",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ ]);
+ const container = document.createElement("div");
+ container.style.width = "1000px";
+ await using screen = await render(
+
+
+ ,
+ { container },
+ );
+ await expect.element(screen.getByText("monthly-only", { exact: true }).first()).toBeVisible();
+ await expect.element(screen.getByText("Next account reset in 29d 17h")).toBeVisible();
+ await expect.element(screen.getByText("Next account reset in 2h 55m")).toBeVisible();
+ await expect.element(screen.getByText("average left", { exact: true })).toBeVisible();
+ await expect.element(screen.getByText("Across 2 accounts", { exact: true })).toBeVisible();
+ await screen.getByRole("button", { name: /^later-session: 99% left/ }).click();
+ await expect.element(page.getByText("100% left on this account")).toBeVisible();
+});
diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx
index 8ec1c7a02afc..e98880ce6645 100644
--- a/apps/web/src/components/usage/UsageLimitsPooled.tsx
+++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx
@@ -189,8 +189,11 @@ function SegmentPopover({
) : null}
{timeLeft !== null ? {timeLeft}% left by now
: null}
- {reset && reset.restoresPercent > 0 ? (
- +{reset.restoresPercent}% of pool
+ {reset && reset.member.refillPercent > 0 ? (
+
+ {Math.round(Math.min(100, remaining + reset.member.refillPercent))}% left on this
+ account
+
) : null}
{choices.length > 0 && redeem ? (
@@ -498,10 +501,12 @@ function PoolBar({
pool,
color,
now,
+ providerAccountCount,
}: {
readonly pool: LimitPoolWindow;
readonly color: string;
readonly now: number;
+ readonly providerAccountCount: number;
}) {
const restores = new Map(pool.resets.map((reset) => [reset.member.account.key, reset]));
return (
@@ -521,7 +526,7 @@ function PoolBar({
color={normalizeProviderAccentColor(member.account.accentColor) ?? color}
now={now}
index={position + 1}
- showAccountName={pool.columns.length > 1}
+ showAccountName={providerAccountCount > 1}
/>
) : pool.kind === "monthly" && member.account.limits.zeroSpendCap ? (
reset.restoresPercent > 0);
+ const nextReset = pool.resets[0];
return (
@@ -567,17 +574,23 @@ function PoolWindowCard({
{pool.remainingPercent}%
- left
+ {pool.members.length > 1 ? "average left" : "left"}
{pool.pace ? : null}
- {nextRefill && pool.columns.length > 1 ? (
+ {pool.members.length > 1 ? (
+
+ Across {pool.members.length} accounts
+
+ ) : null}
+ {nextReset && providerAccountCount > 1 ? (
- ↻ +{nextRefill.restoresPercent}%
+ Next account reset{" "}
+ {formatResetsIn(nextReset.member.window, now)?.replace("resets ", "")}
) : null}
-
+
{description ? (
{description}
) : null}
@@ -608,6 +621,7 @@ function PoolSection({ pool, now }: { readonly pool: LimitPool; readonly now: nu
{
});
});
- it("preserves gaps without counting missing windows toward pooled quota", () => {
+ it("fills each row with only accounts reporting that window", () => {
const [pool] = collectLimitPools(
[
account("a", [window]),
@@ -1352,18 +1352,49 @@ describe("pooled account columns", () => {
{ ...weekly, usedPercent: 80 },
]),
account("c", [weekly]),
+ {
+ ...account("capped", []),
+ limits: { checkedAt: "2026-09-03T11:00:00.000Z", windows: [], zeroSpendCap: true },
+ },
+ account("monthly", [{ ...window, kind: "monthly" }]),
],
now,
);
expect(keys(pool!)).toEqual([
- ["a", "b", null],
- [null, "b", "c"],
+ ["a", "b"],
+ ["b", "c"],
+ [null, "monthly"],
+ ]);
+ expect(pool!.windows[2]!.columns.map(({ account }) => account.key)).toEqual([
+ "capped",
+ "monthly",
]);
expect(pool!.windows[1]!.members.map((member) => member.account.key)).toEqual(["b", "c"]);
expect(pool!.windows[1]!.remainingPercent).toBe(40);
expect(pool!.windows[1]!.resets.map((reset) => reset.restoresPercent)).toEqual([40, 20]);
});
+ it("gives two Codex session accounts equal slots and a monthly-only account its own full row", () => {
+ const codex = (key: string, windows: LimitAccount["limits"]["windows"]) => ({
+ ...account(key, windows),
+ driver: ProviderDriverKind.make("codex"),
+ });
+ const [pool] = collectLimitPools(
+ [
+ codex("a", [window, { ...weekly, usedPercent: 90 }]),
+ codex("b", [window, { ...weekly, usedPercent: 100 }]),
+ codex("monthly", [{ ...window, kind: "monthly", usedPercent: 2 }]),
+ ],
+ now,
+ );
+ expect(keys(pool!)).toEqual([["a", "b"], ["a", "b"], ["monthly"]]);
+ expect(pool!.windows[1]!.remainingPercent).toBe(5);
+ expect(pool!.windows[1]!.members.map((member) => 100 - member.window.usedPercent)).toEqual([
+ 10, 0,
+ ]);
+ expect(pool!.windows[2]!.remainingPercent).toBe(98);
+ });
+
it("falls back to weekly resets when no account reports a session", () => {
const [pool] = collectLimitPools(
[
diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts
index 6fe6c70a89a4..e65d6c3fb337 100644
--- a/packages/shared/src/usageLimits.ts
+++ b/packages/shared/src/usageLimits.ts
@@ -693,9 +693,14 @@ function poolWindows(accounts: readonly LimitAccount[], now: number): readonly L
kind: first.kind,
label: first.label,
members,
- columns: accounts.map(
- (account) => memberByAccount.get(account.key) ?? { account, window: null },
- ),
+ columns: accounts.flatMap((account) => {
+ const member = memberByAccount.get(account.key);
+ return member
+ ? [member]
+ : first.kind === "monthly" && account.limits.zeroSpendCap
+ ? [{ account, window: null }]
+ : [];
+ }),
usedPercent: Math.round(usedPercent),
remainingPercent: Math.round(100 - usedPercent),
pace: meanElapsed === null ? null : paceOfShares(timedUsed, meanElapsed),