import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { X, Terminal as TerminalIcon, Trash2, Copy, Check, Loader2, } from "lucide-react"; interface TerminalLine { id: number; type: "input" | "output" | "error" | "info"; text: string; timestamp: number; } interface TerminalPanelProps { open: boolean; onClose: () => void; } export function TerminalPanel({ open, onClose }: TerminalPanelProps) { const [lines, setLines] = useState([ { id: 0, type: "info", text: "Claw Terminal v1.0 — Type commands below. Use Ctrl+C to cancel.", timestamp: Date.now(), }, ]); const [input, setInput] = useState(""); const [isRunning, setIsRunning] = useState(false); const [cwd, setCwd] = useState("/home/ubuntu"); const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); const [copied, setCopied] = useState(false); const inputRef = useRef(null); const scrollRef = useRef(null); const lineIdRef = useRef(1); // Auto-scroll to bottom useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [lines]); // Focus input when panel opens useEffect(() => { if (open) { setTimeout(() => inputRef.current?.focus(), 100); } }, [open]); const addLine = useCallback( (type: TerminalLine["type"], text: string) => { const id = lineIdRef.current++; setLines((prev) => [...prev, { id, type, text, timestamp: Date.now() }]); }, [] ); const executeCommand = useCallback( async (cmd: string) => { if (!cmd.trim()) return; // Add to history setHistory((prev) => [...prev.filter((h) => h !== cmd), cmd]); setHistoryIndex(-1); // Show the command addLine("input", `${cwd}$ ${cmd}`); // Handle built-in commands if (cmd.trim() === "clear") { setLines([]); return; } if (cmd.trim().startsWith("cd ")) { const target = cmd.trim().slice(3).trim(); // Resolve path let newPath: string; if (target === "~" || target === "") { newPath = "/home/ubuntu"; } else if (target === "..") { newPath = cwd.split("/").slice(0, -1).join("/") || "/"; } else if (target.startsWith("/")) { newPath = target; } else { newPath = `${cwd}/${target}`.replace(/\/+/g, "/"); } setCwd(newPath); addLine("info", `Changed directory to ${newPath}`); return; } setIsRunning(true); try { const response = await fetch("/api/chat/slash", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "/terminal", args: `${cwd} ${cmd}`, }), }); const data = await response.json(); if (data.result) { const outputLines = data.result.split("\n"); for (const line of outputLines) { if (line.startsWith("STDERR:")) { addLine("error", line.replace("STDERR: ", "")); } else { addLine("output", line); } } } if (data.error) { addLine("error", data.error); } } catch (err: any) { addLine("error", `Network error: ${err.message}`); } finally { setIsRunning(false); } }, [cwd, addLine] ); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !isRunning) { executeCommand(input); setInput(""); } else if (e.key === "ArrowUp") { e.preventDefault(); if (history.length > 0) { const newIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1); setHistoryIndex(newIndex); setInput(history[newIndex]); } } else if (e.key === "ArrowDown") { e.preventDefault(); if (historyIndex >= 0) { const newIndex = historyIndex + 1; if (newIndex >= history.length) { setHistoryIndex(-1); setInput(""); } else { setHistoryIndex(newIndex); setInput(history[newIndex]); } } } else if (e.key === "c" && e.ctrlKey) { if (isRunning) { addLine("error", "^C"); setIsRunning(false); } } }; const copyOutput = () => { const text = lines.map((l) => l.text).join("\n"); navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const clearTerminal = () => { setLines([ { id: lineIdRef.current++, type: "info", text: "Terminal cleared.", timestamp: Date.now(), }, ]); }; if (!open) return null; return (
{/* Header */}
Terminal {cwd}
{/* Terminal output */}
inputRef.current?.focus()} > {lines.map((line) => (
{line.text}
))}
{/* Input line */}
{cwd.replace("/home/ubuntu", "~")}$ setInput(e.target.value)} onKeyDown={handleKeyDown} disabled={isRunning} placeholder={isRunning ? "Running..." : "Type a command..."} className="flex-1 bg-transparent text-xs font-mono text-foreground outline-none placeholder:text-muted-foreground" autoFocus /> {isRunning && ( )}
); }