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.
This commit is contained in:
brooklyn! 2026-06-07 22:43:09 -05:00 committed by GitHub
parent d65b513f23
commit b5a457c033
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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))