import { useEffect, useRef } from "react"; import { Terminal } from "@xterm/xterm"; import { WebglAddon } from "@xterm/addon-webgl"; import { SearchAddon } from "@xterm/addon-search"; import { FitAddon } from "@xterm/addon-fit"; import { attachSurface, detachSurface, sendInput, resizeSurface } from "./socketBridge"; import { registerSearch, unregisterSearch } from "./searchRegistry"; const decoder = new TextDecoder(); const encoder = new TextEncoder(); export function TerminalView({ surfaceId }: { surfaceId: string }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; // allowProposedApi is required by the search addon: its match decorations // call registerMarker/registerDecoration (proposed API). Without it findNext // throws and the scrollback search counter never updates. const term = new Terminal({ fontFamily: "'JetBrains Mono Variable', 'JetBrains Mono', monospace", fontSize: 13, convertEol: false, scrollback: 10000, allowProposedApi: true }); try { term.loadAddon(new WebglAddon()); } catch { // webgl unavailable → fall back to canvas/dom renderer silently } term.open(ref.current); const search = new SearchAddon(); term.loadAddon(search); registerSearch(surfaceId, search); const fit = new FitAddon(); term.loadAddon(fit); // Fit the grid to the container and tell the daemon the new size. Coalesced // through rAF so a burst of resize callbacks yields one resize per frame. let rafId = 0; let lastCols = 0, lastRows = 0; const doFit = () => { rafId = 0; try { fit.fit(); } catch { return; } if (term.cols !== lastCols || term.rows !== lastRows) { lastCols = term.cols; lastRows = term.rows; void resizeSurface(surfaceId, term.cols, term.rows); } }; const scheduleFit = () => { if (!rafId) rafId = requestAnimationFrame(doFit); }; const ro = new ResizeObserver(scheduleFit); ro.observe(ref.current); // Input → daemon. const inputDisposable = term.onData((data) => { void sendInput(surfaceId, encoder.encode(data)); }); let disposed = false; // Attach: fresh xterm instance, write snapshot, then stream live output. void attachSurface(surfaceId, (bytes) => { if (!disposed) term.write(decoder.decode(bytes)); }).then((res) => { if (disposed) return; if (res.snapshot) term.write(res.snapshot); // Fit to the actual container rather than the daemon's stored geometry, // then push the resulting size back so the PTY reflows to match. scheduleFit(); }); return () => { disposed = true; if (rafId) cancelAnimationFrame(rafId); ro.disconnect(); inputDisposable.dispose(); void detachSurface(surfaceId); unregisterSearch(surfaceId); term.dispose(); }; }, [surfaceId]); return
; }