// ======================================== // CLI Viewer Page // ======================================== // Multi-pane CLI output viewer with configurable layouts // Integrates with viewerStore for state management // Includes WebSocket integration and execution recovery import { useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { LayoutContainer, CliViewerToolbar } from '@/components/cli-viewer'; import { useViewerStore, useViewerLayout, useViewerPanes, useFocusedPaneId, type AllotmentLayout, } from '@/stores/viewerStore'; import { useCliStreamStore } from '@/stores/cliStreamStore'; import { useActiveCliExecutions } from '@/hooks/useActiveCliExecutions'; import { useCliStreamWebSocket } from '@/hooks/useCliStreamWebSocket'; import { getStaleViewerTabs } from './cliViewerPage.utils'; // ======================================== // Constants // ======================================== const DEFAULT_LAYOUT = 'split-h' as const; // ======================================== // Helper Functions // ======================================== /** * Count total panes in layout */ function countPanes(layout: AllotmentLayout): number { let count = 0; const traverse = (children: (string | AllotmentLayout)[]) => { for (const child of children) { if (typeof child === 'string') { count++; } else { traverse(child.children); } } }; traverse(layout.children); return count; } // ======================================== // Main Component // ======================================== export function CliViewerPage() { const [searchParams, setSearchParams] = useSearchParams(); // Fullscreen state const [isFullscreen, setIsFullscreen] = useState(false); // Store hooks const layout = useViewerLayout(); const panes = useViewerPanes(); const focusedPaneId = useFocusedPaneId(); const { initializeDefaultLayout, addTab, removeTab } = useViewerStore(); // CLI Stream Store hooks const executions = useCliStreamStore((state) => state.executions); // Active execution sync from server const { isLoading: isSyncing, isFetching: isRefreshing } = useActiveCliExecutions(true); // CENTRALIZED WebSocket handler - processes each message only ONCE globally useCliStreamWebSocket(); // Auto-add new executions as tabs, distributing across available panes const addedExecutionsRef = useRef>(new Set()); // FIX-001: Initialize addedExecutionsRef with existing tab executionIds on mount // This prevents duplicate tabs from being added after page refresh useEffect(() => { // Extract executionIds from all existing tabs in all panes const existingExecutionIds = Object.values(panes).flatMap((pane) => pane.tabs.map((tab) => tab.executionId) ); existingExecutionIds.forEach((id) => addedExecutionsRef.current.add(id)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Empty deps - only run once on mount useEffect(() => { const paneIds = Object.keys(panes); if (paneIds.length === 0) return; const storeAddTab = useViewerStore.getState().addTab; const newExecutionIds = Object.keys(executions).filter( (id) => !addedExecutionsRef.current.has(id) ); if (newExecutionIds.length === 0) return; newExecutionIds.forEach((executionId, index) => { addedExecutionsRef.current.add(executionId); const exec = executions[executionId]; const toolShort = exec.tool.split('-')[0]; const targetPaneId = paneIds[index % paneIds.length]; storeAddTab(targetPaneId, executionId, `${toolShort} (${exec.mode})`); }); }, [executions, panes]); useEffect(() => { if (isSyncing || isRefreshing) return; const staleTabs = getStaleViewerTabs(panes, executions); if (staleTabs.length === 0) return; staleTabs.forEach(({ paneId, tabId, executionId }) => { addedExecutionsRef.current.delete(executionId); removeTab(paneId, tabId); }); }, [executions, isRefreshing, isSyncing, panes, removeTab]); // Initialize layout if empty useEffect(() => { const paneCount = countPanes(layout); if (paneCount === 0) { initializeDefaultLayout(DEFAULT_LAYOUT); } }, [layout, initializeDefaultLayout]); // Handle executionId from URL params useEffect(() => { const executionId = searchParams.get('executionId'); if (executionId && focusedPaneId) { addTab(focusedPaneId, executionId, `Execution ${executionId.slice(0, 8)}`); setSearchParams((prev) => { const newParams = new URLSearchParams(prev); newParams.delete('executionId'); return newParams; }); } }, [searchParams, focusedPaneId, addTab, setSearchParams]); // Toggle fullscreen handler const handleToggleFullscreen = () => { setIsFullscreen((prev) => !prev); }; return (
{/* ======================================== */} {/* Toolbar */} {/* ======================================== */} {/* ======================================== */} {/* Layout Container */} {/* ======================================== */}
); } export default CliViewerPage;