From e6327692699f05d83f80c06103bc871bc61a048c Mon Sep 17 00:00:00 2001 From: Sahil-SS9 <218421507+Sahil-SS9@users.noreply.github.com> Date: Sat, 18 Jul 2026 04:47:38 -0700 Subject: [PATCH] fix(desktop): persist zoom to JSON and save window state on first show (#56726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surgical reapply of the surviving halves of PR #57414 by @Sahil-SS9 (the branch predates the ts-ify migration and the zoom apply/notify funnel, so a direct cherry-pick no longer applies): - Zoom persists to a main-process zoom-state.json as the primary store. The old localStorage-only store lives under Electron's cache/storage folders, which crash recovery can move or recreate — wiping zoom exactly when the user recovers from a crash. localStorage stays as a secondary mirror; pre-JSON installs migrate on first read. - Window geometry persists at ready-to-show, so a crash before the first resize/move/close still captures the restored bounds. The third half of #57414 (one-shot --no-sandbox relaunch on Windows renderer crash loops) was superseded by #66842, which ships the same recovery gated on the 0x80000003 sandbox-crash signature. Adapted to current main: restore/persist route through the applyZoomLevel funnel (39230d173) so the settings UI Scale control stays in sync, and JSON writes go through writeFileAtomic. --- apps/desktop/electron/main.ts | 52 ++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 710f0c460564..b7ef799eea94 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -2077,6 +2077,33 @@ function persistWindowState() { // resized/moved fire many times mid-drag on Linux; debounce to one write. const schedulePersistWindowState = debounce(persistWindowState, 250) +// Zoom's primary store is a main-process JSON file. The renderer localStorage +// mirror lives under Electron's cache/storage folders, which crash recovery +// can move or recreate — wiping the zoom setting exactly when the user just +// recovered from a crash (#56726). JSON survives; localStorage is kept as a +// secondary mirror so pre-JSON installs migrate transparently on first read. +const DESKTOP_ZOOM_STATE_PATH = path.join(app.getPath('userData'), 'zoom-state.json') + +function readZoomState() { + try { + const raw = JSON.parse(fs.readFileSync(DESKTOP_ZOOM_STATE_PATH, 'utf8')) + const level = Number(raw?.zoomLevel) + + return Number.isFinite(level) ? level : null + } catch { + return null + } +} + +function writeZoomState(zoomLevel) { + try { + fs.mkdirSync(path.dirname(DESKTOP_ZOOM_STATE_PATH), { recursive: true }) + writeFileAtomic(DESKTOP_ZOOM_STATE_PATH, JSON.stringify({ zoomLevel }, null, 2)) + } catch (error) { + rememberLog(`[zoom] json persist failed: ${error?.message || error}`) + } +} + // Match the backend's source resolution but bias toward a real git checkout. // Dev → SOURCE_REPO_ROOT. Packaged/CLI install → ACTIVE_HERMES_ROOT. // HERMES_DESKTOP_HERMES_ROOT always wins so devs can pin a worktree. @@ -4884,6 +4911,11 @@ function setAndPersistZoomLevel(window, zoomLevel) { // Apply + notify in one funnel so the settings UI stays in sync, including // changes made via the keyboard shortcuts or the View menu. const next = applyZoomLevel(window.webContents, zoomLevel) + + // Primary store: main-process JSON (survives crash recovery — #56726). + writeZoomState(next) + // Secondary mirror: renderer localStorage (legacy store; kept in sync so a + // downgrade or JSON read failure still finds a sane value). window.webContents .executeJavaScript( `try { localStorage.setItem(${JSON.stringify(ZOOM_STORAGE_KEY)}, ${JSON.stringify(String(next))}) } catch {}` @@ -4896,6 +4928,19 @@ function restorePersistedZoomLevel(window) { return } + // Prefer the JSON file — it survives crash recovery wiping Electron's + // cache/storage folders (#56726). applyZoomLevel notifies the renderer so + // the Appearance UI Scale control stays in sync. + const saved = readZoomState() + + if (saved != null) { + applyZoomLevel(window.webContents, saved) + + return + } + + // Fall back to localStorage for installs that predate zoom-state.json, + // migrating the value into the JSON store on first read. window.webContents .executeJavaScript( `(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()` @@ -4907,7 +4952,8 @@ function restorePersistedZoomLevel(window) { // Notify the renderer too — otherwise the Appearance UI Scale control // can stay stuck at 100% even though the window zoom was restored. - applyZoomLevel(window.webContents, Number(stored)) + const applied = applyZoomLevel(window.webContents, Number(stored)) + writeZoomState(applied) }) .catch(error => rememberLog(`[zoom] restore failed: ${error?.message || error}`)) } @@ -7441,6 +7487,10 @@ function createWindow() { mainWindow.show() } + // Persist geometry as soon as the window is visible so a crash before the + // first clean resize/move/close still captures the restored bounds (#56726). + schedulePersistWindowState() + // #38216: clear the mid-boot marker only after a window is actually usable. // Keep sticky `fallback` when we launched with --no-sandbox so the next // Start Menu click does not re-enter the GPU FATAL crash loop. The marker