From 5c9dc16e4f1e49410584410c93454ba8856b94b3 Mon Sep 17 00:00:00 2001 From: ProtonDev-sys Date: Fri, 4 Sep 2026 05:46:38 +0100 Subject: [PATCH] feat(web): show usage limits and reset credits in provider settings The Usage page already draws each provider's quota windows and banked reset credits, but the provider editor in Settings said nothing about them. Reuse the same block there so a user checking a Codex or Claude account sees its windows and can redeem a reset credit without leaving Settings. The section is skipped for accounts that can never report limits (API key) and the redeem button follows the panel's read-only gate. --- .../settings/ProviderInstanceCard.tsx | 13 ++++ ...ProviderSettingsPanel.environment.test.tsx | 68 +++++++++++++++++++ .../settings/ProviderSettingsPanel.tsx | 13 ++++ apps/web/src/components/usage/UsageLimits.tsx | 52 +++++++++++--- 4 files changed, 136 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx index 265030a73ff2..6ed5080d0169 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.tsx +++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx @@ -362,6 +362,12 @@ interface ProviderInstanceCardProps { */ readonly headerAction?: ReactNode | undefined; readonly setup?: ReactNode; + /** + * Subscription quota windows and banked reset credits the live provider + * reports. Only editor mode renders it; omitted when the driver cannot + * report limits. + */ + readonly usageLimits?: ReactNode; readonly hiddenModels: ReadonlyArray; readonly favoriteModels: ReadonlyArray; readonly modelOrder: ReadonlyArray; @@ -404,6 +410,7 @@ export function ProviderInstanceCard({ onDelete, headerAction, setup, + usageLimits, hiddenModels, favoriteModels, modelOrder, @@ -805,6 +812,12 @@ export function ProviderInstanceCard({ ) : null} + {usageLimits ? ( + +
{usageLimits}
+
+ ) : null} + { expect(visitElements(panel, isAddProviderButton)).not.toBeNull(); }); + it("shows the live provider's usage limits and reset credits in the editor", () => { + const live: ServerProvider = { + ...provider(), + usageLimits: { + checkedAt: "2026-07-24T12:00:00.000Z", + windows: [ + { + id: "primary", + kind: "session", + label: "5h", + usedPercent: 40, + resetsAt: "2026-07-24T15:00:00.000Z", + }, + ], + resetCredits: { availableCount: 2, nextExpiresAt: "2026-08-01T00:00:00.000Z" }, + }, + }; + atoms.providers = [live]; + + let panel = renderPanel(); + let editor = visitElements( + panel, + (element) => element.props.instanceId === codexId && element.props.mode === "editor", + ); + const block = editor?.props.usageLimits as ReactElement> | null; + expect(block).not.toBeNull(); + expect(block?.props.provider).toBe(live); + expect(block?.props.environmentId).toBe(environmentId); + expect(block?.props.readOnly).toBe(false); + + panel = renderPanel({ readOnly: true }); + editor = visitElements( + panel, + (element) => element.props.instanceId === codexId && element.props.mode === "editor", + ); + expect( + (editor?.props.usageLimits as ReactElement> | null)?.props.readOnly, + ).toBe(true); + }); + + it("omits the usage-limits block when the provider reports none or cannot have any", () => { + atoms.providers = [provider()]; + let panel = renderPanel(); + let editor = visitElements( + panel, + (element) => element.props.instanceId === codexId && element.props.mode === "editor", + ); + expect(editor).not.toBeNull(); + expect(editor?.props.usageLimits).toBeNull(); + + atoms.providers = [ + { + ...provider(), + usageLimits: { + checkedAt: "2026-07-24T12:00:00.000Z", + windows: [], + unavailable: { reason: "unsupported" }, + }, + }, + ]; + panel = renderPanel(); + editor = visitElements( + panel, + (element) => element.props.instanceId === codexId && element.props.mode === "editor", + ); + expect(editor?.props.usageLimits).toBeNull(); + }); + it("keeps Advanced visible when search targets the provider health interval", () => { let panel = renderPanel(); expect(visitElements(panel, (element) => element.props.title === "Advanced")).not.toBeNull(); diff --git a/apps/web/src/components/settings/ProviderSettingsPanel.tsx b/apps/web/src/components/settings/ProviderSettingsPanel.tsx index 88ea31a8ffe2..d126ad5ddb46 100644 --- a/apps/web/src/components/settings/ProviderSettingsPanel.tsx +++ b/apps/web/src/components/settings/ProviderSettingsPanel.tsx @@ -66,6 +66,7 @@ import { import { ScrollArea } from "../ui/scroll-area"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { stackedThreadToast, toastManager } from "../ui/toast"; +import { ProviderUsageLimitsBlock } from "../usage/UsageLimits"; import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog"; import { ProviderInstanceCard } from "./ProviderInstanceCard"; import { ProviderSetupSection, readAntigravityAuthMethod } from "./ProviderSetupSection"; @@ -850,6 +851,18 @@ export function EnvironmentProviderSettings({ /> ) : null } + usageLimits={ + mode === "editor" && + liveProvider?.usageLimits && + liveProvider.usageLimits.unavailable?.reason !== "unsupported" ? ( + + ) : null + } onUpdate={(next) => { const wasEnabled = resolveProviderInstanceEnabled(row.instance); const isDisabling = next.enabled === false && wasEnabled; diff --git a/apps/web/src/components/usage/UsageLimits.tsx b/apps/web/src/components/usage/UsageLimits.tsx index eff21977f65b..304d2ce3918f 100644 --- a/apps/web/src/components/usage/UsageLimits.tsx +++ b/apps/web/src/components/usage/UsageLimits.tsx @@ -257,27 +257,27 @@ function AccountHeading({ ); } -function ProviderLimits({ +/** + * The windows and banked reset credits one provider reports, without the + * account heading. Shared by the Usage → Limits page and the provider's + * editor in Settings → Providers so both surfaces read and act the same. + */ +export function ProviderUsageLimitsBlock({ provider, environmentId, now, + readOnly = false, }: { readonly provider: ServerProvider; readonly environmentId: EnvironmentId; readonly now: number; + readonly readOnly?: boolean | undefined; }) { const limits = provider.usageLimits; if (!limits) return null; const notice = limitsNotice(limits); return ( -
- getDriverOption(driver)?.label)} - plan={provider.auth.label} - email={provider.auth.email} - accentColor={provider.accentColor} - /> + <> {notice ? ( {notice} ) : ( @@ -289,8 +289,33 @@ function ProviderLimits({ instanceId={provider.instanceId} credits={limits.resetCredits} now={now} + readOnly={readOnly} /> ) : null} + + ); +} + +function ProviderLimits({ + provider, + environmentId, + now, +}: { + readonly provider: ServerProvider; + readonly environmentId: EnvironmentId; + readonly now: number; +}) { + if (!provider.usageLimits) return null; + return ( +
+ getDriverOption(driver)?.label)} + plan={provider.auth.label} + email={provider.auth.email} + accentColor={provider.accentColor} + /> +
); } @@ -311,11 +336,13 @@ function ResetCredits({ instanceId, credits, now, + readOnly = false, }: { readonly environmentId: EnvironmentId; readonly instanceId: ProviderInstanceId; readonly credits: ServerProviderResetCredits; readonly now: number; + readonly readOnly?: boolean | undefined; }) { const consume = useAtomCommand(serverEnvironment.consumeResetCredit, { reportFailure: false }); const [confirming, setConfirming] = useState(false); @@ -354,7 +381,12 @@ function ResetCredits({
{summary} {credits.availableCount > 0 ? ( - ) : null}