/* eslint-disable react-refresh/only-export-components */ import { LayoutPanelLeft, Puzzle, Server, Settings } from "lucide-react"; import { lazy, memo, Suspense } from "react"; import { useTranslation } from "react-i18next"; import type { Tab, TabType } from "@/types/ui-types"; import { trackTabHandle } from "@/shell/tab-handles"; import { hostToSSHHost } from "@/lib/host-to-ssh-host"; import { PluginViewPlaceholder } from "@/plugin-host/PluginViewPlaceholder"; import { getTabType, type TabShellCallbacks, type TabTypeDef, } from "./tab-registry"; import { getPanel, type PanelDef } from "./panel-registry"; import { CompanionFrame } from "./CompanionFrame"; import { markAdaptiveResourceUsed, runAdaptiveBackgroundTask, } from "@/lib/adaptive-resource-budget"; // Heavy tab surfaces, kept out of the AppShell critical path. const PluginsTab = lazy(() => import("@/plugins/PluginsTab").then((m) => ({ default: m.PluginsTab })), ); /** Download a likely next tab without starting a connection or mounting UI. */ export function preloadTabSurface(type: TabType): void { const loader = getTabType(type)?.preload; if (loader) runAdaptiveBackgroundTask("module", `tab:${type}`, loader); } export function markTabSurfaceUsed(type: TabType): void { markAdaptiveResourceUsed("module", `tab:${type}`); } function EmptyState({ icon: Icon, messageKey, }: { icon: React.ElementType; messageKey: string; }) { const { t } = useTranslation(); return (
{t(messageKey)}
); } function TabChunkFallback() { return (
); } function withTabSuspense(node: React.ReactNode) { return }>{node}; } /** * Host frame for rail panels opened as tabs. Panels expect a full-height flex * column like the sidebar gives them. The max width keeps forms readable on a * wide monitor instead of stretching them edge to edge. */ function PanelTabFrame({ children }: { children: React.ReactNode }) { return (
{children}
); } export function tabIcon(type: TabType) { switch (type) { case "user-profile": case "admin-settings": return ; case "split-screen": return ; case "plugins": return ; default: { const Icon = getTabType(type)?.icon; return Icon ? : null; } } } /** * A plugin's tab: its registered component with the generic render props. * Unregistered types get the ownership placeholder in renderTabContent. */ const RegisteredTab = memo(function RegisteredTab({ def, tab, isVisible, isFocusedPane, inSplit, shell, }: { def: TabTypeDef; tab: Tab; isVisible: boolean; isFocusedPane: boolean; inSplit: boolean; shell: TabShellCallbacks; }) { const { host, label } = tab; if (def.requiresHost && !host) { return ( ); } const Component = def.component; const content = ( ) : undefined } label={label} isVisible={isVisible} isFocusedPane={isFocusedPane} inSplit={inSplit} handleRef={trackTabHandle( tab.terminalRef as React.RefObject | undefined, )} shell={shell} /> ); return withTabSuspense( {def.panelFrame ? {content} : content} , ); }); /** A rail panel opened as a tab when its plugin registers no tab of its own. */ const PanelAsTab = memo(function PanelAsTab({ panel, isVisible, targetTab, shell, }: { panel: PanelDef; isVisible: boolean; targetTab?: Tab; shell: TabShellCallbacks; }) { const Panel = panel.component; return withTabSuspense( , ); }); function noop() {} export interface TabRenderContext { shell: TabShellCallbacks; /** The terminal a panel shown as a tab sends commands to. */ panelTargetTab?: Tab; isVisible?: boolean; isFocusedPane?: boolean; inSplit?: boolean; } export function renderTabContent(tab: Tab, context: TabRenderContext) { const { shell, isVisible = true, isFocusedPane = true, inSplit = false, } = context; switch (tab.type) { case "split-screen": return null; case "plugins": return withTabSuspense(); case "user-profile": case "admin-settings": return null; default: { const def = getTabType(tab.type); // The home tab is empty until the layout gives it a tab type. if (!def && tab.type === "dashboard") return null; if (!def) { const panel = getPanel(tab.type); if (panel) { return ( ); } return ; } return ( ); } } }