Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 91 additions & 52 deletions frontend/src/components/editor/chrome/wrapper/app-chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ 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 { 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,
Expand All @@ -49,33 +69,26 @@ 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"));

// 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 (
<div
aria-label="Loading terminal"
role="status"
className="w-full h-full flex items-start p-3 font-mono text-xs select-none"
style={{
background: isDark ? "#0f172a" : "#ffffff",
color: isDark ? "#94a3b8" : "#64748b",
}}
>
<span className="opacity-70">Starting terminal</span>
<span className="ml-1 inline-block w-2 h-3.5 align-middle bg-current animate-pulse" />
</div>
);
};
export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
const {
isSidebarOpen,
Expand All @@ -94,6 +107,26 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
// Subscribe to capabilities to re-render when they change (e.g., terminal capability)
const capabilities = useAtomValue(capabilitiesAtom);

// 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 schedule =
typeof window !== "undefined" &&
typeof window.requestIdleCallback === "function"
? (cb: () => void) => window.requestIdleCallback(cb, { timeout: 2000 })
: (cb: () => void) => setTimeout(cb, 300);
schedule(() => {
if (isSidebarOpen && selectedPanel) {
PANEL_PRELOADERS[selectedPanel]?.();
}
if (isDeveloperPanelOpen && selectedDeveloperPanelTab) {
PANEL_PRELOADERS[selectedDeveloperPanelTab]?.();
}
});
Comment on lines +115 to +127
}, []);

