import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Outlet, useLocation } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Menu } from 'lucide-react'; import { defineMessages, useIntl } from '../../i18n'; import { Button } from '../ui/button'; import ChatSessionsContainer from '../ChatSessionsContainer'; import { useChatContext } from '../../contexts/ChatContext'; import { NavigationProvider, useNavigationContext } from './NavigationContext'; import { Navigation } from './NavigationPanel'; import { NAV_DIMENSIONS, Z_INDEX } from './constants'; import { cn } from '../../utils'; import { UserInput } from '../../types/message'; const i18n = defineMessages({ closeNavigation: { id: 'appLayout.closeNavigation', defaultMessage: 'Close navigation', }, openNavigation: { id: 'appLayout.openNavigation', defaultMessage: 'Open navigation', }, }); interface AppLayoutContentProps { activeSessions: Array<{ sessionId: string; initialMessage?: UserInput; }>; } const AppLayoutContent: React.FC = ({ activeSessions }) => { const intl = useIntl(); const location = useLocation(); const safeIsMacOS = (window?.electron?.platform || 'darwin') === 'darwin'; const chatContext = useChatContext(); const isOnPairRoute = location.pathname === '/pair'; const { isNavExpanded, setIsNavExpanded, effectiveNavigationMode, effectiveNavigationStyle, navigationPosition, isHorizontalNav, isCondensedIconOnly, } = useNavigationContext(); const [navWidth, setNavWidth] = useState(null); const navWidthRef = useRef(null); useEffect(() => { window.electron.getSetting('navExpandedWidth').then((delta) => { if (delta !== null) { setNavWidth( Math.min( NAV_DIMENSIONS.MAX_NAV_WIDTH, Math.max(NAV_DIMENSIONS.MIN_NAV_WIDTH, NAV_DIMENSIONS.CONDENSED_WIDTH + delta) ) ); } }); }, []); const isResizable = !isHorizontalNav && !isCondensedIconOnly && effectiveNavigationMode === 'push' && isNavExpanded; const dragStateRef = useRef<{ startX: number; startWidth: number; direction: 1 | -1 } | null>( null ); const navRef = useRef(null); const onMouseMove = useCallback((e: MouseEvent) => { if (!dragStateRef.current) return; const delta = (e.clientX - dragStateRef.current.startX) * dragStateRef.current.direction; const newWidth = Math.min( NAV_DIMENSIONS.MAX_NAV_WIDTH, Math.max(NAV_DIMENSIONS.MIN_NAV_WIDTH, dragStateRef.current.startWidth + delta) ); navWidthRef.current = newWidth; setNavWidth(newWidth); }, []); const onMouseUp = useCallback(() => { dragStateRef.current = null; document.body.style.cursor = ''; document.body.style.userSelect = ''; window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); if (navWidthRef.current !== null) { window.electron.setSetting( 'navExpandedWidth', navWidthRef.current - NAV_DIMENSIONS.CONDENSED_WIDTH ); } }, [onMouseMove]); const onHandleMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); const currentWidth = navRef.current?.getBoundingClientRect().width ?? NAV_DIMENSIONS.CONDENSED_WIDTH; dragStateRef.current = { startX: e.clientX, startWidth: currentWidth, direction: navigationPosition === 'right' ? -1 : 1, }; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }, [navigationPosition, onMouseMove, onMouseUp] ); useEffect(() => { return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; }, [onMouseMove, onMouseUp]); if (!chatContext) { throw new Error('AppLayoutContent must be used within ChatProvider'); } const { setChat } = chatContext; // Hide the titlebar drag region when nav is at the top in push mode, // since the nav occupies that space and the drag region blocks interactions const isPushTopNav = effectiveNavigationMode === 'push' && navigationPosition === 'top' && isNavExpanded; React.useEffect(() => { const dragRegion = document.querySelector('.titlebar-drag-region') as HTMLElement | null; if (!dragRegion) return; if (isPushTopNav) { dragRegion.style.display = 'none'; } else { dragRegion.style.display = ''; } return () => { dragRegion.style.display = ''; }; }, [isPushTopNav]); const headerPadding = safeIsMacOS ? 'pl-[96px]' : 'pl-4'; const headerTop = safeIsMacOS ? 'top-[15px]' : 'top-[11px]'; // Determine flex direction based on navigation position (for push mode) const getLayoutClass = () => { if (effectiveNavigationMode === 'overlay') { return 'flex-row'; } switch (navigationPosition) { case 'top': return 'flex-col'; case 'bottom': return 'flex-col-reverse'; case 'left': return 'flex-row'; case 'right': return 'flex-row-reverse'; default: return 'flex-row'; } }; // Main content area const mainContent = (
{/* Always render ChatSessionsContainer to keep SSE connections alive. When navigating away from /pair, hide it with CSS */}
); return (
{/* Header controls */}
{/* Navigation trigger */}
{/* Main content with navigation */}
{/* Push mode navigation (inline) with animation */} {effectiveNavigationMode === 'push' && (
{isResizable && (
)} )} {/* Main content */} {mainContent}
{/* Overlay mode navigation */} {effectiveNavigationMode === 'overlay' && }
); }; interface AppLayoutProps { activeSessions: Array<{ sessionId: string; initialMessage?: UserInput; }>; } export const AppLayout: React.FC = ({ activeSessions }) => { return ( ); };