From b405d2f4572a1dfdccc7ff17de3d38f8c9d5533b Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 30 Jun 2026 10:44:54 +0100 Subject: [PATCH 1/3] feat(window): persist UI zoom level across restarts The View-menu zoom items used Electron's built-in zoomIn/zoomOut/resetZoom roles, which only adjust Chromium's in-memory per-webContents zoom and expose no click hook to persist it. Zoom therefore reset to 100% on every relaunch. Store the zoom level alongside the existing window state, re-apply it on did-finish-load (covering fresh launches and in-app reloads), and persist both menu-driven and wheel/pinch zoom changes. Generated-By: PostHog Code Task-Id: 20eca4a9-7e69-4258-b681-5b60caae5a47 --- apps/code/src/main/menu.ts | 38 ++++++++++++++++++++++++++++--- apps/code/src/main/utils/store.ts | 2 ++ apps/code/src/main/window.ts | 20 ++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/apps/code/src/main/menu.ts b/apps/code/src/main/menu.ts index 29276b4ecd..f83c82047a 100644 --- a/apps/code/src/main/menu.ts +++ b/apps/code/src/main/menu.ts @@ -20,6 +20,18 @@ import { container } from "./di/container"; import { AUTH_SERVICE, UPDATES_SERVICE } from "./di/tokens"; import { isDevBuild } from "./utils/env"; import { getLogFilePath } from "./utils/logger"; +import { saveZoomLevel } from "./window"; + +// Apply a zoom change to the focused window and persist the new level so it +// survives restarts. `delta` adjusts relative to the current level; "reset" +// returns to 100%. +function applyZoom(delta: number | "reset"): void { + const webContents = BrowserWindow.getFocusedWindow()?.webContents; + if (!webContents) return; + const level = delta === "reset" ? 0 : webContents.getZoomLevel() + delta; + webContents.setZoomLevel(level); + saveZoomLevel(level); +} function findLatestCrashDump(): string | null { const pendingDir = path.join(app.getPath("crashDumps"), "pending"); @@ -308,9 +320,29 @@ function buildViewMenu(): MenuItemConstructorOptions { }, { role: "toggleDevTools" }, { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, + { + label: "Actual Size", + accelerator: "CmdOrCtrl+0", + click: () => applyZoom("reset"), + }, + { + label: "Zoom In", + accelerator: "CmdOrCtrl+Plus", + click: () => applyZoom(0.5), + }, + // Hidden duplicate so Cmd+= (i.e. Cmd++ without Shift) also zooms in, + // matching the built-in zoomIn role's dual accelerator. + { + label: "Zoom In", + accelerator: "CmdOrCtrl+=", + visible: false, + click: () => applyZoom(0.5), + }, + { + label: "Zoom Out", + accelerator: "CmdOrCtrl+-", + click: () => applyZoom(-0.5), + }, { type: "separator" }, { role: "togglefullscreen" }, { type: "separator" }, diff --git a/apps/code/src/main/utils/store.ts b/apps/code/src/main/utils/store.ts index 4f511563e5..ae0a895a74 100644 --- a/apps/code/src/main/utils/store.ts +++ b/apps/code/src/main/utils/store.ts @@ -24,6 +24,7 @@ export interface WindowStateSchema { width: number; height: number; isMaximized: boolean; + zoomLevel: number; } const userDataDir = getUserDataDir(); @@ -50,5 +51,6 @@ export const windowStateStore = new Store({ width: 1200, height: 600, isMaximized: true, + zoomLevel: 0, }, }); diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 653945c449..cb31d7f2e5 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -44,6 +44,7 @@ function getSavedWindowState(): WindowStateSchema { width: windowStateStore.get("width", 1200), height: windowStateStore.get("height", 600), isMaximized: windowStateStore.get("isMaximized", true), + zoomLevel: windowStateStore.get("zoomLevel", 0), }; // Validate position is still on a connected display @@ -72,6 +73,10 @@ export function saveWindowState(window: BrowserWindow): void { } } +export function saveZoomLevel(level: number): void { + windowStateStore.set("zoomLevel", level); +} + let mainWindow: BrowserWindow | null = null; export function focusMainWindow(reason: string): void { @@ -233,6 +238,21 @@ export function createWindow(): void { mainWindow.once("ready-to-show", showWindow); const showFallback = setTimeout(showWindow, 3000); + // Restore the saved zoom level once the renderer has loaded. Using + // did-finish-load (rather than ready-to-show) also re-applies it after + // in-app reloads, which reset Chromium's per-webContents zoom. + mainWindow.webContents.on("did-finish-load", () => { + mainWindow?.webContents.setZoomLevel(savedState.zoomLevel); + }); + + // Persist mouse-wheel/pinch zoom. Menu-driven zoom is persisted by the + // menu items themselves (see buildViewMenu in menu.ts). + mainWindow.webContents.on("zoom-changed", () => { + if (mainWindow) { + saveZoomLevel(mainWindow.webContents.getZoomLevel()); + } + }); + // Persist window state on changes mainWindow.on( "resize", From 0ba211e856ac622e0c0162baa609ad39622bfe4b Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 30 Jun 2026 10:52:14 +0100 Subject: [PATCH 2/3] fix(window): apply latest zoom on reload and clamp the level Address qa-swarm review on #3017: - did-finish-load now reads the latest persisted zoomLevel from the store instead of the create-time snapshot, so zooming done during a session survives in-app reloads. - Clamp the zoom level (ZOOM_MIN/ZOOM_MAX) so a runaway accelerator can't persist an unusable zoom across restarts. - Extract the ZOOM_STEP constant in place of the repeated 0.5 literals. Generated-By: PostHog Code Task-Id: 20eca4a9-7e69-4258-b681-5b60caae5a47 --- apps/code/src/main/menu.ts | 16 ++++++++++++---- apps/code/src/main/window.ts | 9 +++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/code/src/main/menu.ts b/apps/code/src/main/menu.ts index f83c82047a..bfb6ec4d1b 100644 --- a/apps/code/src/main/menu.ts +++ b/apps/code/src/main/menu.ts @@ -22,13 +22,21 @@ import { isDevBuild } from "./utils/env"; import { getLogFilePath } from "./utils/logger"; import { saveZoomLevel } from "./window"; +// Zoom is measured in Electron "levels" (factor = 1.2 ** level; 0 = 100%). +// ZOOM_STEP is one Zoom In/Out notch; the bounds clamp the level so a runaway +// accelerator can't persist an unusable zoom across restarts. +const ZOOM_STEP = 0.5; +const ZOOM_MIN = -3; +const ZOOM_MAX = 3; + // Apply a zoom change to the focused window and persist the new level so it // survives restarts. `delta` adjusts relative to the current level; "reset" // returns to 100%. function applyZoom(delta: number | "reset"): void { const webContents = BrowserWindow.getFocusedWindow()?.webContents; if (!webContents) return; - const level = delta === "reset" ? 0 : webContents.getZoomLevel() + delta; + const next = delta === "reset" ? 0 : webContents.getZoomLevel() + delta; + const level = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, next)); webContents.setZoomLevel(level); saveZoomLevel(level); } @@ -328,7 +336,7 @@ function buildViewMenu(): MenuItemConstructorOptions { { label: "Zoom In", accelerator: "CmdOrCtrl+Plus", - click: () => applyZoom(0.5), + click: () => applyZoom(ZOOM_STEP), }, // Hidden duplicate so Cmd+= (i.e. Cmd++ without Shift) also zooms in, // matching the built-in zoomIn role's dual accelerator. @@ -336,12 +344,12 @@ function buildViewMenu(): MenuItemConstructorOptions { label: "Zoom In", accelerator: "CmdOrCtrl+=", visible: false, - click: () => applyZoom(0.5), + click: () => applyZoom(ZOOM_STEP), }, { label: "Zoom Out", accelerator: "CmdOrCtrl+-", - click: () => applyZoom(-0.5), + click: () => applyZoom(-ZOOM_STEP), }, { type: "separator" }, { role: "togglefullscreen" }, diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index cb31d7f2e5..2dd5193d3a 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -238,11 +238,12 @@ export function createWindow(): void { mainWindow.once("ready-to-show", showWindow); const showFallback = setTimeout(showWindow, 3000); - // Restore the saved zoom level once the renderer has loaded. Using - // did-finish-load (rather than ready-to-show) also re-applies it after - // in-app reloads, which reset Chromium's per-webContents zoom. + // Restore the zoom level once the renderer has loaded. Read the latest + // persisted value from the store (not the create-time snapshot) so zooming + // done during the session survives in-app reloads, which otherwise reset + // Chromium's per-webContents zoom. mainWindow.webContents.on("did-finish-load", () => { - mainWindow?.webContents.setZoomLevel(savedState.zoomLevel); + mainWindow?.webContents.setZoomLevel(windowStateStore.get("zoomLevel", 0)); }); // Persist mouse-wheel/pinch zoom. Menu-driven zoom is persisted by the From f94aa6df7a97fd9d6f1d3ca6a3dc33ad4fab3862 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 30 Jun 2026 10:56:07 +0100 Subject: [PATCH 3/3] refactor(window): move saveZoomLevel into store to break import cycle menu.ts importing saveZoomLevel from window.ts created a circular dependency (window.ts already imports buildApplicationMenu from menu.ts). saveZoomLevel only wraps windowStateStore.set, so it belongs beside the store; both menu.ts and window.ts now import it from utils/store. Resolves greptile's circular-dependency note on #3017. Generated-By: PostHog Code Task-Id: 20eca4a9-7e69-4258-b681-5b60caae5a47 --- apps/code/src/main/menu.ts | 2 +- apps/code/src/main/utils/store.ts | 4 ++++ apps/code/src/main/window.ts | 10 +++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/code/src/main/menu.ts b/apps/code/src/main/menu.ts index bfb6ec4d1b..ca79b8dfa8 100644 --- a/apps/code/src/main/menu.ts +++ b/apps/code/src/main/menu.ts @@ -20,7 +20,7 @@ import { container } from "./di/container"; import { AUTH_SERVICE, UPDATES_SERVICE } from "./di/tokens"; import { isDevBuild } from "./utils/env"; import { getLogFilePath } from "./utils/logger"; -import { saveZoomLevel } from "./window"; +import { saveZoomLevel } from "./utils/store"; // Zoom is measured in Electron "levels" (factor = 1.2 ** level; 0 = 100%). // ZOOM_STEP is one Zoom In/Out notch; the bounds clamp the level so a runaway diff --git a/apps/code/src/main/utils/store.ts b/apps/code/src/main/utils/store.ts index ae0a895a74..26a1b51ebc 100644 --- a/apps/code/src/main/utils/store.ts +++ b/apps/code/src/main/utils/store.ts @@ -54,3 +54,7 @@ export const windowStateStore = new Store({ zoomLevel: 0, }, }); + +export function saveZoomLevel(level: number): void { + windowStateStore.set("zoomLevel", level); +} diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 2dd5193d3a..64ffbf1a27 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -19,7 +19,11 @@ import { trpcRouter } from "./trpc/router"; import { collectMemorySnapshot } from "./utils/crash-diagnostics"; import { isDevBuild } from "./utils/env"; import { logger, readChromiumLogTail } from "./utils/logger"; -import { type WindowStateSchema, windowStateStore } from "./utils/store"; +import { + saveZoomLevel, + type WindowStateSchema, + windowStateStore, +} from "./utils/store"; const log = logger.scope("window"); @@ -73,10 +77,6 @@ export function saveWindowState(window: BrowserWindow): void { } } -export function saveZoomLevel(level: number): void { - windowStateStore.set("zoomLevel", level); -} - let mainWindow: BrowserWindow | null = null; export function focusMainWindow(reason: string): void {