import React, { useEffect } from 'react'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { SortableTabsStrip, type SortableTabsStripItem } from '@/components/ui/sortable-tabs-strip'; import { RiArrowLeftSLine, RiChat4Line, RiCheckLine, RiCloseLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGithubFill, RiLayoutLeftLine, RiLayoutRightLine, RiPlayListAddLine, RiRefreshLine, RiServerLine, RiStackLine, RiTerminalBoxLine, RiTimerLine, type RemixiconComponentType } from '@remixicon/react'; import { DiffIcon } from '@/components/icons/DiffIcon'; import { useUIStore, type MainTab } from '@/stores/useUIStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { useDeviceInfo } from '@/lib/device'; import { cn, hasModifier } from '@/lib/utils'; import { McpDropdownContent } from '@/components/mcp/McpDropdown'; import { McpIcon } from '@/components/icons/McpIcon'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { formatPercent, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota'; import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar'; import { PaceIndicator } from '@/components/sections/usage/PaceIndicator'; import { updateDesktopSettings } from '@/lib/persistence'; import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { getAllModelFamilies, getDisplayModelName, groupModelsByFamily, sortModelFamilies, } from '@/lib/quota/model-families'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'; import type { UsageWindow } from '@/types'; import type { GitHubAuthStatus } from '@/lib/api/types'; import type { SessionContextUsage } from '@/stores/types/sessionTypes'; import { DesktopHostSwitcherDialog } from '@/components/desktop/DesktopHostSwitcher'; import { OpenInAppButton } from '@/components/desktop/OpenInAppButton'; import { ProjectActionsButton } from '@/components/layout/ProjectActionsButton'; import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; import { desktopHostsGet, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts'; const isSameContextUsage = ( a: SessionContextUsage | null, b: SessionContextUsage | null, ): boolean => { if (a === b) return true; if (!a || !b) return false; return a.totalTokens === b.totalTokens && a.percentage === b.percentage && a.contextLimit === b.contextLimit && (a.outputLimit ?? 0) === (b.outputLimit ?? 0) && (a.normalizedOutput ?? 0) === (b.normalizedOutput ?? 0) && a.thresholdLimit === b.thresholdLimit && (a.lastMessageId ?? '') === (b.lastMessageId ?? ''); }; const formatTime = (timestamp: number | null) => { if (!timestamp) return '-'; try { return new Date(timestamp).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', }); } catch { return '-'; } }; const normalize = (value: string): string => { if (!value) return ''; const replaced = value.replace(/\\/g, '/'); return replaced === '/' ? '/' : replaced.replace(/\/+$/, ''); }; const joinPath = (base: string, segment: string): string => { const normalizedBase = normalize(base); const cleanSegment = segment.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, ''); if (!normalizedBase || normalizedBase === '/') { return `/${cleanSegment}`; } return `${normalizedBase}/${cleanSegment}`; }; const buildRepoPlansDirectory = (directory: string): string => { return joinPath(joinPath(directory, '.opencode'), 'plans'); }; const buildHomePlansDirectory = (): string => { return '~/.opencode/plans'; }; const resolveTilde = (path: string, homeDir: string | null): string => { const trimmed = path.trim(); if (!trimmed.startsWith('~')) return trimmed; if (trimmed === '~') return homeDir || trimmed; if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) { return homeDir ? `${homeDir}${trimmed.slice(1)}` : trimmed; } return trimmed; }; const getActiveContextMode = (panelState: { isOpen: boolean; activeTabId: string | null; tabs: Array<{ id: string; mode: 'diff' | 'file' | 'context' | 'plan' | 'chat' }>; } | undefined): 'diff' | 'file' | 'context' | 'plan' | 'chat' | null => { if (!panelState?.isOpen || !Array.isArray(panelState.tabs) || panelState.tabs.length === 0) { return null; } const activeTab = panelState.tabs.find((tab) => tab.id === panelState.activeTabId) ?? panelState.tabs[panelState.tabs.length - 1]; return activeTab?.mode ?? null; }; interface TabConfig { id: MainTab; label: string; icon: RemixiconComponentType | 'diff'; badge?: number; showDot?: boolean; } interface HeaderProps { onToggleLeftDrawer?: () => void; onToggleRightDrawer?: () => void; leftDrawerOpen?: boolean; rightDrawerOpen?: boolean; } export const Header: React.FC = ({ onToggleLeftDrawer, onToggleRightDrawer, leftDrawerOpen, rightDrawerOpen, }) => { const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal); const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar); const openContextOverview = useUIStore((state) => state.openContextOverview); const openContextPlan = useUIStore((state) => state.openContextPlan); const closeContextPanel = useUIStore((state) => state.closeContextPanel); const contextPanelByDirectory = useUIStore((state) => state.contextPanelByDirectory); const activeMainTab = useUIStore((state) => state.activeMainTab); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const { getCurrentModel } = useConfigStore(); const runtimeApis = useRuntimeAPIs(); const getContextUsage = useSessionStore((state) => state.getContextUsage); const currentSessionId = useSessionStore((state) => state.currentSessionId); const currentSessionMessages = useSessionStore((state) => { if (!currentSessionId) { return undefined; } return state.messages.get(currentSessionId); }); const sessions = useSessionStore((state) => state.sessions); const activeProject = useProjectsStore((state) => { if (!state.activeProjectId) { return null; } return state.projects.find((project) => project.id === state.activeProjectId) ?? null; }); const activeProjectLabel = React.useMemo(() => { if (!activeProject) { return null; } const trimmedLabel = activeProject.label?.trim(); if (trimmedLabel) { return trimmedLabel; } const pathSegments = activeProject.path.split(/[\\/]/).filter(Boolean); return pathSegments[pathSegments.length - 1] ?? null; }, [activeProject]); 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); const homeDirectory = useDirectoryStore((state) => state.homeDirectory); const { isMobile } = useDeviceInfo(); const githubAuthStatus = useGitHubAuthStore((state) => state.status); const setGitHubAuthStatus = useGitHubAuthStore((state) => state.setStatus); const headerRef = React.useRef(null); const [isDesktopApp, setIsDesktopApp] = React.useState(() => { if (typeof window === 'undefined') { return false; } return isDesktopShell(); }); const isMacPlatform = React.useMemo(() => { if (typeof navigator === 'undefined') { return false; } return /Macintosh|Mac OS X/.test(navigator.userAgent || ''); }, []); const macosMajorVersion = React.useMemo(() => { if (typeof window === 'undefined') { return null; } const injected = (window as unknown as { __OPENCHAMBER_MACOS_MAJOR__?: unknown }).__OPENCHAMBER_MACOS_MAJOR__; if (typeof injected === 'number' && Number.isFinite(injected) && injected > 0) { return injected; } // Fallback: WebKit reports "Mac OS X 10_15_7" format where 10 is legacy prefix if (typeof navigator === 'undefined') { return null; } const match = (navigator.userAgent || '').match(/Mac OS X (\d+)[._](\d+)/); if (!match) { return null; } const first = Number.parseInt(match[1], 10); const second = Number.parseInt(match[2], 10); if (Number.isNaN(first)) { return null; } return first === 10 ? second : first; }, []); useEffect(() => { if (typeof window === 'undefined') { return; } setIsDesktopApp(isDesktopShell()); }, []); const currentModel = getCurrentModel(); const limit = currentModel && typeof currentModel.limit === 'object' && currentModel.limit !== null ? (currentModel.limit as Record) : null; const contextLimit = (limit && typeof limit.context === 'number' ? limit.context : 0); const outputLimit = (limit && typeof limit.output === 'number' ? limit.output : 0); const contextUsage = getContextUsage(contextLimit, outputLimit); const [stableDesktopContextUsage, setStableDesktopContextUsage] = React.useState(null); const isContextUsageResolvedForSession = !currentSessionId || currentSessionMessages !== undefined; useEffect(() => { if (!currentSessionId) { setStableDesktopContextUsage((prev) => (prev === null ? prev : null)); return; } if (contextUsage && contextUsage.totalTokens > 0) { setStableDesktopContextUsage((prev) => (isSameContextUsage(prev, contextUsage) ? prev : contextUsage)); return; } if (isContextUsageResolvedForSession) { setStableDesktopContextUsage((prev) => (prev === null ? prev : null)); } }, [contextUsage, currentSessionId, isContextUsageResolvedForSession]); const isSessionSwitcherOpen = useUIStore((state) => state.isSessionSwitcherOpen); const githubAvatarUrl = githubAuthStatus?.connected ? githubAuthStatus.user?.avatarUrl : null; const githubLogin = githubAuthStatus?.connected ? githubAuthStatus.user?.login : null; const githubAccounts = githubAuthStatus?.accounts ?? []; const [isSwitchingGitHubAccount, setIsSwitchingGitHubAccount] = React.useState(false); const [isMobileRateLimitsOpen, setIsMobileRateLimitsOpen] = React.useState(false); const [isDesktopServicesOpen, setIsDesktopServicesOpen] = React.useState(false); const [isUsageRefreshSpinning, setIsUsageRefreshSpinning] = React.useState(false); const [currentInstanceLabel, setCurrentInstanceLabel] = React.useState('Local'); const [desktopServicesTab, setDesktopServicesTab] = React.useState<'instance' | 'usage' | 'mcp'>( isDesktopApp ? 'instance' : 'usage' ); const [mobileServicesTab, setMobileServicesTab] = React.useState<'usage' | 'mcp'>('usage'); useEffect(() => { if (!isDesktopApp && desktopServicesTab === 'instance') { setDesktopServicesTab('usage'); } }, [desktopServicesTab, isDesktopApp]); const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); const showDesktopHeaderContextUsage = !isVSCode && activeMainTab === 'chat' && !!stableDesktopContextUsage && stableDesktopContextUsage.totalTokens > 0; const refreshCurrentInstanceLabel = React.useCallback(async () => { if (typeof window === 'undefined' || !isDesktopApp) { return; } try { const cfg = await desktopHostsGet(); const currentHref = window.location.href; const localOrigin = window.__OPENCHAMBER_LOCAL_ORIGIN__ || window.location.origin; if (locationMatchesHost(currentHref, localOrigin)) { setCurrentInstanceLabel('Local'); return; } const match = cfg.hosts.find((host) => { return locationMatchesHost(currentHref, host.url); }); if (match?.label?.trim()) { setCurrentInstanceLabel(redactSensitiveUrl(match.label.trim())); return; } setCurrentInstanceLabel('Instance'); } catch { setCurrentInstanceLabel('Local'); } }, [isDesktopApp]); useEffect(() => { void refreshCurrentInstanceLabel(); }, [refreshCurrentInstanceLabel]); useQuotaAutoRefresh(); const selectedModels = useQuotaStore((state) => state.selectedModels); const expandedFamilies = useQuotaStore((state) => state.expandedFamilies); const toggleFamilyExpanded = useQuotaStore((state) => state.toggleFamilyExpanded); interface RateLimitGroup { providerId: string; providerName: string; entries: Array<[string, UsageWindow]>; error?: string; modelFamilies?: Array<{ familyId: string | null; familyLabel: string; models: Array<[string, UsageWindow]>; }>; } const rateLimitGroups = React.useMemo(() => { const groups: RateLimitGroup[] = []; 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 models = result?.usage?.models; const entries = Object.entries(windows); const group: RateLimitGroup = { providerId: provider.id, providerName: provider.name, entries, error: (result && !result.ok && result.configured) ? result.error : undefined, }; // Add model families if provider has per-model quotas if (models && Object.keys(models).length > 0) { const providerSelectedModels = selectedModels[provider.id] ?? []; // hasExplicitSelection = true means user has selected specific models to show // If the array exists but is empty, treat as "show all" (user cleared selection) const hasExplicitSelection = providerSelectedModels.length > 0; const modelGroups = groupModelsByFamily(models, provider.id); const families = getAllModelFamilies(provider.id); const sortedFamilies = sortModelFamilies(families); group.modelFamilies = []; // Add predefined families first for (const family of sortedFamilies) { const modelNames = modelGroups.get(family.id) ?? []; if (modelNames.length === 0) continue; // Filter to selected models only, OR show all if nothing selected const selectedModelNames = hasExplicitSelection ? modelNames.filter((m: string) => providerSelectedModels.includes(m)) : modelNames; if (selectedModelNames.length === 0) continue; const familyModels: Array<[string, UsageWindow]> = []; for (const modelName of selectedModelNames) { const modelUsage = models[modelName] as { windows?: Record } | undefined; if (modelUsage?.windows) { const windowEntries = Object.entries(modelUsage.windows); if (windowEntries.length > 0) { familyModels.push([modelName, windowEntries[0][1]]); } } } if (familyModels.length > 0) { group.modelFamilies.push({ familyId: family.id, familyLabel: family.label, models: familyModels, }); } } // Add "Other" family for remaining models const otherModelNames = modelGroups.get(null) ?? []; const selectedOtherModels = hasExplicitSelection ? otherModelNames.filter((m: string) => providerSelectedModels.includes(m)) : otherModelNames; if (selectedOtherModels.length > 0) { const otherModels: Array<[string, UsageWindow]> = []; for (const modelName of selectedOtherModels) { const modelUsage = models[modelName] as { windows?: Record } | undefined; if (modelUsage?.windows) { const windowEntries = Object.entries(modelUsage.windows); if (windowEntries.length > 0) { otherModels.push([modelName, windowEntries[0][1]]); } } } if (otherModels.length > 0) { group.modelFamilies.push({ familyId: null, familyLabel: 'Other', models: otherModels, }); } } } if (entries.length > 0 || (group.modelFamilies && group.modelFamilies.length > 0) || group.error) { groups.push(group); } } return groups; }, [dropdownProviderIds, quotaResults, selectedModels]); const hasRateLimits = rateLimitGroups.length > 0; React.useEffect(() => { void loadQuotaSettings(); }, [loadQuotaSettings]); 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]); const handleUsageRefresh = React.useCallback(() => { if (isUsageRefreshSpinning) return; setIsUsageRefreshSpinning(true); const minSpinPromise = new Promise(resolve => setTimeout(resolve, 500)); Promise.all([fetchAllQuotas(), minSpinPromise]).finally(() => { setIsUsageRefreshSpinning(false); }); }, [fetchAllQuotas, isUsageRefreshSpinning]); const currentSession = React.useMemo(() => { if (!currentSessionId) return null; return sessions.find((s) => s.id === currentSessionId) ?? null; }, [currentSessionId, sessions]); const worktreePath = useSessionStore((state) => { if (!currentSessionId) return ''; return state.worktreeMetadata.get(currentSessionId)?.path ?? ''; }); const worktreeDirectory = React.useMemo(() => { return normalize(worktreePath || ''); }, [worktreePath]); const sessionDirectory = React.useMemo(() => { const raw = typeof currentSession?.directory === 'string' ? currentSession.directory : ''; return normalize(raw || ''); }, [currentSession?.directory]); const draftDirectory = useSessionStore((state) => { if (!state.newSessionDraft?.open) { return ''; } return normalize(state.newSessionDraft.directoryOverride ?? ''); }); const openDirectory = React.useMemo(() => { return worktreeDirectory || sessionDirectory || draftDirectory; }, [draftDirectory, sessionDirectory, worktreeDirectory]); const selectedFilePath = useFilesViewTabsStore((state) => { const directory = normalize(openDirectory || ''); if (!directory) { return null; } return state.byRoot[directory]?.selectedPath ?? null; }); const actionDirectory = React.useMemo(() => { return normalize(openDirectory || activeProject?.path || ''); }, [activeProject?.path, openDirectory]); const activeProjectRef = React.useMemo(() => { if (!activeProject) { return null; } return { id: activeProject.id, path: activeProject.path }; }, [activeProject]); const lastProjectActionsContextRef = React.useRef<{ projectRef: { id: string; path: string }; directory: string; } | null>(null); React.useEffect(() => { if (!activeProjectRef || !actionDirectory) { return; } lastProjectActionsContextRef.current = { projectRef: activeProjectRef, directory: actionDirectory, }; }, [actionDirectory, activeProjectRef]); const projectActionsContext = React.useMemo(() => { if (activeProjectRef && actionDirectory) { return { projectRef: activeProjectRef, directory: actionDirectory }; } return lastProjectActionsContextRef.current; }, [actionDirectory, activeProjectRef]); const [planTabAvailable, setPlanTabAvailable] = React.useState(false); const showPlanTab = planTabAvailable; const lastPlanSessionKeyRef = React.useRef(''); const handleGitHubAccountSwitch = React.useCallback(async (accountId: string) => { if (!accountId || isSwitchingGitHubAccount) return; setIsSwitchingGitHubAccount(true); try { const payload = runtimeApis.github ? await runtimeApis.github.authActivate(accountId) : await (async () => { const response = await fetch('/api/github/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ accountId }), }); const body = (await response.json().catch(() => null)) as | (GitHubAuthStatus & { error?: string }) | null; if (!response.ok || !body) { throw new Error(body?.error || response.statusText); } return body; })(); setGitHubAuthStatus(payload); } catch (error) { console.error('Failed to switch GitHub account:', error); } finally { setIsSwitchingGitHubAccount(false); } }, [isSwitchingGitHubAccount, runtimeApis.github, setGitHubAuthStatus]); React.useEffect(() => { let cancelled = false; const checkExists = async (directory: string, fileName: string): Promise => { if (!directory || !fileName) return false; if (!runtimeApis.files?.listDirectory) return false; try { const listing = await runtimeApis.files.listDirectory(directory); const entries = Array.isArray(listing?.entries) ? listing.entries : []; return entries.some((entry) => entry?.name === fileName && !entry?.isDirectory); } catch { return false; } }; const runOnce = async () => { if (cancelled) return; if (!currentSession?.slug || !currentSession?.time?.created || !sessionDirectory) { setPlanTabAvailable(false); if (useUIStore.getState().activeMainTab === 'plan') { useUIStore.getState().setActiveMainTab('chat'); } return; } const fileName = `${currentSession.time.created}-${currentSession.slug}.md`; const repoDir = buildRepoPlansDirectory(sessionDirectory); const homeDir = resolveTilde(buildHomePlansDirectory(), homeDirectory || null); const [repoExists, homeExists] = await Promise.all([ checkExists(repoDir, fileName), checkExists(homeDir, fileName), ]); if (cancelled) return; const available = repoExists || homeExists; setPlanTabAvailable(available); if (!available && useUIStore.getState().activeMainTab === 'plan') { useUIStore.getState().setActiveMainTab('chat'); } }; const sessionKey = `${currentSessionId || 'none'}:${sessionDirectory || 'none'}:${currentSession?.time?.created || 0}:${currentSession?.slug || 'none'}`; if (lastPlanSessionKeyRef.current !== sessionKey) { lastPlanSessionKeyRef.current = sessionKey; setPlanTabAvailable(false); } void runOnce(); const interval = window.setInterval(() => { void runOnce(); }, 3000); return () => { cancelled = true; window.clearInterval(interval); }; }, [ sessionDirectory, currentSession?.slug, currentSession?.time?.created, currentSessionId, homeDirectory, runtimeApis.files, ]); const blurActiveElement = React.useCallback(() => { if (typeof document === 'undefined') { return; } const active = document.activeElement as HTMLElement | null; if (!active) { return; } const tagName = active.tagName; const isInput = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT'; if (isInput || active.isContentEditable) { active.blur(); } }, []); const handleOpenSessionSwitcher = React.useCallback(() => { if (isMobile) { blurActiveElement(); setSessionSwitcherOpen(!isSessionSwitcherOpen); return; } toggleSidebar(); }, [blurActiveElement, isMobile, isSessionSwitcherOpen, setSessionSwitcherOpen, toggleSidebar]); const handleOpenContextPanel = React.useCallback(() => { const directory = normalize(openDirectory || ''); if (!directory) { return; } const panelState = contextPanelByDirectory[directory]; if (getActiveContextMode(panelState) === 'context') { closeContextPanel(directory); return; } openContextOverview(directory); }, [closeContextPanel, contextPanelByDirectory, openContextOverview, openDirectory]); const isContextPanelActive = React.useMemo(() => { const directory = normalize(openDirectory || ''); if (!directory) { return false; } const panelState = contextPanelByDirectory[directory]; return getActiveContextMode(panelState) === 'context'; }, [contextPanelByDirectory, openDirectory]); const handleOpenContextPlan = React.useCallback(() => { const directory = normalize(openDirectory || ''); if (!directory) { return; } const panelState = contextPanelByDirectory[directory]; if (getActiveContextMode(panelState) === 'plan') { closeContextPanel(directory); return; } openContextPlan(directory); }, [closeContextPanel, contextPanelByDirectory, openContextPlan, openDirectory]); const isContextPlanActive = React.useMemo(() => { const directory = normalize(openDirectory || ''); if (!directory) { return false; } const panelState = contextPanelByDirectory[directory]; return getActiveContextMode(panelState) === 'plan'; }, [contextPanelByDirectory, openDirectory]); const desktopHeaderIconButtonClass = 'app-region-no-drag inline-flex h-8 w-8 items-center justify-center gap-2 rounded-md typography-ui-label font-medium text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:bg-interactive-hover transition-colors'; const mobileHeaderIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-interactive-hover transition-colors'; const desktopPaddingClass = React.useMemo(() => { if (isDesktopApp && isMacPlatform) { // Always reserve space for Mac traffic lights since header is always on top return 'pl-[5.5rem]'; } return 'pl-3'; }, [isDesktopApp, isMacPlatform]); const macosHeaderSizeClass = React.useMemo(() => { if (!isDesktopApp || !isMacPlatform || macosMajorVersion === null) { return ''; } if (macosMajorVersion >= 26) { return 'h-12'; } if (macosMajorVersion <= 15) { return 'h-14'; } return ''; }, [isDesktopApp, isMacPlatform, macosMajorVersion]); const updateHeaderHeight = React.useCallback(() => { if (typeof document === 'undefined') { return; } const height = headerRef.current?.getBoundingClientRect().height; if (height) { document.documentElement.style.setProperty('--oc-header-height', `${height}px`); } }, []); useEffect(() => { if (typeof window === 'undefined') { return; } updateHeaderHeight(); const node = headerRef.current; if (!node || typeof ResizeObserver === 'undefined') { return () => { }; } const observer = new ResizeObserver(() => { updateHeaderHeight(); }); observer.observe(node); window.addEventListener('resize', updateHeaderHeight); window.addEventListener('orientationchange', updateHeaderHeight); return () => { observer.disconnect(); window.removeEventListener('resize', updateHeaderHeight); window.removeEventListener('orientationchange', updateHeaderHeight); }; }, [updateHeaderHeight]); useEffect(() => { updateHeaderHeight(); }, [updateHeaderHeight, isMobile, macosHeaderSizeClass]); const handleDragStart = React.useCallback(async (e: React.MouseEvent) => { const target = e.target as HTMLElement; if (target.closest('.app-region-no-drag')) { return; } if (target.closest('button, a, input, select, textarea')) { return; } if (e.button !== 0) { return; } if (isDesktopApp) { try { const { getCurrentWindow } = await import('@tauri-apps/api/window'); const window = getCurrentWindow(); await window.startDragging(); } catch (error) { console.error('Failed to start window dragging:', error); } } }, [isDesktopApp]); const tabs: TabConfig[] = React.useMemo(() => { if (isMobile) { const base: TabConfig[] = [ { id: 'chat', label: 'Chat', icon: RiChat4Line }, ]; if (showPlanTab) { base.push({ id: 'plan', label: 'Plan', icon: RiFileTextLine }); } base.push( { id: 'diff', label: 'Diff', icon: 'diff' }, { id: 'files', label: 'Files', icon: RiFolder6Line }, { id: 'terminal', label: 'Terminal', icon: RiTerminalBoxLine }, ); return base; } // Desktop: no tabs in header return []; }, [isMobile, showPlanTab]); const shortcutLabel = React.useCallback((actionId: string) => { return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides)); }, [shortcutOverrides]); useEffect(() => { if (!isMobile && (activeMainTab === 'git' || activeMainTab === 'terminal' || activeMainTab === 'diff' || activeMainTab === 'files')) { setActiveMainTab('chat'); } }, [activeMainTab, isMobile, setActiveMainTab]); const servicesTabs = React.useMemo(() => { const base: Array<{ value: 'instance' | 'usage' | 'mcp'; label: string; icon: RemixiconComponentType }> = []; if (isDesktopApp) { base.push({ value: 'instance', label: 'Instance', icon: RiServerLine }); } base.push( { value: 'usage', label: 'Usage', icon: RiTimerLine }, { value: 'mcp', label: 'MCP', icon: McpIcon as unknown as RemixiconComponentType } ); return base; }, [isDesktopApp]); const servicesTabItems = React.useMemo(() => { return servicesTabs.map((tab) => ({ id: tab.value, label: tab.label, icon: , })); }, [servicesTabs]); const quotaDisplayTabs = React.useMemo(() => { return [ { value: 'usage' as const, label: 'Used' }, { value: 'remaining' as const, label: 'Remaining' }, ]; }, []); const quotaDisplayTabItems = React.useMemo(() => { return quotaDisplayTabs.map((tab) => ({ id: tab.value, label: tab.label })); }, [quotaDisplayTabs]); const mobileServicesTabItems = React.useMemo(() => { return [ { id: 'usage', label: 'Usage', icon: }, { id: 'mcp', label: 'MCP', icon: }, ]; }, []); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (hasModifier(e) && !e.shiftKey && !e.altKey) { const num = parseInt(e.key, 10); if (num >= 1 && num <= tabs.length) { e.preventDefault(); setActiveMainTab(tabs[num - 1].id); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [tabs, setActiveMainTab]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const toggleServicesCombo = getEffectiveShortcutCombo('toggle_services_menu', shortcutOverrides); if (eventMatchesShortcut(e, toggleServicesCombo)) { e.preventDefault(); if (isDesktopServicesOpen) { setIsDesktopServicesOpen(false); } else { setIsDesktopServicesOpen(true); void refreshCurrentInstanceLabel(); if (desktopServicesTab === 'usage' && quotaResults.length === 0) { void fetchAllQuotas(); } } return; } const cycleServicesCombo = getEffectiveShortcutCombo('cycle_services_tab', shortcutOverrides); if (eventMatchesShortcut(e, cycleServicesCombo)) { e.preventDefault(); const tabValues = servicesTabs.map((tab) => tab.value) as Array<'instance' | 'usage' | 'mcp'>; if (tabValues.length === 0) { return; } const currentIndex = tabValues.indexOf(desktopServicesTab); const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % tabValues.length; const nextTab = tabValues[nextIndex]; setDesktopServicesTab(nextTab); setIsDesktopServicesOpen(true); void refreshCurrentInstanceLabel(); if (nextTab === 'usage' && quotaResults.length === 0) { void fetchAllQuotas(); } return; } const toggleContextPlanCombo = getEffectiveShortcutCombo('toggle_context_plan', shortcutOverrides); if (eventMatchesShortcut(e, toggleContextPlanCombo)) { e.preventDefault(); handleOpenContextPlan(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [ shortcutOverrides, isDesktopServicesOpen, desktopServicesTab, servicesTabs, quotaResults.length, fetchAllQuotas, refreshCurrentInstanceLabel, handleOpenContextPlan, ]); const renderTab = (tab: TabConfig) => { const isActive = activeMainTab === tab.id; const isDiffTab = tab.icon === 'diff'; const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType); const isChatTab = tab.id === 'chat'; const renderIcon = (iconSize: number) => { if (isDiffTab) { return ; } return Icon ? : null; }; const tabButton = ( ); return {tabButton}; }; const renderDesktop = () => (

Open sessions ({shortcutLabel('toggle_sidebar')})

{activeProjectLabel && (
{activeProjectLabel}
)} {projectActionsContext && ( )} {tabs.length > 0 && (
{tabs.map((tab) => renderTab(tab))}
)}
{showDesktopHeaderContextUsage && stableDesktopContextUsage && ( )} {showPlanTab && (

Plan ({shortcutLabel('toggle_context_plan')})

)} { setIsDesktopServicesOpen(open); if (open) { void refreshCurrentInstanceLabel(); if (desktopServicesTab === 'usage' && quotaResults.length === 0) { fetchAllQuotas(); } } }} >

{isDesktopApp ? `Current instance: ${currentInstanceLabel}` : 'Services'} ({shortcutLabel('toggle_services_menu')}; next tab {shortcutLabel('cycle_services_tab')})

{ const value = tabID as 'instance' | 'usage' | 'mcp'; setDesktopServicesTab(value); if (value === 'usage' && quotaResults.length === 0) { fetchAllQuotas(); } }} layoutMode="fit" variant="active-pill" activePillInsetClassName="gap-0.5 px-px py-0" activePillButtonClassName="h-8" className="h-full" />
{isDesktopApp && desktopServicesTab === 'instance' && ( {}} onHostSwitched={() => setIsDesktopServicesOpen(false)} /> )} {desktopServicesTab === 'mcp' && ( )} {desktopServicesTab === 'usage' && (
Rate limits Last updated {formatTime(quotaLastUpdated)}
handleDisplayModeChange(tabID as 'usage' | 'remaining')} layoutMode="fit" variant="active-pill" activePillInsetClassName="gap-0.5 px-px py-0" className="h-full" />
{!hasRateLimits && ( event.preventDefault()} > No rate limits available. )} {rateLimitGroups.map((group, index) => { const providerExpandedFamilies = expandedFamilies[group.providerId] ?? []; return ( {group.providerName} {group.entries.length === 0 && (!group.modelFamilies || group.modelFamilies.length === 0) ? ( event.preventDefault()} > {group.error ?? 'No rate limits reported.'} ) : ( <> {group.entries.map(([label, window]) => { const displayPercent = quotaDisplayMode === 'remaining' ? window.remainingPercent : window.usedPercent; const paceInfo = calculatePace(window.usedPercent, window.resetAt, window.windowSeconds, label); const expectedMarker = paceInfo?.dailyAllocationPercent != null ? (quotaDisplayMode === 'remaining' ? 100 - calculateExpectedUsagePercent(paceInfo.elapsedRatio) : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; return ( event.preventDefault()} > {formatWindowLabel(label)} {(window.resetAfterFormatted ?? window.resetAtFormatted) ? ( {window.resetAfterFormatted ?? window.resetAtFormatted} ) : null} {formatPercent(displayPercent) === '-' ? '' : formatPercent(displayPercent)} {paceInfo && (
)}
); })} {group.modelFamilies && group.modelFamilies.length > 0 && (
{group.modelFamilies.map((family) => { const isExpanded = providerExpandedFamilies.includes(family.familyId ?? 'other'); return ( toggleFamilyExpanded(group.providerId, family.familyId ?? 'other')} > {family.familyLabel} {isExpanded ? ( ) : ( )}
{family.models.map(([modelName, window]) => { const displayPercent = quotaDisplayMode === 'remaining' ? window.remainingPercent : window.usedPercent; const paceInfo = calculatePace(window.usedPercent, window.resetAt, window.windowSeconds); const expectedMarker = paceInfo?.dailyAllocationPercent != null ? (quotaDisplayMode === 'remaining' ? 100 - calculateExpectedUsagePercent(paceInfo.elapsedRatio) : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; return (
{getDisplayModelName(modelName)} {formatPercent(displayPercent) === '-' ? '' : formatPercent(displayPercent)} {paceInfo && (
)}
); })}
); })}
)} )} {index < rateLimitGroups.length - 1 && }
); })}
)}

