import React, { useEffect, useRef, useState } from "react"; import { Terminal } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import { WebLinksAddon } from "xterm-addon-web-links"; import "xterm/css/xterm.css"; interface TerminalProps { instanceId: string; onClose?: () => void; } export const TerminalComponent: React.FC = ({ instanceId, onClose }) => { const terminalRef = useRef(null); const wsRef = useRef(null); const [status, setStatus] = useState<"connecting" | "connected" | "disconnected" | "error">( "connecting", ); const [error, setError] = useState(null); useEffect(() => { if (!terminalRef.current) return; // Initialize terminal const term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: 'Menlo, Monaco, "Courier New", monospace', theme: { background: "#1e1e1e", foreground: "#d4d4d4", cursor: "#d4d4d4", selectionBackground: "#264f78", black: "#000000", red: "#cd3131", green: "#0dbc79", yellow: "#e5e510", blue: "#2472c8", magenta: "#bc3fbc", cyan: "#11a8cd", white: "#e5e5e5", brightBlack: "#666666", brightRed: "#f14c4c", brightGreen: "#23d18b", brightYellow: "#f5f543", brightBlue: "#3b8eea", brightMagenta: "#d670d6", brightCyan: "#29b8db", brightWhite: "#e5e5e5", }, }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.loadAddon(new WebLinksAddon()); term.open(terminalRef.current); fitAddon.fit(); // Build WebSocket URL const apiUrl = import.meta.env.VITE_API_BASE_URL || ""; const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsHost = apiUrl.replace(/^https?:\/\//, "").replace(/\/+$/, ""); const wsUrl = `${wsProtocol}//${wsHost}/ws/tool-instances/${instanceId}/terminal`; // Connect WebSocket const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { setStatus("connected"); setError(null); }; ws.onmessage = (event) => { if (event.data instanceof Blob) { event.data.arrayBuffer().then((buffer) => { const data = new Uint8Array(buffer); term.write(data); }); } else if (typeof event.data === "string") { try { const msg = JSON.parse(event.data); if (msg.type === "status" && msg.status === "connected") { setStatus("connected"); } } catch { term.write(event.data); } } }; ws.onclose = (event) => { setStatus("disconnected"); if (event.code !== 1000) { setError(`Connection closed (code: ${event.code})`); } }; ws.onerror = () => { setStatus("error"); setError("WebSocket error"); }; // Handle terminal input term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } }); // Handle resize const handleResize = () => { fitAddon.fit(); const { cols, rows } = term; if (ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: "resize", cols, rows, }), ); } }; window.addEventListener("resize", handleResize); // Initial resize setTimeout(handleResize, 100); return () => { window.removeEventListener("resize", handleResize); ws.close(); term.dispose(); }; }, [instanceId]); return (
{status}
{onClose && ( )}
{error &&
{error}
}
); };