diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts index 090e082a8..e67670c2c 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts @@ -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(); diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index b19ebc7d3..171ca97f7 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -21,6 +21,7 @@ import { getMainWindowChromeOptions, getWindowBackgroundColor, resolveSystemWindowTheme, + setupDarwinPaintRefresh, setupWindowResizeEvents, setupDefaultContextMenu, setupDragDropPrevention, @@ -189,6 +190,7 @@ async function createMainWindow(): Promise { app.dock?.setBadge(devWorktreeName); } + setupDarwinPaintRefresh(mainWindow); setupWindowResizeEvents(mainWindow); setupDefaultContextMenu(mainWindow); setupDragDropPrevention(mainWindow); diff --git a/packages/desktop/src/window/window-manager.ts b/packages/desktop/src/window/window-manager.ts index 8b44662eb..35c1e1cb5 100644 --- a/packages/desktop/src/window/window-manager.ts +++ b/packages/desktop/src/window/window-manager.ts @@ -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([