/* 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" && (
)}
setIsSidebarOpen(false)}
>
{Object.entries(SIDEBAR_PANELS).map(([key, Panel]) => (
{Panel}
))}
);
const helperPanel = (
);
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" && (
)}
setIsDeveloperPanelOpen(false)}
>
{/* Panel content */}
}>
{Object.entries(DEVELOPER_PANELS).map(([key, Panel]) => (
{Panel}
))}
);
return (
{helperPanel}
{appBodyPanel}
{bottomPanel}
);
};