From 380034edf9f9c2a9381d6ffad3d91e7ba25fbd4e Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Tue, 28 Jul 2026 01:45:59 -0700 Subject: [PATCH] fix(macos): avoid scene deadlock on app reactivation (#11055) * fix(macos): avoid redundant focus on app activation * test(macos): cover passive app activation --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- src/main/index.ts | 10 ++++- .../serve-desktop-activation-wiring.test.ts | 5 ++- src/main/window/focus-existing-window.test.ts | 4 +- src/main/window/focus-existing-window.ts | 2 +- src/main/window/macos-app-activation.test.ts | 38 +++++++++++++++++++ src/main/window/macos-app-activation.ts | 14 +++++++ 6 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 src/main/window/macos-app-activation.test.ts create mode 100644 src/main/window/macos-app-activation.ts diff --git a/src/main/index.ts b/src/main/index.ts index ceb1bf618..9f7e559cb 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -165,6 +165,7 @@ import { setTrayAttention, type SystemTrayOptions } from './tray/system-tray' +import { createMacAppActivationHandler } from './window/macos-app-activation' import { focusExistingMainWindow } from './window/focus-existing-window' import { notifyMainWindowBecameVisible } from './window/main-window-visibility' import { CodexAccountService } from './codex-accounts/service' @@ -557,6 +558,11 @@ function requestDesktopActivation(): void { desktopActivationGate.requestActivation() } +const handleMacAppActivation = createMacAppActivationHandler({ + getWindow: () => mainWindow, + requestActivation: requestDesktopActivation +}) + function getDesktopWindowStatus(): RuntimeDesktopWindowStatus { const state = desktopActivationGate.getState() return state === 'ready' ? 'openable' : state @@ -1843,7 +1849,7 @@ function shouldSuppressCodexAutoApprovalSyntheticTitleFromHook(args: { ) } -app.whenReady().then(async () => { +void app.whenReady().then(async () => { logStartupMilestone('app-ready') // Why: install certificate decisions before any webview or headless window issues its first TLS request. app.on( @@ -2602,7 +2608,7 @@ app.whenReady().then(async () => { }) startTerminalRuntimeStartupServices() - app.on('activate', requestDesktopActivation) + app.on('activate', handleMacAppActivation) if (serveOptions) { // Why: give managed WSL launchers a brief chance to migrate before headless PTYs go live, without slow repairs withholding all RPC readiness. diff --git a/src/main/startup/serve-desktop-activation-wiring.test.ts b/src/main/startup/serve-desktop-activation-wiring.test.ts index 57e7e682a..d1cb5bfd2 100644 --- a/src/main/startup/serve-desktop-activation-wiring.test.ts +++ b/src/main/startup/serve-desktop-activation-wiring.test.ts @@ -5,10 +5,11 @@ import { describe, expect, it } from 'vitest' describe('serve desktop activation wiring', () => { const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8') - it('routes second-instance and app activation through one safety gate', () => { + it('routes second-instance and windowless app activation through one safety gate', () => { expect(source).toContain('createServeDesktopActivationGate({') expect(source).toContain('acquireSingleInstanceLock(app, requestDesktopActivation)') - expect(source).toContain("app.on('activate', requestDesktopActivation)") + expect(source).toContain('createMacAppActivationHandler({') + expect(source).toContain("app.on('activate', handleMacAppActivation)") expect(source).toContain('getDesktopWindowStatus: getDesktopWindowStatus') }) diff --git a/src/main/window/focus-existing-window.test.ts b/src/main/window/focus-existing-window.test.ts index e12e71ae1..85b422e02 100644 --- a/src/main/window/focus-existing-window.test.ts +++ b/src/main/window/focus-existing-window.test.ts @@ -110,19 +110,21 @@ describe('focusExistingMainWindow', () => { it('restores minimized windows before focusing them', () => { const window = makeFakeWindow({ minimized: true }) + const timer = makeTimer() focusExistingMainWindow({ app: makeFakeApp(), getWindow: () => window, openWindow: vi.fn(), platform: 'darwin', - setTimeout: makeTimer().setTimeout + setTimeout: timer.setTimeout }) expect(window.calls.restore).toHaveBeenCalledTimes(1) expect(window.calls.show).toHaveBeenCalledTimes(1) expect(window.calls.focus).toHaveBeenCalledTimes(1) expect(window.calls.moveTop).not.toHaveBeenCalled() + expect(timer.scheduledMs()).toEqual([]) }) it('waits for normal startup when no window exists before app readiness', () => { diff --git a/src/main/window/focus-existing-window.ts b/src/main/window/focus-existing-window.ts index 3ba740cd2..659f73948 100644 --- a/src/main/window/focus-existing-window.ts +++ b/src/main/window/focus-existing-window.ts @@ -81,8 +81,8 @@ function activateWindow( // Older Electron versions or destroyed windows may reject this; focus retry remains. } pulseAlwaysOnTop(window, setTimer) + retryFocus(window, app, setTimer) } - retryFocus(window, app, setTimer) } // Why: a second-instance/activate reopen can race transient startup pressure diff --git a/src/main/window/macos-app-activation.test.ts b/src/main/window/macos-app-activation.test.ts new file mode 100644 index 000000000..e4670422c --- /dev/null +++ b/src/main/window/macos-app-activation.test.ts @@ -0,0 +1,38 @@ +import type { BrowserWindow } from 'electron' +import { describe, expect, it, vi } from 'vitest' +import { createMacAppActivationHandler } from './macos-app-activation' + +function makeWindow(destroyed = false): BrowserWindow { + return { + isDestroyed: vi.fn(() => destroyed) + } as unknown as BrowserWindow +} + +describe('createMacAppActivationHandler', () => { + it('leaves an existing window to native macOS activation', () => { + const requestActivation = vi.fn() + const handler = createMacAppActivationHandler({ + getWindow: () => makeWindow(), + requestActivation + }) + + handler() + + expect(requestActivation).not.toHaveBeenCalled() + }) + + it.each([null, makeWindow(true)])( + 'requests desktop activation for a missing or destroyed window', + (window) => { + const requestActivation = vi.fn() + const handler = createMacAppActivationHandler({ + getWindow: () => window, + requestActivation + }) + + handler() + + expect(requestActivation).toHaveBeenCalledTimes(1) + } + ) +}) diff --git a/src/main/window/macos-app-activation.ts b/src/main/window/macos-app-activation.ts new file mode 100644 index 000000000..42b309cf8 --- /dev/null +++ b/src/main/window/macos-app-activation.ts @@ -0,0 +1,14 @@ +import type { BrowserWindow } from 'electron' + +export function createMacAppActivationHandler(options: { + getWindow: () => BrowserWindow | null + requestActivation: () => void +}): () => void { + return () => { + const window = options.getWindow() + // Why: re-focusing an existing macOS window can race its scene-backed Space transition. + if (!window || window.isDestroyed()) { + options.requestActivation() + } + } +}