Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/architecture/desktop-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 19 additions & 3 deletions main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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'
Expand All @@ -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) => {
Expand Down
21 changes: 19 additions & 2 deletions main/window-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BrowserWindowConstructorOptions>,
options?: Partial<BrowserWindowConstructorOptions>,
): 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,
Expand All @@ -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,
});
}

Expand Down
1 change: 1 addition & 0 deletions tests/platform-titlebar-runtime-probe.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions tests/titlebar-window-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
Loading