From 34d142dbe14d50c5f7a95343f15ac4392d071574 Mon Sep 17 00:00:00 2001
From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 04:08:18 +0000
Subject: [PATCH] fix(web): refresh usage limit countdowns without switching
tabs
Co-authored-by: Exotic <118054752+extoci@users.noreply.github.com>
---
apps/web/src/components/usage/UsageLimits.tsx | 8 +-
.../usage/UsagePage.refresh.test.tsx | 179 ++++++++++++++++++
apps/web/src/components/usage/UsagePage.tsx | 5 +-
3 files changed, 187 insertions(+), 5 deletions(-)
create mode 100644 apps/web/src/components/usage/UsagePage.refresh.test.tsx
diff --git a/apps/web/src/components/usage/UsageLimits.tsx b/apps/web/src/components/usage/UsageLimits.tsx
index e50547b66008..6d30d49a7e4a 100644
--- a/apps/web/src/components/usage/UsageLimits.tsx
+++ b/apps/web/src/components/usage/UsageLimits.tsx
@@ -316,17 +316,17 @@ export function ResetCredits({
/**
* Subscription quota across every connected environment's providers and hubs,
- * pooled per provider. Countdowns anchor to render time rather than ticking: a
- * live clock would repaint the page every minute for no decision-changing gain.
+ * pooled per provider. The page advances `now` on explicit refresh rather than
+ * ticking: a live clock would repaint the page for no decision-changing gain.
*/
export function UsageLimitsSection({
selectedEnvironmentIds,
+ now,
}: {
readonly selectedEnvironmentIds: ReadonlySet | null;
+ readonly now: number;
}) {
const presentations = useAtomValue(environmentPresentations.presentationsAtom);
- // Anchored once per mount on purpose: countdowns must not tick (see above).
- const [now] = useState(() => Date.now());
const selected =
selectedEnvironmentIds === null
? presentations
diff --git a/apps/web/src/components/usage/UsagePage.refresh.test.tsx b/apps/web/src/components/usage/UsagePage.refresh.test.tsx
new file mode 100644
index 000000000000..d6d300ce35b0
--- /dev/null
+++ b/apps/web/src/components/usage/UsagePage.refresh.test.tsx
@@ -0,0 +1,179 @@
+import { EnvironmentId, ProviderInstanceId, USAGE_CONTRACT_VERSION } from "@t3tools/contracts";
+import { mergeUsage } from "@t3tools/shared/usageMerge";
+import { act } from "react";
+import { create, type ReactTestRenderer } from "react-test-renderer";
+import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
+
+const state = vi.hoisted(() => ({
+ presentations: new Map(),
+ refreshProviders: vi.fn(async () => undefined),
+}));
+vi.mock("@effect/atom-react", () => ({ useAtomValue: () => state.presentations }));
+vi.mock("../../state/presentation", () => ({
+ environmentPresentations: { presentationsAtom: null },
+}));
+vi.mock("../../state/server", () => ({ serverEnvironment: { refreshProviders: null } }));
+vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => state.refreshProviders }));
+vi.mock("../../env", () => ({ isElectron: false }));
+vi.mock("../../hooks/useSettings", () => ({ usePrimarySettings: () => "24h" }));
+vi.mock("../../state/usage", () => ({
+ useUsage: () => ({
+ merged: mergeUsage([], USAGE_CONTRACT_VERSION),
+ environments: [
+ {
+ environmentId: EnvironmentId.make("test"),
+ label: "Test",
+ isPending: false,
+ error: null,
+ summary: null,
+ },
+ ],
+ selectedEnvironments: [
+ {
+ environmentId: EnvironmentId.make("test"),
+ label: "Test",
+ isPending: false,
+ error: null,
+ summary: null,
+ },
+ ],
+ isPending: false,
+ isPartial: false,
+ refresh: async () => undefined,
+ }),
+}));
+vi.mock("./usagePagePreferences", () => ({
+ readUsagePagePreferences: () => ({ metric: "limits", windowDays: 30 }),
+ saveUsagePagePreferences: vi.fn(),
+}));
+vi.mock("../ui/button", () => ({ Button: "button" }));
+vi.mock("../ui/scroll-area", () => ({ ScrollArea: "div" }));
+vi.mock("../ui/select", () => ({
+ Select: "select",
+ SelectItem: "option",
+ SelectPopup: "div",
+ SelectTrigger: "div",
+ SelectValue: "span",
+}));
+vi.mock("../ui/sidebar", () => ({ SidebarInset: "div" }));
+vi.mock("../ui/toggle-group", () => ({ Toggle: "button", ToggleGroup: "div" }));
+vi.mock("../ui/tooltip", () => ({ Tooltip: "div", TooltipPopup: "div", TooltipTrigger: "div" }));
+vi.mock("../ui/popover", () => ({ Popover: "div", PopoverPopup: "div", PopoverTrigger: "div" }));
+vi.mock("../ui/menu", () => ({
+ Menu: "div",
+ MenuCheckboxItem: "div",
+ MenuItem: "div",
+ MenuPopup: "div",
+ MenuSeparator: "hr",
+ MenuTrigger: "div",
+}));
+vi.mock("../WorkspaceBreadcrumb", () => ({
+ WorkspaceBreadcrumb: "div",
+ WorkspaceBreadcrumbItem: "div",
+ WorkspaceBreadcrumbSeparator: "span",
+}));
+vi.mock("../WorkspacePageContainer", () => ({ WorkspacePageContainer: "main" }));
+vi.mock("../WorkspacePageHeader", () => ({ WorkspacePageHeader: "header" }));
+vi.mock("./UsageProviderChart", () => ({ UsageProviderChart: "div" }));
+vi.mock("./UsagePriceOverrides", () => ({ UsagePriceOverrides: () => null }));
+vi.mock("../chat/ProviderInstanceIcon", () => ({ ProviderInstanceIcon: () => null }));
+vi.mock("../settings/RedactedSensitiveText", () => ({ RedactedSensitiveText: "span" }));
+vi.mock("../settings/providerDriverMeta", () => ({ getDriverOption: () => ({ label: "Codex" }) }));
+
+import { UsagePage } from "./UsagePage";
+
+let renderer: ReactTestRenderer;
+beforeEach(() => {
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+ vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-09-11T12:00:00Z"));
+ state.refreshProviders.mockClear();
+ state.presentations = new Map([
+ [
+ EnvironmentId.make("test"),
+ {
+ entry: { target: { label: "Test" } },
+ connection: { phase: "connected" },
+ serverConfig: {
+ providers: [
+ {
+ instanceId: ProviderInstanceId.make("codex"),
+ driver: "codex",
+ enabled: true,
+ installed: true,
+ version: null,
+ status: "ready",
+ auth: { status: "authenticated" },
+ checkedAt: "2026-09-11T12:00:00Z",
+ models: [],
+ slashCommands: [],
+ skills: [],
+ usageLimits: {
+ checkedAt: "2026-09-11T12:00:00Z",
+ windows: [
+ {
+ id: "five_hour",
+ kind: "session",
+ label: "Session",
+ usedPercent: 40,
+ windowDurationMins: 300,
+ resetsAt: "2026-09-11T14:00:00Z",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ ]);
+});
+afterEach(async () => {
+ await act(() => renderer?.unmount());
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+});
+
+it.each([0, 1])(
+ "refreshes the visible limits countdown with refresh button %i without switching tabs, even when quota is unchanged",
+ async (buttonIndex) => {
+ await act(() => {
+ renderer = create();
+ });
+ expect(
+ JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)),
+ ).toContain("in 2h 0m");
+ vi.mocked(Date.now).mockReturnValue(Date.parse("2026-09-11T12:30:00Z"));
+ await act(async () => {
+ renderer.root
+ .findAllByProps({ "aria-label": "Refresh limits" })
+ .filter((node) => node.type === "button")
+ .at(buttonIndex)!
+ .props.onClick();
+ });
+ expect(state.refreshProviders).toHaveBeenCalledWith({ environmentId: "test", input: {} });
+ expect(
+ JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)),
+ ).toContain("in 1h 30m");
+ expect(
+ JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)),
+ ).not.toContain("in 2h 0m");
+ },
+);
+
+it("uses the current time when returning to limits from tokens", async () => {
+ await act(() => {
+ renderer = create();
+ });
+ const selectMetric = (metric: string) => {
+ renderer.root
+ .findAll((node) => node.type === "div" && node.props["aria-label"] === "Usage metric")[0]!
+ .props.onValueChange([metric]);
+ };
+ await act(() => selectMetric("tokens"));
+ vi.mocked(Date.now).mockReturnValue(Date.parse("2026-09-11T13:00:00Z"));
+ await act(() => selectMetric("limits"));
+ expect(
+ JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)),
+ ).toContain("in 1h 0m");
+ expect(state.refreshProviders).not.toHaveBeenCalled();
+});
diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx
index 21970c675596..2da7414d9337 100644
--- a/apps/web/src/components/usage/UsagePage.tsx
+++ b/apps/web/src/components/usage/UsagePage.tsx
@@ -104,6 +104,7 @@ export function UsagePage() {
const metric = preferences.metric;
const showingLimits = metric === "limits";
const [isRefreshing, setIsRefreshing] = useState(false);
+ const [limitsNow, setLimitsNow] = useState(() => Date.now());
const refreshingRef = useRef(false);
const [breakdown, setBreakdown] = useState<"model" | "time">("model");
const [selectedEnvironmentIds, setSelectedEnvironmentIds] =
@@ -159,6 +160,7 @@ export function UsagePage() {
});
};
const selectMetric = (nextMetric: UsageMetric) => {
+ if (nextMetric === "limits") setLimitsNow(Date.now());
const nextPreferences = { metric: nextMetric, windowDays };
setPreferences(nextPreferences);
saveUsagePagePreferences(nextPreferences);
@@ -177,6 +179,7 @@ export function UsagePage() {
}
}),
).finally(() => {
+ setLimitsNow(Date.now());
refreshingRef.current = false;
setIsRefreshing(false);
});
@@ -350,7 +353,7 @@ export function UsagePage() {
: `Select an environment to see ${showingLimits ? "limits" : "usage"}.`}
) : showingLimits ? (
-
+
) : isPending ? (
) : (