diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx
index 09b13e53..2ea58a8c 100644
--- a/packages/ui/src/components/layout/VSCodeLayout.tsx
+++ b/packages/ui/src/components/layout/VSCodeLayout.tsx
@@ -6,8 +6,34 @@ import { useSessionStore } from '@/stores/useSessionStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
import { McpDropdown } from '@/components/mcp/McpDropdown';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
-import { RiAddLine, RiArrowLeftLine, RiRobot2Line, RiSettings3Line } from '@remixicon/react';
+import { ProviderLogo } from '@/components/ui/ProviderLogo';
+import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
+import { formatPercent, formatWindowLabel, QUOTA_PROVIDERS } from '@/lib/quota';
+import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
+import { updateDesktopSettings } from '@/lib/persistence';
+import type { UsageWindow } from '@/types';
+import { RiAddLine, RiArrowLeftLine, RiRefreshLine, RiRobot2Line, RiSettings3Line, RiTimerLine } from '@remixicon/react';
+
+const formatTime = (timestamp: number | null) => {
+ if (!timestamp) return '-';
+ try {
+ return new Date(timestamp).toLocaleTimeString(undefined, {
+ hour: 'numeric',
+ minute: '2-digit',
+ });
+ } catch {
+ return '-';
+ }
+};
// Width threshold for mobile vs desktop layout in settings
const MOBILE_WIDTH_THRESHOLD = 550;
@@ -365,6 +391,7 @@ export const VSCodeLayout: React.FC = () => {
onBack={handleBackToSessions}
showMcp
showContextUsage
+ showRateLimits
/>
@@ -386,11 +413,26 @@ interface VSCodeHeaderProps {
onAgentManager?: () => void;
showMcp?: boolean;
showContextUsage?: boolean;
+ showRateLimits?: boolean;
}
-const VSCodeHeader: React.FC = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage }) => {
+const VSCodeHeader: React.FC = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits }) => {
const { getCurrentModel } = useConfigStore();
const getContextUsage = useSessionStore((state) => state.getContextUsage);
+ const quotaResults = useQuotaStore((state) => state.results);
+ const fetchAllQuotas = useQuotaStore((state) => state.fetchAllQuotas);
+ const isQuotaLoading = useQuotaStore((state) => state.isLoading);
+ const quotaLastUpdated = useQuotaStore((state) => state.lastUpdated);
+ const quotaDisplayMode = useQuotaStore((state) => state.displayMode);
+ const dropdownProviderIds = useQuotaStore((state) => state.dropdownProviderIds);
+ const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
+ const setQuotaDisplayMode = useQuotaStore((state) => state.setDisplayMode);
+
+ useQuotaAutoRefresh();
+
+ React.useEffect(() => {
+ void loadQuotaSettings();
+ }, [loadQuotaSettings]);
const currentModel = getCurrentModel();
const limits = (currentModel?.limit && typeof currentModel.limit === 'object'
@@ -400,6 +442,38 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on
const outputLimit = typeof limits?.output === 'number' ? limits.output : 0;
const contextUsage = getContextUsage(contextLimit, outputLimit);
+ const rateLimitGroups = React.useMemo(() => {
+ const groups: Array<{
+ providerId: string;
+ providerName: string;
+ entries: Array<[string, UsageWindow]>;
+ }> = [];
+
+ for (const provider of QUOTA_PROVIDERS) {
+ if (!dropdownProviderIds.includes(provider.id)) {
+ continue;
+ }
+ const result = quotaResults.find((entry) => entry.providerId === provider.id);
+ const windows = (result?.usage?.windows ?? {}) as Record;
+ const entries = Object.entries(windows);
+ if (entries.length > 0) {
+ groups.push({ providerId: provider.id, providerName: provider.name, entries });
+ }
+ }
+
+ return groups;
+ }, [dropdownProviderIds, quotaResults]);
+ const hasRateLimits = rateLimitGroups.length > 0;
+
+ const handleDisplayModeChange = React.useCallback(async (mode: 'usage' | 'remaining') => {
+ setQuotaDisplayMode(mode);
+ try {
+ await updateDesktopSettings({ usageDisplayMode: mode });
+ } catch (error) {
+ console.warn('Failed to update usage display mode:', error);
+ }
+ }, [setQuotaDisplayMode]);
+
return (
{showBack && onBack && (
@@ -435,6 +509,130 @@ const VSCodeHeader: React.FC
= ({ title, showBack, onBack, on
headerIconButtonClass="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
/>
)}
+ {showRateLimits && (
+ {
+ if (open && quotaResults.length === 0) {
+ fetchAllQuotas();
+ }
+ }}
+ >
+
+
+
+
+
+
+ Rate limits
+
+
+
+
+
+
+
+
+
+ Last updated {formatTime(quotaLastUpdated)}
+
+
+ {!hasRateLimits && (
+ event.preventDefault()}>
+ No rate limits available.
+
+ )}
+ {rateLimitGroups.map((group, index) => (
+
+
+
+ {group.providerName}
+
+ {group.entries.length === 0 ? (
+ event.preventDefault()}
+ >
+ No rate limits reported.
+
+ ) : (
+ group.entries.map(([label, window]) => (
+ event.preventDefault()}
+ >
+
+ {(() => {
+ const displayPercent = quotaDisplayMode === 'remaining'
+ ? window.remainingPercent
+ : window.usedPercent;
+ return (
+ <>
+
+ {formatWindowLabel(label)}
+
+ {formatPercent(displayPercent) === '-' ? '' : formatPercent(displayPercent)}
+
+
+
+
+ {window.resetAfterFormatted ?? window.resetAtFormatted ?? ''}
+
+ >
+ );
+ })()}
+
+
+ ))
+ )}
+ {index < rateLimitGroups.length - 1 && }
+
+ ))}
+
+
+ )}
{onSettings && (