import { useState, useEffect, useCallback, useRef } from "react"; import { X, Trash2, Terminal as TerminalIcon } from "lucide-react"; import { useTerminal } from "../hooks/useTerminal"; interface TerminalModalProps { isOpen: boolean; onClose: () => void; initialCommand?: string; } /** * Interactive terminal modal component. * * Provides a fully functional shell terminal where users can execute commands * in the project's working directory. Features include: * - Real-time command output streaming via SSE * - Command history with Up/Down arrow navigation * - Keyboard shortcuts (Ctrl+C to kill, Ctrl+L to clear) * - Persistent session during modal lifetime * - Scrollable output history * * The terminal is independent of task state and always available. */ export function TerminalModal({ isOpen, onClose, initialCommand }: TerminalModalProps) { const { history, isRunning, inputValue, setInputValue, executeCommand, clearHistory, killCurrentCommand, navigateHistoryUp, navigateHistoryDown, resetHistoryNavigation, } = useTerminal(); const inputRef = useRef(null); const outputRef = useRef(null); const [showWelcome, setShowWelcome] = useState(true); // Auto-scroll to bottom when output changes useEffect(() => { if (outputRef.current) { outputRef.current.scrollTop = outputRef.current.scrollHeight; } }, [history]); // Focus input when modal opens useEffect(() => { if (isOpen && inputRef.current) { setTimeout(() => inputRef.current?.focus(), 100); } }, [isOpen]); // Execute initial command if provided useEffect(() => { if (isOpen && initialCommand && showWelcome) { setShowWelcome(false); executeCommand(initialCommand); } }, [isOpen, initialCommand, executeCommand, showWelcome]); // Handle overlay click to close const handleOverlayClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose(); }, [onClose], ); // Handle command submission const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!inputValue.trim() || isRunning) return; setShowWelcome(false); resetHistoryNavigation(); await executeCommand(inputValue.trim()); }, [inputValue, isRunning, executeCommand, resetHistoryNavigation], ); // Handle keyboard shortcuts const handleKeyDown = useCallback( async (e: React.KeyboardEvent) => { // Ctrl+C - Kill running process if (e.key === "c" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (isRunning) { await killCurrentCommand(); } return; } // Ctrl+L - Clear screen if (e.key === "l" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); clearHistory(); setShowWelcome(true); return; } // Up arrow - Navigate history back if (e.key === "ArrowUp") { e.preventDefault(); navigateHistoryUp(); return; } // Down arrow - Navigate history forward if (e.key === "ArrowDown") { e.preventDefault(); navigateHistoryDown(); return; } }, [isRunning, killCurrentCommand, clearHistory, navigateHistoryUp, navigateHistoryDown], ); // Handle escape key to close useEffect(() => { if (!isOpen) return; const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [isOpen, onClose]); // Handle clear button click const handleClear = useCallback(() => { clearHistory(); setShowWelcome(true); }, [clearHistory]); if (!isOpen) return null; return (
{/* Header */}
Interactive Terminal
{/* Output area */}
{showWelcome && history.length === 0 ? (

Interactive Terminal

Execute shell commands in the project directory. Available commands include:

git npm/pnpm/yarn ls/cat node python curl make ps

Navigate history  •  Ctrl+C Kill process  •  Ctrl+L Clear

) : (
{history.map((entry) => (
$ {entry.command}
{entry.output && (
                      {entry.output}
                    
)} {entry.isRunning && (
Running...
)}
))}
)}
{/* Input area */}
$ setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder={isRunning ? "Command running..." : "Type a command..."} disabled={isRunning} autoFocus data-testid="terminal-input" /> {isRunning && ( )}
); }