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 { RiArrowLeftSLine, RiChat4Line, RiCheckLine, RiCloseLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiRefreshLine, RiSettings3Line, RiTerminalBoxLine, RiTimerLine, type RemixiconComponentType } from '@remixicon/react'; import { DiffIcon } from '@/components/icons/DiffIcon'; import { useUIStore, type MainTab } from '@/stores/useUIStore'; import { useUpdateStore } from '@/stores/useUpdateStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { useDeviceInfo } from '@/lib/device'; import { cn, getModifierLabel, hasModifier } from '@/lib/utils'; import { useDiffFileCount } from '@/components/views/DiffView'; import { McpDropdown } from '@/components/mcp/McpDropdown'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { formatPercent, formatWindowLabel, QUOTA_PROVIDERS } from '@/lib/quota'; import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar'; import { updateDesktopSettings } from '@/lib/persistence'; import type { UsageWindow } from '@/types'; import type { GitHubAuthStatus } from '@/lib/api/types'; import { DesktopHostSwitcherButton } from '@/components/desktop/DesktopHostSwitcher'; import { isDesktopShell } from '@/lib/desktop'; 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; }; interface TabConfig { id: MainTab; label: string; icon: RemixiconComponentType | 'diff'; badge?: number; showDot?: boolean; } export const Header: React.FC = () => { const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const toggleCommandPalette = useUIStore((state) => state.toggleCommandPalette); const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog); const activeMainTab = useUIStore((state) => state.activeMainTab); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const { getCurrentModel } = useConfigStore(); const runtimeApis = useRuntimeAPIs(); const getContextUsage = useSessionStore((state) => state.getContextUsage); const currentSessionId = useSessionStore((state) => state.currentSessionId); const sessions = useSessionStore((state) => state.sessions); 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 diffFileCount = useDiffFileCount(); const updateAvailable = useUpdateStore((state) => state.available); 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 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); useQuotaAutoRefresh(); 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; 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 currentSession = React.useMemo(() => { if (!currentSessionId) return null; return sessions.find((s) => s.id === currentSessionId) ?? null; }, [currentSessionId, sessions]); const sessionDirectory = React.useMemo(() => { const raw = typeof currentSession?.directory === 'string' ? currentSession.directory : ''; return normalize(raw || ''); }, [currentSession?.directory]); 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 handleOpenSettings = React.useCallback(() => { if (isMobile) { blurActiveElement(); } setSessionSwitcherOpen(false); setSettingsDialogOpen(true); }, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]); const headerIconButtonClass = '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) => { if ((e.target as HTMLElement).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 handleActiveTabDragStart = React.useCallback(async (e: React.MouseEvent) => { 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(() => { 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', badge: !isMobile && diffFileCount > 0 ? diffFileCount : undefined, }, { id: 'files', label: 'Files', icon: RiFolder6Line }, { id: 'terminal', label: 'Terminal', icon: RiTerminalBoxLine }, { id: 'git', label: 'Git', icon: RiGitBranchLine, showDot: isMobile && diffFileCount > 0, }, ); return base; }, [diffFileCount, isMobile, showPlanTab]); 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]); 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 showContextTooltip = isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0; const renderIcon = (iconSize: number) => { if (isDiffTab) { return ; } return Icon ? : null; }; const formatTokens = (tokens: number) => { if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`; if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`; return tokens.toFixed(1).replace(/\.0$/, ''); }; const tabButton = ( ); if (showContextTooltip) { const safeOutputLimit = typeof contextUsage.outputLimit === 'number' ? Math.max(contextUsage.outputLimit, 0) : 0; return ( {tabButton}

Used tokens: {formatTokens(contextUsage.totalTokens)}

Context limit: {formatTokens(contextUsage.contextLimit)}

Output limit: {formatTokens(safeOutputLimit)}

); } return {tabButton}; }; const renderDesktop = () => (
{tabs.map((tab) => renderTab(tab))}
{isDesktopApp && ( )}

Command Palette ({getModifierLabel()}+K)

{ if (open && quotaResults.length === 0) { fetchAllQuotas(); } }}>

Rate limits

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 && } ))}

Keyboard Shortcuts ({getModifierLabel()}+.)

{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 = () => (
{/* Show back button when sessions sidebar is open, otherwise show sessions toggle */} {isSessionSwitcherOpen ? ( ) : ( )} {!isSessionSwitcherOpen && contextUsage && contextUsage.totalTokens > 0 && activeMainTab === 'chat' && ( )} {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}

); })}
{ setIsMobileRateLimitsOpen(open); if (open && quotaResults.length === 0) { fetchAllQuotas(); } }} >

Rate limits

Rate limits
Last updated {formatTime(quotaLastUpdated)}
{!hasRateLimits && (
No rate limits available.
)} {rateLimitGroups.map((group) => (
{group.providerName}
{group.entries.map(([label, window]) => { const displayPercent = quotaDisplayMode === 'remaining' ? window.remainingPercent : window.usedPercent; return (
{formatWindowLabel(label)} {formatPercent(displayPercent)}
{window.resetAfterFormatted ?? window.resetAtFormatted ?? ''}
); })}
))}

{updateAvailable ? 'Settings (Update available)' : 'Settings'}

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