import React, { useEffect } from 'react'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { RiChat4Line, RiCodeLine, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react'; import { useUIStore, type MainTab } from '@/stores/useUIStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { useDeviceInfo } from '@/lib/device'; import { cn } from '@/lib/utils'; import { useDiffFileCount } from '@/components/views/DiffView'; interface TabConfig { id: MainTab; label: string; icon: RemixiconComponentType; badge?: number; } export const FixedSessionsButton: React.FC = () => { const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); const { isMobile } = useDeviceInfo(); const [isDesktopApp, setIsDesktopApp] = React.useState(() => { if (typeof window === 'undefined') { return false; } return typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; }); const isMacPlatform = React.useMemo(() => { if (typeof navigator === 'undefined') { return false; } return /Macintosh|Mac OS X/.test(navigator.userAgent || ''); }, []); React.useEffect(() => { if (typeof window === 'undefined') { return; } const detected = typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; setIsDesktopApp(detected); }, []); const handleOpenSessionSwitcher = React.useCallback(() => { if (isMobile) { setSessionSwitcherOpen(true); } else { toggleSidebar(); } }, [isMobile, setSessionSwitcherOpen, toggleSidebar]); const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 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'; if (isMobile || !isDesktopApp || !isMacPlatform) { return null; } return (
); }; export const Header: React.FC = () => { const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); const sidebarWidth = useUIStore((state) => state.sidebarWidth); const activeMainTab = useUIStore((state) => state.activeMainTab); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const { getCurrentModel } = useConfigStore(); const getContextUsage = useSessionStore((state) => state.getContextUsage); const { isMobile } = useDeviceInfo(); const diffFileCount = useDiffFileCount(); const headerRef = React.useRef(null); const [isDesktopApp, setIsDesktopApp] = React.useState(() => { if (typeof window === 'undefined') { return false; } return typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; }); const isMacPlatform = React.useMemo(() => { if (typeof navigator === 'undefined') { return false; } return /Macintosh|Mac OS X/.test(navigator.userAgent || ''); }, []); useEffect(() => { if (typeof window === 'undefined') { return; } const detected = typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; setIsDesktopApp(detected); }, []); 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 handleOpenSessionSwitcher = React.useCallback(() => { if (isMobile) { setSessionSwitcherOpen(!isSessionSwitcherOpen); return; } toggleSidebar(); }, [isMobile, isSessionSwitcherOpen, setSessionSwitcherOpen, toggleSidebar]); const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 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'; const desktopPaddingClass = React.useMemo(() => { if (isDesktopApp && isMacPlatform) { return isSidebarOpen ? 'pl-0' : 'pl-[8.0rem]'; } return 'pl-3'; }, [isDesktopApp, isMacPlatform, isSidebarOpen]); 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]); 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(() => [ { id: 'chat', label: 'Chat', icon: RiChat4Line }, { id: 'diff', label: 'Diff', icon: RiCodeLine, badge: !isMobile && diffFileCount > 0 ? diffFileCount : undefined, }, { id: 'terminal', label: 'Terminal', icon: RiTerminalBoxLine }, { id: 'git', label: 'Git', icon: RiGitBranchLine }, ], [diffFileCount, isMobile]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && !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, isLast: boolean) => { const isActive = activeMainTab === tab.id; const Icon = tab.icon; const isChatTab = tab.id === 'chat'; const isGitTab = tab.id === 'git'; return ( {} {!isLast && (