From 2d391cb5d1ef9a1268ed00fd9737c869b61bf3b2 Mon Sep 17 00:00:00 2001 From: Logan <95306463+LaganYT@users.noreply.github.com> Date: Wed, 4 Nov 2026 08:45:13 -0600 Subject: [PATCH] fix(web): offer hub limits from the model usage popup --- apps/web/src/components/ChatView.tsx | 1 + .../chat/ThreadDetailsUsage.browser.test.tsx | 119 ++++++++++++++++++ .../components/chat/ThreadDetailsUsage.tsx | 28 ++++- 3 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/components/chat/ThreadDetailsUsage.browser.test.tsx diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d1225065aeeca..fb49394f600a4 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3907,6 +3907,7 @@ export default function ChatView(props: ChatViewProps) { providers: providerStatuses, sources: serverConfig?.usageLimitSources ?? EMPTY_USAGE_LIMIT_SOURCES, contextWindow: activeContextWindow, + onOpenUsageLimits: openUsageLimits, }} /> diff --git a/apps/web/src/components/chat/ThreadDetailsUsage.browser.test.tsx b/apps/web/src/components/chat/ThreadDetailsUsage.browser.test.tsx new file mode 100644 index 0000000000000..9c3c329c5d294 --- /dev/null +++ b/apps/web/src/components/chat/ThreadDetailsUsage.browser.test.tsx @@ -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( + , + ); + 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( + , + ); + await expect.element(screen.getByText("Unavailable", { exact: true })).toBeVisible(); + await expect + .element(screen.getByRole("button", { name: "View hub limits" })) + .not.toBeInTheDocument(); + } + + await screen.rerender( + , + ); + await expect.element(screen.getByText("40%", { exact: true })).toBeVisible(); + await expect + .element(screen.getByRole("button", { name: "View hub limits" })) + .not.toBeInTheDocument(); +}); diff --git a/apps/web/src/components/chat/ThreadDetailsUsage.tsx b/apps/web/src/components/chat/ThreadDetailsUsage.tsx index 05211d6ace44e..17d4ecb3d3b9c 100644 --- a/apps/web/src/components/chat/ThreadDetailsUsage.tsx +++ b/apps/web/src/components/chat/ThreadDetailsUsage.tsx @@ -19,6 +19,7 @@ import { useNowMinute } from "../../hooks/useNowMinute"; import { type ContextWindowSnapshot, 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"; @@ -34,6 +35,7 @@ export interface ThreadDetailsUsageInput { readonly providers: readonly ServerProvider[]; readonly sources: UsageLimitSourceSnapshots; readonly contextWindow: ContextWindowSnapshot | null; + readonly onOpenUsageLimits: () => void; } type UsageTone = "error" | "warning" | null; @@ -43,6 +45,7 @@ export function ThreadDetailsUsage({ providerInstanceId, providers, sources, + onOpenUsageLimits, }: Omit) { const nowMinute = useNowMinute(); const now = Date.parse(`${nowMinute}:00Z`); @@ -52,11 +55,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 ( - {notice ? ( + {showHubLimits ? ( + + Limits + + View hub limits + + + ) : notice ? ( {notice}} /> ) : ( - limits.windows.map((window) => ) + limits?.windows.map((window) => ) )}