From 83f0a9675a77b6472eb0f7c16f0e73f234fb65c1 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Fri, 15 May 2026 15:36:10 +0200 Subject: [PATCH 1/3] fix: persist window geometry across restarts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously every launch called mainWindow.maximize(), so the app always opened taking over the whole screen regardless of how the user had sized it last time. The frame is now saved on resize/move (debounced 500ms) into the app's settings table and restored on the next launch. First-time users get a 1280×800 window offset from the corner instead of an immediate maximize. Co-Authored-By: Claude Opus 4.7 --- src/backend-desktop/index.ts | 73 +++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/src/backend-desktop/index.ts b/src/backend-desktop/index.ts index 4ddd7e7..54ec418 100644 --- a/src/backend-desktop/index.ts +++ b/src/backend-desktop/index.ts @@ -88,7 +88,6 @@ const rpc = BrowserView.defineRPC({ const url = await getMainViewUrl() const isMac = process.platform === 'darwin' -const isWindows = process.platform === 'win32' // Set up native application menu (macOS only). // The Edit menu with roles is required for clipboard shortcuts (Cmd+C/V/X/A) @@ -187,29 +186,75 @@ if (isMac) { }) } +// ── Window geometry persistence ────────────────────────── +// Restore the user's last-used window frame so the app feels like a normal +// desktop app instead of relaunching maximized every time. + +const SAVED_FRAME_KEY = 'window.frame' + +interface SavedFrame { + x: number + y: number + width: number + height: number +} + +function loadSavedFrame(): SavedFrame | null { + const raw = appDb.getSetting(SAVED_FRAME_KEY) + if (!raw) return null + try { + const parsed = JSON.parse(raw) as Partial + if ( + typeof parsed.x !== 'number' + || typeof parsed.y !== 'number' + || typeof parsed.width !== 'number' + || typeof parsed.height !== 'number' + || !Number.isFinite(parsed.x) + || !Number.isFinite(parsed.y) + || parsed.width < 480 + || parsed.height < 320 + || parsed.width > 20000 + || parsed.height > 20000 + ) { + return null + } + return parsed as SavedFrame + } catch { + return null + } +} + +const DEFAULT_FRAME: SavedFrame = { x: 100, y: 100, width: 1280, height: 800 } +const initialFrame = loadSavedFrame() ?? DEFAULT_FRAME + const mainWindow = new BrowserWindow({ title: 'Dotaz', titleBarStyle: isMac ? 'hiddenInset' : 'default', transparent: false, url, rpc, - frame: { - width: 1280, - height: 800, - x: 0, - y: 0, - }, + frame: initialFrame, }) -// Maximize on startup so the window appears on the primary monitor at full size -// On Windows, the webview doesn't resize correctly on initial maximize, -// so we delay it briefly to let the window finish initializing. -if (isWindows) { - setTimeout(() => mainWindow.maximize(), 500) -} else { - mainWindow.maximize() +// Persist the frame on resize / move so it survives restart. Debounced so we +// don't write to the settings table on every pixel the user drags. +let frameSaveTimer: ReturnType | undefined +function scheduleFrameSave() { + if (frameSaveTimer) clearTimeout(frameSaveTimer) + frameSaveTimer = setTimeout(() => { + frameSaveTimer = undefined + try { + const frame = mainWindow.getFrame() + appDb.setSetting(SAVED_FRAME_KEY, JSON.stringify(frame)) + } catch { + // Best-effort — losing one save is fine, the next resize will retry. + } + }, 500) } +mainWindow.on('resize', scheduleFrameSave) +mainWindow.on('move', scheduleFrameSave) + // Wire up BE→FE message emitter after window creation emitToFrontend = (channel: string, payload: unknown) => { ;(mainWindow as any).webview.rpc.send[channel](payload) From b64d86366907819eac454bbd2cc377a6c7823269 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Fri, 15 May 2026 15:43:04 +0200 Subject: [PATCH 2/3] fix: keep maximize-on-startup behavior for Windows and Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frame persistence is the right macOS behavior — that's what other Mac apps do — but Windows/Linux users have always had a maximized window on launch, and the Windows variant has a webview timing workaround (the 500ms setTimeout) that we'd lose by removing it. Gate the new frame-restore + persistence path to macOS only and keep the old maximize behavior for the other platforms. Co-Authored-By: Claude Opus 4.7 --- src/backend-desktop/index.ts | 53 ++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/backend-desktop/index.ts b/src/backend-desktop/index.ts index 54ec418..730f027 100644 --- a/src/backend-desktop/index.ts +++ b/src/backend-desktop/index.ts @@ -88,6 +88,7 @@ const rpc = BrowserView.defineRPC({ const url = await getMainViewUrl() const isMac = process.platform === 'darwin' +const isWindows = process.platform === 'win32' // Set up native application menu (macOS only). // The Edit menu with roles is required for clipboard shortcuts (Cmd+C/V/X/A) @@ -186,9 +187,12 @@ if (isMac) { }) } -// ── Window geometry persistence ────────────────────────── -// Restore the user's last-used window frame so the app feels like a normal -// desktop app instead of relaunching maximized every time. +// ── Window geometry persistence (macOS only) ───────────── +// On macOS we restore the user's last frame so the app behaves like a normal +// desktop app. On Windows/Linux we keep the old "maximize on startup" behavior +// — the webview there doesn't always lay out correctly with arbitrary initial +// frames, and the Windows setTimeout works around a separate webview timing +// bug on first maximize. const SAVED_FRAME_KEY = 'window.frame' @@ -225,7 +229,7 @@ function loadSavedFrame(): SavedFrame | null { } const DEFAULT_FRAME: SavedFrame = { x: 100, y: 100, width: 1280, height: 800 } -const initialFrame = loadSavedFrame() ?? DEFAULT_FRAME +const initialFrame = isMac ? (loadSavedFrame() ?? DEFAULT_FRAME) : DEFAULT_FRAME const mainWindow = new BrowserWindow({ title: 'Dotaz', @@ -236,24 +240,31 @@ const mainWindow = new BrowserWindow({ frame: initialFrame, }) -// Persist the frame on resize / move so it survives restart. Debounced so we -// don't write to the settings table on every pixel the user drags. -let frameSaveTimer: ReturnType | undefined -function scheduleFrameSave() { - if (frameSaveTimer) clearTimeout(frameSaveTimer) - frameSaveTimer = setTimeout(() => { - frameSaveTimer = undefined - try { - const frame = mainWindow.getFrame() - appDb.setSetting(SAVED_FRAME_KEY, JSON.stringify(frame)) - } catch { - // Best-effort — losing one save is fine, the next resize will retry. - } - }, 500) -} +if (isMac) { + // Persist the frame on resize / move so it survives restart. Debounced so + // we don't write to the settings table on every pixel the user drags. + let frameSaveTimer: ReturnType | undefined + const scheduleFrameSave = () => { + if (frameSaveTimer) clearTimeout(frameSaveTimer) + frameSaveTimer = setTimeout(() => { + frameSaveTimer = undefined + try { + const frame = mainWindow.getFrame() + appDb.setSetting(SAVED_FRAME_KEY, JSON.stringify(frame)) + } catch { + // Best-effort — losing one save is fine, the next resize will retry. + } + }, 500) + } -mainWindow.on('resize', scheduleFrameSave) -mainWindow.on('move', scheduleFrameSave) + mainWindow.on('resize', scheduleFrameSave) + mainWindow.on('move', scheduleFrameSave) +} else if (isWindows) { + // Windows webview doesn't resize correctly on initial maximize; delay briefly. + setTimeout(() => mainWindow.maximize(), 500) +} else { + mainWindow.maximize() +} // Wire up BE→FE message emitter after window creation emitToFrontend = (channel: string, payload: unknown) => { From c7c979c543e3c5c1bb4e2996344a08ea0ee0797b Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Fri, 15 May 2026 15:49:10 +0200 Subject: [PATCH 3/3] fix: unify window geometry behavior across platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit kept maximize-on-startup for Windows/Linux out of caution, but the only real platform-specific bit was a setTimeout workaround for the Windows webview's timing bug on maximize() — which this code path no longer calls. Removing the platform split keeps the fix consistent everywhere: restore the saved frame or use a sensible default, then persist resize/move events. Co-Authored-By: Claude Opus 4.7 --- src/backend-desktop/index.ts | 53 ++++++++++++++---------------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/src/backend-desktop/index.ts b/src/backend-desktop/index.ts index 730f027..cfe8b3a 100644 --- a/src/backend-desktop/index.ts +++ b/src/backend-desktop/index.ts @@ -88,7 +88,6 @@ const rpc = BrowserView.defineRPC({ const url = await getMainViewUrl() const isMac = process.platform === 'darwin' -const isWindows = process.platform === 'win32' // Set up native application menu (macOS only). // The Edit menu with roles is required for clipboard shortcuts (Cmd+C/V/X/A) @@ -187,12 +186,9 @@ if (isMac) { }) } -// ── Window geometry persistence (macOS only) ───────────── -// On macOS we restore the user's last frame so the app behaves like a normal -// desktop app. On Windows/Linux we keep the old "maximize on startup" behavior -// — the webview there doesn't always lay out correctly with arbitrary initial -// frames, and the Windows setTimeout works around a separate webview timing -// bug on first maximize. +// ── Window geometry persistence ────────────────────────── +// Restore the user's last frame on launch and persist resize/move events so +// the app behaves like a normal desktop app on every platform. const SAVED_FRAME_KEY = 'window.frame' @@ -229,7 +225,7 @@ function loadSavedFrame(): SavedFrame | null { } const DEFAULT_FRAME: SavedFrame = { x: 100, y: 100, width: 1280, height: 800 } -const initialFrame = isMac ? (loadSavedFrame() ?? DEFAULT_FRAME) : DEFAULT_FRAME +const initialFrame = loadSavedFrame() ?? DEFAULT_FRAME const mainWindow = new BrowserWindow({ title: 'Dotaz', @@ -240,32 +236,25 @@ const mainWindow = new BrowserWindow({ frame: initialFrame, }) -if (isMac) { - // Persist the frame on resize / move so it survives restart. Debounced so - // we don't write to the settings table on every pixel the user drags. - let frameSaveTimer: ReturnType | undefined - const scheduleFrameSave = () => { - if (frameSaveTimer) clearTimeout(frameSaveTimer) - frameSaveTimer = setTimeout(() => { - frameSaveTimer = undefined - try { - const frame = mainWindow.getFrame() - appDb.setSetting(SAVED_FRAME_KEY, JSON.stringify(frame)) - } catch { - // Best-effort — losing one save is fine, the next resize will retry. - } - }, 500) - } - - mainWindow.on('resize', scheduleFrameSave) - mainWindow.on('move', scheduleFrameSave) -} else if (isWindows) { - // Windows webview doesn't resize correctly on initial maximize; delay briefly. - setTimeout(() => mainWindow.maximize(), 500) -} else { - mainWindow.maximize() +// Persist the frame on resize / move so it survives restart. Debounced so we +// don't write to the settings table on every pixel the user drags. +let frameSaveTimer: ReturnType | undefined +function scheduleFrameSave() { + if (frameSaveTimer) clearTimeout(frameSaveTimer) + frameSaveTimer = setTimeout(() => { + frameSaveTimer = undefined + try { + const frame = mainWindow.getFrame() + appDb.setSetting(SAVED_FRAME_KEY, JSON.stringify(frame)) + } catch { + // Best-effort — losing one save is fine, the next resize will retry. + } + }, 500) } +mainWindow.on('resize', scheduleFrameSave) +mainWindow.on('move', scheduleFrameSave) + // Wire up BE→FE message emitter after window creation emitToFrontend = (channel: string, payload: unknown) => { ;(mainWindow as any).webview.rpc.send[channel](payload)