From 4a6893a4bf4337393c6ecefc00f53e5b783fdb99 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 7 Oct 2026 14:47:43 -0700 Subject: [PATCH 1/2] feat(web): filter the Usage page by provider Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/web/src/components/usage/UsageLimits.tsx | 38 +++++++- .../src/components/usage/UsagePage.test.tsx | 1 + apps/web/src/components/usage/UsagePage.tsx | 96 +++++++++++++++++-- .../usage/usagePagePreferences.test.ts | 8 ++ .../components/usage/usagePagePreferences.ts | 3 + apps/web/src/state/usage.test.tsx | 42 ++++++-- apps/web/src/state/usage.ts | 36 ++++++- 7 files changed, 204 insertions(+), 20 deletions(-) diff --git a/apps/web/src/components/usage/UsageLimits.tsx b/apps/web/src/components/usage/UsageLimits.tsx index a031cd0d1564..8de0dbc10443 100644 --- a/apps/web/src/components/usage/UsageLimits.tsx +++ b/apps/web/src/components/usage/UsageLimits.tsx @@ -340,17 +340,47 @@ export function ResetCredits({ */ export function UsageLimitsSection({ selectedEnvironmentIds, + hiddenProviders, now, cursorPrompt, }: { readonly selectedEnvironmentIds: ReadonlySet | null; + readonly hiddenProviders: ReadonlySet; readonly now: number; readonly cursorPrompt?: ReactNode; }) { const presentations = useAtomValue(environmentPresentations.presentationsAtom); - const selected = - selectedEnvironmentIds === null - ? presentations - : new Map([...presentations].filter(([id]) => selectedEnvironmentIds.has(id))); + const hiddenDrivers = new Set( + [...hiddenProviders].map((provider) => PROVIDER_PRESENTATION[provider].driverKind), + ); + const isVisible = (driver: ServerProvider["driver"]) => !hiddenDrivers.has(driver); + // Hidden drivers leave both native accounts and hub accounts, so their + // bars, notices, and external links all drop out together. + const selected = new Map( + [...presentations].flatMap(([id, presentation]) => { + if (selectedEnvironmentIds !== null && !selectedEnvironmentIds.has(id)) return []; + const config = presentation.serverConfig; + if (hiddenDrivers.size === 0 || config === null) return [[id, presentation] as const]; + return [ + [ + id, + { + ...presentation, + serverConfig: { + ...config, + providers: config.providers.filter((provider) => isVisible(provider.driver)), + // A hub left with no visible accounts is dropped, not reported as empty. + usageLimitSources: config.usageLimitSources?.flatMap((source) => { + const accounts = source.accounts.filter((account) => isVisible(account.driver)); + return accounts.length === 0 && source.accounts.length > 0 + ? [] + : [{ ...source, accounts }]; + }), + }, + }, + ] as const, + ]; + }), + ); return ; } diff --git a/apps/web/src/components/usage/UsagePage.test.tsx b/apps/web/src/components/usage/UsagePage.test.tsx index 53093e7ef23f..6118595c41bf 100644 --- a/apps/web/src/components/usage/UsagePage.test.tsx +++ b/apps/web/src/components/usage/UsagePage.test.tsx @@ -39,6 +39,7 @@ vi.mock("./usageProviders", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, + PROVIDER_ORDER: ["codex", "claude"], PROVIDER_PRESENTATION: { codex: { color: "white", label: "Codex", mark: "span" }, claude: { color: "orange", label: "Claude Code", mark: "span" }, diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index 0a525054915f..a14d04085702 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -139,14 +139,21 @@ export function UsagePage() { const [selectedModelKey, setSelectedModelKey] = useState(null); const [selectedEnvironmentIds, setSelectedEnvironmentIds] = useState | null>(null); + const hiddenProviders = useMemo( + () => new Set(preferences.hiddenProviders), + [preferences.hiddenProviders], + ); const { days: windowDays, window } = windowSelection; const isPast24Hours = windowDays === 1; const { merged, environments, selectedEnvironments, isPending, isPartial, refresh } = useUsage( window, selectedEnvironmentIds, + hiddenProviders, ); const presentations = useAtomValue(environmentPresentations.presentationsAtom); - const cursorAccessEnvironments = cursorKeychainAccessEnvironments(selectedEnvironments); + const cursorAccessEnvironments = hiddenProviders.has("cursor") + ? [] + : cursorKeychainAccessEnvironments(selectedEnvironments); const sourceMessages = [ ...new Set( selectedEnvironments.flatMap( @@ -154,6 +161,7 @@ export function UsagePage() { environment.summary?.sources.flatMap((source) => source.message && !source.action && + !hiddenProviders.has(source.fingerprint.provider) && (source.status === "partial" || source.status === "failed" || source.fingerprint.provider === "cursor") @@ -217,11 +225,14 @@ export function UsagePage() { ); const timeValueColumnWidth = `${60 / (activeProviders.length + 2)}%`; - const selectWindow = (days: number) => { - if (!isUsageWindowDays(days)) return; - const nextPreferences = { metric, windowDays: days }; + const updatePreferences = (patch: Partial) => { + const nextPreferences = { ...preferences, ...patch }; setPreferences(nextPreferences); saveUsagePagePreferences(nextPreferences); + }; + const selectWindow = (days: number) => { + if (!isUsageWindowDays(days)) return; + updatePreferences({ windowDays: days }); setWindowSelection({ days, window: makeWindow(days, undefined, days === 1 ? "hour" : "day"), @@ -229,9 +240,7 @@ export function UsagePage() { }; const selectMetric = (nextMetric: UsageMetric) => { if (nextMetric === "limits") setLimitsNow(Date.now()); - const nextPreferences = { metric: nextMetric, windowDays }; - setPreferences(nextPreferences); - saveUsagePagePreferences(nextPreferences); + updatePreferences({ metric: nextMetric }); }; const refreshLimits = async (automatic = false, afterPending = false) => { try { @@ -334,7 +343,7 @@ export function UsagePage() {

Usage

- + setPriceDialog({})} /> + + + updatePreferences({ hiddenProviders: next })} + /> + {!showingLimits ? ( @@ -482,6 +498,7 @@ export function UsagePage() { ) : showingLimits ? ( 0 ? ( @@ -550,7 +567,8 @@ export function UsagePage() { - {[...presentations].some( + {!hiddenProviders.has("codex") && + [...presentations].some( ([id, presentation]) => (selectedEnvironmentIds === null || selectedEnvironmentIds.has(id)) && presentation.serverConfig?.providers.some(usesChatGptSharing), @@ -1234,6 +1252,66 @@ function UsageEnvironmentFilter({ ); } +/** Provider visibility shared by every tab. Stored as the hidden set. */ +function UsageProviderFilter({ + hiddenProviders, + onChange, +}: { + readonly hiddenProviders: ReadonlySet; + readonly onChange: (hiddenProviders: readonly UsageProviderKind[]) => void; +}) { + const visible = PROVIDER_ORDER.filter((provider) => !hiddenProviders.has(provider)); + const label = + visible.length === PROVIDER_ORDER.length + ? "All providers" + : visible.length === 0 + ? "No providers" + : visible.length === 1 + ? PROVIDER_PRESENTATION[visible[0]!].label + : `${visible.length} providers`; + + return ( + + } className="group/usage-provider min-w-0 max-w-full"> + {label} + + + + onChange(checked ? [] : PROVIDER_ORDER)} + > + All providers + + + {PROVIDER_ORDER.map((provider) => ( + + onChange( + PROVIDER_ORDER.filter((entry) => + entry === provider ? !checked : hiddenProviders.has(entry), + ), + ) + } + > + + + {PROVIDER_PRESENTATION[provider].label} + + + ))} + + + ); +} + /** * Stand-in with the loaded page's shape, using the shared `Skeleton` bars so it * breathes with the same `animate-skeleton` pulse as every other loading state. diff --git a/apps/web/src/components/usage/usagePagePreferences.test.ts b/apps/web/src/components/usage/usagePagePreferences.test.ts index f5686de012be..a06d4e6218c3 100644 --- a/apps/web/src/components/usage/usagePagePreferences.test.ts +++ b/apps/web/src/components/usage/usagePagePreferences.test.ts @@ -35,6 +35,14 @@ describe("Usage page preferences", () => { } }); + it("round-trips hidden providers and reads older saves without them", () => { + saveUsagePagePreferences({ metric: "cost", windowDays: 7, hiddenProviders: ["codex"] }); + expect(readUsagePagePreferences().hiddenProviders).toEqual(["codex"]); + + values.set(key, '{"metric":"tokens","windowDays":30}'); + expect(readUsagePagePreferences()).toEqual({ metric: "tokens", windowDays: 30 }); + }); + it.each([ "not-json", '{"metric":"unknown","windowDays":7}', diff --git a/apps/web/src/components/usage/usagePagePreferences.ts b/apps/web/src/components/usage/usagePagePreferences.ts index 630e78e98fe2..84e6723658c9 100644 --- a/apps/web/src/components/usage/usagePagePreferences.ts +++ b/apps/web/src/components/usage/usagePagePreferences.ts @@ -1,3 +1,4 @@ +import { UsageProviderKind } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { getLocalStorageItem, setLocalStorageItem } from "../../hooks/useLocalStorage"; @@ -6,6 +7,8 @@ const STORAGE_KEY = "t3code:usage-page-preferences:v1"; const UsagePagePreferencesSchema = Schema.Struct({ metric: Schema.Literals(["cost", "tokens", "limits"]), windowDays: Schema.Literals([1, 7, 30, 90]), + /** Providers filtered out of the page. Stored as hidden so new providers show by default. */ + hiddenProviders: Schema.optional(Schema.Array(UsageProviderKind)), }); export type UsagePagePreferences = typeof UsagePagePreferencesSchema.Type; diff --git a/apps/web/src/state/usage.test.tsx b/apps/web/src/state/usage.test.tsx index afe7457ea80e..c084d0a5e9ab 100644 --- a/apps/web/src/state/usage.test.tsx +++ b/apps/web/src/state/usage.test.tsx @@ -1,4 +1,9 @@ -import { EnvironmentId, UsageDay, USAGE_CONTRACT_VERSION } from "@t3tools/contracts"; +import { + EnvironmentId, + UsageDay, + USAGE_CONTRACT_VERSION, + type UsageProviderKind, +} from "@t3tools/contracts"; import { act, useLayoutEffect } from "react"; import { create, type ReactTestRenderer } from "react-test-renderer"; import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; @@ -17,7 +22,12 @@ const input = { timeZone: "UTC", }; -function environment(id: string, cost: number | null, hostId = id): EnvironmentUsageStatus { +function environment( + id: string, + cost: number | null, + hostId = id, + provider: UsageProviderKind = "codex", +): EnvironmentUsageStatus { return { environmentId: EnvironmentId.make(id), label: id, @@ -35,7 +45,7 @@ function environment(id: string, cost: number | null, hostId = id): EnvironmentU buckets: [ { day: input.sinceDay, - provider: "codex", + provider, model: id, totals: { uncachedInputTokens: 100, @@ -56,7 +66,7 @@ function environment(id: string, cost: number | null, hostId = id): EnvironmentU { fingerprint: { hostId, - provider: "codex", + provider, resolvedHomePath: "/sessions", volumeId: hostId, }, @@ -77,8 +87,14 @@ function environment(id: string, cost: number | null, hostId = id): EnvironmentU let renderer: ReactTestRenderer | undefined; let latest: UsageView; -function Probe({ selected }: { selected: ReadonlySet | null }) { - const usage = useUsage(input, selected); +function Probe({ + selected, + hidden, +}: { + selected: ReadonlySet | null; + hidden?: ReadonlySet; +}) { + const usage = useUsage(input, selected, hidden); useLayoutEffect(() => { latest = usage; }, [usage]); @@ -166,3 +182,17 @@ describe("usage environment selection", () => { expect(latest.isPartial).toBe(false); }); }); + +describe("usage provider filter", () => { + it("drops hidden providers from totals and sessions, then restores them", async () => { + testState.environments = [environment("a", 10), environment("c", 5, "c", "claude")]; + await act(() => renderer?.update( - +