From f081fd00d7f6404bf80e075849d74cff2e46952f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:54:17 -0700 Subject: [PATCH] fix(window): preserve user resizes during reveal repaint --- src/main/window/createMainWindow.test.ts | 22 +++++++++++++++++++++- src/main/window/createMainWindow.ts | 6 +++++- src/renderer/src/App.tsx | 3 +++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/window/createMainWindow.test.ts b/src/main/window/createMainWindow.test.ts index f01b3fa01fa..12384d1b018 100644 --- a/src/main/window/createMainWindow.test.ts +++ b/src/main/window/createMainWindow.test.ts @@ -375,6 +375,7 @@ describe('createMainWindow', () => { it('runs a full repaint when the renderer relays a genuine window reveal (STA-2383)', () => { vi.useFakeTimers() + const windowHandlers = new Map void)[]>() let windowSize: [number, number] = [1200, 800] const webContents = { on: vi.fn(), @@ -390,7 +391,11 @@ describe('createMainWindow', () => { } const browserWindowInstance = { webContents, - on: vi.fn(), + on: vi.fn((event: string, handler: (...args: any[]) => void) => { + const handlers = windowHandlers.get(event) ?? [] + handlers.push(handler) + windowHandlers.set(event, handlers) + }), isDestroyed: vi.fn(() => false), isMaximized: vi.fn(() => false), isFullScreen: vi.fn(() => false), @@ -426,6 +431,21 @@ describe('createMainWindow', () => { expect(browserWindowInstance.setSize).toHaveBeenNthCalledWith(1, 1201, 800) vi.advanceTimersByTime(32) expect(browserWindowInstance.setSize).toHaveBeenNthCalledWith(2, 1200, 800) + + // Repeated reveal signals while a jiggle is active repaint but do not multiply terminal resizes. + revealHandler?.({ sender: webContents } as never) + revealHandler?.({ sender: webContents } as never) + expect(webContents.invalidate).toHaveBeenCalledTimes(3) + expect(browserWindowInstance.setSize).toHaveBeenNthCalledWith(3, 1201, 800) + expect(browserWindowInstance.setSize).toHaveBeenCalledTimes(3) + + // A user resize that lands during the jiggle must not be rolled back to stale bounds. + windowSize = [1400, 900] + vi.advanceTimersByTime(32) + expect(browserWindowInstance.setSize).toHaveBeenCalledTimes(3) + + windowHandlers.get('closed')?.[0]?.() + expect(ipcMain.removeListener).toHaveBeenCalledWith('ui:window-revealed', revealHandler) }) it('supports all minus key variants for terminal zoom out', () => { diff --git a/src/main/window/createMainWindow.ts b/src/main/window/createMainWindow.ts index e3a224cf12c..307aecf5be7 100644 --- a/src/main/window/createMainWindow.ts +++ b/src/main/window/createMainWindow.ts @@ -66,7 +66,11 @@ function forceRepaint(window: BrowserWindow): void { window.setSize(width + 1, height) setTimeout(() => { if (!window.isDestroyed()) { - window.setSize(width, height) + const [currentWidth, currentHeight] = window.getSize() + // Why: a real user resize during the jiggle owns the final bounds. + if (currentWidth === width + 1 && currentHeight === height) { + window.setSize(width, height) + } } activeRepaintJiggles.delete(window) }, 32) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index eac97b3b640..1a4df267e74 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1364,6 +1364,9 @@ function App(): React.JSX.Element { // is clipped off-screen until a manual resize. Relay the genuine hidden→visible reveal so main // runs the same full repaint (size jiggle) that show/restore/resume get, recomputing the layout. useEffect(() => { + if (!isMac || isPairedWebClientWindow()) { + return + } const handler = (): void => { if (document.visibilityState !== 'visible') { return