diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
index b5359dbd7b0b6..e3fd2ee6743c8 100644
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -3114,7 +3114,7 @@ export default function ChatView(props: ChatViewProps) {
onOpenProviderSetup: openProviderSetupStable,
onOpenTerminal: () => setTerminalOpenStable(true),
onProviderModelSelect: onProviderModelSelectStable,
- onShowLimits: usageLimitsOffered && usageLimitsKey !== null ? openUsageLimits : null,
+ limitsAvailable: usageLimitsOffered && usageLimitsKey !== null,
threadId: activeThread?.id ?? null,
threadError: timelineThreadError,
threadErrorClass: threadErrorIsServerRuntimeError
diff --git a/apps/web/src/components/chat/ChatBannerLane.tsx b/apps/web/src/components/chat/ChatBannerLane.tsx
index 95025e4745120..b00830b679c1b 100644
--- a/apps/web/src/components/chat/ChatBannerLane.tsx
+++ b/apps/web/src/components/chat/ChatBannerLane.tsx
@@ -31,7 +31,7 @@ export function useChatBannerLane(input: {
readonly onOpenProviderSetup: (instanceId: ProviderInstanceId) => void;
readonly onOpenTerminal: () => void;
readonly onProviderModelSelect: (instanceId: ProviderInstanceId, model: string) => void;
- readonly onShowLimits: (() => void) | null;
+ readonly limitsAvailable: boolean;
readonly threadId: ThreadId | null;
readonly threadError: string | null;
readonly threadErrorClass: OrchestrationV2ProviderFailureClass | null;
@@ -54,7 +54,7 @@ export function useChatBannerLane(input: {
onOpenProviderSetup,
onOpenTerminal,
onProviderModelSelect,
- onShowLimits,
+ limitsAvailable,
threadId,
threadError,
threadErrorClass,
@@ -106,7 +106,15 @@ export function useChatBannerLane(input: {
switchTarget: lowLimitSwitchTarget,
onSwitch: (target) =>
onProviderModelSelect(target.instanceId, getDefaultServerModel([target], target.driver)),
- onShowLimits,
+ limits:
+ limitsAvailable && activeProviderStatus !== null
+ ? {
+ environmentId,
+ instanceId: activeProviderStatus.instanceId,
+ providers: providerStatuses,
+ sources: usageLimitSources,
+ }
+ : null,
onDismissed: () => setTrayNoticeDismissTick((tick) => tick + 1),
}),
);
diff --git a/apps/web/src/components/chat/ComposerUsageLimits.tsx b/apps/web/src/components/chat/ComposerUsageLimits.tsx
index 64994ce07bf1d..48b78652cab59 100644
--- a/apps/web/src/components/chat/ComposerUsageLimits.tsx
+++ b/apps/web/src/components/chat/ComposerUsageLimits.tsx
@@ -1,6 +1,7 @@
import {
type EnvironmentId,
isProviderAvailable,
+ type ProviderInstanceId,
type ServerProvider,
type ServerProviderUsageWindow,
type UsageLimitSourceSnapshots,
@@ -9,6 +10,7 @@ import {
import type { TimestampFormat } from "@t3tools/contracts/settings";
import {
collectLimitAccounts,
+ collectProviderUsageLimits,
formatResetsIn,
formatRunsOutIn,
isLowLimit,
@@ -17,11 +19,13 @@ import {
tightestSessionLimit,
} from "@t3tools/shared/usageLimits";
import { GaugeIcon } from "lucide-react";
+import { useRef, useState } from "react";
import { useNowMinute } from "../../hooks/useNowMinute";
import { ensureLocalApi } from "../../localApi";
import { formatUpcomingTimestamp } from "../../timestampFormat";
import { Button } from "../ui/button";
+import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { providerClients } from "../settings/providerDriverMeta";
import { RedactedSensitiveText } from "../settings/RedactedSensitiveText";
import { LimitWindows, ResetCredits } from "../usage/UsageLimits";
@@ -165,63 +169,77 @@ function UsageLimitsBannerBody({
readonly report: UsageLimitsReport;
readonly environmentId: EnvironmentId;
}) {
- const now = Date.parse(report.createdAt);
return (