From 0cde745f03ad38134217721116d7bfe6270cdf89 Mon Sep 17 00:00:00 2001 From: topkoa Date: Sun, 12 Jul 2026 21:30:38 -0400 Subject: [PATCH] fix(panes): let toggleWindow be the authority, not a stale hasWindow() check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tray menu asked "do we own a window for this pane?" and then acted on the answer. Windows are destroyed asynchronously, so between the question and the act the answer can go stale: hasWindow() says yes, the window is destroyed, toggleWindow() returns false, the handler has already committed to the main-process path and returns — and the click lands on nothing. A tray item that silently does nothing is the worst possible failure here, because the tray IS the recovery path when a pane is out of sight. toggleWindow() already reports whether it did anything. Use that: if it toggled, we're done; if it didn't, we never had that window (or just lost it), and only the renderer can decide what opening the pane means — it might belong in the dock, and its element lives there. hasWindow()/hasPaneWindow() existed only to support the racy check, so they're gone rather than left lying around for someone to reintroduce the race with. Signed-off-by: topkoa --- src/main/main.ts | 3 +-- src/main/pane-hosts.ts | 5 ----- src/main/pane-tray.ts | 16 +++++++++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 72f1291..7dd5adf 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -128,7 +128,7 @@ import { installAppMenu } from './app-menu'; import { sanitizeWindowBounds, MIN_WIDTH, MIN_HEIGHT } from './window-bounds'; import { initPaneHosts, closeAllPanes, adoptPaneWindow, paneIdFromFrameName, - togglePaneWindow, showAllPaneWindows, hideAllPaneWindows, hasPaneWindow, + togglePaneWindow, showAllPaneWindows, hideAllPaneWindows, } from './pane-hosts'; import { initTray, destroyTray } from './pane-tray'; @@ -1199,7 +1199,6 @@ async function startup(): Promise { toggleWindow: togglePaneWindow, showAll: showAllPaneWindows, hideAll: hideAllPaneWindows, - hasWindow: hasPaneWindow, }); // Install our application menu (replaces Electron's default so View → diff --git a/src/main/pane-hosts.ts b/src/main/pane-hosts.ts index c0fc94f..1c667be 100644 --- a/src/main/pane-hosts.ts +++ b/src/main/pane-hosts.ts @@ -269,11 +269,6 @@ export function hideAllPaneWindows(): void { refreshTray(); } -export function hasPaneWindow(paneId: string): boolean { - const win = windows.get(paneId); - return !!win && !win.isDestroyed(); -} - // ── Wiring ────────────────────────────────────────────────────────────────── export function initPaneHosts(deps: { getMainWindow: () => BrowserWindow | null }): void { diff --git a/src/main/pane-tray.ts b/src/main/pane-tray.ts index 5804b22..67c84c9 100644 --- a/src/main/pane-tray.ts +++ b/src/main/pane-tray.ts @@ -36,10 +36,11 @@ export interface TrayPane { // through initTray(). export interface TrayPaneActions { getMainWindow: () => Electron.BrowserWindow | null; + // Returns true if it owned a window for this pane and toggled it; false if it + // did not, in which case only the renderer can decide what opening it means. toggleWindow: (paneId: string) => boolean; showAll: () => void; hideAll: () => void; - hasWindow: (paneId: string) => boolean; } let tray: Tray | null = null; @@ -71,10 +72,15 @@ function buildMenu(): Menu { type: 'checkbox', checked: p.open === true, click: () => { - // If we already own a window for this pane, showing/hiding it is a - // main-process job and instant. If we don't, only the renderer can - // decide what opening it means (it might belong in the dock), so ask. - if (actions?.hasWindow(p.id)) { actions.toggleWindow(p.id); return; } + // Let toggleWindow() be the authority, rather than asking "do we have a + // window?" and then acting on the answer. Windows are destroyed + // asynchronously, so between the question and the act the answer can go + // stale — and the click would land on nothing and silently do nothing. + // + // If it toggled, we're done. If it didn't, we never had that window (or + // just lost it), and only the renderer can decide what opening the pane + // means — it might belong in the dock, and its element lives there. + if (actions?.toggleWindow(p.id)) return; const win = actions?.getMainWindow() ?? null; if (win && !win.isDestroyed()) win.webContents.send(IPC_PANE_EVENT_TOGGLE, { paneId: p.id }); },