From 8c4c1d149d965058a31db53c93e70d8a78f2562c Mon Sep 17 00:00:00 2001 From: M4NU5 <31220123+M4NU5@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:42:47 +0100 Subject: [PATCH] fix(client): usage limits skip disabled connections usage limits, the mobile widget and the chatgpt shared usage row now skip disabled or unsupported connections, because their last cached server config kept showing that server's accounts, stale windows and reset credits as if current. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/usage/UsagePage.tsx | 10 ++-- docs/user/usage.md | 2 +- .../src/state/presentation.test.ts | 47 +++++++++++++++++++ .../client-runtime/src/state/presentation.ts | 8 +++- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index e0ab70d4511cf..4a940a8d3572f 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -224,7 +224,9 @@ export function UsagePage() { const refreshingUsage = isRefreshing && !showingLimits; const showingKept = shown !== null && shown.window !== window; const presentations = useAtomValue(environmentPresentations.presentationsAtom); - const providersByEnvironment = useAtomValue(environmentPresentations.providersByEnvironmentAtom); + const presentationsWithProviders = useAtomValue( + environmentPresentations.presentationsWithProvidersAtom, + ); const cursorAccessEnvironments = hiddenProviders.has("cursor") ? [] : cursorKeychainAccessEnvironments(selectedEnvironments); @@ -351,10 +353,10 @@ export function UsagePage() { : enumerateHourStarts(shownWindow.sinceTime, shownWindow.untilTime); const showChatGptSharing = !hiddenProviders.has("codex") && - [...providersByEnvironment].some( - ([id, providers]) => + [...presentationsWithProviders].some( + ([id, presentation]) => (selectedEnvironmentIds === null || selectedEnvironmentIds.has(id)) && - providers.some(usesChatGptSharing), + (presentation.serverConfig?.providers ?? []).some(usesChatGptSharing), ); const windowLabel = diff --git a/docs/user/usage.md b/docs/user/usage.md index cab53870bb14c..a012e73d94b89 100644 --- a/docs/user/usage.md +++ b/docs/user/usage.md @@ -79,7 +79,7 @@ the dialog. ## Track subscription limits **Usage → Limits** pools every subscription account it can see per provider, so with several Codex -or Claude accounts across your environments and hubs you read one number per window rather than a +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, kept in the same column across windows. Accounts are ordered by their next 5-hour refill, soonest first. For an account with no weekly quota left, this is when its session becomes usable again. diff --git a/packages/client-runtime/src/state/presentation.test.ts b/packages/client-runtime/src/state/presentation.test.ts index 0240ee9be05c0..3c3fa20a4a976 100644 --- a/packages/client-runtime/src/state/presentation.test.ts +++ b/packages/client-runtime/src/state/presentation.test.ts @@ -3,6 +3,7 @@ import { EnvironmentId, type ServerConfig } from "@t3tools/contracts"; import * as Option from "effect/Option"; import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity"; +import type { ConnectionCatalogEntry } from "../connection/catalog.ts"; import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget } from "../connection/model.ts"; import { createEnvironmentPresentationAtoms, @@ -88,3 +89,49 @@ describe("environment summary subscriptions", () => { } }); }); + +describe("presentations with providers", () => { + it("leaves out disabled connections until they are re-enabled", () => { + const h = harness(); + const atom = h.full.presentationsWithProvidersAtom; + const withSecond = (enabled: boolean): EnvironmentCatalogState => ({ + isReady: true, + entries: new Map([ + [FIRST, entry(FIRST)], + [SECOND, { ...entry(SECOND), enabled }], + ]), + }); + const stop = h.registry.subscribe(atom, () => {}); + try { + h.registry.set(h.configs(SECOND), config(false, "/second")); + h.registry.set(h.catalog, withSecond(false)); + expect([...h.registry.get(atom).keys()]).toEqual([FIRST]); + expect(h.registry.get(h.full.presentationsAtom).has(SECOND)).toBe(true); + h.registry.set(h.catalog, withSecond(true)); + expect([...h.registry.get(atom).keys()]).toEqual([FIRST, SECOND]); + expect(h.registry.get(atom).get(SECOND)?.serverConfig?.cwd).toBe("/second"); + } finally { + stop(); + h.registry.dispose(); + } + }); + + it("leaves out unsupported environments", () => { + const h = harness(); + const atom = h.full.presentationsWithProvidersAtom; + const stop = h.registry.subscribe(atom, () => {}); + try { + h.registry.set(h.catalog, { + isReady: true, + entries: new Map([ + [FIRST, entry(FIRST)], + [SECOND, { ...entry(SECOND), unsupportedReason: "Server is too old." }], + ]), + }); + expect([...h.registry.get(atom).keys()]).toEqual([FIRST]); + } finally { + stop(); + h.registry.dispose(); + } + }); +}); diff --git a/packages/client-runtime/src/state/presentation.ts b/packages/client-runtime/src/state/presentation.ts index 29782aacfa613..48d27974a2c8c 100644 --- a/packages/client-runtime/src/state/presentation.ts +++ b/packages/client-runtime/src/state/presentation.ts @@ -152,13 +152,17 @@ export function createEnvironmentPresentationAtoms(input: { return previous !== null && mapsEqual(previous, next) ? previous : next; }).pipe(Atom.withLabel("environment-providers")); - // Usage limits ride on provider statuses, so these surfaces rebuild on every push by design. + // Enabled environments only, like serverConfigsAtom: a disabled connection keeps its cached + // config, and its accounts would otherwise count in usage limits. Usage limits ride on provider + // statuses, so these surfaces rebuild on every push by design. const presentationsWithProvidersAtom = Atom.make((get) => { const previous = Option.getOrNull( get.self>(), ); const next = new Map(); - for (const [environmentId, presentation] of get(presentationsAtom)) { + for (const environmentId of enabledEnvironmentIds(get(input.catalogValueAtom))) { + const presentation = get(presentationAtom(environmentId)); + if (presentation === null) continue; const serverConfig = get(input.serverConfigValueAtom(environmentId)); const prior = previous?.get(environmentId); next.set(