From 5ea9daba97f591ed73522388a51f126fa9c72a6f Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:55:00 -0700 Subject: [PATCH] fix(window): keep automated Electron launches out of the foreground (#17347) --- .../run-codex-real-account-validation.mjs | 6 +- config/scripts/run-electron-vite-dev.mjs | 5 + .../scripts/run-terminal-ibus-hangul-e2e.mjs | 5 +- .../verify-linux-wayland-gpu-sandbox.mjs | 1 + config/tsconfig.tc.web.json | 1 + src/main/index.ts | 11 +- src/main/ipc/dashboard-popout.ts | 11 +- src/main/ipc/native-notification-delivery.ts | 10 +- src/main/window/dashboard-popout-window.ts | 9 +- src/main/window/focus-existing-window.ts | 16 ++- .../foreground-activation-policy.test.ts | 124 ++++++++++++++++++ .../window/foreground-activation-policy.ts | 83 ++++++++++++ .../window/main-window-state-lifecycle.ts | 9 +- tests/AGENTS.md | 21 +++ .../helpers/paired-client-window-reveal.ts | 6 +- tests/tools/win-update-e2e/app-driver.mjs | 2 + 16 files changed, 288 insertions(+), 32 deletions(-) create mode 100644 src/main/window/foreground-activation-policy.test.ts create mode 100644 src/main/window/foreground-activation-policy.ts create mode 100644 tests/AGENTS.md diff --git a/config/scripts/run-codex-real-account-validation.mjs b/config/scripts/run-codex-real-account-validation.mjs index 9318f412ffe..0ad15e3bfd5 100644 --- a/config/scripts/run-codex-real-account-validation.mjs +++ b/config/scripts/run-codex-real-account-validation.mjs @@ -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'), diff --git a/config/scripts/run-electron-vite-dev.mjs b/config/scripts/run-electron-vite-dev.mjs index 40bb7454793..65bcf069016 100644 --- a/config/scripts/run-electron-vite-dev.mjs +++ b/config/scripts/run-electron-vite-dev.mjs @@ -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 diff --git a/config/scripts/run-terminal-ibus-hangul-e2e.mjs b/config/scripts/run-terminal-ibus-hangul-e2e.mjs index 9e8eb07cf47..62079243f04 100644 --- a/config/scripts/run-terminal-ibus-hangul-e2e.mjs +++ b/config/scripts/run-terminal-ibus-hangul-e2e.mjs @@ -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' } diff --git a/config/scripts/verify-linux-wayland-gpu-sandbox.mjs b/config/scripts/verify-linux-wayland-gpu-sandbox.mjs index a36f81db86b..21770508e10 100644 --- a/config/scripts/verify-linux-wayland-gpu-sandbox.mjs +++ b/config/scripts/verify-linux-wayland-gpu-sandbox.mjs @@ -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, diff --git a/config/tsconfig.tc.web.json b/config/tsconfig.tc.web.json index c2de6e5dcbb..d6adfd471ce 100644 --- a/config/tsconfig.tc.web.json +++ b/config/tsconfig.tc.web.json @@ -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": { diff --git a/src/main/index.ts b/src/main/index.ts index 00b059885ad..06af692d786 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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()) diff --git a/src/main/ipc/dashboard-popout.ts b/src/main/ipc/dashboard-popout.ts index 5b4781ec761..9e71d9ac32a 100644 --- a/src/main/ipc/dashboard-popout.ts +++ b/src/main/ipc/dashboard-popout.ts @@ -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. + } } }) diff --git a/src/main/ipc/native-notification-delivery.ts b/src/main/ipc/native-notification-delivery.ts index 0630fdde6dc..6b82d183c64 100644 --- a/src/main/ipc/native-notification-delivery.ts +++ b/src/main/ipc/native-notification-delivery.ts @@ -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 diff --git a/src/main/window/dashboard-popout-window.ts b/src/main/window/dashboard-popout-window.ts index 2d32d14e523..5e765c7e432 100644 --- a/src/main/window/dashboard-popout-window.ts +++ b/src/main/window/dashboard-popout-window.ts @@ -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 diff --git a/src/main/window/focus-existing-window.ts b/src/main/window/focus-existing-window.ts index 659f7394887..8c8ac85ca2e 100644 --- a/src/main/window/focus-existing-window.ts +++ b/src/main/window/focus-existing-window.ts @@ -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): 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 { diff --git a/src/main/window/foreground-activation-policy.test.ts b/src/main/window/foreground-activation-policy.test.ts new file mode 100644 index 00000000000..0a45f00387e --- /dev/null +++ b/src/main/window/foreground-activation-policy.test.ts @@ -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 + showInactive: ReturnType +} { + return { + isDestroyed: () => destroyed, + show: vi.fn(() => {}), + showInactive: vi.fn(() => {}) + } as unknown as BrowserWindow & { + show: ReturnType + showInactive: ReturnType + } +} + +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() + }) +}) diff --git a/src/main/window/foreground-activation-policy.ts b/src/main/window/foreground-activation-policy.ts new file mode 100644 index 00000000000..c2ee6b19e73 --- /dev/null +++ b/src/main/window/foreground-activation-policy.ts @@ -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> + +/** 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() +} diff --git a/src/main/window/main-window-state-lifecycle.ts b/src/main/window/main-window-state-lifecycle.ts index 74c14bf1895..443534d8352 100644 --- a/src/main/window/main-window-state-lifecycle.ts +++ b/src/main/window/main-window-state-lifecycle.ts @@ -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) { diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000000..f445415e7df --- /dev/null +++ b/tests/AGENTS.md @@ -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. diff --git a/tests/e2e/helpers/paired-client-window-reveal.ts b/tests/e2e/helpers/paired-client-window-reveal.ts index 9a8dfebbce6..302d573c3d1 100644 --- a/tests/e2e/helpers/paired-client-window-reveal.ts +++ b/tests/e2e/helpers/paired-client-window-reveal.ts @@ -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, diff --git a/tests/tools/win-update-e2e/app-driver.mjs b/tests/tools/win-update-e2e/app-driver.mjs index d95e7f2c342..33f59121f87 100644 --- a/tests/tools/win-update-e2e/app-driver.mjs +++ b/tests/tools/win-update-e2e/app-driver.mjs @@ -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