/* Copyright 2026 Marimo. All rights reserved. */ import React, { type PropsWithChildren, Suspense, useEffect, useMemo, } from "react"; import { type ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle, } from "react-resizable-panels"; import { Footer } from "./footer"; import { Sidebar } from "./sidebar"; import "./app-chrome.css"; import { Tooltip } from "radix-ui"; const TooltipProvider = Tooltip.Provider; import { useAtom, useAtomValue } from "jotai"; import { XIcon } from "lucide-react"; import useEvent from "react-use-event-hook"; import { Button } from "@/components/ui/button"; import { ReorderableList } from "@/components/ui/reorderable-list"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { LazyActivity } from "@/components/utils/lazy-mount"; import { cellErrorCount } from "@/core/cells/cells"; import { capabilitiesAtom } from "@/core/config/capabilities"; import { aiEnabledAtom } from "@/core/config/config"; import { getFeatureFlag } from "@/core/config/feature-flag"; import { cn } from "@/utils/cn"; import { ErrorBoundary } from "../../boundary/ErrorBoundary"; import { raf2 } from "../../navigation/focus-utils"; import { ContextAwarePanel } from "../panels/context-aware-panel/context-aware-panel"; import { PanelSectionProvider } from "../panels/panel-context"; import { SandboxToggle } from "../panels/sandbox-toggle"; import { useTheme } from "@/theme/useTheme"; import { LazyAgentPanel, LazyCachePanel, LazyChatPanel, LazyDependencyGraphPanel, LazyDocumentationPanel, LazyErrorsPanel, LazyFileExplorerPanel, LazyLogsPanel, LazyOutlinePanel, LazyPackagesPanel, LazyScratchpadPanel, LazySecretsPanel, LazySessionPanel, LazySnippetsPanel, LazyTerminal, LazyTracingPanel, PANEL_PRELOADERS, } from "./lazy-panels"; import { panelLayoutAtom, useChromeActions, useChromeState } from "../state"; import { isPanelHidden, PANEL_MAP, PANELS, type PanelDescriptor, type PanelType, } from "../types"; import { BackendConnectionStatus } from "./footer-items/backend-status"; import { LspStatus } from "./footer-items/lsp-status"; import { PanelsWrapper } from "./panels"; import { PendingAICells } from "./pending-ai-cells"; import { useAiPanelTab } from "./useAiPanel"; import { useDependencyPanelTab } from "./useDependencyPanelTab"; import { handleDragging } from "./utils"; // Placeholder that matches the eventual xterm theme background so the // transition into the loaded terminal is seamless rather than a blank flash. const TerminalSkeleton: React.FC = () => { const { theme } = useTheme(); const isDark = theme === "dark"; return (
Starting terminal
); }; export const AppChrome: React.FC = ({ children }) => { const { isSidebarOpen, isDeveloperPanelOpen, selectedPanel, selectedDeveloperPanelTab, } = useChromeState(); const { setIsSidebarOpen, setIsDeveloperPanelOpen, openApplication } = useChromeActions(); const sidebarRef = React.useRef(null); const developerPanelRef = React.useRef(null); const { aiPanelTab, setAiPanelTab } = useAiPanelTab(); const { dependencyPanelTab, setDependencyPanelTab } = useDependencyPanelTab(); const errorCount = useAtomValue(cellErrorCount); const [panelLayout, setPanelLayout] = useAtom(panelLayoutAtom); // Subscribe to capabilities to re-render when they change (e.g., terminal capability) const capabilities = useAtomValue(capabilitiesAtom); const aiEnabled = useAtomValue(aiEnabledAtom); // On mount, idle-preload whichever panels the user had open at last unload, // so the first interaction with the sidebar/dev panel doesn't hit a cold // chunk fetch. Runs once. // oxlint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { const preloadOpenPanels = () => { if (isSidebarOpen && selectedPanel) { PANEL_PRELOADERS[selectedPanel]?.(); } if (isDeveloperPanelOpen && selectedDeveloperPanelTab) { PANEL_PRELOADERS[selectedDeveloperPanelTab]?.(); } }; const canIdle = typeof window !== "undefined" && typeof window.requestIdleCallback === "function"; if (canIdle) { const handle = window.requestIdleCallback(preloadOpenPanels, { timeout: 2000, }); return () => window.cancelIdleCallback(handle); } const handle = setTimeout(preloadOpenPanels, 300); return () => clearTimeout(handle); }, []); // Convert current developer panel items to PanelDescriptors // Filter out hidden panels (e.g., terminal when capability is not available) const devPanelItems = useMemo(() => { return panelLayout.developerPanel.flatMap((id) => { const panel = PANEL_MAP.get(id); if (!panel || isPanelHidden({ panel, capabilities, aiEnabled })) { return []; } return [panel]; }); }, [panelLayout.developerPanel, capabilities, aiEnabled]); const handleSetDevPanelItems = (items: PanelDescriptor[]) => { setPanelLayout((prev) => ({ ...prev, developerPanel: items.map((item) => item.type), })); }; const handleDevPanelReceive = (item: PanelDescriptor, fromListId: string) => { // Remove from the source list if (fromListId === "sidebar") { setPanelLayout((prev) => ({ ...prev, sidebar: prev.sidebar.filter((id) => id !== item.type), })); // If the moved item was selected in sidebar, select the first remaining item if (selectedPanel === item.type) { const remainingSidebar = panelLayout.sidebar.filter( (id) => id !== item.type, ); if (remainingSidebar.length > 0) { openApplication(remainingSidebar[0]); } } } // Select the dropped item in developer panel openApplication(item.type); }; // Get panels available for developer panel context menu // Only show panels that are NOT in the sidebar const availableDevPanels = useMemo(() => { const sidebarIds = new Set(panelLayout.sidebar); return PANELS.filter((p) => { if (isPanelHidden({ panel: p, capabilities, aiEnabled })) { return false; } // Exclude panels that are in the sidebar if (sidebarIds.has(p.type)) { return false; } return true; }); }, [panelLayout.sidebar, capabilities, aiEnabled]); const emitResizeEvent = useEvent(() => { // HACK: Unfortunately, we have to do this twice to make sure the // panel is fully expanded before we dispatch the resize event raf2(() => { window.dispatchEvent(new Event("resize")); }); }); // sync sidebar useEffect(() => { if (!sidebarRef.current) { return; } const isCurrentlyCollapsed = sidebarRef.current.isCollapsed(); if (isSidebarOpen && isCurrentlyCollapsed) { sidebarRef.current.expand(); } if (!isSidebarOpen && !isCurrentlyCollapsed) { sidebarRef.current.collapse(); } // Dispatch a resize event so widgets know to resize emitResizeEvent(); }, [isSidebarOpen]); // sync panel useEffect(() => { if (!developerPanelRef.current) { return; } const isCurrentlyCollapsed = developerPanelRef.current.isCollapsed(); if (isDeveloperPanelOpen && isCurrentlyCollapsed) { developerPanelRef.current.expand(); } if (!isDeveloperPanelOpen && !isCurrentlyCollapsed) { developerPanelRef.current.collapse(); } // Dispatch a resize event so widgets know to resize emitResizeEvent(); }, [isDeveloperPanelOpen]); // Auto-correct developer panel selection when the selected tab is no longer available useEffect(() => { if (!isDeveloperPanelOpen) { return; } const isSelectionValid = devPanelItems.some( (p) => p.type === selectedDeveloperPanelTab, ); if (!isSelectionValid) { if (devPanelItems.length > 0) { openApplication(devPanelItems[0].type); } else { setIsDeveloperPanelOpen(false); } } }, [ isDeveloperPanelOpen, devPanelItems, selectedDeveloperPanelTab, openApplication, setIsDeveloperPanelOpen, ]); const appBodyPanel = ( {children} ); const helperResizeHandle = ( ); const panelResizeHandle = ( ); const agentsEnabled = getFeatureFlag("external_agents"); const renderAiPanel = () => { if (agentsEnabled && aiPanelTab === "agents") { return ; } return ; }; const SIDEBAR_PANELS: Record = { files: , variables: , dependencies: , packages: , outline: , documentation: , snippets: , ai: renderAiPanel(), errors: , scratchpad: , tracing: , secrets: , logs: , terminal: ( }> setIsSidebarOpen(false)} /> ), cache: , }; const helpPaneBody = (
{selectedPanel === "dependencies" ? (
Dependencies { if (value === "minimap" || value === "graph") { setDependencyPanelTab(value); } }} > Minimap Graph
) : selectedPanel === "ai" && agentsEnabled ? ( { if (value === "chat" || value === "agents") { setAiPanelTab(value); } }} > Chat Agents ) : ( {selectedPanel} )} {selectedPanel === "packages" && ( )}
{Object.entries(SIDEBAR_PANELS).map(([key, Panel]) => ( {Panel} ))}
); const helperPanel = ( { // This means it started closed and is opening for the first time if (prevSize === 0 && size === 10) { sidebarRef.current?.resize(30); } }} onCollapse={() => setIsSidebarOpen(false)} onExpand={() => setIsSidebarOpen(true)} > {helpPaneBody} {helperResizeHandle} ); const DEVELOPER_PANELS: Record = { ...SIDEBAR_PANELS, terminal: ( }> setIsDeveloperPanelOpen(false)} /> ), }; const bottomPanel = ( { // This means it started closed and is opening for the first time if (prevSize === 0 && size === 10) { developerPanelRef.current?.resize(30); } }} onCollapse={() => setIsDeveloperPanelOpen(false)} onExpand={() => setIsDeveloperPanelOpen(true)} > {panelResizeHandle}
{/* Panel header with tabs */}
value={devPanelItems} setValue={handleSetDevPanelItems} getKey={(p) => p.type} availableItems={availableDevPanels} crossListDrag={{ dragType: "panels", listId: "developer-panel", onReceive: handleDevPanelReceive, }} getItemLabel={(panel) => ( {panel.label} )} ariaLabel="Developer panel tabs" className="flex flex-row gap-1" minItems={0} onAction={(panel) => openApplication(panel.type)} onItemPreloadHint={(panel) => PANEL_PRELOADERS[panel.type]?.()} renderItem={(panel) => (
0 && "text-destructive", )} /> {panel.label}
)} />
{selectedDeveloperPanelTab === "packages" && ( )}
{/* Panel content */} }>
{Object.entries(DEVELOPER_PANELS).map(([key, Panel]) => ( {Panel} ))}
); return ( {helperPanel} {appBodyPanel} {bottomPanel}