diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx
index 55dcdf94..138bda13 100644
--- a/frontend/src/components/EditorLayout.tsx
+++ b/frontend/src/components/EditorLayout.tsx
@@ -5,14 +5,9 @@ import { Editor } from '@/lib/monacoLoader';
import { useImageUpdates } from '@/hooks/useImageUpdates';
import TerminalComponent from './Terminal';
import ErrorBoundary from './ErrorBoundary';
-import HomeDashboard from './HomeDashboard';
import type { NotificationItem } from './dashboard/types';
import BashExecModal from './BashExecModal';
import LazyBoundary from './LazyBoundary';
-import { Skeleton } from '@/components/ui/skeleton';
-import { AdmiralGate } from './AdmiralGate';
-import { CapabilityGate } from './CapabilityGate';
-import ResourcesView from './ResourcesView';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from './ui/dialog';
@@ -37,58 +32,24 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSepara
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { TopBar } from './TopBar';
import { cn } from '@/lib/utils';
-import { SettingsPage } from './settings/SettingsPage';
import type { SectionId } from './settings/types';
+import { ViewRouter } from './EditorLayout/ViewRouter';
import { StackAlertSheet } from './StackAlertSheet';
import { StackAutoHealSheet } from '@/components/StackAutoHealSheet';
import { GitSourcePanel } from './stack/GitSourcePanel';
-import { AppStoreView } from './AppStoreView';
import { LogViewer } from './LogViewer';
import StructuredLogViewer from './StructuredLogViewer';
import StackAnatomyPanel from './StackAnatomyPanel';
import { Sparkline } from './ui/sparkline';
import type { ScheduleTaskPrefill } from './ScheduledOperationsView';
-// Paid-tier views and the security-history overlay are loaded on demand.
-// Their internal PaidGate / AdmiralGate / CapabilityGate wrappers render
-// the upsell or capability-missing card with blurred children rather than
-// short-circuiting, so a tier-locked or capability-missing operator
-// opening one of these tabs still triggers the chunk fetch to render the
-// blurred preview. The gate-short-circuit fix lives in a follow-up
-// branch. What this lazy split closes is the much larger initial-bundle
-// leak: every Community user used to download the full FleetView,
-// AuditLogView, etc. on first page load even if they never clicked those
-// tabs. After this change, the chunks fetch only on tab open.
-//
-// GlobalObservabilityView is a free-tier feature with no internal gate;
-// it is split here purely for the bundle-size win, not for IP protection.
-const HostConsole = lazy(() => import('./HostConsole'));
-const GlobalObservabilityView = lazy(() =>
- import('./GlobalObservabilityView').then(m => ({ default: m.GlobalObservabilityView })),
-);
-const FleetView = lazy(() =>
- import('./FleetView').then(m => ({ default: m.FleetView })),
-);
-const AuditLogView = lazy(() =>
- import('./AuditLogView').then(m => ({ default: m.AuditLogView })),
-);
+// SecurityHistoryView is the only lazy-loaded view that lives outside
+// the ViewRouter switch — it renders as an overlay sheet wired into the
+// settings flow, not as a top-level tab. The other tab-level lazy views
+// (HostConsole, FleetView, AuditLogView, etc.) live inside ViewRouter.
const SecurityHistoryView = lazy(() =>
import('./SecurityHistoryView').then(m => ({ default: m.SecurityHistoryView })),
);
-const ScheduledOperationsView = lazy(() => import('./ScheduledOperationsView'));
-const AutoUpdateReadinessView = lazy(() => import('./AutoUpdateReadinessView'));
-
-// Sized for the main workspace area (flex-1 with p-6 padding). Visible
-// only during the brief window between an unlocked view's chunk request
-// and its first render.
-function ViewSkeleton() {
- return (
-
-
-
-
- );
-}
import { SENCHO_NAVIGATE_EVENT } from './NodeManager';
import type { SenchoNavigateDetail } from './NodeManager';
import { NodeSwitcher } from './NodeSwitcher';
@@ -2512,26 +2473,34 @@ export default function EditorLayout() {
{/* Main Workspace */}
- {activeView === 'settings' ? (
-
- ) : activeView === 'templates' ? (
-
{ refreshStacks(); loadFile(stackName); }} />
- ) : activeView === 'resources' ? (
-
- ) : activeView === 'host-console' ? (
-
-
-
- }>
- setActiveView(selectedFile ? 'editor' : 'dashboard')} />
-
-
-
-
- ) : !isLoading && selectedFile && activeView === 'editor' ? (
+ { refreshStacks(); loadFile(stackName); }}
+ onHostConsoleClose={() => setActiveView(selectedFile ? 'editor' : 'dashboard')}
+ onFleetNavigateToNode={(nodeId, stackName) => {
+ const node = nodes.find(n => n.id === nodeId);
+ if (node) {
+ if (activeNode?.id === nodeId) {
+ loadFile(stackName);
+ } else {
+ pendingStackLoadRef.current = stackName;
+ setActiveNode(node);
+ }
+ }
+ }}
+ filterNodeId={filterNodeId}
+ onClearScheduledOpsFilter={() => setFilterNodeId(null)}
+ schedulePrefill={schedulePrefill}
+ onPrefillConsumed={handlePrefillConsumed}
+ notifications={notifications}
+ onNavigateToStack={(stackFile) => { loadFile(stackFile); }}
+ onOpenSettingsSection={(section) => handleOpenSettings(section)}
+ onClearNotifications={clearAllNotifications}
+ renderEditor={() => (
{/* Left column: identity + health strip + logs, stacked */}
@@ -3066,67 +3035,8 @@ export default function EditorLayout() {
)}
- ) : activeView === 'global-observability' ? (
-
- }>
-
-
-
- ) : activeView === 'fleet' ? (
-
-
- }>
- {
- const node = nodes.find(n => n.id === nodeId);
- if (node) {
- if (activeNode?.id === nodeId) {
- loadFile(stackName);
- } else {
- pendingStackLoadRef.current = stackName;
- setActiveNode(node);
- }
- }
- }} />
-
-
-
- ) : activeView === 'audit-log' ? (
-
-
- }>
-
-
-
-
- ) : activeView === 'auto-updates' ? (
-
-
- }>
-
-
-
-
- ) : activeView === 'scheduled-ops' ? (
-
-
- }>
- setFilterNodeId(null)}
- prefill={schedulePrefill}
- onPrefillConsumed={handlePrefillConsumed}
- />
-
-
-
- ) : (
- { loadFile(stackFile); }}
- onOpenSettingsSection={(section) => handleOpenSettings(section)}
- notifications={notifications}
- onClearNotifications={clearAllNotifications}
- />
- )}
+ )}
+ />
diff --git a/frontend/src/components/EditorLayout/ViewRouter.tsx b/frontend/src/components/EditorLayout/ViewRouter.tsx
new file mode 100644
index 00000000..eaa81fc8
--- /dev/null
+++ b/frontend/src/components/EditorLayout/ViewRouter.tsx
@@ -0,0 +1,205 @@
+import { Suspense, lazy, type ReactNode } from 'react';
+import { Skeleton } from '@/components/ui/skeleton';
+import { AdmiralGate } from '../AdmiralGate';
+import { CapabilityGate } from '../CapabilityGate';
+import LazyBoundary from '../LazyBoundary';
+import { SettingsPage } from '../settings/SettingsPage';
+import type { SectionId } from '../settings/types';
+import { AppStoreView } from '../AppStoreView';
+import ResourcesView from '../ResourcesView';
+import HomeDashboard from '../HomeDashboard';
+import type { NotificationItem } from '../dashboard/types';
+import type { ScheduleTaskPrefill } from '../ScheduledOperationsView';
+
+// Paid-tier views and the security-history overlay are loaded on demand.
+// Their internal PaidGate / AdmiralGate / CapabilityGate wrappers render
+// the upsell or capability-missing card with blurred children rather than
+// short-circuiting, so a tier-locked or capability-missing operator
+// opening one of these tabs still triggers the chunk fetch to render the
+// blurred preview. What this lazy split closes is the much larger
+// initial-bundle leak: every Community user used to download the full
+// FleetView, AuditLogView, etc. on first page load even if they never
+// clicked those tabs. After this change, the chunks fetch only on tab
+// open.
+//
+// GlobalObservabilityView is a free-tier feature with no internal gate;
+// it is split here purely for the bundle-size win, not for IP protection.
+const HostConsole = lazy(() => import('../HostConsole'));
+const GlobalObservabilityView = lazy(() =>
+ import('../GlobalObservabilityView').then(m => ({ default: m.GlobalObservabilityView })),
+);
+const FleetView = lazy(() =>
+ import('../FleetView').then(m => ({ default: m.FleetView })),
+);
+const AuditLogView = lazy(() =>
+ import('../AuditLogView').then(m => ({ default: m.AuditLogView })),
+);
+const ScheduledOperationsView = lazy(() => import('../ScheduledOperationsView'));
+const AutoUpdateReadinessView = lazy(() => import('../AutoUpdateReadinessView'));
+
+// Sized for the main workspace area (flex-1 with p-6 padding). Visible
+// only during the brief window between an unlocked view's chunk request
+// and its first render.
+function ViewSkeleton() {
+ return (
+
+
+
+
+ );
+}
+
+function LazyView({ children }: { children: ReactNode }) {
+ return (
+
+ }>
+ {children}
+
+
+ );
+}
+
+export type ActiveView =
+ | 'dashboard'
+ | 'editor'
+ | 'host-console'
+ | 'resources'
+ | 'templates'
+ | 'global-observability'
+ | 'fleet'
+ | 'audit-log'
+ | 'scheduled-ops'
+ | 'auto-updates'
+ | 'settings';
+
+export interface ViewRouterProps {
+ activeView: ActiveView;
+ selectedFile: string | null;
+ isLoading: boolean;
+ settingsSection: SectionId;
+ onSettingsSectionChange: (section: SectionId) => void;
+ onTemplateDeploySuccess: (stackName: string) => void;
+ onHostConsoleClose: () => void;
+ onFleetNavigateToNode: (nodeId: number, stackName: string) => void;
+ filterNodeId: number | null;
+ onClearScheduledOpsFilter: () => void;
+ schedulePrefill: ScheduleTaskPrefill | null;
+ onPrefillConsumed: () => void;
+ notifications: NotificationItem[];
+ onNavigateToStack: (stackFile: string) => void;
+ onOpenSettingsSection: (section: SectionId) => void;
+ onClearNotifications: () => void;
+ // Render slot for the inline editor view. Kept as a callback so the
+ // (large) editor JSX is only allocated when activeView === 'editor',
+ // not on every parent render that lands on a different view.
+ renderEditor: () => ReactNode;
+}
+
+export function ViewRouter({
+ activeView,
+ selectedFile,
+ isLoading,
+ settingsSection,
+ onSettingsSectionChange,
+ onTemplateDeploySuccess,
+ onHostConsoleClose,
+ onFleetNavigateToNode,
+ filterNodeId,
+ onClearScheduledOpsFilter,
+ schedulePrefill,
+ onPrefillConsumed,
+ notifications,
+ onNavigateToStack,
+ onOpenSettingsSection,
+ onClearNotifications,
+ renderEditor,
+}: ViewRouterProps): ReactNode {
+ if (activeView === 'settings') {
+ return (
+
+ );
+ }
+ if (activeView === 'templates') {
+ return ;
+ }
+ if (activeView === 'resources') {
+ return ;
+ }
+ if (activeView === 'host-console') {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ // Fall-through: when activeView === 'editor' but selectedFile is
+ // null or the stack is still loading, drop through to the default
+ // HomeDashboard render below. This matches the pre-extraction
+ // behavior of the conditional ternary chain in EditorLayout.tsx.
+ if (!isLoading && selectedFile && activeView === 'editor') {
+ return renderEditor();
+ }
+ if (activeView === 'global-observability') {
+ return (
+
+
+
+ );
+ }
+ if (activeView === 'fleet') {
+ return (
+
+
+
+
+
+ );
+ }
+ if (activeView === 'audit-log') {
+ return (
+
+
+
+
+
+ );
+ }
+ if (activeView === 'auto-updates') {
+ return (
+
+
+
+
+
+ );
+ }
+ if (activeView === 'scheduled-ops') {
+ return (
+
+
+
+
+
+ );
+ }
+ return (
+
+ );
+}