import type { TerminalCapability } from "@opengeni/sdk"; import { type ReactNode, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react"; import { cn } from "../lib/cn"; import { type TerminalStreamStatus, useTerminalStream } from "../hooks/use-terminal-stream"; import type { UseSandboxTerminalResult } from "../hooks/use-sandbox-terminal"; import { resolveTerminalFont, xtermThemeFromTokens } from "../lib/xterm-theme"; import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness"; import { terminalCanAcquirePty } from "../lib/terminal-capability"; import { attachRenderer, type RendererLoaders, type RendererTier } from "../lib/xterm-renderer"; import { sandboxTerminalPeers } from "../lib/workbench-peers"; import { useOpenGeniText } from "../messages/runtime"; /** * A COMPLETE xterm `ITheme` (subset by intent, but every field xterm colors a * cell with is present so it never falls back to its stock VGA palette). Built * from the og tokens via `xtermThemeFromTokens`. */ export type XtermTheme = { background?: string; foreground?: string; cursor?: string; cursorAccent?: string; selectionBackground?: string; selectionForeground?: string; selectionInactiveBackground?: string; black?: string; red?: string; green?: string; yellow?: string; blue?: string; magenta?: string; cyan?: string; white?: string; brightBlack?: string; brightRed?: string; brightGreen?: string; brightYellow?: string; brightBlue?: string; brightMagenta?: string; brightCyan?: string; brightWhite?: string; }; export type SandboxTerminalProps = { /** From `useSandboxTerminal(...)`. */ result: UseSandboxTerminalResult; /** * The negotiated Terminal capability cell. When it advertises `transport: * "pty-ws"` + a live `url` (a warm box with a viewer attached), the terminal is * driven by a REAL bidirectional PTY over the Modal tunnel (ttyd-over-websocket) * INSTEAD of the broken ptyWrite-over-HTTP path: xterm input → the socket, the * socket's output → xterm, xterm resize → the socket. On a cold box / no url * (`transport: "sse-events"`) it stays on the read-only Channel-A firehose * (`result.chunks`). Omit to force the legacy firehose-only behavior. */ terminalCapability?: TerminalCapability | null | undefined; /** * Full xterm theme. When omitted the terminal self-derives it from the `--og-*` * tokens of its own container (correct even inside a nested `data-og-theme` * subtree). Passing it lets the host drive re-theme on a theme flip. */ theme?: XtermTheme | undefined; fontFamily?: string | undefined; fontSize?: number | undefined; /** * Lease liveness (`cold` | `warming` | `warm` | `draining`). Drives the * boot-in-terminal status lines: once the user has engaged the terminal and * the box is not yet warm, a styled "● waking machine — Ns…" line is rendered * INSIDE xterm (no overlay/spinner) until the live PTY connects. */ liveness?: string | undefined; /** * Force read-only even when the PTY accepts stdin. Default: interactive * whenever a live pty-ws stream is connected OR `result.write !== null` (the box * advertises an interactive PTY); otherwise the read-only agent firehose. */ readOnly?: boolean | undefined; /** Shown on the server / before xterm hydrates (SSR-safe placeholder). */ placeholder?: ReactNode | undefined; /** Render the small status header (pty/shell + running dot + read-only pill). */ showHeader?: boolean | undefined; /** Shell label for the header (e.g. `/bin/bash`). */ shell?: string | undefined; /** * Fired the first time the user engages the terminal surface (focus or click). * The host wires this to warm the box for the REAL pty-ws terminal (the viewer * attach), so a cold box upgrades from the read-only firehose to a live PTY ON * INTERACT — never on mere mount (which would force a box spin-up and regress * the firehose-only default). */ onActivate?: (() => void) | undefined; /** Re-negotiate the terminal capability after an unexpected socket failure. */ onReconnectNeeded?: (() => void) | undefined; className?: string | undefined; }; // The xterm.js handle the effect owns. `any`-free structural shape so we can // drive it without a hard type dep on the lazily-imported lib. type XtermLike = { open: (el: HTMLElement) => void; focus: () => void; write: (data: string) => void; clear: () => void; /** Full reset (screen, scrollback and modes); optional for test doubles. */ reset?: () => void; onData: (cb: (data: string) => void) => { dispose: () => void }; onResize: (cb: (size: { cols: number; rows: number }) => void) => { dispose: () => void }; loadAddon: (addon: unknown) => void; dispose: () => void; cols: number; rows: number; options: { theme?: XtermTheme; disableStdin?: boolean; cursorBlink?: boolean; fontFamily?: string; fontSize?: number; }; }; type FitAddonLike = { fit: () => void }; type TerminalPeerBundle = { Terminal: new (options: Record) => XtermLike; FitAddon: new () => FitAddonLike; WebLinksAddon: new (handler: (event: MouseEvent, uri: string) => void) => unknown; loadWebgl?: | (() => Promise<{ WebglAddon: new () => { dispose: () => void; onContextLoss?: (callback: () => void) => void; }; }>) | undefined; }; export type TerminalSurfaceState = | "loading" | "connecting" | "error" | "waking" | "output-only" | "interactive"; /** * Stable semantic state for hosts and browser acceptance. The xterm helper * textarea exists before the live PTY socket is ready, so its presence alone * must never be treated as proof that keystrokes can reach the sandbox. */ export function terminalSurfaceState(input: { ready: boolean; acceptsInput: boolean; inputPending: boolean; ptyStatus: TerminalStreamStatus; booting: boolean; }): TerminalSurfaceState { if (!input.ready) return "loading"; if (input.acceptsInput) return "interactive"; if (input.inputPending) return "connecting"; if (input.ptyStatus === "connecting") return "connecting"; if (input.ptyStatus === "error") return "error"; if (input.booting) return "waking"; return "output-only"; } export function subscribeTerminalInput( term: Pick | null, sink: ((data: string) => void) | null, enabled: boolean, ): { dispose: () => void } | null { if (!enabled || !term || !sink) return null; return term.onData(sink); } /** Clear the transient wake line and put the first live prompt at cell 1,1. * `Terminal.clear()` removes scrollback but deliberately preserves the cursor, * which lets the first PTY frame append to the old wake message. */ export function clearTerminalBootStatus(term: Pick): void { term.write("\r\x1b[2K\x1b[2J\x1b[H"); } /** Empty the screen and scrollback before the transcript is written again. */ function resetTerminal(term: Pick): void { if (term.reset) { term.reset(); return; } term.clear(); term.write("\x1b[2J\x1b[3J\x1b[H"); } /** Debug seam (dev/evidence only, never a typed public API): a global hook the * screenshot harness reads to probe the settled renderer tier + resolved font * without mounting a real WebGL context in unit tests. */ type TerminalDebugInfo = { renderer: RendererTier | null; fontFamily: string | undefined; fontSize: number | undefined; hasVarInFont: boolean; theme: XtermTheme | undefined; /** The live xterm instance (opaque) — the evidence harness reads its buffer to * prove screen preservation (E5) and drive the burst-responsiveness probe. */ term: unknown; }; declare global { // eslint-disable-next-line no-var var __OG_TERMINAL_DEBUG__: ((info: TerminalDebugInfo) => void) | undefined; // A space/comma list of renderer tiers to force-fail, so the demo can prove the // WebGL→canvas→DOM fallback ladder without a real GPU context loss. // eslint-disable-next-line no-var var __OG_FORCE_RENDERER_FAIL__: string | undefined; } /** Which renderer tiers are force-failed (demo/evidence only). */ function forcedRendererFailures(): Set { const raw = typeof globalThis !== "undefined" ? globalThis.__OG_FORCE_RENDERER_FAIL__ : undefined; const set = new Set(); if (typeof raw === "string") { for (const tier of raw.split(/[\s,]+/)) { if (tier === "webgl" || tier === "canvas") set.add(tier); } } return set; } /** * An xterm.js terminal fed by the Channel-A event projection * (`useSandboxTerminal`) or a live ttyd PTY. xterm + its addons are lazy-imported * inside an effect, so SSR renders the placeholder and the terminal mounts on * hydration. * * Renderer: WebGL (GPU-composited cells) with a canvas → DOM fallback ladder * (`attachRenderer`), so bursty output stays smooth but a lost/blocklisted * context degrades gracefully. Font + theme are resolved to CONCRETE values * from the og tokens BEFORE construction (xterm measures glyphs in a canvas * where `var()` never resolves, and colors every cell from a full ANSI palette). * * The terminal is mounted ONCE and its data source is swapped in place: the * read-only firehose (`result.chunks`) → the live PTY. Stdin is toggled via * `term.options.disableStdin` at runtime (not a remount), so scrollback and the * visible screen survive the projection→PTY handoff. * * Resizes are tracked with a `ResizeObserver` on the container (not just * `window.resize`) so dragging the dock handle refits the grid. */ export function SandboxTerminal({ result, terminalCapability, theme, fontFamily, fontSize, liveness, readOnly, placeholder, showHeader, shell, onActivate, onReconnectNeeded, className, }: SandboxTerminalProps) { const t = useOpenGeniText(); const containerRef = useRef(null); const termRef = useRef(null); const fitRef = useRef(null); const rendererRef = useRef(null); const writtenRef = useRef>(new Set()); // Newest written chunk, and the newest one a Clear removed. const writtenThroughRef = useRef(0); const clearedThroughRef = useRef(0); const wroteFirehoseRef = useRef(false); const bootActiveRef = useRef(false); const [ready, setReady] = useState(false); const [loadError, setLoadError] = useState(null); const peerRevision = useSyncExternalStore( sandboxTerminalPeers.subscribe, sandboxTerminalPeers.revision, sandboxTerminalPeers.revision, ); const [inputReady, setInputReady] = useState(false); const [activated, setActivated] = useState(false); // ── Interactive transport switch ───────────────────────────────────────────── const ptyDescriptor = terminalCapability?.transport === "pty-ws" || terminalCapability?.transport === "relay-pty"; const ptyAttachable = terminalCanAcquirePty(terminalCapability); const { status: ptyStatus, write: ptyWrite, resize: resizePty, connected: ptyConnected, inputReadiness, useLegacyInput, } = useTerminalStream({ // Connect only after xterm exists. This makes the renderer the sole output // owner from the first websocket frame and removes an otherwise unbounded // pre-mount output queue during large command bursts. capability: ptyAttachable && ready ? { transport: terminalCapability!.transport, url: ptyDescriptor ? terminalCapability!.url : null, token: ptyDescriptor ? terminalCapability!.token : null, expiresAt: ptyDescriptor ? terminalCapability!.expiresAt : null, ptyCapable: terminalCapability!.ptyCapable, reason: terminalCapability!.reason, } : null, onOutput: (data) => { termRef.current?.write(data); }, onReconnectNeeded, }); // A pty-ws capability exclusively owns the screen even while connecting or // after a visible connection failure; never silently switch one terminal UI // between two PTYs. Input is enabled during the handshake because the stream // hook buffers it strictly until negotiated readiness. Old servers are // explicitly legacy: their transport-open state is not shell readiness. const ptyMode = ptyDescriptor; // Keep xterm stdin live for an attachable descriptor even before its URL has // arrived. Focus/click triggers the grant; any immediately following input is // bounded in the stream hook instead of disappearing in that HTTP interval. const ptyCapturesInput = ptyAttachable && ptyStatus !== "error"; const interactive = !readOnly && (ptyAttachable ? ptyCapturesInput : result.write !== null); // Public "interactive" means the mounted input subscription can reach a live // PTY now. Before that point xterm still captures into the bounded queue, but // acceptance and embedders see an explicit connecting state. const acceptsInput = inputReady && (!ptyAttachable || ptyConnected); const inputPending = inputReady && ptyAttachable && !ptyConnected && activated; let terminalModeLabel: string | null; if (readOnly) terminalModeLabel = t("sandboxTerminal.readOnly"); else if (!ptyAttachable) terminalModeLabel = acceptsInput ? null : t("sandboxTerminal.outputOnly"); else if (ptyStatus === "error") terminalModeLabel = t("sandboxTerminal.connectionError"); else if (acceptsInput) terminalModeLabel = inputReadiness === "legacy" ? t("sandboxTerminal.legacyInput") : null; else terminalModeLabel = activated ? t("sandboxTerminal.connecting") : t("sandboxTerminal.connectOnInput"); // Boot-in-terminal: after the user engages a not-yet-warm box, show styled // status lines INSIDE xterm instead of an overlay — but only when there is no // firehose transcript to show yet (otherwise the projected output is shown). const warm = sandboxAcceptsLiveIo(liveness); const booting = activated && !ptyMode && !warm && liveness !== null && liveness !== undefined && result.chunks.length === 0; const surfaceState = terminalSurfaceState({ ready, acceptsInput, inputPending, ptyStatus, booting, }); function handleActivate() { // Capture-phase activation may synchronously negotiate a fresh terminal // capability and rerender the shell. Focus xterm before that state change so // the user's first keystroke or paste reaches its already-bounded input // queue instead of landing on the surrounding dock. const term = termRef.current; if (term && !readOnly && (ptyAttachable || result.write !== null)) { term.options.disableStdin = false; term.options.cursorBlink = true; } term?.focus(); if (!activated) setActivated(true); onActivate?.(); } // Mount xterm once (client-only). Never re-mounts on interactive/theme flips — // stdin + theme are runtime options synced by the effects below, so the visible // screen survives the projection→PTY handoff (E5). useEffect(() => { if (typeof window === "undefined") return; const el = containerRef.current; if (!el) return; let disposed = false; // Wait for a real measured size before opening xterm so the first fit is at // the true width (belt-and-braces with the visibility gate below). const waitForSize = (node: HTMLElement) => new Promise((resolve) => { if (node.clientWidth > 0 && node.clientHeight > 0) return resolve(); const ro = new ResizeObserver(() => { if (node.clientWidth > 0 && node.clientHeight > 0) { ro.disconnect(); resolve(); } }); ro.observe(node); setTimeout(() => { ro.disconnect(); resolve(); }, 1000); }); ensureXtermBaseCss(); setLoadError(null); void (async () => { const { Terminal, FitAddon, WebLinksAddon, loadWebgl } = (await sandboxTerminalPeers.load()) as TerminalPeerBundle; if (disposed) return; await waitForSize(el); if (disposed) return; // Resolve font + theme to CONCRETE values BEFORE construction. `var()` in a // font family never resolves in xterm's canvas measurement, so we read the // computed `--og-font-mono` here; the theme is a full ANSI palette. const font = resolveTerminalFont(el, { fontFamily, fontSize }); const initialTheme = theme ?? xtermThemeFromTokens(el); const term = new Terminal({ convertEol: true, disableStdin: !interactive, cursorBlink: interactive, fontFamily: font.fontFamily, fontSize: font.fontSize, lineHeight: font.lineHeight, ...(initialTheme ? { theme: initialTheme } : {}), }) as unknown as XtermLike; const fit = new FitAddon() as unknown as FitAddonLike; term.loadAddon(fit); term.loadAddon( new WebLinksAddon((_e: MouseEvent, uri: string) => window.open(uri, "_blank", "noopener,noreferrer"), ) as unknown, ); term.open(el); termRef.current = term; fitRef.current = fit; // Attach the fastest available renderer. For xterm-6 the supported ladder // is WebGL → DOM (the standalone 2D-canvas addon targets xterm-5 internals, // so it is intentionally NOT in the shipped path). Init failure / context // loss steps down to xterm's built-in DOM renderer; the settled tier is // exposed for evidence. const failSet = forcedRendererFailures(); const loaders: RendererLoaders = { webgl: async (onLoss) => { if (disposed) throw new Error("Terminal was disposed"); if (failSet.has("webgl")) throw new Error("forced webgl failure"); if (!loadWebgl) throw new Error("WebGL terminal renderer is not enabled"); const { WebglAddon } = await loadWebgl(); if (disposed) throw new Error("Terminal was disposed"); const addon = new WebglAddon() as unknown as { dispose: () => void; onContextLoss?: (cb: () => void) => void; }; addon.onContextLoss?.(() => { if (disposed) return; try { addon.dispose(); } catch { // already disposed } onLoss(); }); term.loadAddon(addon); return addon; }, }; await attachRenderer("webgl", loaders, (tier) => { if (disposed) return; rendererRef.current = tier; el.setAttribute("data-og-term-renderer", tier); }); if (disposed) return; // Fit BEFORE reveal so the first visible frame is already correctly sized // (no 80×24 rasterize-then-reflow flash — the container stays hidden until // now via the `ready` visibility gate). const settle = () => { try { fit.fit(); } catch { // ignore fit before layout } }; settle(); requestAnimationFrame(() => { if (disposed) return; settle(); el.setAttribute("data-og-term-ready", "true"); if (typeof globalThis.__OG_TERMINAL_DEBUG__ === "function") { globalThis.__OG_TERMINAL_DEBUG__({ renderer: rendererRef.current, fontFamily: term.options.fontFamily, fontSize: term.options.fontSize, hasVarInFont: String(term.options.fontFamily ?? "").includes("var("), theme: term.options.theme, term, }); } setReady(true); }); })().catch((cause: unknown) => { if (!disposed) setLoadError(cause instanceof Error ? cause : new Error(String(cause))); }); return () => { disposed = true; termRef.current?.dispose(); termRef.current = null; fitRef.current = null; rendererRef.current = null; writtenRef.current = new Set(); writtenThroughRef.current = 0; wroteFirehoseRef.current = false; bootActiveRef.current = false; setReady(false); }; // Mount once per peer registration; runtime options sync without a remount. // eslint-disable-next-line react-hooks/exhaustive-deps }, [peerRevision]); // Sync stdin/cursor at runtime (NOT a remount) when interactivity flips, so the // projection→PTY handoff keeps the single Terminal instance + its scrollback. useLayoutEffect(() => { const term = termRef.current; if (!ready || !term) return; term.options.disableStdin = !interactive; term.options.cursorBlink = interactive; }, [ready, interactive]); // Re-theme live (dark↔light flip) without re-mounting / losing scrollback. Uses // the passed theme, or self-derives from the container's tokens. useEffect(() => { const term = termRef.current; if (!ready || !term) return; const next = theme ?? xtermThemeFromTokens(containerRef.current); if (next) term.options.theme = next; }, [ready, theme]); // Boot-in-terminal status lines. While `booting`, paint a styled "● waking // machine — Ns…" line and tick it. On exit, if those lines were the ONLY // content (no firehose transcript), clear them for the incoming live screen — // never clear a real transcript (E5 preservation). useEffect(() => { const term = termRef.current; if (!ready || !term) return; if (!booting) { if (bootActiveRef.current) { bootActiveRef.current = false; if (!wroteFirehoseRef.current) clearTerminalBootStatus(term); } return; } bootActiveRef.current = true; const start = Date.now(); const paint = () => { const s = Math.max(0, Math.round((Date.now() - start) / 1000)); // Bright-blue dot (accent) + dim label; `\r` + clear-line keeps it on one // line so the counter updates in place. term.write( `\r\x1b[2K\x1b[94m●\x1b[0m \x1b[2m${t("sandboxTerminal.wakingMachineSeconds", { seconds: s })}\x1b[0m`, ); }; term.write("\r\n"); paint(); const id = setInterval(paint, 1000); return () => clearInterval(id); }, [ready, booting, t]); // Write new firehose chunks incrementally — ONLY in firehose mode. In PTY mode // the ttyd socket owns the screen. Records that a real transcript exists so the // boot cleanup never wipes it. History read after live output arrives below // what is on screen: then the transcript is written again, in order. useEffect(() => { const term = termRef.current; if (!ready || !term || ptyMode) return; const shown = result.chunks.filter((chunk) => chunk.seq > clearedThroughRef.current); const fresh = shown.filter((chunk) => !writtenRef.current.has(chunk.id)); if (fresh.length === 0) return; let pending = fresh; if (fresh[0]!.seq < writtenThroughRef.current) { resetTerminal(term); writtenRef.current = new Set(); pending = shown; } for (const chunk of pending) { writtenRef.current.add(chunk.id); writtenThroughRef.current = Math.max(writtenThroughRef.current, chunk.seq); wroteFirehoseRef.current = true; term.write(chunk.text); } }, [ready, result.chunks, ptyMode]); // Wire interactive input when allowed. `ptyWrite` is stable across socket // status changes, so this subscription does not tear down while a grant opens // or rotates and cannot create a first-keystroke gap. useLayoutEffect(() => { const term = termRef.current; if (!ready || !term || !interactive) { setInputReady(false); return; } const sink = ptyAttachable ? ptyWrite : result.write; if (!sink) { setInputReady(false); return; } const sub = subscribeTerminalInput(term, sink, true); setInputReady(sub !== null); return () => sub?.dispose(); }, [ready, interactive, ptyAttachable, ptyWrite, result.write]); // In PTY mode, tell ttyd the window size on the xterm resize event. useEffect(() => { const term = termRef.current; if (!ready || !term || !ptyMode) return; resizePty(term.cols, term.rows); const sub = term.onResize(({ cols, rows }) => resizePty(cols, rows)); return () => sub.dispose(); }, [ready, ptyMode, ptyConnected, resizePty]); // Refit on container resize (dock drag) AND window resize. useEffect(() => { if (typeof window === "undefined") return; const refit = () => { try { fitRef.current?.fit(); } catch { // ignore } }; window.addEventListener("resize", refit); let observer: ResizeObserver | null = null; const el = containerRef.current; if (el && typeof ResizeObserver !== "undefined") { observer = new ResizeObserver(() => refit()); observer.observe(el); } return () => { window.removeEventListener("resize", refit); observer?.disconnect(); }; }, [ready]); return (
{showHeader && (
{t("sandboxTerminal.pty")} {shell ?? t("sandboxTerminal.shell")} {terminalModeLabel && ( {terminalModeLabel} )}
)} {ptyAttachable && !readOnly && !showHeader && inputReadiness === "legacy" && (

{t("sandboxTerminal.legacyInputWaitForShell")}

)} {ptyAttachable && !readOnly && inputReadiness === "waiting" && (
{t("sandboxTerminal.waitingForShellReadinessManual")}
)} {/* `onPointerDownCapture`/`onFocusCapture` fire on the FIRST user engagement with the terminal surface (capture so they win even though xterm's own listeners stop propagation). The host warms the box for pty-ws here. The 12px inset + `--og-color-bg` ground match the dock surface. */}
{!ready && (loadError ? (

{loadError.message}

) : ( (placeholder ?? ) ))} {/* Kept `visibility:hidden` until the first successful fit so the first VISIBLE frame is already correctly sized (no 80×24 flash — E4). */}
); } // xterm.js ships a stylesheet (`@xterm/xterm/css/xterm.css`) that is REQUIRED for // correct layout — without it the `.xterm-helper-textarea` (an off-screen input // xterm uses for IME/keystrokes) is NOT clipped and renders as a visible 1-line // box in the top-left (the "weird input box" bug), and the viewport/screen aren't // positioned. The host app never imports that CSS (no global stylesheet here), so // we inject the structural rules once, idempotently (keyed by id), the first time // any terminal mounts. Scoped under `.xterm` so it can't leak into host styles. const XTERM_STYLE_ID = "opengeni-xterm-base-css"; const XTERM_BASE_CSS = ` .xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none} .xterm.focus,.xterm:focus{outline:none} .xterm .xterm-helpers{position:absolute;top:0;z-index:5} .xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none} .xterm .composition-view{background:var(--og-color-bg);color:var(--og-color-fg);display:none;position:absolute;white-space:nowrap;z-index:1} .xterm .composition-view.active{display:block} .xterm .xterm-viewport{background-color:transparent;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0} .xterm .xterm-screen{position:relative} .xterm .xterm-screen canvas{position:absolute;left:0;top:0} .xterm .xterm-scroll-area{visibility:hidden} .xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal} .xterm.enable-mouse-events{cursor:default} .xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer} .xterm.column-select.focus{cursor:crosshair} .xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none} .xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent} .xterm .xterm-accessibility-tree{user-select:text;white-space:pre} .xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden} .xterm-dim{opacity:1!important} .xterm-underline-1{text-decoration:underline} .xterm-underline-2{text-decoration:double underline} .xterm-underline-3{text-decoration:wavy underline} .xterm-underline-4{text-decoration:dotted underline} .xterm-underline-5{text-decoration:dashed underline} .xterm-overline{text-decoration:overline} .xterm-strikethrough{text-decoration:line-through} .xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute} .xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7} .xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none} .xterm-decoration-top{z-index:2;position:relative} `; function ensureXtermBaseCss(): void { if (typeof document === "undefined") return; if (document.getElementById(XTERM_STYLE_ID)) return; const style = document.createElement("style"); style.id = XTERM_STYLE_ID; style.textContent = XTERM_BASE_CSS; document.head.appendChild(style); } function TerminalPlaceholder() { const t = useOpenGeniText(); return (
{t("sandboxTerminal.loadingTerminal")}
); }