Files
orca/src/main/window/createMainWindow.ts
T
Neil e1d4314a46 refactor(window): organize clipboard, lifecycle, security, and relay owners (#41634)
* refactor(window): group clipboard, lifecycle, security, and relay owners

* refactor(window): keep Electron paste handling with desktop clipboard
2026-11-12 11:47:00 -08:00

259 lines
11 KiB
TypeScript

import { app, BrowserWindow, nativeTheme, powerMonitor, screen } from 'electron'
import { release } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { installRendererHangStackPolicy } from '../hang-watchdog/renderer-hang-stack-policy'
import { getAppIconPath } from './desktop-integration/app-icon'
import { browserManager } from '../browser/guest-lifecycle/manager/browser-manager'
import { getBrowserClientHostId } from '../browser/client-hosts/lifecycle/browser-client-host-id'
import { formatBrowserClientHostIdArgument } from '../../shared/browser/client-hosting/browser-client-host-id-argument'
import { formatWindowMaterialArgument, resolveWindowMaterial } from '../../shared/window-material'
import { windowMaterialOptions } from './presentation/window-material-options'
import { rememberWindowMaterial } from './presentation/window-material-suppression'
import { markSystemSessionEnding } from '../crash-reporting/process-exits/expected-teardown-state'
import { recordDurableCrashBreadcrumb } from '../crash-reporting/breadcrumbs/durable-crash-breadcrumb'
import { clearTrustedUIRendererWebContentsId, setTrustedUIRendererWebContentsId } from '../ipc/ui'
import { flushUnsavedCloudSessions } from '../orca-profiles/cloud-session/profile-cloud-session-owner'
import type { Store } from '../persistence/loading-store/store'
import { closeDashboardPopout } from './dashboard-popout-window'
import { closeWorkspaceSpaceWindow } from './workspace-space-window'
import { installMainWindowCloseLifecycle } from './main-window/main-window-close-lifecycle'
import type {
CreateMainWindowOptions,
MainWindowLoadObserver
} from './main-window/main-window-contracts'
import { mainWindowLoadErrorCode } from './renderer-recovery/main-window-load-error-code'
import { installMainWindowFocusLifecycle } from './main-window/main-window-focus-lifecycle'
import { installMainWindowShortcutRouting } from './main-window/main-window-shortcut-routing'
import { installMainWindowStateLifecycle } from './main-window/main-window-state-lifecycle'
import { installMainWindowActivityRelay } from './main-window/main-window-activity'
import { stopHiddenWindowFramePumpOnShow } from './visibility/hidden-window-frame-pump'
import { wslHostsWindowArguments } from '../wsl/host-lifecycle/wsl-host-gate'
import {
formatHiddenWindowFramePumpArgument,
shouldPumpHiddenWindowFrames
} from '../../shared/hidden-window-frame-pump'
import {
forceRepaint,
installMacosVisibilityRepaint,
MIN_HEIGHT,
MIN_WIDTH,
TITLEBAR_CSS_CENTER,
TRAFFIC_LIGHT_RADIUS,
TRAFFIC_LIGHT_X
} from './main-window/main-window-visual-lifecycle'
import { installMainWindowWebviewSecurity } from './security/main-window-webview-security'
import { rectHasVisibleAreaOnAnyDisplay } from './presentation/window-bounds-validation'
import { installWindowsPathRegistryChangeListener } from '../pty/environment/windows-path-registry-change'
import { recordAgentSessionRuntimeEnd } from '../runtime/agent-session/records/agent-session-runtime-end-record'
export function loadMainWindow(mainWindow: BrowserWindow, observer?: MainWindowLoadObserver): void {
installRendererHangStackPolicy(
mainWindow.webContents,
!app.isPackaged && process.env.ELECTRON_RENDERER_URL
? process.env.ELECTRON_RENDERER_URL
: pathToFileURL(join(__dirname, '../renderer/index.html')).href
)
const load =
!app.isPackaged && process.env.ELECTRON_RENDERER_URL
? mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
: mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
// Observe each load promise so failures cannot leave recovery waiting silently.
load.then(
() => observer?.onLoaded?.(),
(cause: unknown) => {
const error = cause instanceof Error ? cause : new Error(String(cause))
const errorCode = mainWindowLoadErrorCode(error)
// Keep durable diagnostics path-free and exclude shutdown/navigation aborts.
if (!mainWindow.isDestroyed() && errorCode !== 'ERR_ABORTED') {
recordDurableCrashBreadcrumb('main_window_load_failed', { errorCode })
}
console.error('[window] Main window load failed', error)
observer?.onError?.(error)
}
)
}
export function createMainWindow(
store: Store | null,
opts?: CreateMainWindowOptions
): BrowserWindow {
const rawSavedBounds = store?.getUI().windowBounds
// Reject tiny/off-screen saved bounds so the titlebar stays reachable after display changes.
const savedBounds =
rawSavedBounds &&
rawSavedBounds.width > MIN_WIDTH &&
rawSavedBounds.height > MIN_HEIGHT &&
rectHasVisibleAreaOnAnyDisplay(rawSavedBounds, MIN_WIDTH / 2, MIN_HEIGHT / 2)
? rawSavedBounds
: undefined
if (rawSavedBounds && !savedBounds) {
console.warn(
'[window] Discarding persisted windowBounds and falling back to defaultBounds:',
rawSavedBounds
)
}
const savedMaximized = store?.getUI().windowMaximized ?? false
const savedFullScreen = store?.getUI().windowFullScreen ?? false
// Fill the primary work area on first launch; later launches honor saved bounds.
const defaultBounds = (() => {
try {
const { width, height } = screen.getPrimaryDisplay().workAreaSize
return { width, height }
} catch {
return { width: 1200, height: 800 }
}
})()
const settings = store?.getSettings()
browserManager.setDictationShortcutForwardingPredicate(() => {
const voice = store?.getSettings().voice
// Chromium drops a page's key releases after a press cancelled here, so Hold can't see its release; the page keeps the key then.
return voice?.enabled === true && voice.dictationMode !== 'hold'
})
const windowMaterial = resolveWindowMaterial(
settings,
{ platform: process.platform, osRelease: release() },
nativeTheme.prefersReducedTransparency
)
const pumpHiddenFrames = shouldPumpHiddenWindowFrames(process.env, process.platform)
const mainWindow = new BrowserWindow({
width: savedBounds?.width ?? defaultBounds.width,
height: savedBounds?.height ?? defaultBounds.height,
...(savedBounds ? { x: savedBounds.x, y: savedBounds.y } : {}),
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
title: opts?.title ?? 'Orca',
show: false,
// Deliver the app-activating click on macOS instead of requiring a second click.
acceptFirstMouse: true,
// Save menu-bar space on Windows/Linux; Alt reveals it and macOS uses the system menu.
autoHideMenuBar: true,
...windowMaterialOptions(
windowMaterial,
process.platform,
nativeTheme.shouldUseDarkColors ? '#0a0a0a' : '#ffffff'
),
// Keep macOS traffic lights inset; hide Windows native chrome to avoid duplicate titlebars.
titleBarStyle:
process.platform === 'darwin'
? 'hiddenInset'
: process.platform === 'win32'
? 'hidden'
: undefined,
// Linux ignores hidden titleBarStyle; remove its native frame to avoid duplicate titlebars.
...(process.platform === 'linux' ? { frame: false } : {}),
// Start at 1x traffic-light position; syncTrafficLightPosition follows zoom.
...(process.platform === 'darwin'
? {
trafficLightPosition: {
x: TRAFFIC_LIGHT_X,
y: TITLEBAR_CSS_CENTER - TRAFFIC_LIGHT_RADIUS
}
}
: {}),
icon: getAppIconPath(settings?.appIcon),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: true,
webviewTag: true,
// Pass host identity at birth so the renderer has it before first snapshot, without waiting for IPC registration.
additionalArguments: [
formatBrowserClientHostIdArgument(getBrowserClientHostId()),
...(windowMaterial === 'off' ? [] : [formatWindowMaterialArgument(windowMaterial)]),
...(pumpHiddenFrames ? [formatHiddenWindowFramePumpArgument()] : []),
...wslHostsWindowArguments()
]
}
})
rememberWindowMaterial(mainWindow, windowMaterial)
const rendererWebContentsId = mainWindow.webContents.id
installWindowsPathRegistryChangeListener(mainWindow)
// Native paste fallback is privileged; trust only the top-level renderer.
setTrustedUIRendererWebContentsId(rendererWebContentsId)
// Unlike query-session-end, session-end cannot be canceled before this signal is recorded.
if (process.platform === 'win32') {
mainWindow.on('session-end', (event) => {
markSystemSessionEnding()
// Record positive OS shutdown evidence; task-kill and shutdown tree deaths otherwise look identical.
recordDurableCrashBreadcrumb('system_session_end', {
reasons: Array.isArray(event?.reasons)
? event.reasons.filter((reason) => typeof reason === 'string').join(',')
: ''
})
flushUnsavedCloudSessions()
// Windows emits no will-quit for an OS shutdown or logoff, so its teardown never records it.
recordAgentSessionRuntimeEnd('quit')
})
}
if (process.platform === 'darwin') {
// Keep hidden-window throttling; native sizing/frame invalidation recovers wake without an active viewport.
mainWindow.webContents.setBackgroundThrottling(true)
installMacosVisibilityRepaint(mainWindow)
}
// Resume may preserve focus and emit no visibility events; explicitly relay terminal wake and repaint stale surfaces.
const onSystemResume = (): void => {
if (mainWindow.isDestroyed() || mainWindow.webContents.isDestroyed?.() === true) {
return
}
forceRepaint(mainWindow)
mainWindow.webContents.send('system:resumed')
// Give a suspended recovery load its full budget on wake.
focus.notifySystemResume()
}
const state = installMainWindowStateLifecycle({
mainWindow,
revealOnDidFinishLoad: opts?.revealOnDidFinishLoad === true,
savedMaximized,
savedFullScreen,
store
})
installMainWindowWebviewSecurity(mainWindow)
const activity = installMainWindowActivityRelay(mainWindow)
if (pumpHiddenFrames) {
stopHiddenWindowFramePumpOnShow(mainWindow)
}
const focus = installMainWindowFocusLifecycle({
isWindowClosing: state.isWindowClosing,
mainWindow,
opts,
reloadMainWindow: (observer) => loadMainWindow(mainWindow, observer),
rendererWebContentsId
})
// Register after focus is initialized because the resume callback uses it.
powerMonitor.on('resume', onSystemResume)
installMainWindowShortcutRouting({ focus, mainWindow, opts, store })
const closeLifecycle = installMainWindowCloseLifecycle({
focus,
mainWindow,
opts,
rendererWebContentsId,
state,
store
})
mainWindow.on('closed', () => {
closeDashboardPopout()
closeWorkspaceSpaceWindow()
state.clearInitialRevealFallbackTimer()
closeLifecycle.dispose()
activity.dispose()
focus.dispose()
browserManager.setDictationShortcutForwardingPredicate(null)
powerMonitor.removeListener('resume', onSystemResume)
clearTrustedUIRendererWebContentsId(rendererWebContentsId)
state.dispose()
})
if (!opts?.deferLoad) {
loadMainWindow(mainWindow)
}
return mainWindow
}