diff --git a/src/main/pane-hosts.ts b/src/main/pane-hosts.ts index 9c5a047..dc56f60 100644 --- a/src/main/pane-hosts.ts +++ b/src/main/pane-hosts.ts @@ -261,7 +261,18 @@ export function initPaneHosts(deps: { getMainWindow: () => BrowserWindow | null // The renderer pushes its registry whenever a pane is registered, opened or // closed, so the tray can list panes it otherwise knows nothing about. // Fire-and-forget: the tray is a view of the renderer's truth. - ipcMain.on(IPC_PANE_SYNC, (_event, panes: unknown) => { + // + // Only the MAIN window's truth, though. Pane windows are same-origin top-level + // frames, so preload.ts's isMainFrame gate gives them the bridge too — meaning a + // pane window (or any allowed pop-up) could send pane:sync and overwrite the + // tray's registry, most simply by pushing an empty list and emptying the menu. + // Only one renderer owns the pane registry; accept it from that one only. + ipcMain.on(IPC_PANE_SYNC, (event, panes: unknown) => { + const main = getMainWindow(); + if (!main || main.isDestroyed() || event.sender !== main.webContents) { + console.warn('[panes] ignoring pane:sync from a webContents that is not the main window'); + return; + } lastSync = Array.isArray(panes) ? panes.filter((p): p is TrayPane => !!p && typeof p.id === 'string' && typeof p.title === 'string') : []; diff --git a/src/main/window-bounds.ts b/src/main/window-bounds.ts index 0c91656..c8b1555 100644 --- a/src/main/window-bounds.ts +++ b/src/main/window-bounds.ts @@ -67,7 +67,18 @@ export function sanitizeWindowBounds( displays: DisplayRect[], sizing: WindowSizing = MAIN_WINDOW_SIZING, ): RestoredWindowBounds { - const defaults: RestoredWindowBounds = { width: sizing.defaultWidth, height: sizing.defaultHeight, maximized: false }; + // The floor applies to the FALLBACK too, not just to saved bounds. + // + // The min clamp below only runs when `saved` parses. So a caller whose defaults + // are smaller than its own minimums would get a window under the floor on + // exactly the paths where nothing is saved — first launch, or a corrupt config — + // and a perfectly sized one everywhere else. That is the worst shape a bug can + // have: invisible in the common case, and only in front of a new user. + const defaults: RestoredWindowBounds = { + width: Math.max(sizing.defaultWidth, sizing.minWidth), + height: Math.max(sizing.defaultHeight, sizing.minHeight), + maximized: false, + }; if (displays.length === 0) return defaults; const b = saved as SavedWindowBounds | undefined; diff --git a/tests/window-bounds.test.js b/tests/window-bounds.test.js index 035b431..88a8886 100644 --- a/tests/window-bounds.test.js +++ b/tests/window-bounds.test.js @@ -95,3 +95,18 @@ test('omitting sizing keeps the main window behaviour exactly as before', () => const out = sanitizeWindowBounds({ x: 10, y: 10, width: 50, height: 20 }, [PRIMARY]); assert.deepEqual(out, { x: 10, y: 10, width: MIN_WIDTH, height: MIN_HEIGHT, maximized: false }); }); + +test('custom sizing: defaults below the configured minimum are clamped up', () => { + // A caller whose defaults undercut its own floor. The min clamp only runs on + // saved bounds, so without clamping the fallback too, the floor would hold + // everywhere EXCEPT first launch and a corrupt config — i.e. only for new users. + const silly = { minWidth: 240, minHeight: 180, defaultWidth: 100, defaultHeight: 50 }; + assert.deepEqual( + sanitizeWindowBounds(undefined, [PRIMARY], silly), + { width: 240, height: 180, maximized: false }, + ); + assert.deepEqual( + sanitizeWindowBounds({ x: 0, y: 0, width: 'bad', height: 'bad' }, [PRIMARY], silly), + { width: 240, height: 180, maximized: false }, + ); +});