/* 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 { 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 { 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"; const LazyTerminal = React.lazy(() => import("@/components/terminal/terminal")); const LazyChatPanel = React.lazy(() => import("@/components/chat/chat-panel")); const LazyAgentPanel = React.lazy( () => import("@/components/chat/acp/agent-panel"), ); const LazyDependencyGraphPanel = React.lazy( () => import("@/components/editor/chrome/panels/dependency-graph-panel"), ); const LazySessionPanel = React.lazy(() => import("../panels/session-panel")); const LazyDocumentationPanel = React.lazy( () => import("../panels/documentation-panel"), ); const LazyErrorsPanel = React.lazy(() => import("../panels/error-panel")); const LazyFileExplorerPanel = React.lazy( () => import("../panels/file-explorer-panel"), ); const LazyLogsPanel = React.lazy(() => import("../panels/logs-panel")); const LazyOutlinePanel = React.lazy(() => import("../panels/outline-panel")); const LazyPackagesPanel = React.lazy(() => import("../panels/packages-panel")); const LazyScratchpadPanel = React.lazy( () => import("../panels/scratchpad-panel"), ); const LazySecretsPanel = React.lazy(() => import("../panels/secrets-panel")); const LazySnippetsPanel = React.lazy(() => import("../panels/snippets-panel")); const LazyTracingPanel = React.lazy(() => import("../panels/tracing-panel")); const LazyCachePanel = React.lazy(() => import("../panels/cache-panel")); 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); // 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)) { return []; } return [panel]; }); }, [panelLayout.developerPanel, capabilities]); 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(p, capabilities)) { return false; } // Exclude panels that are in the sidebar if (sidebarIds.has(p.type)) { return false; } return true; }); }, [panelLayout.sidebar, capabilities]); 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} )}
{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)} renderItem={(panel) => (
0 && "text-destructive", )} /> {panel.label}
)} />
{/* Panel content */} }>
{Object.entries(DEVELOPER_PANELS).map(([key, Panel]) => ( {Panel} ))}
); return ( {helperPanel} {appBodyPanel} {bottomPanel}