Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => {
const {
xterm,
fitAddon,
renderer,
cleanup: cleanupQuerySuppression,
} = createTerminalInstance(container, {
cwd: workspaceCwd,
Expand Down Expand Up @@ -547,8 +548,58 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => {
},
});

// Fix WebGL texture atlas corruption when app returns from background.
// The WebGL renderer caches glyphs in a texture atlas for performance. When the app
// is backgrounded, the WebGL context can be invalidated, leaving stale/corrupt glyphs
// in the atlas. Clearing the atlas and forcing a full refresh rebuilds glyphs from
// the (correct) terminal buffer, "healing" the display.
//
// We need BOTH visibilitychange AND window.focus handlers because:
// - visibilitychange: Fires when document becomes hidden/visible (minimize, switch apps)
// - window.focus: Fires on window blur/focus which may NOT trigger visibilitychange
// in Electron (e.g., alt-tab where window loses focus but document isn't "hidden")
//
// A debounce prevents double-refresh when both events fire in quick succession.
let lastRefreshTime = 0;
const REFRESH_DEBOUNCE_MS = 100;

const refreshTerminalDisplay = () => {
if (isUnmounted) return;

// Debounce: skip if we just refreshed (e.g., both events fired together)
const now = Date.now();
if (now - lastRefreshTime < REFRESH_DEBOUNCE_MS) return;
lastRefreshTime = now;

// Capture dimensions before fit() to detect if resize occurred while backgrounded
const prevCols = xterm.cols;
const prevRows = xterm.rows;
fitAddon.fit();

// If dimensions changed (e.g., DPI/layout change while backgrounded), sync PTY
if (xterm.cols !== prevCols || xterm.rows !== prevRows) {
resizeRef.current({ paneId, cols: xterm.cols, rows: xterm.rows });
}

renderer.clearTextureAtlasAndRefresh();
};

const handleVisibilityChange = () => {
if (document.hidden) return;
refreshTerminalDisplay();
};

const handleWindowFocus = () => {
refreshTerminalDisplay();
};

document.addEventListener("visibilitychange", handleVisibilityChange);
window.addEventListener("focus", handleWindowFocus);

return () => {
isUnmounted = true;
document.removeEventListener("visibilitychange", handleVisibilityChange);
window.removeEventListener("focus", handleWindowFocus);
inputDisposable.dispose();
keyDisposable.dispose();
titleDisposable.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,40 @@ export function getDefaultTerminalBg(): string {
return getDefaultTerminalTheme().background ?? "#1a1a1a";
}

export interface RendererHandle {
dispose: () => void;
/** Clear WebGL texture atlas and refresh terminal. Call on visibility change to fix rendering corruption. */
clearTextureAtlasAndRefresh: () => void;
}

/**
* Load GPU-accelerated renderer with automatic fallback.
* Tries WebGL first, falls back to Canvas if WebGL fails.
*/
function loadRenderer(xterm: XTerm): { dispose: () => void } {
function loadRenderer(xterm: XTerm): RendererHandle {
let renderer: WebglAddon | CanvasAddon | null = null;
let usingWebGL = false;

try {
const webglAddon = new WebglAddon();

webglAddon.onContextLoss(() => {
webglAddon.dispose();
usingWebGL = false;
try {
renderer = new CanvasAddon();
xterm.loadAddon(renderer);
// Force refresh after context loss recovery
xterm.refresh(0, xterm.rows - 1);
} catch {
// Canvas fallback failed, use default renderer
renderer = null;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

xterm.loadAddon(webglAddon);
renderer = webglAddon;
usingWebGL = true;
} catch {
try {
renderer = new CanvasAddon();
Expand All @@ -90,6 +102,13 @@ function loadRenderer(xterm: XTerm): { dispose: () => void } {

return {
dispose: () => renderer?.dispose(),
clearTextureAtlasAndRefresh: () => {
if (usingWebGL && renderer instanceof WebglAddon) {
renderer.clearTextureAtlas();
}
// Always refresh to ensure display is up-to-date
xterm.refresh(0, xterm.rows - 1);
},
};
}

Expand All @@ -105,6 +124,7 @@ export function createTerminalInstance(
): {
xterm: XTerm;
fitAddon: FitAddon;
renderer: RendererHandle;
cleanup: () => void;
} {
const { cwd, initialTheme, onFileLinkClick } = options;
Expand Down Expand Up @@ -185,6 +205,7 @@ export function createTerminalInstance(
return {
xterm,
fitAddon,
renderer,
cleanup: () => {
cleanupQuerySuppression();
renderer.dispose();
Expand Down
Loading