diff --git a/frontend/src/components/editor/chrome/wrapper/app-chrome.tsx b/frontend/src/components/editor/chrome/wrapper/app-chrome.tsx
index aa4555502d5..832ff53d4ca 100644
--- a/frontend/src/components/editor/chrome/wrapper/app-chrome.tsx
+++ b/frontend/src/components/editor/chrome/wrapper/app-chrome.tsx
@@ -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,
@@ -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 (
+
+ Starting terminal
+
+
+ );
+};
export const AppChrome: React.FC = ({ children }) => {
const {
isSidebarOpen,
@@ -94,6 +107,26 @@ export const AppChrome: React.FC = ({ 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]?.();
+ }
+ });
+ }, []);
+
// Convert current developer panel items to PanelDescriptors
// Filter out hidden panels (e.g., terminal when capability is not available)
const devPanelItems = useMemo(() => {
@@ -254,32 +287,34 @@ export const AppChrome: React.FC = ({ children }) => {
const renderAiPanel = () => {
if (agentsEnabled && aiPanelTab === "agents") {
- return ;
+ return ;
}
- return ;
+ return ;
};
const SIDEBAR_PANELS: Record = {
- files: ,
- variables: ,
- dependencies: ,
- packages: ,
- outline: ,
- documentation: ,
- snippets: ,
+ files: ,
+ variables: ,
+ dependencies: ,
+ packages: ,
+ outline: ,
+ documentation: ,
+ snippets: ,
ai: renderAiPanel(),
- errors: ,
- scratchpad: ,
- tracing: ,
- secrets: ,
- logs: ,
+ errors: ,
+ scratchpad: ,
+ tracing: ,
+ secrets: ,
+ logs: ,
terminal: (
- setIsSidebarOpen(false)}
- />
+ }>
+ setIsSidebarOpen(false)}
+ />
+
),
- cache: ,
+ cache: ,
};
const helpPaneBody = (
@@ -412,12 +447,14 @@ export const AppChrome: React.FC = ({ children }) => {
const DEVELOPER_PANELS: Record = {
...SIDEBAR_PANELS,
terminal: (
- setIsDeveloperPanelOpen(false)}
- />
+ }>
+ setIsDeveloperPanelOpen(false)}
+ />
+
),
};
@@ -480,6 +517,8 @@ export const AppChrome: React.FC = ({ children }) => {
? "bg-muted"
: "hover:bg-muted/50",
)}
+ onMouseEnter={PANEL_PRELOADERS[panel.type]}
+ onFocus={PANEL_PRELOADERS[panel.type]}
>
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 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,
+};
diff --git a/frontend/src/components/editor/chrome/wrapper/sidebar.tsx b/frontend/src/components/editor/chrome/wrapper/sidebar.tsx
index ae80e0a4f8d..68c229902c1 100644
--- a/frontend/src/components/editor/chrome/wrapper/sidebar.tsx
+++ b/frontend/src/components/editor/chrome/wrapper/sidebar.tsx
@@ -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 } =
@@ -143,6 +144,7 @@ export const Sidebar: React.FC = () => {
{panel.type === "errors" ? (
@@ -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)",
@@ -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 ? (
-