// SshTerminal — xterm.js WebSocket bridge function SshTerminal({ instanceId, commandRef }) { const containerRef = React.useRef(null); const termRef = React.useRef(null); const wsRef = React.useRef(null); const fitRef = React.useRef(null); const [status, setStatus] = React.useState("connecting"); const captureRef = React.useRef({ buffer: [], resolve: null, timeout: null }); React.useEffect(() => { if (!containerRef.current) return; const term = new window.Terminal({ cursorBlink: true, fontFamily: "JetBrains Mono, ui-monospace, monospace", fontSize: 13, theme: { background: "#0b0c0e", foreground: "#eceae4", cursor: "#6fa3d0", }, scrollback: 5000, }); termRef.current = term; const fitAddon = new window.FitAddon.FitAddon(); fitRef.current = fitAddon; term.loadAddon(fitAddon); term.loadAddon(new window.WebLinksAddon.WebLinksAddon()); term.open(containerRef.current); fitAddon.fit(); const proto = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${proto}//${location.host}/ws/ssh/${instanceId}`); wsRef.current = ws; ws.binaryType = "arraybuffer"; if (commandRef) { commandRef.current = { send: (cmd) => { if (ws.readyState === WebSocket.OPEN) ws.send(cmd + "\n"); }, runAndCapture: (cmd) => { return new Promise((resolve) => { captureRef.current.buffer = []; captureRef.current.resolve = resolve; const settle = () => { const cap = captureRef.current; if (cap.resolve) { const result = cap.buffer.join("").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "").trim(); cap.buffer = []; cap.resolve(result); cap.resolve = null; } }; captureRef.current.timeout = setTimeout(settle, 2500); if (ws.readyState === WebSocket.OPEN) ws.send(cmd + "\n"); }); }, }; } ws.onopen = () => setStatus("connected"); ws.onclose = (e) => { const msgs = { 4001: "NOT AUTHENTICATED", 4002: "SSH CONNECTION FAILED", 4003: "NO SSH KEY — GENERATE A KEY IN SETTINGS", 4004: "INSTANCE NOT FOUND", }; setStatus("disconnected"); term.writeln(`\r\n\x1b[31m[SEIS] DISCONNECTED${msgs[e.code] ? ": " + msgs[e.code] : ""}\x1b[0m`); }; ws.onerror = () => setStatus("error"); ws.onmessage = (e) => { let text; if (e.data instanceof ArrayBuffer) { term.write(new Uint8Array(e.data)); text = new TextDecoder().decode(e.data); } else { term.write(e.data); text = e.data; } if (captureRef.current.resolve) { captureRef.current.buffer.push(text); clearTimeout(captureRef.current.timeout); captureRef.current.timeout = setTimeout(() => { const cap = captureRef.current; if (cap.resolve) { const result = cap.buffer.join("").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "").trim(); cap.buffer = []; cap.resolve(result); cap.resolve = null; } }, 2500); } }; term.onData(data => { if (ws.readyState === WebSocket.OPEN) { ws.send(data); // Reset capture timeout on user keystrokes if (captureRef.current.resolve) { clearTimeout(captureRef.current.timeout); captureRef.current.timeout = setTimeout(() => { const cap = captureRef.current; if (cap.resolve) { const result = cap.buffer.join("").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "").trim(); cap.buffer = []; cap.resolve(result); cap.resolve = null; } }, 2500); } } }); const resizeObserver = new ResizeObserver(() => { fitAddon.fit(); if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows })); } }); resizeObserver.observe(containerRef.current); return () => { resizeObserver.disconnect(); ws.close(); term.dispose(); }; }, [instanceId]); const exportTerminal = () => { const term = termRef.current; if (!term) return; const lines = []; const rows = term.buffer.active.baseY + term.buffer.active.length; for (let r = 0; r < rows; r++) { const line = term.buffer.active.getLine(r); if (line) lines.push(line.translateToString()); } const text = lines.join("\n"); const blob = new Blob([text], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `terminal-${instanceId}-${new Date().toISOString().slice(0,10)}.txt`; a.click(); URL.revokeObjectURL(url); }; const statusColor = { connecting: "var(--warn)", connected: "var(--ok)", disconnected: "var(--muted)", error: "var(--danger)" }[status] || "var(--muted)"; return (