"use client"; import React, { useEffect, useRef, useState } from "react"; import { Terminal } from "xterm"; import { FitAddon } from "@xterm/addon-fit"; import "xterm/css/xterm.css"; interface InteractiveTerminalProps { repoUrl?: string; } export default function InteractiveTerminal({ repoUrl }: InteractiveTerminalProps) { const terminalRef = useRef(null); const terminalInstance = useRef(null); const fitAddon = useRef(null); const ws = useRef(null); const [isConnected, setIsConnected] = useState(false); const [error, setError] = useState(null); const [reconnectKey, setReconnectKey] = useState(0); useEffect(() => { if (!terminalRef.current) return; // Initialize xterm const term = new Terminal({ cursorBlink: true, theme: { background: "#1e1e1e", foreground: "#d4d4d4", }, fontFamily: 'Menlo, Monaco, "Courier New", monospace', fontSize: 14, }); const fit = new FitAddon(); term.loadAddon(fit); term.open(terminalRef.current); fit.fit(); terminalInstance.current = term; fitAddon.current = fit; // Build the WebSocket URL let backendUrl = "http://localhost:8000"; if (process.env.NEXT_PUBLIC_BACKEND_URL) { backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL.replace(/\/$/, ""); } else if (typeof window !== "undefined") { const port = window.location.port; const hostname = window.location.hostname; if (port === "3000") { backendUrl = `${window.location.protocol}//${hostname}:8000`; } else { backendUrl = `${window.location.protocol}//${window.location.host}`; } } const protocol = backendUrl.startsWith("https") ? "wss:" : "ws:"; const host = backendUrl.replace(/^https?:\/\//, ""); let wsUrl = `${protocol}//${host}/api/terminal/ws`; if (repoUrl) { wsUrl += `?repo_url=${encodeURIComponent(repoUrl)}`; } const socket = new WebSocket(wsUrl); ws.current = socket; socket.onopen = () => { setIsConnected(true); setError(null); // Initial resize if (terminalInstance.current) { const { cols, rows } = terminalInstance.current; socket.send(JSON.stringify({ type: "resize", cols, rows })); } }; socket.onmessage = (event) => { if (typeof event.data === "string") { term.write(event.data); } }; socket.onerror = () => { setError("WebSocket connection error"); setIsConnected(false); }; socket.onclose = () => { setIsConnected(false); setError("Disconnected"); term.write("\r\n\x1b[31m[Process Exited / Connection Closed]\x1b[0m\r\n"); }; // Handle user input term.onData((data) => { if (socket.readyState === WebSocket.OPEN) { socket.send(data); } }); // Handle window resize const handleResize = () => { if (fitAddon.current && terminalInstance.current && socket.readyState === WebSocket.OPEN) { fitAddon.current.fit(); const { cols, rows } = terminalInstance.current; socket.send(JSON.stringify({ type: "resize", cols, rows })); } }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); socket.close(); term.dispose(); }; }, [repoUrl, reconnectKey]); return (
Terminal
{isConnected ? ( Connected ) : error ? ( <> Disconnected ) : ( Connecting... )}
); }