# NousResearch/hermes-agent · Desktop zoom level persistence and consistency fixes

## Open now

- 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.

## TL;DR

- 7 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 3 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #66901. Verify it addresses your case.
- **Recommended:** read PR #66901 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-94396`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Closed / duplicate issues

- #38854: Desktop zoom level is persisted per hash route instead of app-wide
- #50760: \[Feature\]: Hermes Desktop bad UI experience

## Prior & competing PRs

- **#66901: fix(desktop): persist zoom to JSON and save window state on first show (#56726)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/electron/main.ts`
  - fix hunks:
  - `apps/desktop/electron/main.ts`

    ```diff
    @@ -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.
    ```

    ```diff
    @@ -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 {}`
    ```

    ```diff
    @@ -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 } })()`
    ```

    ```diff
    @@ -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}`))
     }
    ```

    ```diff
    @@ -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 · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/electron/main.ts`, `contributors/emails/[redacted:email]`
- **#94396: fix(desktop): keep UI scale across in-page route navigation**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/e2e/zoom-preservation.spec.ts`, `apps/desktop/electron/main.ts`, `apps/desktop/electron/zoom.test.ts`, `apps/desktop/electron/zoom.ts`
- **#38908: fix(desktop): persist zoom level across routes**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#66901`, `#66989`
- **#75015: fix(desktop): preserve UI scale across hash-route navigation**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#94396`, `#94396`, `#76627`

## Fix status

- Merged to main in PR #66901 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-08-25`.
- Subsystem: `apps/desktop`
