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>
This commit is contained in:
OrcaWin 2026-07-28 01:45:59 -07:00 committed by GitHub
parent d548641f1d
commit 380034edf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 6 deletions

View File

@ -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.

View File

@ -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')
})

View File

@ -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', () => {

View File

@ -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

View File

@ -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)
}
)
})

View File

@ -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()
}
}
}