From 6744954d2312894c2ed4c6812c3e692569f2b103 Mon Sep 17 00:00:00 2001 From: Your GitHub Username Date: Tue, 8 Sep 2026 23:54:58 +0200 Subject: [PATCH] feat(web): preview provider limits from the Usage icon --- .../src/components/sidebar/SidebarChrome.tsx | 16 +++- apps/web/src/components/usage/UsageLimits.tsx | 10 +- .../components/usage/UsageLimitsPooled.tsx | 9 +- .../components/usage/UsageLimitsTooltip.tsx | 91 +++++++++++++++++++ .../src/components/usage/usageProviders.ts | 9 +- 5 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/components/usage/UsageLimitsTooltip.tsx diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index 4f115a751422..32c9b9cf7c98 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -5,7 +5,7 @@ import { SettingsIcon, } from "lucide-react"; import type { ReactNode } from "react"; -import { memo, useCallback } from "react"; +import { memo, useCallback, useId } from "react"; import { Link, useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router"; import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; @@ -30,6 +30,7 @@ import { useSidebar, } from "../ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { UsageLimitsTooltip } from "../usage/UsageLimitsTooltip"; import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences"; import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill"; import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill"; @@ -110,22 +111,30 @@ function SidebarUtilityItem({ icon, label, onClick, + renderTooltip, }: { icon: ReactNode; label: string; onClick: () => void; + renderTooltip?: (id: string) => ReactNode; }) { + const tooltipId = useId(); return ( + {icon} } /> - {label} + {renderTooltip ? renderTooltip(tooltipId) : {label}} ); @@ -213,6 +222,7 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { icon={} label="Usage" onClick={handleUsageClick} + renderTooltip={(id) => } /> )} diff --git a/apps/web/src/components/usage/UsageLimits.tsx b/apps/web/src/components/usage/UsageLimits.tsx index e50547b66008..25be99cdcb9e 100644 --- a/apps/web/src/components/usage/UsageLimits.tsx +++ b/apps/web/src/components/usage/UsageLimits.tsx @@ -5,7 +5,6 @@ import { ServerProvider, ServerProviderResetCredits, ServerProviderUsageWindow, - UsageProviderKind, } from "@t3tools/contracts"; import { useAtomValue } from "@effect/atom-react"; import { @@ -36,7 +35,7 @@ import { import { Button } from "../ui/button"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { UsageLimitsPooled } from "./UsageLimitsPooled"; -import { PROVIDER_PRESENTATION } from "./usageProviders"; +import { barColor } from "./usageProviders"; const PACE: Record = { ahead: { label: "Ahead of pace: spending faster than the window elapses", icon: TrendingUpIcon }, @@ -44,13 +43,6 @@ const PACE: Record Date.now()); + const pools = collectLimitPools(collectLimitAccounts(presentations), now); + const notices = collectLimitNotices(presentations); + + return ( +
+

Usage limits

+ {pools.map((pool) => ( +
+

+ {getDriverOption(pool.driver)?.label ?? String(pool.driver)} + {pool.accounts.length > 1 ? ( + + {pool.accounts.length} accounts + + ) : null} +

+ {pool.windows.map((window) => { + const nextRefill = window.resets.find((reset) => reset.restoresPercent > 0); + return ( +
+
+ {window.label} + + {window.remainingPercent}% left + +
+
+
+
+ {nextRefill ? ( + + +{nextRefill.restoresPercent}%{" "} + {nextRefill.at <= now ? "now" : `in ${formatDuration(nextRefill.at - now)}`} + + ) : null} +
+ ); + })} +
+ ))} + {pools.length === 0 && notices.length === 0 ?

No subscription limits reported.

: null} + {notices.length > 0 ? ( +
    + {notices.map((notice) => ( +
  • + {notice} +
  • + ))} +
+ ) : null} +
+ ); +} + +export function UsageLimitsTooltip({ id }: { readonly id: string }) { + return ( + + + + ); +} diff --git a/apps/web/src/components/usage/usageProviders.ts b/apps/web/src/components/usage/usageProviders.ts index efad95e531ad..43d5b6f6e8d7 100644 --- a/apps/web/src/components/usage/usageProviders.ts +++ b/apps/web/src/components/usage/usageProviders.ts @@ -1,4 +1,4 @@ -import type { UsageProviderKind } from "@t3tools/contracts"; +import type { ServerProvider, UsageProviderKind } from "@t3tools/contracts"; import { ClaudeAI, GrokIcon, type Icon, OpenAI } from "../Icons"; @@ -32,6 +32,13 @@ export const PROVIDER_PRESENTATION = { }, } satisfies Record; +/** The series colour the cost chart uses for this driver, so the two views read as one. */ +export function barColor(driver: ServerProvider["driver"]): string { + const kind: UsageProviderKind | undefined = + driver === "codex" ? "codex" : driver === "claudeAgent" ? "claude" : undefined; + return kind ? PROVIDER_PRESENTATION[kind].color : "var(--foreground)"; +} + /** Stable provider reading order across charts, summaries, tables, and hover rows. */ export const PROVIDER_ORDER = Object.keys(PROVIDER_PRESENTATION) as UsageProviderKind[];