// Convert current developer panel items to PanelDescriptors
// Filter out hidden panels (e.g., terminal when capability is not available)
const devPanelItems = useMemo(() => {
Expand Down Expand Up @@ -254,32 +287,34 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {

const renderAiPanel = () => {
if (agentsEnabled && aiPanelTab === "agents") {
return <LazyAgentPanel />;
return <LazyAgentPanel.Component />;
}
return <LazyChatPanel />;
return <LazyChatPanel.Component />;
};

const SIDEBAR_PANELS: Record<PanelType, React.ReactNode> = {
files: <LazyFileExplorerPanel />,
variables: <LazySessionPanel />,
dependencies: <LazyDependencyGraphPanel />,
packages: <LazyPackagesPanel />,
outline: <LazyOutlinePanel />,
documentation: <LazyDocumentationPanel />,
snippets: <LazySnippetsPanel />,
files: <LazyFileExplorerPanel.Component />,
variables: <LazySessionPanel.Component />,
dependencies: <LazyDependencyGraphPanel.Component />,
packages: <LazyPackagesPanel.Component />,
outline: <LazyOutlinePanel.Component />,
documentation: <LazyDocumentationPanel.Component />,
snippets: <LazySnippetsPanel.Component />,
ai: renderAiPanel(),
errors: <LazyErrorsPanel />,
scratchpad: <LazyScratchpadPanel />,
tracing: <LazyTracingPanel />,
secrets: <LazySecretsPanel />,
logs: <LazyLogsPanel />,
errors: <LazyErrorsPanel.Component />,
scratchpad: <LazyScratchpadPanel.Component />,
tracing: <LazyTracingPanel.Component />,
secrets: <LazySecretsPanel.Component />,
logs: <LazyLogsPanel.Component />,
terminal: (
<LazyTerminal
visible={isSidebarOpen && selectedPanel === "terminal"}
onClose={() => setIsSidebarOpen(false)}
/>
<Suspense fallback={<TerminalSkeleton />}>
<LazyTerminal.Component
visible={isSidebarOpen && selectedPanel === "terminal"}
onClose={() => setIsSidebarOpen(false)}
/>
</Suspense>
),
cache: <LazyCachePanel />,
cache: <LazyCachePanel.Component />,
};

const helpPaneBody = (
Expand Down Expand Up @@ -412,12 +447,14 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
const DEVELOPER_PANELS: Record<PanelType, React.ReactNode> = {
...SIDEBAR_PANELS,
terminal: (
<LazyTerminal
visible={
isDeveloperPanelOpen && selectedDeveloperPanelTab === "terminal"
}
onClose={() => setIsDeveloperPanelOpen(false)}
/>
<Suspense fallback={<TerminalSkeleton />}>
<LazyTerminal.Component
visible={
isDeveloperPanelOpen && selectedDeveloperPanelTab === "terminal"
}
onClose={() => setIsDeveloperPanelOpen(false)}
/>
</Suspense>
),
};

Expand Down Expand Up @@ -480,6 +517,8 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
? "bg-muted"
: "hover:bg-muted/50",
)}
onMouseEnter={PANEL_PRELOADERS[panel.type]}
onFocus={PANEL_PRELOADERS[panel.type]}
>
<panel.Icon
className={cn(
Expand Down
81 changes: 81 additions & 0 deletions frontend/src/components/editor/chrome/wrapper/lazy-panels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* Copyright 2026 Marimo. All rights reserved. */

import { reactLazyWithPreload } from "@/utils/lazy";
import type { PanelType } from "../types";

// Centralized lazy panels. Using reactLazyWithPreload (instead of React.lazy)
// gives each panel a .preload() method so the chunk can be fetched on intent
// (hovering the sidebar icon or developer-panel tab) before the user clicks.

export const LazyTerminal = reactLazyWithPreload(
() => import("@/components/terminal/terminal"),
);
export const LazyChatPanel = reactLazyWithPreload(
() => import("@/components/chat/chat-panel"),
);
export const LazyAgentPanel = reactLazyWithPreload(
() => import("@/components/chat/acp/agent-panel"),
);
export const LazyDependencyGraphPanel = reactLazyWithPreload(
() => import("../panels/dependency-graph-panel"),
);
export const LazySessionPanel = reactLazyWithPreload(
() => import("../panels/session-panel"),
);
export const LazyDocumentationPanel = reactLazyWithPreload(
() => import("../panels/documentation-panel"),
);
export const LazyErrorsPanel = reactLazyWithPreload(
() => import("../panels/error-panel"),
);
export const LazyFileExplorerPanel = reactLazyWithPreload(
() => import("../panels/file-explorer-panel"),
);
export const LazyLogsPanel = reactLazyWithPreload(
() => import("../panels/logs-panel"),
);
export const LazyOutlinePanel = reactLazyWithPreload(
() => import("../panels/outline-panel"),
);
export const LazyPackagesPanel = reactLazyWithPreload(
() => import("../panels/packages-panel"),
);
export const LazyScratchpadPanel = reactLazyWithPreload(
() => import("../panels/scratchpad-panel"),
);
export const LazySecretsPanel = reactLazyWithPreload(
() => import("../panels/secrets-panel"),
);
export const LazySnippetsPanel = reactLazyWithPreload(
() => import("../panels/snippets-panel"),
);
export const LazyTracingPanel = reactLazyWithPreload(
() => import("../panels/tracing-panel"),
);
export const LazyCachePanel = reactLazyWithPreload(
() => import("../panels/cache-panel"),
);

// Preloader registry: hovering an icon/tab calls into this map to warm the
// corresponding chunk. Two panel types (chat and agents) share the "ai" slot,
// so we preload both.
export const PANEL_PRELOADERS: Record<PanelType, () => void> = {
files: LazyFileExplorerPanel.preload,
variables: LazySessionPanel.preload,
dependencies: LazyDependencyGraphPanel.preload,
packages: LazyPackagesPanel.preload,
outline: LazyOutlinePanel.preload,
documentation: LazyDocumentationPanel.preload,
snippets: LazySnippetsPanel.preload,
ai: () => {
LazyChatPanel.preload();
LazyAgentPanel.preload();
},
errors: LazyErrorsPanel.preload,
scratchpad: LazyScratchpadPanel.preload,
tracing: LazyTracingPanel.preload,
secrets: LazySecretsPanel.preload,
logs: LazyLogsPanel.preload,
terminal: LazyTerminal.preload,
cache: LazyCachePanel.preload,
Comment on lines +59 to +80
};
21 changes: 18 additions & 3 deletions frontend/src/components/editor/chrome/wrapper/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
PANELS,
type PanelDescriptor,
} from "../types";
import { PANEL_PRELOADERS } from "./lazy-panels";

export const Sidebar: React.FC = () => {
const { selectedPanel, selectedDeveloperPanelTab, isSidebarOpen } =
Expand Down Expand Up @@ -143,6 +144,7 @@ export const Sidebar: React.FC = () => {
<SidebarItem
tooltip={panel.tooltip}
selected={selectedPanel === panel.type}
onPreloadHint={PANEL_PRELOADERS[panel.type]}
>
{panel.type === "errors" ? (
<ErrorPanelIcon Icon={panel.Icon} />
Expand Down Expand Up @@ -206,8 +208,10 @@ const SidebarItem: React.FC<
tooltip: React.ReactNode;
className?: string;
onClick?: () => void;
/** Fired on hover or focus — used to preload the panel's chunk. */
onPreloadHint?: () => void;
}>
> = ({ children, tooltip, selected, className, onClick }) => {
> = ({ children, tooltip, selected, className, onClick, onPreloadHint }) => {
const itemClassName = cn(
"flex items-center p-2 text-sm mx-px shadow-inset font-mono rounded",
!selected && "hover:bg-(--sage-3)",
Expand All @@ -218,11 +222,22 @@ const SidebarItem: React.FC<
// Render as div when not clickable (e.g., inside ReorderableList)
// This avoids nested interactive elements which break react-aria's drag behavior
const content = onClick ? (
<button className={itemClassName} onClick={onClick}>
<button
className={itemClassName}
onClick={onClick}
onMouseEnter={onPreloadHint}
onFocus={onPreloadHint}
>
{children}
</button>
) : (
<div className={itemClassName}>{children}</div>
<div
className={itemClassName}
onMouseEnter={onPreloadHint}
onFocus={onPreloadHint}
>
Comment on lines +234 to +238
{children}
</div>
);

return (
Expand Down
Loading