// ======================================== // TerminalMainArea Component // ======================================== // Main content area inside TerminalPanel. // Renders terminal output (xterm.js) or queue view based on panelView. import { useEffect, useRef, useState, useCallback } from 'react'; import { useIntl } from 'react-intl'; import { X, Terminal as TerminalIcon } from 'lucide-react'; import { Terminal as XTerm } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import { Button } from '@/components/ui/Button'; import { cn } from '@/lib/utils'; import { useTerminalPanelStore } from '@/stores/terminalPanelStore'; import { useCliSessionStore, type CliSessionMeta } from '@/stores/cliSessionStore'; import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import { fetchCliSessionBuffer, sendCliSessionText, resizeCliSession, executeInCliSession, } from '@/lib/api'; // ========== Types ========== interface TerminalMainAreaProps { onClose: () => void; } // ========== Component ========== export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { const { formatMessage } = useIntl(); const panelView = useTerminalPanelStore((s) => s.panelView); const activeTerminalId = useTerminalPanelStore((s) => s.activeTerminalId); const sessions = useCliSessionStore((s) => s.sessions); const outputChunks = useCliSessionStore((s) => s.outputChunks); const setBuffer = useCliSessionStore((s) => s.setBuffer); const clearOutput = useCliSessionStore((s) => s.clearOutput); const projectPath = useWorkflowStore(selectProjectPath); const activeSession: CliSessionMeta | undefined = activeTerminalId ? sessions[activeTerminalId] : undefined; // ========== xterm State ========== const terminalHostRef = useRef(null); const xtermRef = useRef(null); const fitAddonRef = useRef(null); const lastChunkIndexRef = useRef(0); // PTY input batching const pendingInputRef = useRef(''); const flushTimerRef = useRef(null); // Command execution const [prompt, setPrompt] = useState(''); const [isExecuting, setIsExecuting] = useState(false); const flushInput = useCallback(async () => { const sessionKey = activeTerminalId; if (!sessionKey) return; const pending = pendingInputRef.current; pendingInputRef.current = ''; if (!pending) return; try { await sendCliSessionText(sessionKey, { text: pending, appendNewline: false }, projectPath || undefined); } catch { // Ignore transient failures } }, [activeTerminalId, projectPath]); const scheduleFlush = useCallback(() => { if (flushTimerRef.current !== null) return; flushTimerRef.current = window.setTimeout(async () => { flushTimerRef.current = null; await flushInput(); }, 30); }, [flushInput]); // ========== xterm Lifecycle ========== // Init xterm instance useEffect(() => { if (!terminalHostRef.current) return; if (xtermRef.current) return; const term = new XTerm({ convertEol: true, cursorBlink: true, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', fontSize: 12, scrollback: 5000, }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(terminalHostRef.current); fitAddon.fit(); // Forward keystrokes to backend (batched) term.onData((data) => { if (!activeTerminalId) return; pendingInputRef.current += data; scheduleFlush(); }); xtermRef.current = term; fitAddonRef.current = fitAddon; return () => { try { term.dispose(); } finally { xtermRef.current = null; fitAddonRef.current = null; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Attach to selected session: clear terminal and load buffer useEffect(() => { const term = xtermRef.current; const fitAddon = fitAddonRef.current; if (!term || !fitAddon) return; lastChunkIndexRef.current = 0; term.reset(); term.clear(); if (!activeTerminalId) return; clearOutput(activeTerminalId); fetchCliSessionBuffer(activeTerminalId, projectPath || undefined) .then(({ buffer }) => { setBuffer(activeTerminalId, buffer || ''); }) .catch(() => { // ignore }) .finally(() => { fitAddon.fit(); }); }, [activeTerminalId, projectPath, setBuffer, clearOutput]); // Stream new output chunks into xterm useEffect(() => { const term = xtermRef.current; if (!term) return; if (!activeTerminalId) return; const chunks = outputChunks[activeTerminalId] ?? []; const start = lastChunkIndexRef.current; if (start >= chunks.length) return; for (let i = start; i < chunks.length; i++) { term.write(chunks[i].data); } lastChunkIndexRef.current = chunks.length; }, [outputChunks, activeTerminalId]); // Resize observer -> fit + resize backend useEffect(() => { const host = terminalHostRef.current; const term = xtermRef.current; const fitAddon = fitAddonRef.current; if (!host || !term || !fitAddon) return; const resize = () => { fitAddon.fit(); if (activeTerminalId) { void (async () => { try { await resizeCliSession(activeTerminalId, { cols: term.cols, rows: term.rows }, projectPath || undefined); } catch { // ignore } })(); } }; const ro = new ResizeObserver(resize); ro.observe(host); return () => ro.disconnect(); }, [activeTerminalId, projectPath]); // ========== Command Execution ========== const handleExecute = async () => { if (!activeTerminalId || !prompt.trim()) return; setIsExecuting(true); const sessionTool = (activeSession?.tool || 'claude') as 'claude' | 'codex' | 'gemini'; try { await executeInCliSession(activeTerminalId, { tool: sessionTool, prompt: prompt.trim(), mode: 'analysis', category: 'user', }, projectPath || undefined); setPrompt(''); } catch { // Error shown in terminal output } finally { setIsExecuting(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); void handleExecute(); } }; // ========== Render ========== return (
{/* Header */}
{panelView === 'queue' ? formatMessage({ id: 'home.terminalPanel.executionQueue' }) : activeSession ? `${activeSession.tool || 'cli'} - ${activeSession.sessionKey}` : formatMessage({ id: 'home.terminalPanel.title' })} {activeSession?.workingDir && panelView === 'terminal' && ( {activeSession.workingDir} )}
{/* Content */} {panelView === 'queue' ? ( /* Queue View - Placeholder */

{formatMessage({ id: 'home.terminalPanel.executionQueueDesc' })}

{formatMessage({ id: 'home.terminalPanel.executionQueuePhase2' })}

) : activeTerminalId ? ( /* Terminal View */
{/* xterm container */}
{/* Command Input */}