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 && (