Terminal panel ({shortcutLabel('toggle_terminal')})

Right sidebar ({shortcutLabel('toggle_right_sidebar')})

{githubAuthStatus?.connected && !isMobile ? ( githubAccounts.length > 1 ? ( GitHub Accounts {githubAccounts.map((account) => { const accountUser = account.user; const isCurrent = Boolean(account.current); return ( { if (!isCurrent) { void handleGitHubAccountSwitch(account.id); } }} > {accountUser?.avatarUrl ? ( {accountUser.login ) : (
)} {accountUser?.name?.trim() || accountUser?.login || 'GitHub'} {accountUser?.login ? ( {accountUser.login} ) : null} {isCurrent ? ( ) : null}
); })}
) : (
{githubAvatarUrl ? ( {githubLogin ) : ( )}
) ) : null}
); const renderMobile = () => (
{/* Use drawer toggle when onToggleLeftDrawer is provided, otherwise use legacy session switcher */} {onToggleLeftDrawer ? ( ) : isSessionSwitcherOpen ? ( ) : ( )} {isSessionSwitcherOpen && ( Sessions )}
{/* Hide tabs and right-side buttons when sessions sidebar is open */} {!isSessionSwitcherOpen && ( <>
{tabs.map((tab) => { const isActive = activeMainTab === tab.id; const isDiffTab = tab.icon === 'diff'; const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType); return (

{tab.label}

); })}
{projectActionsContext && ( )} {/* Mobile Services Menu (Usage + MCP) */} { setIsMobileRateLimitsOpen(open); if (open && quotaResults.length === 0) { fetchAllQuotas(); } }} >

Services

{ const value = tabID as 'usage' | 'mcp'; setMobileServicesTab(value); if (value === 'usage' && quotaResults.length === 0) { fetchAllQuotas(); } }} layoutMode="fit" variant="active-pill" activePillInsetClassName="gap-0.5 px-px py-0" activePillButtonClassName="h-8" className="h-full" />
{mobileServicesTab === 'mcp' && ( )} {mobileServicesTab === 'usage' && (
Rate limits Last updated {formatTime(quotaLastUpdated)}
{/* Light-weight text toggle for Used/Remaining */}
ยท
{!hasRateLimits && ( event.preventDefault()} > No rate limits available. )} {rateLimitGroups.map((group) => ( {group.providerName} {group.entries.length === 0 && (!group.modelFamilies || group.modelFamilies.length === 0) ? ( event.preventDefault()} > {group.error ?? 'No rate limits reported.'} ) : ( <> {group.entries.map(([label, window]) => { const displayPercent = quotaDisplayMode === 'remaining' ? window.remainingPercent : window.usedPercent; const paceInfo = calculatePace(window.usedPercent, window.resetAt, window.windowSeconds, label); const expectedMarker = paceInfo?.dailyAllocationPercent != null ? (quotaDisplayMode === 'remaining' ? 100 - calculateExpectedUsagePercent(paceInfo.elapsedRatio) : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; return ( event.preventDefault()} > {formatWindowLabel(label)} {(window.resetAfterFormatted ?? window.resetAtFormatted) ? ( {window.resetAfterFormatted ?? window.resetAtFormatted} ) : null} {formatPercent(displayPercent) === '-' ? '' : formatPercent(displayPercent)} {paceInfo && (
)}
); })} {group.modelFamilies && group.modelFamilies.length > 0 && (
{group.modelFamilies.map((family) => { const providerExpandedFamilies = expandedFamilies[group.providerId] ?? []; const isExpanded = providerExpandedFamilies.includes(family.familyId ?? 'other'); return ( toggleFamilyExpanded(group.providerId, family.familyId ?? 'other')} > {family.familyLabel} {isExpanded ? ( ) : ( )}
{family.models.map(([modelName, window]) => { const displayPercent = quotaDisplayMode === 'remaining' ? window.remainingPercent : window.usedPercent; const paceInfo = calculatePace(window.usedPercent, window.resetAt, window.windowSeconds); const expectedMarker = paceInfo?.dailyAllocationPercent != null ? (quotaDisplayMode === 'remaining' ? 100 - calculateExpectedUsagePercent(paceInfo.elapsedRatio) : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; return (
{getDisplayModelName(modelName)} {formatPercent(displayPercent) === '-' ? '' : formatPercent(displayPercent)} {paceInfo && (
)}
); })}
); })}
)} )}
))}
)}
{onToggleRightDrawer ? (

{rightDrawerOpen ? 'Close git sidebar' : 'Open git sidebar'}

) : null}
)}
); const headerClassName = cn( 'header-safe-area relative z-10', isMobile && 'border-b border-border/50', isDesktopApp ? 'bg-background' : 'bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80' ); return (
{isMobile ? renderMobile() : renderDesktop()}
); };