Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3953,6 +3953,7 @@ export default function ChatView(props: ChatViewProps) {
providers: providerStatuses,
sources: serverConfig?.usageLimitSources ?? EMPTY_USAGE_LIMIT_SOURCES,
contextWindow: activeContextWindow,
onOpenUsageLimits: openUsageLimits,
}}
/>

Expand Down
119 changes: 119 additions & 0 deletions apps/web/src/components/chat/ThreadDetailsUsage.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
ProviderDriverKind,
ProviderInstanceId,
UsageLimitSourceId,
type ServerProvider,
type UsageLimitSourceSnapshots,
} from "@t3tools/contracts";
import { expect, it, vi } from "vite-plus/test";
import { render } from "../../../test/render";
import { ThreadDetailsUsage } from "./ThreadDetailsUsage";

const checkedAt = "2026-11-04T12:00:00.000Z";
const provider: ServerProvider = {
instanceId: ProviderInstanceId.make("claude-proxy"),
driver: ProviderDriverKind.make("claudeAgent"),
enabled: true,
installed: true,
version: null,
status: "ready",
auth: { status: "authenticated" },
checkedAt,
models: [],
slashCommands: [],
skills: [],
usageLimits: { checkedAt, windows: [], unavailable: { reason: "unsupported" } },
};
const sources: UsageLimitSourceSnapshots = [
{
id: UsageLimitSourceId.make("hub"),
kind: "cliproxy",
label: "Claude pool",
checkedAt,
accounts: [
{
id: "claude-account",
driver: provider.driver,
usageLimits: {
checkedAt,
windows: [{ id: "session", kind: "session", label: "Session", usedPercent: 17 }],
},
},
],
},
];

it("opens available hub limits without attributing a hub account's quota to the endpoint", async () => {
const onOpenUsageLimits = vi.fn();
await using screen = await render(
<ThreadDetailsUsage
providerInstanceId={provider.instanceId}
providers={[provider]}
sources={sources}
onOpenUsageLimits={onOpenUsageLimits}
/>,
);
await expect.element(screen.getByRole("button", { name: "View hub limits" })).toBeVisible();
await expect.element(screen.getByText("Unavailable", { exact: true })).not.toBeInTheDocument();
await screen.getByRole("button", { name: "View hub limits" }).click();
expect(onOpenUsageLimits).toHaveBeenCalledOnce();

for (const unavailableSources of [
[],
[
{
...sources[0]!,
accounts: [{ ...sources[0]!.accounts[0]!, driver: ProviderDriverKind.make("codex") }],
},
],
[
{
...sources[0]!,
accounts: [
{
...sources[0]!.accounts[0]!,
usageLimits: {
checkedAt,
windows: [],
unavailable: { reason: "probeFailed" as const },
},
},
],
},
],
]) {
await screen.rerender(
<ThreadDetailsUsage
providerInstanceId={provider.instanceId}
providers={[provider]}
sources={unavailableSources}
onOpenUsageLimits={onOpenUsageLimits}
/>,
);
await expect.element(screen.getByText("Unavailable", { exact: true })).toBeVisible();
await expect
.element(screen.getByRole("button", { name: "View hub limits" }))
.not.toBeInTheDocument();
}

await screen.rerender(
<ThreadDetailsUsage
providerInstanceId={provider.instanceId}
providers={[
{
...provider,
usageLimits: {
checkedAt,
windows: [{ id: "session", kind: "session", label: "Session", usedPercent: 60 }],
},
},
]}
sources={sources}
onOpenUsageLimits={onOpenUsageLimits}
/>,
);
await expect.element(screen.getByText("40%", { exact: true })).toBeVisible();
await expect
.element(screen.getByRole("button", { name: "View hub limits" }))
.not.toBeInTheDocument();
});
28 changes: 23 additions & 5 deletions apps/web/src/components/chat/ThreadDetailsUsage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { ContextWindowSnapshot } from "@t3tools/client-runtime/state/contex
import { formatContextWindowTokens } from "../../lib/contextWindow";
import { cn } from "../../lib/utils";
import { formatUpcomingTimestamp } from "../../timestampFormat";
import { Button } from "../ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { formatContextWindowCompactionMessage } from "./ContextWindowMeter.logic";
import { ThreadDetailsSection } from "./ThreadDetailsSection";
Expand All @@ -35,6 +36,7 @@ export interface ThreadDetailsUsageInput {
readonly providers: readonly ServerProvider[];
readonly sources: UsageLimitSourceSnapshots;
readonly contextWindow: ContextWindowSnapshot | null;
readonly onOpenUsageLimits: () => void;
}

type UsageTone = "error" | "warning" | null;
Expand All @@ -44,6 +46,7 @@ export function ThreadDetailsUsage({
providerInstanceId,
providers,
sources,
onOpenUsageLimits,
}: Omit<ThreadDetailsUsageInput, "contextWindow">) {
const nowMinute = useNowMinute();
const now = Date.parse(`${nowMinute}:00Z`);
Expand All @@ -53,11 +56,19 @@ export function ThreadDetailsUsage({
: null;
// Only this instance's data: the report keeps one instance per login, and any other entry may be
// a different login or workspace, so fall back to the instance's own snapshot.
const selected = providers.find((provider) => provider.instanceId === providerInstanceId);
const limits =
report?.accounts.find((entry) => entry.instanceId === providerInstanceId)?.limits ??
providers.find((provider) => provider.instanceId === providerInstanceId)?.usageLimits;
if (!limits) return null;
const notice = limitsNotice(limits);
selected?.usageLimits;
const hasHubLimits = sources.some((source) =>
source.accounts.some(
(account) =>
account.driver === selected?.driver && limitsNotice(account.usageLimits) === null,
),
);
if (!limits && !hasHubLimits) return null;
const notice = limits ? limitsNotice(limits) : null;
const showHubLimits = hasHubLimits && (!limits || notice !== null);

return (
<ThreadDetailsSection
Expand All @@ -66,7 +77,14 @@ export function ThreadDetailsUsage({
showHeading={false}
>
<div className="grid grid-cols-[auto_minmax(3rem,1fr)_auto] gap-x-3 text-xs">
{notice ? (
{showHubLimits ? (
<div className="col-span-3 flex items-center justify-between gap-2 px-2.5">
<span className="text-muted-foreground">Limits</span>
<Button variant="link" size="micro" onClick={onOpenUsageLimits}>
View hub limits
</Button>
</div>
) : notice ? (
<UsageRow
label="Limits"
fill={null}
Expand All @@ -75,7 +93,7 @@ export function ThreadDetailsUsage({
tooltip={<span className="text-foreground">{notice}</span>}
/>
) : (
limits.windows.map((window) => <LimitRow key={window.id} window={window} now={now} />)
limits?.windows.map((window) => <LimitRow key={window.id} window={window} now={now} />)
)}
</div>
</ThreadDetailsSection>
Expand Down
Loading