fix(window): keep automated Electron launches out of the foreground (#17347)

This commit is contained in:
Neil
2026-08-29 23:55:00 -07:00
committed by GitHub
parent 2daea491b9
commit 5ea9daba97
16 changed files with 288 additions and 32 deletions
@@ -520,7 +520,11 @@ async function main() {
if (!options.dryRun) {
const mainPath = buildAppIfNeeded(repoRoot, options.skipBuild)
app = await electron.launch({ args: [mainPath], env: launchEnv })
app = await electron.launch({
args: [mainPath],
// Why: a validation run must not pull the window over the developer's work.
env: { ...launchEnv, ORCA_BACKGROUND_LAUNCH: '1' }
})
report.electronPaths = await app.evaluate(({ app: electronApp }) => ({
home: electronApp.getPath('home'),
userData: electronApp.getPath('userData'),
+5
View File
@@ -624,6 +624,11 @@ const isHelpOrVersion = forwardedRaw.some((a) => a === '--help' || a === '-h' ||
if (!isHelpOrVersion && process.env.ORCA_DEV_INSTANCE_LABEL) {
console.error(`[orca-dev] Instance: ${process.env.ORCA_DEV_INSTANCE_LABEL}`)
}
// Why: automation launches this app while someone is working; announce that the
// window will come up without taking the foreground so the mode is visible in logs.
if (!isHelpOrVersion && process.env.ORCA_BACKGROUND_LAUNCH === '1') {
console.error('[orca-dev] Background launch: window shows without stealing focus')
}
let forwardedExtras = []
if (!userPassedPort && !isHelpOrVersion) {
const envPortRaw = process.env.REMOTE_DEBUGGING_PORT
@@ -191,7 +191,10 @@ async function runInsideSession(evidenceDir) {
env: {
...process.env,
ORCA_E2E_FORWARD_APP_LOGS: '1',
ORCA_E2E_NATIVE_IBUS_HANGUL: '1'
ORCA_E2E_NATIVE_IBUS_HANGUL: '1',
// Why: native IBus key injection only reaches a window the window manager
// has focused, so this run opts out of the background-launch policy.
ORCA_E2E_FOREGROUND: '1'
},
stdio: 'inherit'
}
@@ -216,6 +216,7 @@ async function runValidation(mode) {
env: {
...env,
NODE_ENV: 'development',
ORCA_BACKGROUND_LAUNCH: '1',
ORCA_DEV_USER_DATA_PATH: userDataPath,
HOME: isolatedHome,
USERPROFILE: isolatedHome,
+1
View File
@@ -27,6 +27,7 @@
"../src/main/startup/single-instance-lock.ts",
"../src/main/startup/startup-diagnostics.ts",
"../src/main/window/focus-existing-window.ts",
"../src/main/window/foreground-activation-policy.ts",
"../src/main/window/macos-app-activation.ts"
],
"compilerOptions": {
+5 -6
View File
@@ -260,7 +260,8 @@ import {
type SystemTrayOptions
} from './tray/system-tray'
import { createMacAppActivationHandler } from './window/macos-app-activation'
import { focusExistingMainWindow } from './window/focus-existing-window'
import { focusExistingMainWindow, safelyRevealWindow } from './window/focus-existing-window'
import { applyBackgroundActivationPolicy } from './window/foreground-activation-policy'
import { notifyMainWindowBecameVisible } from './window/main-window-visibility'
import { CodexAccountService } from './codex-accounts/service'
import { CodexRuntimeHomeService } from './codex-accounts/runtime-home-service'
@@ -1390,11 +1391,7 @@ async function prepareCodexSessionResumeForLaunch(args: {
// Why: restore the window the close handler may have hidden to tray, or reopen it (dock-reactivation style) if fully torn down.
function showMainWindowFromTray(): void {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.show()
mainWindow.focus()
safelyRevealWindow(mainWindow)
return
}
if (!isQuittingForUpdate()) {
@@ -2362,6 +2359,8 @@ function shouldSuppressCodexAutoApprovalSyntheticTitleFromHook(args: {
void app.whenReady().then(async () => {
logStartupMilestone('app-ready')
// Why: a headless automated run must not claim a macOS Dock tile or the menu bar.
applyBackgroundActivationPolicy({ warn: console.warn })
installMainThreadHangWatchdog({ userDataPath: getCanonicalUserDataPath() })
const hangDetection = consumeHangDetectionMarker(
hangDetectionMarkerPath(getCanonicalUserDataPath())
+7 -4
View File
@@ -10,6 +10,7 @@ import {
onDashboardPopoutOpenChanged
} from '../window/dashboard-popout-window'
import { safelyRevealWindow } from '../window/focus-existing-window'
import { isBackgroundLaunch } from '../window/foreground-activation-policy'
import { getTrustedUIRendererWindow, isTrustedUIRenderer, sendToTrustedUIRenderer } from './ui'
import {
admitDashboardSnapshot,
@@ -142,10 +143,12 @@ export function registerDashboardPopoutHandlers(
}
safelyRevealWindow(mainWindow)
mainWindow.webContents.send('ui:revealDashboardAgent', args)
try {
app.focus({ steal: true })
} catch {
// Best-effort; the per-window focus above may still bring it forward.
if (!isBackgroundLaunch()) {
try {
app.focus({ steal: true })
} catch {
// Best-effort; the per-window focus above may still bring it forward.
}
}
})
+4 -6
View File
@@ -4,6 +4,8 @@ import type {
NotificationDispatchResult,
NotificationSettings
} from '../../shared/notification-settings-types'
import { safelyRevealWindow } from '../window/focus-existing-window'
import { isBackgroundLaunch } from '../window/foreground-activation-policy'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import { parsePaneKey } from '../../shared/stable-pane-id'
import type { buildNotificationOptions } from './notification-options'
@@ -79,14 +81,10 @@ export function deliverNativeNotification(
if (!win || win.isDestroyed()) {
return
}
if (process.platform === 'darwin') {
if (process.platform === 'darwin' && !isBackgroundLaunch()) {
app.focus({ steal: true })
}
if (win.isMinimized()) {
win.restore()
}
win.show()
win.focus()
safelyRevealWindow(win)
win.webContents.send('ui:activateWorktree', {
repoId,
worktreeId: args.worktreeId
+5 -4
View File
@@ -2,6 +2,7 @@ import { app, BrowserWindow, nativeTheme, type WebContents } from 'electron'
import { join } from 'node:path'
import { is } from '@electron-toolkit/utils'
import type { Store } from '../persistence'
import { isBackgroundLaunch, showWindowWithoutStealingFocus } from './foreground-activation-policy'
import { rectHasVisibleAreaOnAnyDisplay } from './window-bounds-validation'
import { sendToTrustedUIRenderer } from '../ipc/ui'
import { installPrivilegedWindowNavigationPolicy } from './privileged-window-navigation'
@@ -145,7 +146,9 @@ export function createOrFocusDashboardPopout(
if (dashboardPopoutWindow.isMinimized()) {
dashboardPopoutWindow.restore()
}
dashboardPopoutWindow.focus()
if (!isBackgroundLaunch()) {
dashboardPopoutWindow.focus()
}
if (view) {
dashboardPopoutWindow.webContents.send('dashboard:viewRequested', view)
}
@@ -242,9 +245,7 @@ export function createOrFocusDashboardPopout(
})
window.once('ready-to-show', () => {
if (!window.isDestroyed()) {
window.show()
}
showWindowWithoutStealingFocus(window)
})
// Bounds persistence — mirrors the main window's debounced/frozen approach so
+13 -3
View File
@@ -1,4 +1,5 @@
import type { App, BrowserWindow } from 'electron'
import { isBackgroundLaunch, showWindowWithoutStealingFocus } from './foreground-activation-policy'
type FocusTimer = (callback: () => void, ms: number) => unknown
@@ -14,6 +15,11 @@ export type FocusExistingMainWindowOptions = {
}
function safelyFocusApp(app: Pick<App, 'focus'>): void {
// Why: stealing the foreground is the whole point of this path for a real
// second-instance launch, and exactly what an automated run must never do.
if (isBackgroundLaunch()) {
return
}
try {
app.focus({ steal: true })
} catch {
@@ -32,8 +38,10 @@ export function safelyRevealWindow(window: BrowserWindow): void {
if (window.isMinimized()) {
window.restore()
}
window.show()
window.focus()
showWindowWithoutStealingFocus(window)
if (!isBackgroundLaunch()) {
window.focus()
}
}
function pulseAlwaysOnTop(window: BrowserWindow, setTimer: FocusTimer): void {
@@ -74,7 +82,9 @@ function activateWindow(
): void {
safelyFocusApp(app)
safelyRevealWindow(window)
if (platform === 'win32') {
// Why: moveTop/always-on-top/refocus are foreground reinforcement; in a
// background launch they would drag the window over the developer's work.
if (platform === 'win32' && !isBackgroundLaunch()) {
try {
window.moveTop()
} catch {
@@ -0,0 +1,124 @@
import type { BrowserWindow } from 'electron'
import { describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({ app: { dock: { hide: vi.fn() }, setActivationPolicy: vi.fn() } }))
const {
applyBackgroundActivationPolicy,
isBackgroundLaunch,
isWindowlessLaunch,
showWindowWithoutStealingFocus
} = await import('./foreground-activation-policy')
function makeWindow(destroyed = false): BrowserWindow & {
show: ReturnType<typeof vi.fn>
showInactive: ReturnType<typeof vi.fn>
} {
return {
isDestroyed: () => destroyed,
show: vi.fn(() => {}),
showInactive: vi.fn(() => {})
} as unknown as BrowserWindow & {
show: ReturnType<typeof vi.fn>
showInactive: ReturnType<typeof vi.fn>
}
}
describe('isBackgroundLaunch', () => {
it('covers headless and headful E2E plus opted-in dev launches', () => {
expect(isBackgroundLaunch({ ORCA_E2E_HEADLESS: '1' })).toBe(true)
expect(isBackgroundLaunch({ ORCA_E2E_HEADFUL: '1' })).toBe(true)
expect(isBackgroundLaunch({ ORCA_BACKGROUND_LAUNCH: '1' })).toBe(true)
expect(isBackgroundLaunch({})).toBe(false)
})
it('lets native-focus specs opt back into the foreground', () => {
expect(isBackgroundLaunch({ ORCA_E2E_HEADFUL: '1', ORCA_E2E_FOREGROUND: '1' })).toBe(false)
expect(isWindowlessLaunch({ ORCA_E2E_HEADLESS: '1', ORCA_E2E_FOREGROUND: '1' })).toBe(false)
})
})
describe('isWindowlessLaunch', () => {
it('is headless-only; a headful run still paints', () => {
expect(isWindowlessLaunch({ ORCA_E2E_HEADLESS: '1' })).toBe(true)
expect(isWindowlessLaunch({ ORCA_E2E_HEADLESS: '1', ORCA_E2E_HEADFUL: '1' })).toBe(false)
expect(isWindowlessLaunch({ ORCA_BACKGROUND_LAUNCH: '1' })).toBe(false)
})
})
describe('showWindowWithoutStealingFocus', () => {
it('keeps a headless window off screen', () => {
const window = makeWindow()
showWindowWithoutStealingFocus(window, { ORCA_E2E_HEADLESS: '1' })
expect(window.show).not.toHaveBeenCalled()
expect(window.showInactive).not.toHaveBeenCalled()
})
it('shows a background window without activating it', () => {
const window = makeWindow()
showWindowWithoutStealingFocus(window, { ORCA_BACKGROUND_LAUNCH: '1' })
expect(window.showInactive).toHaveBeenCalledOnce()
expect(window.show).not.toHaveBeenCalled()
})
it('shows normally for a real user launch', () => {
const window = makeWindow()
showWindowWithoutStealingFocus(window, {})
expect(window.show).toHaveBeenCalledOnce()
expect(window.showInactive).not.toHaveBeenCalled()
})
it('ignores a destroyed window', () => {
const window = makeWindow(true)
showWindowWithoutStealingFocus(window, {})
expect(window.show).not.toHaveBeenCalled()
})
})
describe('applyBackgroundActivationPolicy', () => {
function makeApp() {
return {
dock: { hide: vi.fn(() => {}) },
setActivationPolicy: vi.fn((_policy: 'accessory' | 'prohibited' | 'regular') => {})
}
}
it('drops the macOS Dock tile and menu bar for headless runs', () => {
const app = makeApp()
expect(
applyBackgroundActivationPolicy({
app,
env: { ORCA_E2E_HEADLESS: '1' },
platform: 'darwin'
})
).toBe(true)
expect(app.dock.hide).toHaveBeenCalledOnce()
expect(app.setActivationPolicy).toHaveBeenCalledWith('accessory')
})
it('leaves a headful or user launch with its normal Dock presence', () => {
const headful = makeApp()
applyBackgroundActivationPolicy({
app: headful,
env: { ORCA_E2E_HEADLESS: '1', ORCA_E2E_HEADFUL: '1' },
platform: 'darwin'
})
expect(headful.setActivationPolicy).not.toHaveBeenCalled()
const user = makeApp()
applyBackgroundActivationPolicy({ app: user, env: {}, platform: 'darwin' })
expect(user.setActivationPolicy).not.toHaveBeenCalled()
})
it('is a no-op off macOS', () => {
const app = makeApp()
expect(
applyBackgroundActivationPolicy({
app,
env: { ORCA_E2E_HEADLESS: '1' },
platform: 'win32'
})
).toBe(false)
expect(app.dock.hide).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,83 @@
import { app as electronApp, type BrowserWindow } from 'electron'
/**
* Foreground policy for automated launches (E2E, benchmarks, agent-driven dev
* validation). These runs may use the machine, but must never take the OS
* foreground away from whatever the developer is doing.
*
* ORCA_BACKGROUND_LAUNCH=1 opts a normal launch in; ORCA_E2E_FOREGROUND=1 opts
* back out for the few specs whose subject *is* native focus (IME, key events).
*/
type ActivationPolicyApp = {
dock?: { hide: () => void }
setActivationPolicy: (policy: 'accessory' | 'prohibited' | 'regular') => void
}
/** Reads ORCA_BACKGROUND_LAUNCH, ORCA_E2E_FOREGROUND, ORCA_E2E_HEADLESS, ORCA_E2E_HEADFUL. */
type PolicyEnv = Readonly<Record<string, string | undefined>>
/** True when this process must not steal focus, raise windows, or activate the app. */
export function isBackgroundLaunch(env: PolicyEnv = process.env): boolean {
if (env.ORCA_E2E_FOREGROUND === '1') {
return false
}
return (
env.ORCA_BACKGROUND_LAUNCH === '1' ||
env.ORCA_E2E_HEADLESS === '1' ||
env.ORCA_E2E_HEADFUL === '1'
)
}
/** True when no window should reach the screen at all (headless E2E; Playwright drives via CDP). */
export function isWindowlessLaunch(env: PolicyEnv = process.env): boolean {
return isBackgroundLaunch(env) && env.ORCA_E2E_HEADLESS === '1' && env.ORCA_E2E_HEADFUL !== '1'
}
/**
* macOS: a windowless run still claims a Dock tile and the menu bar as it starts
* and exits. `accessory` drops both while leaving programmatic activation intact.
*/
export function applyBackgroundActivationPolicy(
options: {
app?: ActivationPolicyApp
env?: PolicyEnv
platform?: NodeJS.Platform
warn?: (message: string, error: unknown) => void
} = {}
): boolean {
const platform = options.platform ?? process.platform
if (platform !== 'darwin' || !isWindowlessLaunch(options.env ?? process.env)) {
return false
}
try {
const app = options.app ?? electronApp
app.dock?.hide()
app.setActivationPolicy('accessory')
return true
} catch (error) {
options.warn?.('[window] Failed to apply background activation policy', error)
return false
}
}
/**
* Reveal a window without taking the foreground: hidden entirely when windowless,
* `showInactive()` (visible, not raised over the active app) in background launches.
*/
export function showWindowWithoutStealingFocus(
window: BrowserWindow,
env: PolicyEnv = process.env
): void {
if (window.isDestroyed()) {
return
}
if (isWindowlessLaunch(env)) {
return
}
if (isBackgroundLaunch(env)) {
window.showInactive()
return
}
window.show()
}
@@ -1,6 +1,6 @@
import { app, type BrowserWindow } from 'electron'
import type { Store } from '../persistence'
import { getMainE2EConfig } from '../e2e-config'
import { isWindowlessLaunch, showWindowWithoutStealingFocus } from './foreground-activation-policy'
import { MIN_HEIGHT, MIN_WIDTH, syncTrafficLightPosition } from './main-window-visual-lifecycle'
export type MainWindowStateLifecycle = {
@@ -57,15 +57,14 @@ export function installMainWindowStateLifecycle(args: {
handledInitialReadyToShow = true
clearInitialRevealFallbackTimer()
// Why: in E2E headless mode keep the window hidden (Playwright drives via CDP) so tests don't steal focus.
const e2eConfig = getMainE2EConfig()
if (e2eConfig.headless) {
// Why: headless E2E keeps the window off screen entirely (Playwright drives via CDP).
if (isWindowlessLaunch()) {
return
}
if (savedMaximized) {
mainWindow.maximize()
}
mainWindow.show()
showWindowWithoutStealingFocus(mainWindow)
}
mainWindow.on('ready-to-show', revealInitialWindow)
if (revealOnDidFinishLoad === true) {
+21
View File
@@ -0,0 +1,21 @@
# Keep Automated Runs Out of the Foreground
Tests and agent-driven app launches share the developer's machine. They may use it; they must never
take the foreground — no window raised over the editor, no focus stolen, no Dock tile churn.
`src/main/window/foreground-activation-policy.ts` enforces this in the main process. It is on
whenever `ORCA_E2E_HEADLESS=1`, `ORCA_E2E_HEADFUL=1`, or `ORCA_BACKGROUND_LAUNCH=1`:
- headless → the window never reaches the screen (Playwright drives it via CDP)
- headful / background → `showInactive()`, no `app.focus({ steal: true })`, no
`moveTop()`/always-on-top reinforcement
- macOS headless → `accessory` activation policy, so no Dock tile and no menu-bar takeover
Rules when adding tests or scripts:
- Launch through `tests/e2e/helpers/orca-app.ts` (or `orca-restart.ts`) — they already set the env.
- A raw `electron.launch()` outside those helpers must pass `ORCA_BACKGROUND_LAUNCH: '1'`.
- Call `showInactive()`, never `show()`, when an `app.evaluate()` block reveals a window.
- Tag a spec `@headful` only when it needs real pixels; it still runs in the background.
- `ORCA_E2E_FOREGROUND=1` is the only opt-out, for runs whose subject _is_ native focus (IME and
other OS-level key injection). Add a comment saying why.
@@ -21,7 +21,7 @@ export function assertPairedClientWindowRevealed(report: PairedClientWindowRevea
}
if (!report.isVisible) {
throw new Error(
`Paired client window stayed hidden after show() (windows: ${report.windowCount})`
`Paired client window stayed hidden after showInactive() (windows: ${report.windowCount})`
)
}
}
@@ -64,8 +64,10 @@ export async function revealPairedClientWindow(
const windows = BrowserWindow.getAllWindows()
const window = windows[0]
const wasVisible = window?.isVisible() ?? false
// Why showInactive: the renderer only needs `visibilityState === 'visible'`;
// show() would also raise the window over whatever the developer is doing.
if (window && !wasVisible) {
window.show()
window.showInactive()
}
return {
isVisible: window?.isVisible() ?? false,
@@ -89,6 +89,8 @@ export async function launchInstalledApp({
// (logs/daemon/terminal-history) under a controlled dir.
...extraEnv,
ORCA_E2E_USER_DATA_DIR: userDataDir,
// Why: the driven app stays off the foreground so a local run doesn't steal focus.
ORCA_BACKGROUND_LAUNCH: '1',
HOME: isolatedHome,
USERPROFILE: isolatedHome,
ORCA_E2E_HOME_DIR: isolatedHome