From 1021dc32197e6b758988d9a4d901fb3b3a4c118b Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Wed, 10 Jun 2026 16:11:25 +0300 Subject: [PATCH 1/2] fix(code): persist and restore native window zoom across reloads Native webContents zoom was never persisted, so it reset to 100% on every renderer reload. Since #2003 added crash-recovery auto-reload on recoverable renderer faults, a hiccup during window resize silently reloads and wipes the user's zoom. Persist the zoom level in window-state and re-apply it on each did-finish-load, so it survives crash-recovery reloads, manual reloads, and app restarts. Replace the native zoom menu roles with handlers that persist the level. Generated-By: PostHog Code Task-Id: 548f3e3c-ef01-4627-b67c-2cf7d453b134 --- apps/code/src/main/menu.ts | 38 ++++++++++++++++++++-- apps/code/src/main/utils/store.ts | 3 ++ apps/code/src/main/utils/zoom.ts | 54 +++++++++++++++++++++++++++++++ apps/code/src/main/window.ts | 9 +++++- 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 apps/code/src/main/utils/zoom.ts diff --git a/apps/code/src/main/menu.ts b/apps/code/src/main/menu.ts index be80db42b3..bfcce195e9 100644 --- a/apps/code/src/main/menu.ts +++ b/apps/code/src/main/menu.ts @@ -20,6 +20,7 @@ import { container } from "./di/container"; import { MAIN_TOKENS } from "./di/tokens"; import { isDevBuild } from "./utils/env"; import { getLogFilePath } from "./utils/logger"; +import { adjustZoom, setZoom } from "./utils/zoom"; function findLatestCrashDump(): string | null { const pendingDir = path.join(app.getPath("crashDumps"), "pending"); @@ -308,9 +309,40 @@ function buildViewMenu(): MenuItemConstructorOptions { }, { role: "toggleDevTools" }, { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, + { + label: "Reset Zoom", + accelerator: "CmdOrCtrl+0", + click: () => { + const win = BrowserWindow.getFocusedWindow(); + if (win) setZoom(win, 0); + }, + }, + { + label: "Zoom In", + accelerator: "CmdOrCtrl+Plus", + click: () => { + const win = BrowserWindow.getFocusedWindow(); + if (win) adjustZoom(win, 1); + }, + }, + { + // Alias so Cmd+= (no Shift) also zooms in; accelerator fires while hidden. + label: "Zoom In", + accelerator: "CmdOrCtrl+=", + visible: false, + click: () => { + const win = BrowserWindow.getFocusedWindow(); + if (win) adjustZoom(win, 1); + }, + }, + { + label: "Zoom Out", + accelerator: "CmdOrCtrl+-", + click: () => { + const win = BrowserWindow.getFocusedWindow(); + if (win) adjustZoom(win, -1); + }, + }, { type: "separator" }, { role: "togglefullscreen" }, { type: "separator" }, diff --git a/apps/code/src/main/utils/store.ts b/apps/code/src/main/utils/store.ts index 4f511563e5..0490dbc9b9 100644 --- a/apps/code/src/main/utils/store.ts +++ b/apps/code/src/main/utils/store.ts @@ -24,6 +24,8 @@ export interface WindowStateSchema { width: number; height: number; isMaximized: boolean; + // Native zoom level (0 = 100%), persisted across reloads and restarts. + zoomLevel: number; } const userDataDir = getUserDataDir(); @@ -50,5 +52,6 @@ export const windowStateStore = new Store({ width: 1200, height: 600, isMaximized: true, + zoomLevel: 0, }, }); diff --git a/apps/code/src/main/utils/zoom.ts b/apps/code/src/main/utils/zoom.ts new file mode 100644 index 0000000000..4778d8326d --- /dev/null +++ b/apps/code/src/main/utils/zoom.ts @@ -0,0 +1,54 @@ +import { logger } from "./logger"; +import { windowStateStore } from "./store"; + +const log = logger.scope("zoom"); + +// Structural subset of BrowserWindow, kept local so this util avoids an +// `electron` import. A real BrowserWindow satisfies it. +interface ZoomableWindow { + isDestroyed(): boolean; + webContents: { + getZoomLevel(): number; + setZoomLevel(level: number): void; + }; +} + +// Half a zoom level ≈ 9.5% per press; clamp to ~58%–173%. +const ZOOM_STEP = 0.5; +const ZOOM_MIN = -3; +const ZOOM_MAX = 3; + +function clampZoom(level: number): number { + return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, level)); +} + +export function getSavedZoomLevel(): number { + return clampZoom(windowStateStore.get("zoomLevel", 0)); +} + +// Native zoom resets to 100% on every reload, so call this on each +// `did-finish-load`, not just at startup. +export function applySavedZoom(window: ZoomableWindow): void { + if (window.isDestroyed()) return; + const level = getSavedZoomLevel(); + window.webContents.setZoomLevel(level); +} + +function persistZoom(level: number): void { + windowStateStore.set("zoomLevel", level); +} + +// Set absolute zoom level (0 = 100%) and persist. +export function setZoom(window: ZoomableWindow, level: number): void { + if (window.isDestroyed()) return; + const next = clampZoom(level); + window.webContents.setZoomLevel(next); + persistZoom(next); + log.info("zoom set", { level: next }); +} + +// Adjust zoom by N steps and persist. +export function adjustZoom(window: ZoomableWindow, steps: number): void { + if (window.isDestroyed()) return; + setZoom(window, window.webContents.getZoomLevel() + steps * ZOOM_STEP); +} diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 73d7d3e054..f2b2097c5f 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -17,6 +17,7 @@ import { trpcRouter } from "./trpc/router"; import { isDevBuild } from "./utils/env"; import { logger, readChromiumLogTail } from "./utils/logger"; import { type WindowStateSchema, windowStateStore } from "./utils/store"; +import { applySavedZoom } from "./utils/zoom"; const log = logger.scope("window"); @@ -34,7 +35,7 @@ function isPositionOnScreen(x: number, y: number): boolean { }); } -function getSavedWindowState(): WindowStateSchema { +function getSavedWindowState(): Omit { const state = { x: windowStateStore.get("x"), y: windowStateStore.get("y"), @@ -100,6 +101,11 @@ function setupExternalLinkHandlers(window: BrowserWindow): void { }); } +function setupZoomPersistence(window: BrowserWindow): void { + // Native zoom resets on every load (incl. crash-recovery reload); re-apply it. + window.webContents.on("did-finish-load", () => applySavedZoom(window)); +} + function setupCrashLogging(window: BrowserWindow): void { window.webContents.on("render-process-gone", (_event, details) => { log.error("Renderer process gone", { @@ -248,6 +254,7 @@ export function createWindow(): void { setupExternalLinkHandlers(mainWindow); setupEditableContextMenu(mainWindow); setupCrashLogging(mainWindow); + setupZoomPersistence(mainWindow); buildApplicationMenu(); if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { From f498b0465f5328dabf09ee1fc2d6759b36e40761 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Wed, 10 Jun 2026 16:24:43 +0300 Subject: [PATCH 2/2] fix(code): base adjustZoom on persisted zoom, not live webContents webContents zoom is reset to 0 during a reload, so reading it as the baseline for a zoom step could persist a wrong value if the user zooms mid-reload. Read the baseline from the persisted store instead, making it the single source of truth (matching applySavedZoom). Addresses PR review feedback. Generated-By: PostHog Code Task-Id: 548f3e3c-ef01-4627-b67c-2cf7d453b134 --- apps/code/src/main/utils/zoom.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/code/src/main/utils/zoom.ts b/apps/code/src/main/utils/zoom.ts index 4778d8326d..3f715cebb5 100644 --- a/apps/code/src/main/utils/zoom.ts +++ b/apps/code/src/main/utils/zoom.ts @@ -47,8 +47,9 @@ export function setZoom(window: ZoomableWindow, level: number): void { log.info("zoom set", { level: next }); } -// Adjust zoom by N steps and persist. +// Adjust zoom by N steps and persist. Baseline comes from the persisted store, +// not the live webContents, which is reset to 0 during reloads. export function adjustZoom(window: ZoomableWindow, steps: number): void { if (window.isDestroyed()) return; - setZoom(window, window.webContents.getZoomLevel() + steps * ZOOM_STEP); + setZoom(window, getSavedZoomLevel() + steps * ZOOM_STEP); }