diff --git a/client/src/components/run/TerminalView.tsx b/client/src/components/run/TerminalView.tsx index 1f228467..3ca16927 100644 --- a/client/src/components/run/TerminalView.tsx +++ b/client/src/components/run/TerminalView.tsx @@ -468,7 +468,8 @@ export const TerminalView = memo(function TerminalView({ // A terminal mounted on a hidden tab sits in a 0×0 box (`display:none`): // fitting it there shrinks xterm to its 12×6 minimum, and that size would // reach tmux with the attach. It stays unfitted until it is shown. - const hasBox = () => !!containerRef.current?.clientWidth && !!containerRef.current.clientHeight; + const hasBox = () => + !!containerRef.current?.clientWidth && !!containerRef.current.clientHeight; if (hasBox()) fit.fit(); // Take the keyboard as soon as the pane is on screen: the terminal IS the // page here, and having to click it first meant the first thing typed went @@ -761,6 +762,16 @@ export const TerminalView = memo(function TerminalView({ refit(); }; document.fonts?.addEventListener("loadingdone", onFontLoaded); + // The browser may drop or reuse the WebGL glyph atlas's GPU memory without + // a context-loss event (sleep/resume, a long-backgrounded tab, a display or + // zoom change on macOS): xterm keeps slicing the old coordinates and every + // cell shows a clipped or foreign glyph while the buffer stays correct. + // Rebuilding the atlas when the page comes back is cheap and redraws clean. + const onPageShown = () => { + if (document.visibilityState === "visible") term.clearTextureAtlas(); + }; + document.addEventListener("visibilitychange", onPageShown); + window.addEventListener("focus", onPageShown); // Settings → Appearance changed the terminal's font or size: an open // pane takes it now, re-measured and re-fitted like a font landing. const onFontPref = () => { @@ -779,6 +790,8 @@ export const TerminalView = memo(function TerminalView({ searchRef.current = null; resizeObserver.disconnect(); document.fonts?.removeEventListener("loadingdone", onFontLoaded); + document.removeEventListener("visibilitychange", onPageShown); + window.removeEventListener("focus", onPageShown); window.removeEventListener(FONT_SIZE_EVENT, onFontPref); term.textarea?.removeEventListener("focus", reassert); detachIme();