Improve terminal renderer recovery

This commit is contained in:
Mohamed Boudra
2026-04-24 13:05:58 +07:00
parent c92e4e46d2
commit d152b4f055
3 changed files with 88 additions and 23 deletions
@@ -158,6 +158,7 @@ export class TerminalEmulatorRuntime {
convertEol: false,
cursorBlink: true,
cursorStyle: "bar",
customGlyphs: true,
fontFamily: DEFAULT_TERMINAL_FONT_FAMILY,
fontSize: 13,
lineHeight: 1.0,
@@ -173,6 +174,7 @@ export class TerminalEmulatorRuntime {
const fitAddon = new FitAddon();
const unicode11Addon = new Unicode11Addon();
let webglAddon: WebglAddon | null = null;
let imageAddon: ImageAddon | null = null;
terminal.loadAddon(fitAddon);
terminal.loadAddon(unicode11Addon);
terminal.loadAddon(
@@ -195,34 +197,46 @@ export class TerminalEmulatorRuntime {
// Ignore if unicode API isn't available in this build/runtime.
}
// Prefer GPU rendering when available. This tends to reduce visible seams in block characters
// and improves scroll performance, but must gracefully fall back on platforms without WebGL.
const disposeImageAddon = (): void => {
try {
imageAddon?.dispose();
} catch {
// ignore
}
imageAddon = null;
};
const disposeWebglRenderer = (): void => {
if (!webglAddon) {
return;
}
try {
webglAddon.dispose();
} catch {
// ignore
}
webglAddon = null;
disposeImageAddon();
// WebGL and DOM renderers can have different cell dimensions.
this.fitAndEmitResize?.(true);
};
let webglAddonRaf: number | null = requestAnimationFrame(() => {
webglAddonRaf = null;
try {
disposeWebglRenderer();
webglAddon = new WebglAddon();
webglAddon.onContextLoss(() => {
try {
webglAddon?.dispose();
} catch {
// ignore
}
webglAddon = null;
// Force a refresh after context loss to avoid visual corruption.
try {
terminal.refresh(0, terminal.rows - 1);
} catch {
// ignore
}
disposeWebglRenderer();
});
terminal.loadAddon(webglAddon);
imageAddon = new ImageAddon();
terminal.loadAddon(imageAddon);
this.fitAndEmitResize?.(true);
} catch {
webglAddon = null;
disposeWebglRenderer();
}
});
terminal.loadAddon(new ImageAddon());
// Suppress terminal query responses — the server-side headless xterm handles these.
// Without this, xterm.js generates DA/CPR responses via onData that feed back
// to the PTY as visible text.
@@ -451,12 +465,8 @@ export class TerminalEmulatorRuntime {
cancelAnimationFrame(webglAddonRaf);
webglAddonRaf = null;
}
try {
webglAddon?.dispose();
} catch {
// ignore
}
webglAddon = null;
disposeWebglRenderer();
disposeImageAddon();
},
disposeTerminal: () => {
terminal.dispose();
+2
View File
@@ -21,6 +21,7 @@ import {
getMainWindowChromeOptions,
getWindowBackgroundColor,
resolveSystemWindowTheme,
setupDarwinPaintRefresh,
setupWindowResizeEvents,
setupDefaultContextMenu,
setupDragDropPrevention,
@@ -189,6 +190,7 @@ async function createMainWindow(): Promise<void> {
app.dock?.setBadge(devWorktreeName);
}
setupDarwinPaintRefresh(mainWindow);
setupWindowResizeEvents(mainWindow);
setupDefaultContextMenu(mainWindow);
setupDragDropPrevention(mainWindow);
@@ -219,6 +219,59 @@ export function setupWindowResizeEvents(win: BrowserWindow): void {
});
}
function refreshChromiumSurface(win: BrowserWindow): void {
if (win.isDestroyed()) {
return;
}
win.webContents.invalidate();
if (win.isMaximized() || win.isFullScreen()) {
return;
}
const [width, height] = win.getSize();
win.setSize(width + 1, height);
setTimeout(() => {
if (!win.isDestroyed()) {
win.setSize(width, height);
}
}, 32);
}
export function setupDarwinPaintRefresh(win: BrowserWindow): void {
if (process.platform !== "darwin") {
return;
}
win.webContents.setBackgroundThrottling(false);
const requestSurfaceRefresh = () => {
if (!win.isDestroyed()) {
win.webContents.invalidate();
}
};
const handleChildProcessGone = (
_event: Electron.Event,
details: { type?: string; reason?: string },
) => {
if (details.type !== "GPU") {
return;
}
console.warn("[window] GPU process gone:", details.reason);
refreshChromiumSurface(win);
};
win.on("restore", requestSurfaceRefresh);
win.on("show", requestSurfaceRefresh);
app.on("child-process-gone", handleChildProcessGone);
win.once("closed", () => {
win.off("restore", requestSurfaceRefresh);
win.off("show", requestSurfaceRefresh);
app.off("child-process-gone", handleChildProcessGone);
});
}
export function setupDefaultContextMenu(win: BrowserWindow): void {
win.webContents.on("context-menu", (_event, params) => {
const menu = Menu.buildFromTemplate([