Desktop zoom level persistence and consistency fixes

NousResearch/hermes-agent · cluster 38854 · apps/desktop

Data as of

Nothing open on GitHub. Fix merged in #66901. Check it covers your case.

The short answer

0open issues
0open PRs
7items grouped
Known?
Yes. 7 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #66901. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #94396. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and pull requests address problems with zoom level not being reliably persisted or applied app-wide on the desktop client, particularly across hash-route navigation and full page loads.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #66901 fix(desktop): persist zoom to JSON and save window state on first show (#56726)

    merged to main

    files: apps/desktop/electron/main.ts

    Show the diff
    • 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
  • #66989 fix(desktop): re-apply persisted zoom on every full load (#46429)

    merged to main

    files: apps/desktop/electron/main.ts, contributors/emails/[redacted:email]

  • #94396 fix(desktop): keep UI scale across in-page route navigation

    merged to main

    files: apps/desktop/e2e/zoom-preservation.spec.ts, apps/desktop/electron/main.ts, apps/desktop/electron/zoom.test.ts, apps/desktop/electron/zoom.ts

2 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1

  • #38908 fix(desktop): persist zoom level across routes · done elsewhere → #66901, #66989
  • #75015 fix(desktop): preserve UI scale across hash-route navigation · merged via another change → #94396, #76627
Closed issues · 2
  • Closed#38854Desktop zoom level is persisted per hash route instead of app-wide
  • Closed#50760[Feature]: Hermes Desktop bad UI experience
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.26: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
apps/desktop
Merged PRs
#66901, #66989, #94396
Closed issues
2
Attempts
2 PRs closed without merging
Fix link checked
25 Aug 2026

For your agent

The same facts, typed.