diff --git a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx
index 19e7e7e03c6e3..dd663e5fb7f23 100644
--- a/apps/mobile/src/features/usage/UsageLimitsPooled.tsx
+++ b/apps/mobile/src/features/usage/UsageLimitsPooled.tsx
@@ -186,7 +186,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,
@@ -204,18 +204,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")}
@@ -602,9 +609,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 ab114fbbd6f21..a60f0a85025d8 100644
--- a/apps/web/src/components/usage/UsageLimitsPooled.browser.test.tsx
+++ b/apps/web/src/components/usage/UsageLimitsPooled.browser.test.tsx
@@ -2,12 +2,15 @@ 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 { 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 +66,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(screen.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 e55f5d3857f39..9ce8641ad2bc8 100644
--- a/apps/web/src/components/usage/UsageLimitsPooled.tsx
+++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx
@@ -191,8 +191,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}
{credits && redeem ? (
@@ -495,10 +498,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 (
@@ -518,7 +523,7 @@ function PoolBar({
color={color}
now={now}
index={position + 1}
- showAccountName={pool.columns.length > 1}
+ showAccountName={providerAccountCount > 1}
/>
) : null,
)}
@@ -531,16 +536,18 @@ function PoolWindowCard({
pool,
color,
now,
+ providerAccountCount,
label,
description,
}: {
readonly pool: LimitPoolWindow;
readonly color: string;
readonly now: number;
+ readonly providerAccountCount: number;
readonly label?: string | undefined;
readonly description?: string | undefined;
}) {
- const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0);
+ const nextReset = pool.resets[0];
return (
@@ -550,17 +557,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}
@@ -590,6 +603,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]),
@@ -962,14 +962,35 @@ describe("pooled account columns", () => {
now,
);
expect(keys(pool!)).toEqual([
- ["a", "b", null],
- [null, "b", "c"],
+ ["a", "b"],
+ ["b", "c"],
]);
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 3dca207a2efb7..deb28f802d155 100644
--- a/packages/shared/src/usageLimits.ts
+++ b/packages/shared/src/usageLimits.ts
@@ -550,7 +550,6 @@ function poolWindows(accounts: readonly LimitAccount[], now: number): readonly L
}
}
const pools = [...byKey].map(([key, members]): LimitPoolWindow => {
- const memberByAccount = new Map(members.map((member) => [member.account.key, member]));
const first = members[0]!.window;
const id = members[0]!.account.driver === "codex" ? key : first.id;
const usedPercent = members.reduce((sum, m) => sum + m.window.usedPercent, 0) / members.length;
@@ -581,9 +580,7 @@ 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: members,
usedPercent: Math.round(usedPercent),
remainingPercent: Math.round(100 - usedPercent),
pace: meanElapsed === null ? null : paceOfShares(timedUsed, meanElapsed),