diff --git a/docs/architecture/desktop-build.md b/docs/architecture/desktop-build.md index 6eb41abe6..d841b0f6c 100644 --- a/docs/architecture/desktop-build.md +++ b/docs/architecture/desktop-build.md @@ -76,8 +76,9 @@ drive-by cleanup. ## The native title bar -The main window is 1400x900 with a minimum of 1024x768, is created hidden, and is shown only once -its renderer reports ready (see the readiness contract below). +The main window defaults to 1400x900 with a minimum of 1024x768, persists its window state between +launches, is created hidden, and is shown only once its renderer reports ready (see the readiness +contract below). ### Mode resolution diff --git a/main/index.ts b/main/index.ts index c06b5249d..86a0f64f4 100644 --- a/main/index.ts +++ b/main/index.ts @@ -426,8 +426,8 @@ function recoverFailedWindow(failedWindow: BrowserWindow): void { } windowLoadRecoveryAttempted = true; try { - createWindow(); if (!failedWindow.isDestroyed()) failedWindow.destroy(); + createWindow(); } catch (error) { log.error('[Window] Window recreation failed:', error); requestRuntimeRelaunchOnce('window-load-recovery-create-failed'); @@ -866,8 +866,23 @@ function registerPowerMonitorRecovery(): void { // Track child process crashes and trigger GPU fallback relaunch on GPU crash. function registerChildProcessCrashTelemetry(): void { app.on('child-process-gone', (_event, details) => { - log.error('[Process] Child process gone:', details.type, details.reason, details.exitCode); - console.error('[Process] Child process gone:', details.type, details.reason, details.exitCode); + const systemErrorCode = 'systemErrorCode' in details + ? (details as { systemErrorCode?: number }).systemErrorCode + : undefined; + log.error( + '[Process] Child process gone:', + details.type, + details.reason, + details.exitCode, + systemErrorCode !== undefined ? `systemErrorCode=${systemErrorCode}` : '', + ); + console.error( + '[Process] Child process gone:', + details.type, + details.reason, + details.exitCode, + systemErrorCode !== undefined ? `systemErrorCode=${systemErrorCode}` : '', + ); // Ignore normal shutdown/clean-exit signals. const isGpuFailure = details.type === 'GPU' && details.reason !== 'clean-exit' @@ -882,6 +897,7 @@ function registerChildProcessCrashTelemetry(): void { type: details.type, reason: details.reason, exitCode: details.exitCode, + systemErrorCode, serviceName: details.serviceName, consecutiveGpuCrashCount: isGpuFailure ? consecutiveGpuCrashes : undefined, }).catch((error) => { diff --git a/main/window-options.ts b/main/window-options.ts index 8e13ed69c..5e70f18bf 100644 --- a/main/window-options.ts +++ b/main/window-options.ts @@ -43,11 +43,26 @@ export function createMainWindow( preload: string, platform: NodeJS.Platform = process.platform, isDarkOrTitleBarMode: boolean | TitleBarMode = false, - titleBarMode: TitleBarMode = typeof isDarkOrTitleBarMode === 'string' ? isDarkOrTitleBarMode : 'native-overlay', + titleBarModeOrOptions?: TitleBarMode | Partial, + options?: Partial, ): BrowserWindow { const isDark = typeof isDarkOrTitleBarMode === 'boolean' ? isDarkOrTitleBarMode : false; - const resolvedTitleBarMode = typeof isDarkOrTitleBarMode === 'string' ? isDarkOrTitleBarMode : titleBarMode; + const resolvedTitleBarMode: TitleBarMode = + typeof titleBarModeOrOptions === 'string' + ? titleBarModeOrOptions + : typeof isDarkOrTitleBarMode === 'string' + ? isDarkOrTitleBarMode + : 'native-overlay'; + const resolvedOptions = + typeof titleBarModeOrOptions === 'object' && titleBarModeOrOptions !== null + ? titleBarModeOrOptions + : options; + // Merge webPreferences instead of replacing it, so overriding one preference + // cannot drop the generated preload or the isolation settings it enables. + const { webPreferences: webPreferencesOverrides, ...windowOptions } = resolvedOptions ?? {}; return new BrowserWindowConstructor({ + name: 'flo-main', + windowStatePersistence: true, width: 1400, height: 900, minWidth: 1024, @@ -64,12 +79,14 @@ export function createMainWindow( : {}), ...(platform === 'darwin' ? { trafficLightPosition: MAC_TRAFFIC_LIGHT_POSITION } : {}), webPreferences: { + ...(webPreferencesOverrides ?? {}), preload, contextIsolation: true, nodeIntegration: false, sandbox: false, }, show: false, + ...windowOptions, }); } diff --git a/tests/platform-titlebar-runtime-probe.cjs b/tests/platform-titlebar-runtime-probe.cjs index de1ec183b..29a11b30e 100644 --- a/tests/platform-titlebar-runtime-probe.cjs +++ b/tests/platform-titlebar-runtime-probe.cjs @@ -132,6 +132,7 @@ async function main() { path.join(__dirname, '_probe-preload-noop.js'), platform, resolved, + { name: 'observed-mac-window' }, ); const trafficLightPosition = observedMacOptions?.trafficLightPosition; record( diff --git a/tests/titlebar-window-options.test.ts b/tests/titlebar-window-options.test.ts index 6f7c3e41e..08d065c7d 100644 --- a/tests/titlebar-window-options.test.ts +++ b/tests/titlebar-window-options.test.ts @@ -29,6 +29,8 @@ const windowsMainWindow = createMainWindow(FakeBrowserWindow as any, '/tmp/prelo const linuxMainWindow = createMainWindow(FakeBrowserWindow as any, '/tmp/preload.js', 'linux'); const darkMainWindow = createMainWindow(FakeBrowserWindow as any, '/tmp/preload.js', 'darwin', true); +assert.equal(macMainWindow.options.name, 'flo-main'); +assert.equal(macMainWindow.options.windowStatePersistence, true); assert.equal(macMainWindow.options.titleBarStyle, 'hiddenInset'); assert.equal(windowsMainWindow.options.titleBarStyle, 'hidden'); assert.equal(linuxMainWindow.options.titleBarStyle, 'hidden'); @@ -48,6 +50,22 @@ assert.equal(macMainWindow.options.webPreferences.nodeIntegration, false); assert.equal(macMainWindow.options.webPreferences.sandbox, false); assert.equal('frame' in macMainWindow.options, false, 'the native-controls design does not remove the window frame'); +// An options override merges webPreferences: it cannot replace the generated +// preload and isolation settings, and its other keys still apply. +const overriddenOptionsWindow = createMainWindow( + FakeBrowserWindow as any, + '/tmp/preload.js', + 'darwin', + false, + { width: 1200, webPreferences: { backgroundThrottling: false } }, +); +assert.equal(overriddenOptionsWindow.options.width, 1200); +assert.equal(overriddenOptionsWindow.options.webPreferences.preload, '/tmp/preload.js'); +assert.equal(overriddenOptionsWindow.options.webPreferences.contextIsolation, true); +assert.equal(overriddenOptionsWindow.options.webPreferences.nodeIntegration, false); +assert.equal(overriddenOptionsWindow.options.webPreferences.sandbox, false); +assert.equal(overriddenOptionsWindow.options.webPreferences.backgroundThrottling, false); + // macOS traffic lights are vertically centered in the 40px bar (buttons are 12px tall). assert.deepEqual(macMainWindow.options.trafficLightPosition, { x: 16, y: 14 }); assert.equal('trafficLightPosition' in windowsMainWindow.options, false, 'trafficLightPosition is macOS-only');