From cb9042c4a9985dbcf0d30adf79bf01acb4525b68 Mon Sep 17 00:00:00 2001
From: SunSinD
Date: Tue, 27 Oct 2026 22:33:54 -0400
Subject: [PATCH 1/4] feat(usage): save provider display order
---
apps/web/src/components/usage/UsageLimits.tsx | 15 ++-
.../usage/UsageLimitsOrder.browser.test.tsx | 78 ++++++++++++++
.../components/usage/UsageLimitsPooled.tsx | 101 ++++++++++++++++--
apps/web/src/components/usage/UsagePage.tsx | 4 +
.../components/usage/usageLimitOrder.test.ts | 29 +++++
.../src/components/usage/usageLimitOrder.ts | 8 ++
.../usage/usagePagePreferences.test.ts | 14 +++
.../components/usage/usagePagePreferences.ts | 3 +-
docs/user/usage.md | 5 +
9 files changed, 246 insertions(+), 11 deletions(-)
create mode 100644 apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
create mode 100644 apps/web/src/components/usage/usageLimitOrder.test.ts
create mode 100644 apps/web/src/components/usage/usageLimitOrder.ts
diff --git a/apps/web/src/components/usage/UsageLimits.tsx b/apps/web/src/components/usage/UsageLimits.tsx
index e34965632b030..bb93cb1d5f00d 100644
--- a/apps/web/src/components/usage/UsageLimits.tsx
+++ b/apps/web/src/components/usage/UsageLimits.tsx
@@ -3,6 +3,7 @@ import {
type EnvironmentId,
type ProviderConsumeResetCreditOutcome,
ProviderConsumeResetCreditInput,
+ type ProviderDriverKind,
ServerProvider,
ServerProviderResetCredits,
ServerProviderUsageWindow,
@@ -361,11 +362,15 @@ export function ResetCredits({
export function UsageLimitsSection({
selectedEnvironmentIds,
hiddenProviders,
+ providerOrder,
+ onProviderOrderChange,
now,
cursorPrompt,
}: {
readonly selectedEnvironmentIds: ReadonlySet | null;
readonly hiddenProviders: ReadonlySet;
+ readonly providerOrder: readonly ProviderDriverKind[];
+ readonly onProviderOrderChange: (order: readonly ProviderDriverKind[]) => void;
readonly now: number;
readonly cursorPrompt?: ReactNode;
}) {
@@ -399,5 +404,13 @@ export function UsageLimitsSection({
];
}),
);
- return ;
+ return (
+
+ );
}
diff --git a/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
new file mode 100644
index 0000000000000..462f51523ece3
--- /dev/null
+++ b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
@@ -0,0 +1,78 @@
+import { EnvironmentId, ProviderDriverKind, UsageLimitSourceId } from "@t3tools/contracts";
+import type { LimitPresentations } from "@t3tools/shared/usageLimits";
+import { useState } from "react";
+import { expect, it } from "vite-plus/test";
+import { userEvent } from "vite-plus/test/browser";
+
+import { render } from "../../../test/render";
+import { AppAtomRegistryProvider } from "../../rpc/atomRegistry";
+import { UsageLimitsPooled } from "./UsageLimitsPooled";
+import "../../index.css";
+
+it("reorders providers with the keyboard and cancels an uncommitted move", async () => {
+ const now = Date.parse("2026-10-28T01:00:00.000Z");
+ const checkedAt = new Date(now).toISOString();
+ 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: ["codex", "claudeAgent"].map((driver) => ({
+ id: driver,
+ driver: ProviderDriverKind.make(driver),
+ usageLimits: {
+ checkedAt,
+ windows: [
+ {
+ id: "primary",
+ kind: "session",
+ label: "Session",
+ usedPercent: 29,
+ resetsAt: "2026-10-28T03:55:00.000Z",
+ },
+ ],
+ },
+ })),
+ },
+ ],
+ },
+ },
+ ],
+ ]);
+ function Demo() {
+ const [order, setOrder] = useState([]);
+ return (
+
+
+
+ );
+ }
+ const container = document.createElement("div");
+ container.style.width = "1000px";
+ await using screen = await render(, { container });
+ const headings = () =>
+ [...screen.container.querySelectorAll("h2")].map((node) => node.textContent);
+ expect(headings()).toEqual(["Codex", "Claude"]);
+ await screen.getByRole("button", { name: "Reorder Claude" }).click();
+ await userEvent.keyboard(" ");
+ await userEvent.keyboard("{ArrowUp}");
+ await userEvent.keyboard(" ");
+ await expect.poll(headings).toEqual(["Claude", "Codex"]);
+ await userEvent.keyboard(" ");
+ await userEvent.keyboard("{ArrowDown}");
+ await userEvent.keyboard("{Escape}");
+ await expect.poll(headings).toEqual(["Claude", "Codex"]);
+});
diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx
index 7157607dabafc..d53f5a725bd69 100644
--- a/apps/web/src/components/usage/UsageLimitsPooled.tsx
+++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx
@@ -15,7 +15,25 @@ import {
remainingPercent,
} from "@t3tools/shared/usageLimits";
import type { Popover as PopoverPrimitive } from "@base-ui/react/popover";
-import { AlertTriangleIcon, ExternalLinkIcon, TicketIcon } from "lucide-react";
+import {
+ closestCenter,
+ DndContext,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core";
+import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ useSortable,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import type { ProviderDriverKind } from "@t3tools/contracts";
+import { AlertTriangleIcon, ExternalLinkIcon, GripVerticalIcon, TicketIcon } from "lucide-react";
import {
createContext,
Fragment,
@@ -33,11 +51,13 @@ import { cn } from "../../lib/utils";
import { formatUpcomingTimestamp } from "../../timestampFormat";
import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";
import { providerClients } from "../settings/providerDriverMeta";
+import { usageLimitOrder } from "./usageLimitOrder";
import { RedactedSensitiveText } from "../settings/RedactedSensitiveText";
import { Button } from "../ui/button";
import { OpenAI } from "../Icons";
import { Alert, AlertTitle } from "../ui/alert";
import { Popover, PopoverCreateHandle, PopoverPopup, PopoverTrigger } from "../ui/popover";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import {
PaceIcon,
ResetCreditDialog,
@@ -569,12 +589,42 @@ function PoolWindowCard({
}
function PoolSection({ pool, now }: { readonly pool: LimitPool; readonly now: number }) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ setActivatorNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id: pool.driver });
const color = barColor(pool.driver);
const label = providerClients.get(pool.driver)?.label ?? String(pool.driver);
const windows = displayLimitWindows(pool);
return (
-
+
+
+
+ }
+ >
+
+
+ Drag to reorder, or press Space and use the arrow keys
+
[0];
readonly now: number;
readonly cursorPrompt?: ReactNode;
+ readonly providerOrder: readonly ProviderDriverKind[];
+ readonly onProviderOrderChange: (order: readonly ProviderDriverKind[]) => void;
}) {
const [segmentPopover] = useState(() => PopoverCreateHandle());
const segmentPopoverActions = useRef(null);
- const pools = collectLimitPools(collectLimitAccounts(presentations), now);
+ const collectedPools = collectLimitPools(collectLimitAccounts(presentations), now);
+ const order = usageLimitOrder(
+ providerOrder,
+ collectedPools.map((pool) => pool.driver),
+ );
+ const pools = collectedPools.toSorted(
+ (left, right) => order.indexOf(left.driver) - order.indexOf(right.driver),
+ );
+ const sensors = useSensors(
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
+ );
const notices = collectLimitNotices(presentations);
const externalLinks = collectExternalUsageLinks(presentations);
const cursorPromptAt =
@@ -628,12 +693,30 @@ export function UsageLimitsPooled({
) : null}
- {pools.map((pool, index) => (
-
- {index === cursorPromptAt ? cursorPrompt : null}
-
-
- ))}
+ {
+ if (!over || active.id === over.id) return;
+ const from = order.findIndex((driver) => driver === active.id);
+ const to = order.findIndex((driver) => driver === over.id);
+ if (from < 0 || to < 0) return;
+ onProviderOrderChange(arrayMove(order, from, to));
+ }}
+ >
+ pool.driver)}
+ strategy={verticalListSortingStrategy}
+ >
+ {pools.map((pool, index) => (
+
+ {index === cursorPromptAt ? cursorPrompt : null}
+
+
+ ))}
+
+
{({ payload }) =>
diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx
index 8373d3439eb16..b481ce8cf22c6 100644
--- a/apps/web/src/components/usage/UsagePage.tsx
+++ b/apps/web/src/components/usage/UsagePage.tsx
@@ -523,6 +523,10 @@ export function UsagePage() {
+ updatePreferences({ limitProviderOrder })
+ }
now={limitsNow}
cursorPrompt={
cursorAccessEnvironments.length > 0 ? (
diff --git a/apps/web/src/components/usage/usageLimitOrder.test.ts b/apps/web/src/components/usage/usageLimitOrder.test.ts
new file mode 100644
index 0000000000000..167f04ed74bcc
--- /dev/null
+++ b/apps/web/src/components/usage/usageLimitOrder.test.ts
@@ -0,0 +1,29 @@
+import { ProviderDriverKind } from "@t3tools/contracts";
+import { describe, expect, it } from "vite-plus/test";
+
+import { usageLimitOrder } from "./usageLimitOrder";
+
+const drivers = (...values: string[]) => values.map((value) => ProviderDriverKind.make(value));
+
+describe("usageLimitOrder", () => {
+ it("keeps the existing order until a preference is saved", () => {
+ const available = drivers("codex", "cursor", "claudeAgent");
+ expect(usageLimitOrder([], available)).toEqual(available);
+ });
+
+ it("keeps hidden and disconnected providers in the saved order and appends new ones", () => {
+ const saved = drivers("claudeAgent", "codex", "cursor");
+ const available = drivers("codex", "grok");
+ const order = usageLimitOrder(saved, available);
+ expect(order).toEqual(drivers("claudeAgent", "codex", "cursor", "grok"));
+ expect(usageLimitOrder(order, drivers("cursor", "claudeAgent", "codex", "grok"))).toEqual(
+ order,
+ );
+ });
+
+ it("deduplicates saved providers", () => {
+ expect(usageLimitOrder(drivers("claudeAgent", "claudeAgent"), drivers("codex"))).toEqual(
+ drivers("claudeAgent", "codex"),
+ );
+ });
+});
diff --git a/apps/web/src/components/usage/usageLimitOrder.ts b/apps/web/src/components/usage/usageLimitOrder.ts
new file mode 100644
index 0000000000000..cab95dc4931c4
--- /dev/null
+++ b/apps/web/src/components/usage/usageLimitOrder.ts
@@ -0,0 +1,8 @@
+import type { ProviderDriverKind } from "@t3tools/contracts";
+
+export function usageLimitOrder(
+ saved: readonly ProviderDriverKind[],
+ available: readonly ProviderDriverKind[],
+): ProviderDriverKind[] {
+ return [...new Set([...saved, ...available])];
+}
diff --git a/apps/web/src/components/usage/usagePagePreferences.test.ts b/apps/web/src/components/usage/usagePagePreferences.test.ts
index 075076a9b3c52..733634eacfa39 100644
--- a/apps/web/src/components/usage/usagePagePreferences.test.ts
+++ b/apps/web/src/components/usage/usagePagePreferences.test.ts
@@ -1,3 +1,4 @@
+import { ProviderDriverKind } from "@t3tools/contracts";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { readUsagePagePreferences, saveUsagePagePreferences } from "./usagePagePreferences";
@@ -24,6 +25,19 @@ afterEach(() => {
});
describe("Usage page preferences", () => {
+ it("remembers provider order without losing filters or the selected metric", () => {
+ const preferences = {
+ metric: "limits" as const,
+ windowDays: 7 as const,
+ hiddenProviders: ["cursor" as const],
+ limitProviderOrder: ["claudeAgent", "codex", "cursor"].map((driver) =>
+ ProviderDriverKind.make(driver),
+ ),
+ };
+ saveUsagePagePreferences(preferences);
+ expect(readUsagePagePreferences()).toEqual(preferences);
+ });
+
it("round-trips hidden providers and reads older saves without them", () => {
saveUsagePagePreferences({ metric: "cost", windowDays: 7, hiddenProviders: ["codex"] });
expect(readUsagePagePreferences().hiddenProviders).toEqual(["codex"]);
diff --git a/apps/web/src/components/usage/usagePagePreferences.ts b/apps/web/src/components/usage/usagePagePreferences.ts
index 1aeaefc43da12..e790cf26dcaa6 100644
--- a/apps/web/src/components/usage/usagePagePreferences.ts
+++ b/apps/web/src/components/usage/usagePagePreferences.ts
@@ -1,4 +1,4 @@
-import { UsageProviderKind } from "@t3tools/contracts";
+import { ProviderDriverKind, UsageProviderKind } from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import { getLocalStorageItem, setLocalStorageItem } from "../../hooks/useLocalStorage";
@@ -8,6 +8,7 @@ const UsagePagePreferencesSchema = Schema.Struct({
metric: Schema.Literals(["cost", "tokens", "limits"]),
windowDays: Schema.Literals([1, 7, 30, 90]),
hiddenProviders: Schema.optional(Schema.Array(UsageProviderKind)),
+ limitProviderOrder: Schema.optional(Schema.Array(ProviderDriverKind)),
});
export type UsagePagePreferences = typeof UsagePagePreferencesSchema.Type;
diff --git a/docs/user/usage.md b/docs/user/usage.md
index e41e60ca73d76..9a101e7b20ea6 100644
--- a/docs/user/usage.md
+++ b/docs/user/usage.md
@@ -83,6 +83,11 @@ the dialog.
## Track subscription limits
+On web and desktop, drag the grip beside a provider heading to put your preferred providers
+first. You can also focus the grip, press Space, use the arrow keys, and press Space again to
+drop it. Escape cancels. The order is remembered on this device, including providers you
+temporarily hide or disconnect.
+
**Usage → Limits** pools every subscription account it can see per provider, so with several Codex
or Claude accounts across your enabled environments and hubs you read one number per window rather than a
list. Each window card shows how much of the pool is left and a bar with one segment per account,
From 35a0a39f20213391365a9dbb2772dffa5cc56287 Mon Sep 17 00:00:00 2001
From: SunSinD
Date: Wed, 28 Oct 2026 00:56:28 -0400
Subject: [PATCH 2/4] test(usage): use supported browser assertions
---
.../src/components/usage/UsageLimitsOrder.browser.test.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
index 462f51523ece3..50608dac930eb 100644
--- a/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
+++ b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
@@ -1,7 +1,7 @@
import { EnvironmentId, ProviderDriverKind, UsageLimitSourceId } from "@t3tools/contracts";
import type { LimitPresentations } from "@t3tools/shared/usageLimits";
import { useState } from "react";
-import { expect, it } from "vite-plus/test";
+import { expect, it, vi } from "vite-plus/test";
import { userEvent } from "vite-plus/test/browser";
import { render } from "../../../test/render";
@@ -70,9 +70,9 @@ it("reorders providers with the keyboard and cancels an uncommitted move", async
await userEvent.keyboard(" ");
await userEvent.keyboard("{ArrowUp}");
await userEvent.keyboard(" ");
- await expect.poll(headings).toEqual(["Claude", "Codex"]);
+ await vi.waitFor(() => expect(headings()).toEqual(["Claude", "Codex"]));
await userEvent.keyboard(" ");
await userEvent.keyboard("{ArrowDown}");
await userEvent.keyboard("{Escape}");
- await expect.poll(headings).toEqual(["Claude", "Codex"]);
+ await vi.waitFor(() => expect(headings()).toEqual(["Claude", "Codex"]));
});
From 35e1a97e8c13df448f57a1284854b19cd3993f47 Mon Sep 17 00:00:00 2001
From: SunSinD
Date: Sun, 1 Nov 2026 00:41:48 -0400
Subject: [PATCH 3/4] fix(usage): keep hidden provider positions
---
.../usage/UsageLimitsOrder.browser.test.tsx | 14 +++++++++++---
.../web/src/components/usage/UsageLimitsPooled.tsx | 12 +++++++++---
2 files changed, 20 insertions(+), 6 deletions(-)
diff --git a/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
index 50608dac930eb..08dbf74b3141c 100644
--- a/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
+++ b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
@@ -9,7 +9,7 @@ import { AppAtomRegistryProvider } from "../../rpc/atomRegistry";
import { UsageLimitsPooled } from "./UsageLimitsPooled";
import "../../index.css";
-it("reorders providers with the keyboard and cancels an uncommitted move", async () => {
+it("reorders visible providers without moving hidden ones and cancels an uncommitted move", async () => {
const now = Date.parse("2026-10-28T01:00:00.000Z");
const checkedAt = new Date(now).toISOString();
const presentations: LimitPresentations = new Map([
@@ -47,15 +47,21 @@ it("reorders providers with the keyboard and cancels an uncommitted move", async
},
],
]);
+ const onOrderChange = vi.fn();
function Demo() {
- const [order, setOrder] = useState([]);
+ const [order, setOrder] = useState(
+ ["codex", "cursor", "claudeAgent", "grok"].map((driver) => ProviderDriverKind.make(driver)),
+ );
return (
{
+ onOrderChange(next);
+ setOrder(next);
+ }}
/>
);
@@ -71,8 +77,10 @@ it("reorders providers with the keyboard and cancels an uncommitted move", async
await userEvent.keyboard("{ArrowUp}");
await userEvent.keyboard(" ");
await vi.waitFor(() => expect(headings()).toEqual(["Claude", "Codex"]));
+ expect(onOrderChange).toHaveBeenCalledExactlyOnceWith(["claudeAgent", "cursor", "codex", "grok"]);
await userEvent.keyboard(" ");
await userEvent.keyboard("{ArrowDown}");
await userEvent.keyboard("{Escape}");
await vi.waitFor(() => expect(headings()).toEqual(["Claude", "Codex"]));
+ expect(onOrderChange).toHaveBeenCalledTimes(1);
});
diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx
index e119c439cc474..88d0536ff5c28 100644
--- a/apps/web/src/components/usage/UsageLimitsPooled.tsx
+++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx
@@ -700,10 +700,16 @@ export function UsageLimitsPooled({
modifiers={[restrictToVerticalAxis]}
onDragEnd={({ active, over }) => {
if (!over || active.id === over.id) return;
- const from = order.findIndex((driver) => driver === active.id);
- const to = order.findIndex((driver) => driver === over.id);
+ const visibleOrder = pools.map((pool) => pool.driver);
+ const from = visibleOrder.findIndex((driver) => driver === active.id);
+ const to = visibleOrder.findIndex((driver) => driver === over.id);
if (from < 0 || to < 0) return;
- onProviderOrderChange(arrayMove(order, from, to));
+ const reordered = arrayMove(visibleOrder, from, to);
+ onProviderOrderChange(
+ order.map((driver) =>
+ visibleOrder.includes(driver) ? (reordered.shift() ?? driver) : driver,
+ ),
+ );
}}
>
Date: Mon, 2 Nov 2026 23:53:38 -0500
Subject: [PATCH 4/4] fix(usage): keep modal provider sorting usable
---
apps/web/src/components/AppRouteModal.tsx | 16 ++-
.../usage/UsageLimitsOrder.browser.test.tsx | 115 +++++++++++++-----
.../components/usage/UsageLimitsPooled.tsx | 46 ++++---
3 files changed, 130 insertions(+), 47 deletions(-)
diff --git a/apps/web/src/components/AppRouteModal.tsx b/apps/web/src/components/AppRouteModal.tsx
index 55d7c4112f968..98f22b9bc5b82 100644
--- a/apps/web/src/components/AppRouteModal.tsx
+++ b/apps/web/src/components/AppRouteModal.tsx
@@ -20,14 +20,24 @@ export function AppRouteModal({ label, children }: { label: string; children: Re
// Focus the surface, not its first control: no stray focus ring, no phone keyboard.
// On close, leave focus alone; returning it to the opener paints a focus ring.
const popupRef = useRef(null);
+ // dnd-kit marks the handle being dragged; that drag owns arrow keys and Escape, which its
+ // sensors read on the document.
+ const sortableDragInProgress = () =>
+ popupRef.current?.querySelector('[aria-roledescription="sortable"][aria-pressed="true"]') !=
+ null;
return (
{
+ onOpenChange={(nextOpen, eventDetails) => {
if (nextOpen) return;
+ if (eventDetails.reason === "escape-key" && sortableDragInProgress()) {
+ eventDetails.cancel();
+ eventDetails.allowPropagation();
+ return;
+ }
// Closing unmounts a focused field without a blur, so commit-on-blur rows would
// drop what was typed. Blur first so they commit.
const focused = document.activeElement;
@@ -57,6 +67,10 @@ export function AppRouteModal({ label, children }: { label: string; children: Re
aria-label={label}
data-slot="dialog-popup"
data-app-modal=""
+ // The popup stops arrow-key propagation, which would strand a keyboard drag.
+ onKeyDown={(event) => {
+ if (sortableDragInProgress()) event.preventBaseUIHandler();
+ }}
className="relative flex size-full min-h-0 min-w-0 flex-col overflow-hidden bg-background max-sm:pt-safe max-sm:pb-safe text-foreground outline-none transition-[opacity,scale] duration-150 ease-out [-webkit-app-region:no-drag] data-ending-style:scale-[0.985] data-ending-style:opacity-0 data-starting-style:scale-[0.985] data-starting-style:opacity-0 sm:max-h-[60rem] sm:max-w-[80rem] sm:rounded-2xl sm:border sm:shadow-2xl/40"
>
{children}
diff --git a/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
index 08dbf74b3141c..cbf8c47b2ec99 100644
--- a/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
+++ b/apps/web/src/components/usage/UsageLimitsOrder.browser.test.tsx
@@ -2,16 +2,24 @@ import { EnvironmentId, ProviderDriverKind, UsageLimitSourceId } from "@t3tools/
import type { LimitPresentations } from "@t3tools/shared/usageLimits";
import { useState } from "react";
import { expect, it, vi } from "vite-plus/test";
-import { userEvent } from "vite-plus/test/browser";
+import { page, userEvent } from "vite-plus/test/browser";
import { render } from "../../../test/render";
+import { AppRouteModal } from "../AppRouteModal";
import { AppAtomRegistryProvider } from "../../rpc/atomRegistry";
import { UsageLimitsPooled } from "./UsageLimitsPooled";
import "../../index.css";
-it("reorders visible providers without moving hidden ones and cancels an uncommitted move", async () => {
+it("reorders visible providers without moving hidden ones and cancels inside the Usage modal", async () => {
const now = Date.parse("2026-10-28T01:00:00.000Z");
const checkedAt = new Date(now).toISOString();
+ const usageWindow = (id: string, kind: "session" | "weekly", label: string) => ({
+ id,
+ kind,
+ label,
+ usedPercent: 29,
+ resetsAt: "2026-10-28T03:55:00.000Z",
+ });
const presentations: LimitPresentations = new Map([
[
EnvironmentId.make("demo"),
@@ -25,21 +33,20 @@ it("reorders visible providers without moving hidden ones and cancels an uncommi
kind: "cliproxy",
label: "Demo",
checkedAt,
- accounts: ["codex", "claudeAgent"].map((driver) => ({
- id: driver,
- driver: ProviderDriverKind.make(driver),
- usageLimits: {
- checkedAt,
+ // Unequal section heights: Claude has two windows, Codex one.
+ accounts: [
+ { driver: "codex", windows: [usageWindow("weekly", "weekly", "Weekly")] },
+ {
+ driver: "claudeAgent",
windows: [
- {
- id: "primary",
- kind: "session",
- label: "Session",
- usedPercent: 29,
- resetsAt: "2026-10-28T03:55:00.000Z",
- },
+ usageWindow("primary", "session", "Session"),
+ usageWindow("weekly", "weekly", "Weekly"),
],
},
+ ].map(({ driver, windows }) => ({
+ id: driver,
+ driver: ProviderDriverKind.make(driver),
+ usageLimits: { checkedAt, windows },
})),
},
],
@@ -54,33 +61,83 @@ it("reorders visible providers without moving hidden ones and cancels an uncommi
);
return (
- {
- onOrderChange(next);
- setOrder(next);
- }}
- />
+
+
+ {
+ onOrderChange(next);
+ setOrder(next);
+ }}
+ />
+
+
);
}
- const container = document.createElement("div");
- container.style.width = "1000px";
- await using screen = await render(, { container });
- const headings = () =>
- [...screen.container.querySelectorAll("h2")].map((node) => node.textContent);
+ await using _screen = await render(, { router: "/usage" });
+ const dialog = page.getByRole("dialog", { name: "Usage" });
+ const headings = () => [...document.querySelectorAll("h2")].map((node) => node.textContent);
+ await expect.element(dialog).toBeVisible();
expect(headings()).toEqual(["Codex", "Claude"]);
- await screen.getByRole("button", { name: "Reorder Claude" }).click();
+ await page.getByRole("button", { name: "Reorder Claude" }).click();
await userEvent.keyboard(" ");
await userEvent.keyboard("{ArrowUp}");
await userEvent.keyboard(" ");
await vi.waitFor(() => expect(headings()).toEqual(["Claude", "Codex"]));
expect(onOrderChange).toHaveBeenCalledExactlyOnceWith(["claudeAgent", "cursor", "codex", "grok"]);
+
+ // Escape cancels the drag only; Usage stays open and the same grip picks up again.
await userEvent.keyboard(" ");
await userEvent.keyboard("{ArrowDown}");
await userEvent.keyboard("{Escape}");
await vi.waitFor(() => expect(headings()).toEqual(["Claude", "Codex"]));
+ await expect.element(dialog).toHaveAttribute("data-open");
expect(onOrderChange).toHaveBeenCalledTimes(1);
+ await expect.element(page.getByRole("button", { name: "Reorder Claude" })).toHaveFocus();
+ await userEvent.keyboard(" ");
+ await userEvent.keyboard("{ArrowDown}");
+ await userEvent.keyboard(" ");
+ await vi.waitFor(() => expect(headings()).toEqual(["Codex", "Claude"]));
+ expect(onOrderChange).toHaveBeenLastCalledWith(["codex", "cursor", "claudeAgent", "grok"]);
+
+ // The same holds for a pointer drag, and the grip can be grabbed again afterwards.
+ const grip = page.getByRole("button", { name: "Reorder Claude" });
+ const pointer = (type: string, clientY: number, buttons = 1) =>
+ new PointerEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ isPrimary: true,
+ pointerId: 1,
+ button: 0,
+ buttons,
+ clientX: grip.element().getBoundingClientRect().left,
+ clientY,
+ });
+ const dragClaudeUp = async () => {
+ const top = grip.element().getBoundingClientRect().top;
+ grip.element().dispatchEvent(pointer("pointerdown", top));
+ // The first move only activates the drag; the second one moves it.
+ document.dispatchEvent(pointer("pointermove", top - 10));
+ await expect.element(grip).toHaveAttribute("aria-pressed", "true");
+ document.dispatchEvent(pointer("pointermove", top - 200));
+ await expect.element(page.getByText(/over droppable area codex\./)).toBeInTheDocument();
+ return top - 200;
+ };
+ const cancelledAt = await dragClaudeUp();
+ await userEvent.keyboard("{Escape}");
+ await expect.element(grip).not.toHaveAttribute("aria-pressed");
+ document.dispatchEvent(pointer("pointerup", cancelledAt, 0));
+ await expect.element(dialog).toHaveAttribute("data-open");
+ expect(headings()).toEqual(["Codex", "Claude"]);
+ expect(onOrderChange).toHaveBeenCalledTimes(2);
+ document.dispatchEvent(pointer("pointerup", await dragClaudeUp(), 0));
+ await vi.waitFor(() => expect(headings()).toEqual(["Claude", "Codex"]));
+ expect(onOrderChange).toHaveBeenCalledTimes(3);
+
+ // With no drag in progress, Escape closes Usage as before.
+ await userEvent.keyboard("{Escape}");
+ await expect.element(dialog).not.toBeInTheDocument();
});
diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx
index 88d0536ff5c28..4023f10f0a6c1 100644
--- a/apps/web/src/components/usage/UsageLimitsPooled.tsx
+++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx
@@ -37,7 +37,6 @@ import { AlertTriangleIcon, ExternalLinkIcon, GripVerticalIcon, TicketIcon } fro
import { normalizeProviderAccentColor } from "@t3tools/client-runtime/state/provider-instance-display";
import {
createContext,
- Fragment,
type ReactNode,
type RefObject,
use,
@@ -48,6 +47,7 @@ import {
import { ensureLocalApi } from "../../localApi";
import { usePrimarySettings } from "../../hooks/useSettings";
+import { useMediaQuery } from "../../hooks/useMediaQuery";
import { cn } from "../../lib/utils";
import { formatUpcomingTimestamp } from "../../timestampFormat";
import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";
@@ -589,8 +589,17 @@ function PoolWindowCard({
);
}
-function PoolSection({ pool, now }: { readonly pool: LimitPool; readonly now: number }) {
+function PoolSection({
+ pool,
+ now,
+ animate,
+}: {
+ readonly pool: LimitPool;
+ readonly now: number;
+ readonly animate: boolean;
+}) {
const {
+ active,
attributes,
listeners,
setNodeRef,
@@ -598,18 +607,21 @@ function PoolSection({ pool, now }: { readonly pool: LimitPool; readonly now: nu
transform,
transition,
isDragging,
- } = useSortable({ id: pool.driver });
+ } = useSortable({ id: pool.driver, ...(animate ? {} : { transition: null }) });
const color = barColor(pool.driver);
const label = providerClients.get(pool.driver)?.label ?? String(pool.driver);
const windows = displayLimitWindows(pool);
return (
-
+
void;
}) {
const [segmentPopover] = useState(() => PopoverCreateHandle());
+ const [keyboardDrag, setKeyboardDrag] = useState(false);
+ const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
const segmentPopoverActions = useRef(null);
const collectedPools = collectLimitPools(collectLimitAccounts(presentations), now);
const order = usageLimitOrder(
@@ -681,11 +695,6 @@ export function UsageLimitsPooled({
);
const notices = collectLimitNotices(presentations);
const externalLinks = collectExternalUsageLinks(presentations);
- const cursorPromptAt =
- Math.max(
- pools.findIndex((pool) => pool.driver === "codex"),
- pools.findIndex((pool) => pool.driver === "claudeAgent"),
- ) + 1;
return (
{pools.length === 0 && notices.length === 0 && !cursorPrompt && externalLinks.length === 0 ? (
@@ -698,6 +707,7 @@ export function UsageLimitsPooled({
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
+ onDragStart={({ activatorEvent }) => setKeyboardDrag(activatorEvent.type === "keydown")}
onDragEnd={({ active, over }) => {
if (!over || active.id === over.id) return;
const visibleOrder = pools.map((pool) => pool.driver);
@@ -716,11 +726,13 @@ export function UsageLimitsPooled({
items={pools.map((pool) => pool.driver)}
strategy={verticalListSortingStrategy}
>
- {pools.map((pool, index) => (
-
- {index === cursorPromptAt ? cursorPrompt : null}
-
-
+ {pools.map((pool) => (
+
))}
@@ -738,7 +750,7 @@ export function UsageLimitsPooled({
) : null
}
- {cursorPromptAt === pools.length ? cursorPrompt : null}
+ {cursorPrompt}
{externalLinks.map((link) => (