Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/web/src/components/settings/ProviderInstanceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
readonly favoriteModels: ReadonlyArray<string>;
readonly modelOrder: ReadonlyArray<string>;
Expand Down Expand Up @@ -404,6 +410,7 @@ export function ProviderInstanceCard({
onDelete,
headerAction,
setup,
usageLimits,
hiddenModels,
favoriteModels,
modelOrder,
Expand Down Expand Up @@ -805,6 +812,12 @@ export function ProviderInstanceCard({
</SettingsSection>
) : null}

{usageLimits ? (
<SettingsSection title="Usage limits">
<div className="flex flex-col gap-3 px-3 py-3 sm:px-4">{usageLimits}</div>
</SettingsSection>
) : null}

<SettingsSection
title="Runtime"
inert={readOnly}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,74 @@ describe("EnvironmentProviderSettings routing", () => {
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<Record<string, unknown>> | 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<Record<string, unknown>> | 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();
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/settings/ProviderSettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -850,6 +851,18 @@ export function EnvironmentProviderSettings({
/>
) : null
}
usageLimits={
mode === "editor" &&
liveProvider?.usageLimits &&
liveProvider.usageLimits.unavailable?.reason !== "unsupported" ? (
<ProviderUsageLimitsBlock
provider={liveProvider}
environmentId={environmentId}
now={Date.now()}
readOnly={readOnly}
/>
) : null
}
onUpdate={(next) => {
const wasEnabled = resolveProviderInstanceEnabled(row.instance);
const isDisabling = next.enabled === false && wasEnabled;
Expand Down
52 changes: 42 additions & 10 deletions apps/web/src/components/usage/UsageLimits.tsx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High

<Button onClick={() => void redeem()}>Use credit</Button>

An already-open dialog still lets the user redeem a credit after readOnly becomes true, so clicking Use credit dispatches consumeResetCredit without operate permission. readOnly only disables the opener; also disable the confirmation button.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/usage/UsageLimits.tsx around line 405:

An already-open dialog still lets the user redeem a credit after `readOnly` becomes `true`, so clicking `Use credit` dispatches `consumeResetCredit` without operate permission. `readOnly` only disables the opener; also disable the confirmation button.

Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<section className="flex flex-col gap-3">
<AccountHeading
driver={provider.driver}
label={providerLimitsLabel(provider, (driver) => getDriverOption(driver)?.label)}
plan={provider.auth.label}
email={provider.auth.email}
accentColor={provider.accentColor}
/>
<>
{notice ? (
<span className="text-xs text-muted-foreground">{notice}</span>
) : (
Expand All @@ -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 (
<section className="flex flex-col gap-3">
<AccountHeading
driver={provider.driver}
label={providerLimitsLabel(provider, (driver) => getDriverOption(driver)?.label)}
plan={provider.auth.label}
email={provider.auth.email}
accentColor={provider.accentColor}
/>
<ProviderUsageLimitsBlock provider={provider} environmentId={environmentId} now={now} />
</section>
);
}
Expand All @@ -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);
Expand Down Expand Up @@ -354,7 +381,12 @@ function ResetCredits({
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span className="tabular-nums">{summary}</span>
{credits.availableCount > 0 ? (
<Button size="xs" variant="outline" disabled={busy} onClick={() => setConfirming(true)}>
<Button
size="xs"
variant="outline"
disabled={busy || readOnly}
onClick={() => setConfirming(true)}
>
{busy ? "Using credit…" : "Use a reset credit"}
</Button>
) : null}
Expand Down
Loading