import { Activity, Box, Gauge, LayoutList, Loader2, TerminalSquare } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import React, { memo, useMemo, useState } from 'react';
import { useI18n } from '../../application/i18n/I18nProvider';
import { useSystemManagerBackend } from '../../application/state/useSystemManagerBackend';
import type { TerminalSettings } from '../../domain/models';
import type { Host } from '../../domain/models/connection';
import type { SystemManagerSubTab } from '../../domain/systemManager/types';
import { resolveCapabilityPanelState } from '../../domain/systemManagerPanelState';
import { buildSystemManagerTabs, shouldCollectServerStats } from '../../domain/systemManager/systemTarget';
import type { Snippet, TerminalSession } from '../../types';
import { cn } from '../../lib/utils';
import { DockerManagerTab } from './DockerManagerTab';
import { ProcessManagerTab } from './ProcessManagerTab';
import { SystemOverviewTab } from './SystemOverviewTab';
import { TmuxManagerTab } from './TmuxManagerTab';
import { WorkspaceSidebarHostHeader } from '../terminalLayer/WorkspaceSidebarHostHeader';
import { SystemPanelEmpty, SystemPanelShell } from './SystemPanelUi';
import { useSessionCapabilities } from './hooks/useSystemManager';
const SystemPanelChecking = memo(function SystemPanelChecking({
message,
}: {
message: string;
}) {
return (
{message}
);
});
interface SystemManagerSidePanelProps {
session: TerminalSession | null;
sessionHost: Host | null;
showWorkspaceHostHeader?: boolean;
isVisible: boolean;
terminalSettings: TerminalSettings;
snippets: Snippet[];
onRequestTerminalFocus?: () => void;
}
export const SystemManagerSidePanel = memo(function SystemManagerSidePanel({
session,
sessionHost,
showWorkspaceHostHeader = false,
isVisible,
terminalSettings,
snippets,
onRequestTerminalFocus,
}: SystemManagerSidePanelProps) {
const { t } = useI18n();
const backend = useSystemManagerBackend();
const sessionId = session?.id ?? null;
const isConnected = session?.status === 'connected';
const capabilitiesTtlMs = terminalSettings.systemManagerProcessRefreshInterval * 1000;
const { capabilities, refreshCapabilities } = useSessionCapabilities(sessionId, isConnected, backend, isVisible, capabilitiesTtlMs);
const availableTabs = useMemo(
() => buildSystemManagerTabs(sessionHost, capabilities, session),
[capabilities, session, sessionHost],
);
const isStatsSupportedOs = useMemo(
() => shouldCollectServerStats(sessionHost, capabilities, session),
[capabilities, session, sessionHost],
);
const [activeTab, setActiveTab] = useState('overview');
const resolvedTab = availableTabs.includes(activeTab) ? activeTab : 'overview';
// Must be defined before early returns to comply with React rules of hooks.
const prevTabRef = React.useRef(resolvedTab);
const probingRef = React.useRef(false);
React.useEffect(() => {
const prev = prevTabRef.current;
prevTabRef.current = resolvedTab;
if (prev === resolvedTab) return;
if (resolvedTab === 'docker' && capabilities?.hasDocker !== true) {
if (!probingRef.current) {
probingRef.current = true;
refreshCapabilities().finally(() => { probingRef.current = false; });
}
} else if (resolvedTab === 'tmux' && capabilities?.hasTmux !== true) {
void refreshCapabilities();
}
}, [resolvedTab, capabilities, refreshCapabilities]);
// Auto-poll for Docker capabilities while Docker tab is active and Docker not yet detected.
// Use setTimeout recursion so the next probe only starts after the previous one finishes,
// avoiding overlapping probes (e.g. SSH timeout 8s vs user-configured interval 2s).
// First poll is delayed by one interval to avoid overlapping with the tab-switch probe above.
//
// Use a ref to store refreshCapabilities so that if its reference changes on every render,
// the useEffect below is NOT re-run (which would cancel the timer and bypass the interval).
const refreshRef = React.useRef(refreshCapabilities);
refreshRef.current = refreshCapabilities;
// Auto-poll for Docker capabilities while Docker tab is active and Docker not yet detected.
// Each effect generation gets its own cancelled flag and timerId via closure,
// preventing stale probes from surviving cleanup (unlike cancelledRef which is shared).
// First poll is delayed by one interval to avoid overlapping with the tab-switch probe.
React.useEffect(() => {
if (!isVisible || resolvedTab !== 'docker' || capabilities?.hasDocker === true) return;
let cancelled = false;
let timerId: ReturnType;
const pollOnce = async () => {
if (cancelled) return;
if (probingRef.current) {
// probe is in-flight, reschedule for next cycle
timerId = setTimeout(pollOnce, capabilitiesTtlMs);
return;
}
probingRef.current = true;
try {
await refreshRef.current();
} catch {
// Transient error - keep polling next round
}
probingRef.current = false;
if (cancelled) return;
timerId = setTimeout(pollOnce, capabilitiesTtlMs);
};
timerId = setTimeout(pollOnce, capabilitiesTtlMs);
return () => {
cancelled = true;
if (timerId) clearTimeout(timerId);
};
}, [isVisible, resolvedTab, capabilities?.hasDocker, capabilitiesTtlMs]);
const workspaceHostHeader = showWorkspaceHostHeader && sessionHost ? (
) : null;
if (!sessionId || !session) {
return (
{workspaceHostHeader}
);
}
if (!isConnected) {
return (
{workspaceHostHeader}
);
}
const tabDefs: { id: SystemManagerSubTab; icon: LucideIcon; label: string }[] = [
{ id: 'overview', icon: Gauge, label: t('systemManager.tabs.overview') },
{ id: 'processes', icon: LayoutList, label: t('systemManager.tabs.processes') },
{ id: 'tmux', icon: TerminalSquare, label: t('systemManager.tabs.tmux') },
{ id: 'docker', icon: Box, label: t('systemManager.tabs.docker') },
];
const tmuxReady = capabilities?.hasTmux === true;
const dockerReady = capabilities?.hasDocker === true;
const tmuxPanelState = resolveCapabilityPanelState({
isActive: resolvedTab === 'tmux',
ready: tmuxReady,
capabilitiesKnown: capabilities !== undefined,
});
const dockerPanelState = resolveCapabilityPanelState({
isActive: resolvedTab === 'docker',
ready: dockerReady,
capabilitiesKnown: capabilities !== undefined,
});
return (
{workspaceHostHeader}
{tabDefs.filter((tab) => availableTabs.includes(tab.id)).map(({ id, icon: Icon, label }) => (
))}
{resolvedTab === 'overview' && (
)}
{tmuxPanelState === 'unavailable' ? (
) : tmuxPanelState === 'checking' ? (
) : tmuxPanelState === 'ready' ? (
) : null}
{dockerPanelState === 'unavailable' ? (
) : dockerPanelState === 'checking' ? (
) : dockerPanelState === 'ready' ? (
) : null}
);
});