From df9d85ceb750b704e2d658a8beb7a9f957c4d50a Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Thu, 9 Jul 2026 08:54:27 +0700 Subject: [PATCH 1/2] fix(desktop): re-apply UI zoom after minimize/restore on Windows Chromium can drop webContents zoom when a BrowserWindow is minimized and restored. Re-assert the persisted level on show/restore for every chat window, not only on the initial did-finish-load. --- apps/desktop/electron/main.ts | 11 +++++++++-- apps/desktop/electron/zoom.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index f569bf2dd458..de948408c9a5 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -4744,7 +4744,13 @@ function installPreviewShortcut(window) { // survives reloads/restarts) rather than a main-process JSON file. The main // process owns setZoomLevel, so we mirror each change into localStorage and // read it back on did-finish-load to re-apply after reloads or crash recovery. -import { clampZoomLevel, percentToZoomLevel, ZOOM_STORAGE_KEY, zoomLevelToPercent } from './zoom' +import { + clampZoomLevel, + installZoomReassertOnWindowEvents, + percentToZoomLevel, + ZOOM_STORAGE_KEY, + zoomLevelToPercent +} from './zoom' function setAndPersistZoomLevel(window, zoomLevel) { if (!window || window.isDestroyed()) { @@ -6512,6 +6518,8 @@ function wireCommonWindowHandlers(win) { installPreviewShortcut(win) installDevToolsShortcut(win) installZoomShortcuts(win) + installZoomReassertOnWindowEvents(win, () => restorePersistedZoomLevel(win)) + win.webContents.once('did-finish-load', () => restorePersistedZoomLevel(win)) installContextMenu(win) win.webContents.setWindowOpenHandler(details => { openExternalUrl(details.url) @@ -6894,7 +6902,6 @@ function createWindow() { } mainWindow.webContents.once('did-finish-load', () => { - restorePersistedZoomLevel(mainWindow) broadcastBootProgress() sendWindowStateChanged() startHermes().catch(error => rememberLog(error.stack || error.message)) diff --git a/apps/desktop/electron/zoom.ts b/apps/desktop/electron/zoom.ts index f3518f583d62..31182ea64d9b 100644 --- a/apps/desktop/electron/zoom.ts +++ b/apps/desktop/electron/zoom.ts @@ -31,3 +31,22 @@ export function percentToZoomLevel(percent) { return clampZoomLevel(Math.log(percent / 100) / Math.log(ZOOM_FACTOR_BASE)) } + +// Chromium on Windows can drop webContents zoom when a BrowserWindow is minimized +// and restored. Re-apply the persisted level on these lifecycle transitions. +export const ZOOM_REASSERT_WINDOW_EVENTS = ['show', 'restore'] + +export function installZoomReassertOnWindowEvents(win, reassert) { + if (!win?.on) { + return + } + + for (const event of ZOOM_REASSERT_WINDOW_EVENTS) { + win.on(event, () => { + if (win.isDestroyed?.()) { + return + } + reassert() + }) + } +} From 437cb08e59a1618b952a69a9d18e98d88d9f64be Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Thu, 9 Jul 2026 08:54:27 +0700 Subject: [PATCH 2/2] test(desktop): cover zoom reassert window lifecycle hooks --- apps/desktop/electron/zoom.test.ts | 46 +++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron/zoom.test.ts b/apps/desktop/electron/zoom.test.ts index 0636c8c52e2a..d2870cdb4f48 100644 --- a/apps/desktop/electron/zoom.test.ts +++ b/apps/desktop/electron/zoom.test.ts @@ -7,7 +7,14 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { clampZoomLevel, percentToZoomLevel, ZOOM_STORAGE_KEY, zoomLevelToPercent } from './zoom' +import { + clampZoomLevel, + installZoomReassertOnWindowEvents, + percentToZoomLevel, + ZOOM_REASSERT_WINDOW_EVENTS, + ZOOM_STORAGE_KEY, + zoomLevelToPercent +} from './zoom' test('storage key stays stable so persisted zoom survives upgrades', () => { assert.equal(ZOOM_STORAGE_KEY, 'hermes:desktop:zoomLevel') @@ -53,3 +60,40 @@ test('extreme percentages clamp to the level bounds', () => { assert.equal(percentToZoomLevel(1), -9) assert.equal(percentToZoomLevel(1_000_000), 9) }) + +test('installZoomReassertOnWindowEvents wires show and restore', () => { + const handlers = new Map() + const win = { + isDestroyed: () => false, + on(event, listener) { + handlers.set(event, listener) + } + } + let calls = 0 + installZoomReassertOnWindowEvents(win, () => { + calls += 1 + }) + + assert.deepEqual([...handlers.keys()], [...ZOOM_REASSERT_WINDOW_EVENTS]) + handlers.get('show')() + handlers.get('restore')() + assert.equal(calls, 2) +}) + +test('installZoomReassertOnWindowEvents skips destroyed windows', () => { + const handlers = new Map() + let destroyed = false + const win = { + isDestroyed: () => destroyed, + on(event, listener) { + handlers.set(event, listener) + } + } + let calls = 0 + installZoomReassertOnWindowEvents(win, () => { + calls += 1 + }) + destroyed = true + handlers.get('show')() + assert.equal(calls, 0) +})