/** * Main Layout Component * TitleBar at top, then sidebar + content below. */ import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useCallback, useEffect, useRef, useState } from 'react'; import { LockKeyhole, Settings2 } from 'lucide-react'; import { Sidebar } from './Sidebar'; import { TitleBar } from './TitleBar'; import { Button } from '@/components/ui/button'; import { getAiModuleForPath } from '@/lib/ai-modules'; import { useOpencodeStore } from '@/stores/opencode'; import { useProjectConfigStore } from '@/stores/project-config'; import { useSettingsStore } from '@/stores/settings'; import { cn } from '@/lib/utils'; import type { SidebarPeekSource } from './sidebar-peek'; const SIDEBAR_PEEK_CLOSE_DELAY_MS = 180; export function MainLayout() { const activeProject = useOpencodeStore((state) => state.activeProject); const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); const config = useProjectConfigStore((state) => activeProject ? state.configsByProjectId[activeProject.id] : undefined); const load = useProjectConfigStore((state) => state.load); const location = useLocation(); const navigate = useNavigate(); const [sidebarPeekOpen, setSidebarPeekOpen] = useState(false); const sidebarPeekCloseTimerRef = useRef | null>(null); const sidebarPeekSourcesRef = useRef>({ toggle: false, focus: false, sidebar: false, titlebar: false, }); const activeModule = getAiModuleForPath(location.pathname); const isProgrammingModule = activeModule === 'programming'; const isPaintingModule = activeModule === 'painting'; const isLearningPlayer = location.pathname.startsWith('/learning/course/'); const isPromptMuseum = location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/'); const isChatWorkspace = location.pathname === '/opencode-chat'; const isInitializationSafeRoute = location.pathname === '/project-config' || !isProgrammingModule; const handleSidebarPeekChange = useCallback((open: boolean, source: SidebarPeekSource) => { if (!sidebarCollapsed) return; const sources = sidebarPeekSourcesRef.current; if (sources[source] === open) return; if (sidebarPeekCloseTimerRef.current) { clearTimeout(sidebarPeekCloseTimerRef.current); sidebarPeekCloseTimerRef.current = null; } sources[source] = open; if (open) { setSidebarPeekOpen(true); return; } if (Object.values(sources).some(Boolean)) return; sidebarPeekCloseTimerRef.current = setTimeout(() => { if (Object.values(sidebarPeekSourcesRef.current).some(Boolean)) return; setSidebarPeekOpen(false); sidebarPeekCloseTimerRef.current = null; }, SIDEBAR_PEEK_CLOSE_DELAY_MS); }, [sidebarCollapsed]); useEffect(() => { sidebarPeekSourcesRef.current = { toggle: false, focus: false, sidebar: false, titlebar: false }; // This transient state mirrors the external persisted sidebar preference; // reset it when that preference changes so a stale preview cannot reopen. // eslint-disable-next-line react-hooks/set-state-in-effect setSidebarPeekOpen(false); if (sidebarPeekCloseTimerRef.current) { clearTimeout(sidebarPeekCloseTimerRef.current); sidebarPeekCloseTimerRef.current = null; } }, [sidebarCollapsed]); useEffect(() => () => { if (sidebarPeekCloseTimerRef.current) clearTimeout(sidebarPeekCloseTimerRef.current); }, []); useEffect(() => { if (activeProject && isProgrammingModule) void load(activeProject.id).catch(() => undefined); }, [activeProject, isProgrammingModule, load]); const initializationBlocked = Boolean(activeProject && (!config || !config.initialized) && !isInitializationSafeRoute); return (
{/* Title bar: drag region on macOS, icon + controls on Windows */} {/* Below the title bar: sidebar + content */}
{!isLearningPlayer ? ( ) : null}
{initializationBlocked ? (

项目尚未初始化

请先在项目配置中完成至少一个手动 Agent 的名称、模型和职责设置。

) : null}
); }