diff --git a/bun.lock b/bun.lock index 1236df63f..51cfe3c0a 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@e2b/dashboard", @@ -53,6 +54,9 @@ "@vercel/analytics": "^1.5.0", "@vercel/otel": "^2.1.2", "@vercel/speed-insights": "^1.2.0", + "@xterm/addon-canvas": "^0.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", "cheerio": "^1.0.0", "chrono-node": "^2.8.4", @@ -1095,6 +1099,12 @@ "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + "@xterm/addon-canvas": ["@xterm/addon-canvas@0.7.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw=="], + + "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], + + "@xterm/addon-webgl": ["@xterm/addon-webgl@0.19.0", "", {}, "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A=="], + "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], diff --git a/package.json b/package.json index d32e737e8..a8239698f 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,9 @@ "@vercel/analytics": "^1.5.0", "@vercel/otel": "^2.1.2", "@vercel/speed-insights": "^1.2.0", + "@xterm/addon-canvas": "^0.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", "cheerio": "^1.0.0", "chrono-node": "^2.8.4", diff --git a/src/features/dashboard/terminal/use-terminal-instance.ts b/src/features/dashboard/terminal/use-terminal-instance.ts index 95b70196b..584ce9f85 100644 --- a/src/features/dashboard/terminal/use-terminal-instance.ts +++ b/src/features/dashboard/terminal/use-terminal-instance.ts @@ -1,3 +1,6 @@ +import { CanvasAddon } from '@xterm/addon-canvas' +import { FitAddon } from '@xterm/addon-fit' +import { WebglAddon } from '@xterm/addon-webgl' import '@xterm/xterm/css/xterm.css' import { Terminal as XTerm } from '@xterm/xterm' import { useCallback, useEffect, useRef } from 'react' @@ -27,12 +30,14 @@ export function useTerminalInstance({ onResize, }: UseTerminalInstanceOptions) { const xtermRef = useRef(null) + const fitAddonRef = useRef(null) const terminalContainerRef = useRef(null) const terminalTranscriptRef = useRef(INITIAL_TERMINAL_TEXT) const terminalSizeRef = useRef({ cols: DEFAULT_COLS, rows: DEFAULT_ROWS }) const decoderRef = useRef(new TextDecoder()) const resizeTerminal = useCallback(() => { + fitAddonRef.current?.fit() const nextSize = calculateTerminalSize( terminalContainerRef.current, xtermRef.current @@ -99,26 +104,71 @@ export function useTerminalInstance({ theme: TERMINAL_THEME, }) + const fitAddon = new FitAddon() + let rendererAddon: WebglAddon | CanvasAddon | undefined + let contextLossSubscription: { dispose: () => void } | undefined + xtermRef.current = terminal + fitAddonRef.current = fitAddon + terminal.loadAddon(fitAddon) terminal.open(container) - terminal.write(terminalTranscriptRef.current) + + try { + const webglAddon = new WebglAddon() + const webglContextLossSubscription = webglAddon.onContextLoss(() => { + webglContextLossSubscription.dispose() + webglAddon.dispose() + if (rendererAddon === webglAddon) { + rendererAddon = undefined + } + if (contextLossSubscription === webglContextLossSubscription) { + contextLossSubscription = undefined + } + }) + contextLossSubscription = webglContextLossSubscription + rendererAddon = webglAddon + terminal.loadAddon(webglAddon) + } catch { + contextLossSubscription?.dispose() + contextLossSubscription = undefined + rendererAddon?.dispose() + try { + rendererAddon = new CanvasAddon() + terminal.loadAddon(rendererAddon) + } catch { + rendererAddon?.dispose() + rendererAddon = undefined + } + } + const dataSubscription = terminal.onData(onInput) + terminal.write(terminalTranscriptRef.current, () => { + terminal.scrollToBottom() + }) requestAnimationFrame(() => { resizeTerminal() terminal.focus() + terminal.scrollToBottom() }) const resizeTimer = window.setTimeout(() => { resizeTerminal() + terminal.scrollToBottom() }, 100) return () => { window.clearTimeout(resizeTimer) dataSubscription.dispose() + contextLossSubscription?.dispose() + rendererAddon?.dispose() + fitAddon.dispose() terminal.dispose() if (xtermRef.current === terminal) { xtermRef.current = null } + if (fitAddonRef.current === fitAddon) { + fitAddonRef.current = null + } } }, [onInput, resizeTerminal])