{"repo":"NousResearch/hermes-agent","public_id":"38854","display_label":"Desktop zoom level persistence and consistency fixes","summary":"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.","subsystem":"apps/desktop","counts":{"members":7,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":3,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":66901},"resolution":{"state":"partially_resolved","by":"pr-94396"},"freshness":{"state":"verified","last_verified_at":"2026-08-25T03:01:48+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.25523809523809526},"fix_edges":[{"issue_number":38854,"pr_number":66901,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":38854,"pr_number":66989,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":38854,"pr_number":94396,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":48658,"pr_number":94396,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":56726,"pr_number":66901,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":79863,"pr_number":94396,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":38854,"title":"Desktop zoom level is persisted per hash route instead of app-wide","url":null,"state":"closed"},{"kind":"issue","number":50760,"title":"[Feature]: Hermes Desktop bad UI experience","url":null,"state":"closed"},{"kind":"pr","number":38908,"title":"fix(desktop): persist zoom level across routes","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#66901","number":66901},{"kind":"pr","value":"#66989","number":66989}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":66901,"title":"fix(desktop): persist zoom to JSON and save window state on first show (#56726)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/main.ts"],"fix_hunks":[{"file":"apps/desktop/electron/main.ts","hunks":["@@ -2077,6 +2077,33 @@ function persistWindowState() {\n // resized/moved fire many times mid-drag on Linux; debounce to one write.\n const schedulePersistWindowState = debounce(persistWindowState, 250)\n \n+// Zoom's primary store is a main-process JSON file. The renderer localStorage\n+// mirror lives under Electron's cache/storage folders, which crash recovery\n+// can move or recreate — wiping the zoom setting exactly when the user just\n+// recovered from a crash (#56726). JSON survives; localStorage is kept as a\n+// secondary mirror so pre-JSON installs migrate transparently on first read.\n+const DESKTOP_ZOOM_STATE_PATH = path.join(app.getPath('userData'), 'zoom-state.json')\n+\n+function readZoomState() {\n+  try {\n+    const raw = JSON.parse(fs.readFileSync(DESKTOP_ZOOM_STATE_PATH, 'utf8'))\n+    const level = Number(raw?.zoomLevel)\n+\n+    return Number.isFinite(level) ? level : null\n+  } catch {\n+    return null\n+  }\n+}\n+\n+function writeZoomState(zoomLevel) {\n+  try {\n+    fs.mkdirSync(path.dirname(DESKTOP_ZOOM_STATE_PATH), { recursive: true })\n+    writeFileAtomic(DESKTOP_ZOOM_STATE_PATH, JSON.stringify({ zoomLevel }, null, 2))\n+  } catch (error) {\n+    rememberLog(`[zoom] json persist failed: ${error?.message || error}`)\n+  }\n+}\n+\n // Match the backend's source resolution but bias toward a real git checkout.\n // Dev → SOURCE_REPO_ROOT. Packaged/CLI install → ACTIVE_HERMES_ROOT.\n // HERMES_DESKTOP_HERMES_ROOT always wins so devs can pin a worktree.","@@ -4884,6 +4911,11 @@ function setAndPersistZoomLevel(window, zoomLevel) {\n   // Apply + notify in one funnel so the settings UI stays in sync, including\n   // changes made via the keyboard shortcuts or the View menu.\n   const next = applyZoomLevel(window.webContents, zoomLevel)\n+\n+  // Primary store: main-process JSON (survives crash recovery — #56726).\n+  writeZoomState(next)\n+  // Secondary mirror: renderer localStorage (legacy store; kept in sync so a\n+  // downgrade or JSON read failure still finds a sane value).\n   window.webContents\n     .executeJavaScript(\n       `try { localStorage.setItem(${JSON.stringify(ZOOM_STORAGE_KEY)}, ${JSON.stringify(String(next))}) } catch {}`","@@ -4896,6 +4928,19 @@ function restorePersistedZoomLevel(window) {\n     return\n   }\n \n+  // Prefer the JSON file — it survives crash recovery wiping Electron's\n+  // cache/storage folders (#56726). applyZoomLevel notifies the renderer so\n+  // the Appearance UI Scale control stays in sync.\n+  const saved = readZoomState()\n+\n+  if (saved != null) {\n+    applyZoomLevel(window.webContents, saved)\n+\n+    return\n+  }\n+\n+  // Fall back to localStorage for installs that predate zoom-state.json,\n+  // migrating the value into the JSON store on first read.\n   window.webContents\n     .executeJavaScript(\n       `(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()`","@@ -4907,7 +4952,8 @@ function restorePersistedZoomLevel(window) {\n \n       // Notify the renderer too — otherwise the Appearance UI Scale control\n       // can stay stuck at 100% even though the window zoom was restored.\n-      applyZoomLevel(window.webContents, Number(stored))\n+      const applied = applyZoomLevel(window.webContents, Number(stored))\n+      writeZoomState(applied)\n     })\n     .catch(error => rememberLog(`[zoom] restore failed: ${error?.message || error}`))\n }","@@ -7441,6 +7487,10 @@ function createWindow() {\n       mainWindow.show()\n     }\n \n+    // Persist geometry as soon as the window is visible so a crash before the\n+    // first clean resize/move/close still captures the restored bounds (#56726).\n+    schedulePersistWindowState()\n+\n     // #38216: clear the mid-boot marker only after a window is actually usable.\n     // Keep sticky `fallback` when we launched with --no-sandbox so the next\n     // Start Menu click does not re-enter the GPU FATAL crash loop. The marker\n"]}]},{"kind":"pr","number":66989,"title":"fix(desktop): re-apply persisted zoom on every full load (#46429)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/main.ts","contributors/emails/[redacted:email]"]},{"kind":"pr","number":75015,"title":"fix(desktop): preserve UI scale across hash-route navigation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#94396","number":94396},{"kind":"pr","value":"#94396","number":94396},{"kind":"pr","value":"#76627","number":76627}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":94396,"title":"fix(desktop): keep UI scale across in-page route navigation","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/e2e/zoom-preservation.spec.ts","apps/desktop/electron/main.ts","apps/desktop/electron/zoom.test.ts","apps/desktop/electron/zoom.ts"]}],"trust":{"default_tier":"verified","includes_suggested":false}}