From b5a457c033035e8dcd203745e68be16b50c2390e Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Sun, 7 Jun 2026 22:43:09 -0500 Subject: [PATCH] fix(desktop): persist zoom level via renderer localStorage (#41747) Desktop zoom shortcuts (Cmd/Ctrl +/-/0) and the View menu only called webContents.setZoomLevel(), which mutates the live renderer but persists nothing. On reload, renderer crash/restart, or page recreation the app snapped back to the default zoom, so the shortcuts felt broken for users who need larger text. Persist the selected zoom in the renderer's own localStorage rather than a main-process JSON file. localStorage is per-origin and survives the renderer lifecycle automatically, so there's no atomic-write/userData file machinery to maintain. The main process still owns setZoomLevel: every zoom change is mirrored into localStorage via executeJavaScript, and the value is read back and re-applied on did-finish-load (covering reloads and crash recovery). Clamping to Electron's [-9, 9] range now happens once in setAndPersistZoomLevel instead of at each call site. --- apps/desktop/electron/main.cjs | 49 +++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 35abc987d87..0da63e69c4c 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -3137,7 +3137,7 @@ function buildApplicationMenu() { label: 'Actual Size', accelerator: 'CommandOrControl+0', click: () => { - if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.setZoomLevel(0) + setAndPersistZoomLevel(mainWindow, 0) } }, { @@ -3145,8 +3145,7 @@ function buildApplicationMenu() { accelerator: 'CommandOrControl+Plus', click: () => { if (mainWindow && !mainWindow.isDestroyed()) { - const next = Math.min(mainWindow.webContents.getZoomLevel() + 0.1, 9) - mainWindow.webContents.setZoomLevel(next) + setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() + 0.1) } } }, @@ -3155,8 +3154,7 @@ function buildApplicationMenu() { accelerator: 'CommandOrControl+-', click: () => { if (mainWindow && !mainWindow.isDestroyed()) { - const next = Math.max(mainWindow.webContents.getZoomLevel() - 0.1, -9) - mainWindow.webContents.setZoomLevel(next) + setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() - 0.1) } } }, @@ -3218,6 +3216,38 @@ function installPreviewShortcut(window) { }) } +// Zoom level is persisted in the renderer's own localStorage (per-origin, +// survives reloads/restarts) rather than a main-process JSON file. The main +// process owns setZoomLevel, so we mirror each change into localStorage and +// read it back on did-finish-load to re-apply after reloads or crash recovery. +const ZOOM_STORAGE_KEY = 'hermes:desktop:zoomLevel' + +function clampZoomLevel(value) { + if (!Number.isFinite(value)) return 0 + return Math.min(Math.max(value, -9), 9) +} + +function setAndPersistZoomLevel(window, zoomLevel) { + if (!window || window.isDestroyed()) return + const next = clampZoomLevel(zoomLevel) + window.webContents.setZoomLevel(next) + window.webContents + .executeJavaScript(`try { localStorage.setItem(${JSON.stringify(ZOOM_STORAGE_KEY)}, ${JSON.stringify(String(next))}) } catch {}`) + .catch(error => rememberLog(`[zoom] persist failed: ${error?.message || error}`)) +} + +function restorePersistedZoomLevel(window) { + if (!window || window.isDestroyed()) return + window.webContents + .executeJavaScript(`(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()`) + .then(stored => { + if (stored == null || !window || window.isDestroyed()) return + const level = clampZoomLevel(Number(stored)) + window.webContents.setZoomLevel(level) + }) + .catch(error => rememberLog(`[zoom] restore failed: ${error?.message || error}`)) +} + function installZoomShortcuts(window) { // Override Ctrl/Cmd + +/-/0 with half the default zoom step (0.1 vs 0.2). // The menu items handle this on macOS (where the menu is always present), @@ -3231,15 +3261,13 @@ function installZoomShortcuts(window) { const key = input.key if (key === '0') { event.preventDefault() - window.webContents.setZoomLevel(0) + setAndPersistZoomLevel(window, 0) } else if (key === '=' || key === '+') { event.preventDefault() - const next = Math.min(window.webContents.getZoomLevel() + ZOOM_STEP, 9) - window.webContents.setZoomLevel(next) + setAndPersistZoomLevel(window, window.webContents.getZoomLevel() + ZOOM_STEP) } else if (key === '-') { event.preventDefault() - const next = Math.max(window.webContents.getZoomLevel() - ZOOM_STEP, -9) - window.webContents.setZoomLevel(next) + setAndPersistZoomLevel(window, window.webContents.getZoomLevel() - ZOOM_STEP) } }) } @@ -4730,6 +4758,7 @@ function createWindow() { } mainWindow.webContents.once('did-finish-load', () => { + restorePersistedZoomLevel(mainWindow) broadcastBootProgress() sendWindowStateChanged() startHermes().catch(error => rememberLog(error.stack || error.message))