// ======================================== // TerminalPane Component // ======================================== // Single terminal pane = PaneToolbar + content area. // Content can be terminal output or file preview based on displayMode. // Renders within the TerminalGrid recursive layout. // File preview is triggered from right sidebar FileSidebarPanel. import { useCallback, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { SplitSquareHorizontal, SplitSquareVertical, Eraser, AlertTriangle, X, Terminal, ChevronDown, RotateCcw, Pause, Play, Loader2, FileText, ArrowLeft, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { TerminalInstance } from './TerminalInstance'; import { FilePreview } from '@/components/shared/FilePreview'; import { useTerminalGridStore, selectTerminalGridPanes, selectTerminalGridFocusedPaneId, } from '@/stores/terminalGridStore'; import { useSessionManagerStore, selectGroups, selectTerminalMetas, } from '@/stores/sessionManagerStore'; import { useIssueQueueIntegrationStore, selectAssociationChain, } from '@/stores/issueQueueIntegrationStore'; import { useCliSessionStore } from '@/stores/cliSessionStore'; import { getAllPaneIds } from '@/lib/layout-utils'; import { useFileContent } from '@/hooks/useFileExplorer'; import type { PaneId } from '@/stores/viewerStore'; import type { TerminalStatus } from '@/types/terminal-dashboard'; // ========== Status Styles ========== const statusDotStyles: Record = { active: 'bg-green-500', idle: 'bg-gray-400', error: 'bg-red-500', paused: 'bg-yellow-500', resuming: 'bg-blue-400 animate-pulse', }; // ========== Props ========== interface TerminalPaneProps { paneId: PaneId; } // ========== Component ========== export function TerminalPane({ paneId }: TerminalPaneProps) { const { formatMessage } = useIntl(); // Grid store const panes = useTerminalGridStore(selectTerminalGridPanes); const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId); const layout = useTerminalGridStore((s) => s.layout); const splitPane = useTerminalGridStore((s) => s.splitPane); const closePane = useTerminalGridStore((s) => s.closePane); const assignSession = useTerminalGridStore((s) => s.assignSession); const setFocused = useTerminalGridStore((s) => s.setFocused); const setPaneDisplayMode = useTerminalGridStore((s) => s.setPaneDisplayMode); const pane = panes[paneId]; const sessionId = pane?.sessionId ?? null; const displayMode = pane?.displayMode ?? 'terminal'; const filePath = pane?.filePath ?? null; const isFocused = focusedPaneId === paneId; const canClose = getAllPaneIds(layout).length > 1; const isFileMode = displayMode === 'file' && filePath; // Session data const groups = useSessionManagerStore(selectGroups); const terminalMetas = useSessionManagerStore(selectTerminalMetas); const sessions = useCliSessionStore((s) => s.sessions); // Session lifecycle actions const pauseSession = useSessionManagerStore((s) => s.pauseSession); const resumeSession = useSessionManagerStore((s) => s.resumeSession); const restartSession = useSessionManagerStore((s) => s.restartSession); // Action loading states const [isRestarting, setIsRestarting] = useState(false); const [isTogglingPause, setIsTogglingPause] = useState(false); // File content for preview mode const { content: fileContent, isLoading: isFileLoading, error: fileError } = useFileContent(filePath, { enabled: displayMode === 'file' && !!filePath, }); // Association chain for linked issue badge const associationChain = useIssueQueueIntegrationStore(selectAssociationChain); const linkedIssueId = useMemo(() => { if (!sessionId || !associationChain) return null; if (associationChain.sessionId === sessionId) return associationChain.issueId; return null; }, [sessionId, associationChain]); // Terminal metadata const meta = sessionId ? terminalMetas[sessionId] : null; const status: TerminalStatus = meta?.status ?? 'idle'; const alertCount = meta?.alertCount ?? 0; // Build session options for dropdown const sessionOptions = useMemo(() => { const allSessionIds = groups.flatMap((g) => g.sessionIds); return allSessionIds.map((sid) => { const s = sessions[sid]; const name = s ? (s.tool ? `${s.tool} - ${s.shellKind}` : s.shellKind) : sid; return { id: sid, name }; }); }, [groups, sessions]); // Handlers const handleFocus = useCallback(() => { setFocused(paneId); }, [paneId, setFocused]); const handleSplitH = useCallback(() => { splitPane(paneId, 'horizontal'); }, [paneId, splitPane]); const handleSplitV = useCallback(() => { splitPane(paneId, 'vertical'); }, [paneId, splitPane]); const handleClose = useCallback(() => { closePane(paneId); }, [paneId, closePane]); const handleSessionChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; assignSession(paneId, value || null); }, [paneId, assignSession] ); const handleClear = useCallback(() => { // Clear is handled by re-assigning the same session (triggers reset in TerminalInstance) if (sessionId) { assignSession(paneId, null); // Use microtask to re-assign after clearing queueMicrotask(() => assignSession(paneId, sessionId)); } }, [paneId, sessionId, assignSession]); const handleRestart = useCallback(async () => { if (!sessionId || isRestarting) return; setIsRestarting(true); try { await restartSession(sessionId); } catch (error) { console.error('[TerminalPane] Restart failed:', error); } finally { setIsRestarting(false); } }, [sessionId, isRestarting, restartSession]); const handleTogglePause = useCallback(async () => { if (!sessionId || isTogglingPause) return; setIsTogglingPause(true); try { if (status === 'paused') { await resumeSession(sessionId); } else if (status === 'active' || status === 'idle') { await pauseSession(sessionId); } } catch (error) { console.error('[TerminalPane] Toggle pause failed:', error); } finally { setIsTogglingPause(false); } }, [sessionId, isTogglingPause, status, pauseSession, resumeSession]); // Handle back to terminal from file preview const handleBackToTerminal = useCallback(() => { setPaneDisplayMode(paneId, 'terminal'); }, [paneId, setPaneDisplayMode]); return (
{/* PaneToolbar */}
{/* Left: Session selector + status (or file path in file mode) */}
{isFileMode ? ( // File mode header <> {filePath?.split('/').pop() ?? 'File'} ) : ( // Terminal mode header <> {sessionId && ( )}
)}
{/* Center: Linked issue badge */} {linkedIssueId && !isFileMode && ( {linkedIssueId} )} {/* Right: Action buttons */}
{!isFileMode && sessionId && ( <> {/* Restart button */} {/* Pause/Resume toggle button */} {/* Clear terminal button */} )} {alertCount > 0 && !isFileMode && ( {alertCount > 99 ? '99+' : alertCount} )} {canClose && ( )}
{/* Content area */} {isFileMode ? ( // File preview mode
) : sessionId ? ( // Terminal mode with session
) : ( // Empty terminal state

{formatMessage({ id: 'terminalDashboard.pane.selectSession' })}

{formatMessage({ id: 'terminalDashboard.pane.selectSessionHint' })}

)}
); }