From bef397f0cc24498baeaa38243ad1992c2daec311 Mon Sep 17 00:00:00 2001 From: MatthewFeroz Date: Tue, 22 Sep 2026 15:05:21 -0500 Subject: [PATCH 1/4] fix(desktop): fit windows on small displays --- .../src/settings/DesktopAppSettings.test.ts | 12 ++++ .../src/settings/DesktopAppSettings.ts | 6 +- apps/desktop/src/window/DesktopWindow.test.ts | 59 ++++++++++++++++++- apps/desktop/src/window/DesktopWindow.ts | 23 ++++++-- 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/settings/DesktopAppSettings.test.ts b/apps/desktop/src/settings/DesktopAppSettings.test.ts index f7db3c2778102..a7c04a2f299da 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.test.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.test.ts @@ -278,6 +278,18 @@ describe("DesktopSettings", () => { ), ); + it.effect("preserves window bounds from a small tiled window", () => + withSettings( + Effect.gen(function* () { + const settings = yield* DesktopAppSettings.DesktopAppSettings; + const mainWindowBounds = { x: 12, y: 563, width: 845, height: 510 }; + yield* writeSettingsPatch({ mainWindowBounds }); + const loaded = yield* settings.load; + assert.deepEqual(loaded.mainWindowBounds, mainWindowBounds); + }), + ), + ); + it.effect("rejects window bounds that do not satisfy the domain schema", () => withSettings( Effect.gen(function* () { diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index 19fcf0e75962e..70fc96e5e71e4 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -57,9 +57,9 @@ export interface DesktopSettingsChange { } const DEFAULT_TAILSCALE_SERVE_PORT = 443; -const MIN_MAIN_WINDOW_SIZE = { - width: 840, - height: 620, +export const MIN_MAIN_WINDOW_SIZE = { + width: 360, + height: 320, } as const; export const DesktopWindowBoundsSchema = Schema.Struct({ x: Schema.Int, diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 78834a55690e0..b6e28a628855a 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -29,7 +29,7 @@ vi.mock("electron", async (importOriginal) => ({ screen: { getAllDisplays: vi.fn(() => [ { - bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + workArea: { x: 0, y: 0, width: 1920, height: 1080 }, }, ]), }, @@ -579,6 +579,28 @@ describe("DesktopWindow", () => { ); }); + it("fits a new window inside a small display's available work area", () => { + assert.deepEqual( + DesktopWindow.resolveInitialMainWindowBounds(null, [ + { x: -900, y: 26, width: 900, height: 574 }, + ]), + { x: -900, y: 26, width: 900, height: 574 }, + ); + }); + + it("fits the fallback window after moving from a larger screen", () => { + assert.deepEqual( + DesktopWindow.resolveInitialMainWindowBounds({ x: 2040, y: 80, width: 1320, height: 880 }, [ + { x: 0, y: 26, width: 1280, height: 694 }, + ]), + { x: 90, y: 26, width: 1100, height: 694 }, + ); + assert.deepEqual( + DesktopWindow.resolveInitialMainWindowBounds(null, []), + DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE, + ); + }); + it("recognizes only same-origin renderer navigations", () => { assert.isTrue( DesktopWindow.isSameOriginRendererNavigation({ @@ -983,7 +1005,7 @@ describe("DesktopWindow", () => { it.effect("does not persist bounds that fail the domain schema", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); - fakeWindow.getBounds.mockReturnValue({ x: 100.4, y: 80.2, width: 839.4, height: 619.4 }); + fakeWindow.getBounds.mockReturnValue({ x: 100.4, y: 80.2, width: 359.4, height: 319.4 }); const createCount = yield* Ref.make(0); const mainWindow = yield* Ref.make>(Option.none()); const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; @@ -1123,6 +1145,39 @@ describe("DesktopWindow", () => { }), ); + it.effect("creates a resizable window within a small display's work area", () => + Effect.gen(function* () { + vi.mocked(Electron.screen.getAllDisplays).mockReturnValueOnce([ + { + ...Electron.screen.getAllDisplays()[0]!, + workArea: { x: 0, y: 26, width: 800, height: 574 }, + }, + ]); + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + createdWindowOptions, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + }).pipe(Effect.provide(layer)); + + const options = createdWindowOptions[0]; + assert.equal(options?.width, 800); + assert.equal(options?.height, 574); + assert.equal(options?.y, 26); + assert.equal(options?.minWidth, 360); + assert.equal(options?.minHeight, 320); + }), + ); + it.effect("logs display lookup failures before falling back to the default size", () => Effect.gen(function* () { const displayLookupFailure = new Error("screen API unavailable"); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index b3964cb929c7c..56170a8b325d6 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -193,7 +193,19 @@ export function resolveInitialMainWindowBounds( ) { return persistedBounds; } - return DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE; + const display = displays[0]; + const defaults = DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE; + if (!display || (defaults.width <= display.width && defaults.height <= display.height)) { + return defaults; + } + const width = Math.min(defaults.width, display.width); + const height = Math.min(defaults.height, display.height); + return { + x: display.x + Math.floor((display.width - width) / 2), + y: display.y + Math.floor((display.height - height) / 2), + width, + height, + }; } // A self-contained "Connecting to WSL" splash, shown immediately in wsl-only @@ -376,7 +388,7 @@ export const make = Effect.gen(function* () { try { return { _tag: "Success" as const, - bounds: Electron.screen.getAllDisplays().map((display) => display.bounds), + bounds: Electron.screen.getAllDisplays().map((display) => display.workArea), }; } catch (cause) { return { _tag: "Failure" as const, cause }; @@ -390,13 +402,14 @@ export const make = Effect.gen(function* () { }).pipe(Effect.as([])); const initialBounds = resolveInitialMainWindowBounds(persistedBounds, displayBounds); const restoredPersistedBounds = persistedBounds !== null && initialBounds === persistedBounds; - if (persistedBounds !== null && initialBounds === DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE) { + if (persistedBounds !== null && !restoredPersistedBounds) { yield* logWindowWarning("saved main window bounds could not be restored; using defaults"); } const window = yield* electronWindow.create({ ...initialBounds, - minWidth: 840, - minHeight: 620, + // Let small displays and tiling compositors reach the responsive layout. + minWidth: Math.min(DesktopAppSettings.MIN_MAIN_WINDOW_SIZE.width, initialBounds.width), + minHeight: Math.min(DesktopAppSettings.MIN_MAIN_WINDOW_SIZE.height, initialBounds.height), show: false, autoHideMenuBar: true, ...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}), From 5bcb40b8aa2244fde407f02258df921b240a1a7b Mon Sep 17 00:00:00 2001 From: MatthewFeroz Date: Tue, 22 Sep 2026 15:10:40 -0500 Subject: [PATCH 2/4] fix(desktop): preserve compositor-assigned window bounds --- apps/desktop/src/settings/DesktopAppSettings.test.ts | 4 ++-- apps/desktop/src/settings/DesktopAppSettings.ts | 6 ++++-- apps/desktop/src/window/DesktopWindow.test.ts | 10 +++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/settings/DesktopAppSettings.test.ts b/apps/desktop/src/settings/DesktopAppSettings.test.ts index a7c04a2f299da..c3392572f16d9 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.test.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.test.ts @@ -278,11 +278,11 @@ describe("DesktopSettings", () => { ), ); - it.effect("preserves window bounds from a small tiled window", () => + it.effect("preserves compositor bounds below the requested minimum", () => withSettings( Effect.gen(function* () { const settings = yield* DesktopAppSettings.DesktopAppSettings; - const mainWindowBounds = { x: 12, y: 563, width: 845, height: 510 }; + const mainWindowBounds = { x: 12, y: 563, width: 280, height: 240 }; yield* writeSettingsPatch({ mainWindowBounds }); const loaded = yield* settings.load; assert.deepEqual(loaded.mainWindowBounds, mainWindowBounds); diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index 70fc96e5e71e4..297b4c99c73da 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -61,11 +61,13 @@ export const MIN_MAIN_WINDOW_SIZE = { width: 360, height: 320, } as const; +// A compositor can assign less than the requested minimum. Preserve its actual +// bounds; the native window options enforce the preferred minimum at creation. export const DesktopWindowBoundsSchema = Schema.Struct({ x: Schema.Int, y: Schema.Int, - width: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.width)), - height: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.height)), + width: Schema.Int.check(Schema.isGreaterThan(0)), + height: Schema.Int.check(Schema.isGreaterThan(0)), }); export type DesktopWindowBounds = typeof DesktopWindowBoundsSchema.Type; export const DEFAULT_MAIN_WINDOW_SIZE = { diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index b6e28a628855a..648fe829856d0 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -601,6 +601,14 @@ describe("DesktopWindow", () => { ); }); + it("preserves fallback bounds for displays below the requested minimum", () => { + const bounds = DesktopWindow.resolveInitialMainWindowBounds(null, [ + { x: 0, y: 26, width: 280, height: 240 }, + ]); + assert.deepEqual(bounds, { x: 0, y: 26, width: 280, height: 240 }); + assert.deepEqual(DesktopAppSettings.normalizeMainWindowBounds(bounds), bounds); + }); + it("recognizes only same-origin renderer navigations", () => { assert.isTrue( DesktopWindow.isSameOriginRendererNavigation({ @@ -1005,7 +1013,7 @@ describe("DesktopWindow", () => { it.effect("does not persist bounds that fail the domain schema", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); - fakeWindow.getBounds.mockReturnValue({ x: 100.4, y: 80.2, width: 359.4, height: 319.4 }); + fakeWindow.getBounds.mockReturnValue({ x: 100.4, y: 80.2, width: 0.4, height: 0.4 }); const createCount = yield* Ref.make(0); const mainWindow = yield* Ref.make>(Option.none()); const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; From 0ffa9cbee6cd7b30a69fa932d6cd01882fcf2e4a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 09:26:24 +0000 Subject: [PATCH 3/4] fix(desktop): preserve saved bounds and fit the primary work area --- .../src/settings/DesktopAppSettings.test.ts | 4 +- .../src/settings/DesktopAppSettings.ts | 7 +- apps/desktop/src/window/DesktopWindow.test.ts | 137 ++++++++++-------- apps/desktop/src/window/DesktopWindow.ts | 18 ++- 4 files changed, 94 insertions(+), 72 deletions(-) diff --git a/apps/desktop/src/settings/DesktopAppSettings.test.ts b/apps/desktop/src/settings/DesktopAppSettings.test.ts index 68a1dafa75972..a7aa662ead40b 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.test.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.test.ts @@ -278,12 +278,12 @@ describe("DesktopSettings", () => { ), ); - it.effect("preserves compositor bounds below the requested minimum", () => + it.effect("persists and reloads compositor bounds below the preferred minimum", () => withSettings( Effect.gen(function* () { const settings = yield* DesktopAppSettings.DesktopAppSettings; const mainWindowBounds = { x: 12, y: 563, width: 280, height: 240 }; - yield* writeSettingsPatch({ mainWindowBounds }); + yield* settings.setMainWindowBounds(mainWindowBounds, false); const loaded = yield* settings.load; assert.deepEqual(loaded.mainWindowBounds, mainWindowBounds); }), diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index 92eb0ba3b7454..6126726cd1c3e 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -48,12 +48,7 @@ export interface DesktopSettingsChange { } const DEFAULT_TAILSCALE_SERVE_PORT = 443; -export const MIN_MAIN_WINDOW_SIZE = { - width: 360, - height: 320, -} as const; -// A compositor can assign less than the requested minimum. Preserve its actual -// bounds; the native window options enforce the preferred minimum at creation. +// Compositors can assign bounds smaller than the requested native minimum. export const DesktopWindowBoundsSchema = Schema.Struct({ x: Schema.Int, y: Schema.Int, diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 25c09157e715c..b55a08c762540 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -27,8 +27,13 @@ vi.mock("electron", async (importOriginal) => ({ })), }, screen: { + getPrimaryDisplay: vi.fn(() => ({ + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + workArea: { x: 0, y: 0, width: 1920, height: 1080 }, + })), getAllDisplays: vi.fn(() => [ { + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, workArea: { x: 0, y: 0, width: 1920, height: 1080 }, }, ]), @@ -610,36 +615,6 @@ describe("DesktopWindow", () => { ); }); - it("fits a new window inside a small display's available work area", () => { - assert.deepEqual( - DesktopWindow.resolveInitialMainWindowBounds(null, [ - { x: -900, y: 26, width: 900, height: 574 }, - ]), - { x: -900, y: 26, width: 900, height: 574 }, - ); - }); - - it("fits the fallback window after moving from a larger screen", () => { - assert.deepEqual( - DesktopWindow.resolveInitialMainWindowBounds({ x: 2040, y: 80, width: 1320, height: 880 }, [ - { x: 0, y: 26, width: 1280, height: 694 }, - ]), - { x: 90, y: 26, width: 1100, height: 694 }, - ); - assert.deepEqual( - DesktopWindow.resolveInitialMainWindowBounds(null, []), - DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE, - ); - }); - - it("preserves fallback bounds for displays below the requested minimum", () => { - const bounds = DesktopWindow.resolveInitialMainWindowBounds(null, [ - { x: 0, y: 26, width: 280, height: 240 }, - ]); - assert.deepEqual(bounds, { x: 0, y: 26, width: 280, height: 240 }); - assert.deepEqual(DesktopAppSettings.normalizeMainWindowBounds(bounds), bounds); - }); - it("recognizes only same-origin renderer navigations", () => { assert.isTrue( DesktopWindow.isSameOriginRendererNavigation({ @@ -1267,37 +1242,83 @@ describe("DesktopWindow", () => { }), ); - it.effect("creates a resizable window within a small display's work area", () => - Effect.gen(function* () { - vi.mocked(Electron.screen.getAllDisplays).mockReturnValueOnce([ - { + it.effect.each([ + { + scenario: "missing saved bounds and a taskbar on a small display", + workArea: { x: -1280, y: 26, width: 1280, height: 694 }, + savedBounds: null, + expected: { x: -1190, y: 26, width: 1100, height: 694, minWidth: 360, minHeight: 320 }, + }, + { + scenario: "offscreen saved bounds and a work area below the preferred minimum", + workArea: { x: 40, y: 26, width: 280, height: 240 }, + savedBounds: { x: 2040, y: 80, width: 1320, height: 880 }, + expected: { x: 40, y: 26, width: 280, height: 240, minWidth: 280, minHeight: 240 }, + }, + { + scenario: "saved bounds below the preferred minimum on a large display", + workArea: { x: 0, y: 0, width: 1920, height: 1080 }, + savedBounds: { x: 12, y: 563, width: 280, height: 240 }, + expected: { x: 12, y: 563, width: 280, height: 240, minWidth: 280, minHeight: 240 }, + }, + { + scenario: "saved bounds overlapping the taskbar within the full display", + workArea: { x: 0, y: 0, width: 1920, height: 1040 }, + savedBounds: { x: 100, y: 286, width: 1100, height: 780 }, + expected: { x: 100, y: 286, width: 1100, height: 780, minWidth: 360, minHeight: 320 }, + }, + { + scenario: "a small primary display listed after a large secondary display", + workArea: { x: -800, y: 26, width: 800, height: 574 }, + secondaryWorkArea: { x: 0, y: 0, width: 1920, height: 1080 }, + savedBounds: null, + expected: { x: -800, y: 26, width: 800, height: 574, minWidth: 360, minHeight: 320 }, + }, + ])( + "opens a window that fits with $scenario", + ({ workArea, secondaryWorkArea, savedBounds, expected }) => + Effect.gen(function* () { + const previousPrimary = Electron.screen.getPrimaryDisplay(); + const display = { ...Electron.screen.getAllDisplays()[0]!, - workArea: { x: 0, y: 26, width: 800, height: 574 }, - }, - ]); - const fakeWindow = makeFakeBrowserWindow(); - const createCount = yield* Ref.make(0); - const mainWindow = yield* Ref.make>(Option.none()); - const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = []; - const layer = makeTestLayer({ - window: fakeWindow.window, - createCount, - mainWindow, - createdWindowOptions, - }); + bounds: { x: workArea.x, y: 0, width: workArea.width, height: workArea.height + 26 }, + workArea, + }; + vi.mocked(Electron.screen.getAllDisplays).mockReturnValueOnce([ + ...(secondaryWorkArea + ? [{ ...display, bounds: secondaryWorkArea, workArea: secondaryWorkArea }] + : []), + display, + ]); + vi.mocked(Electron.screen.getPrimaryDisplay).mockReturnValue(display); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + vi.mocked(Electron.screen.getPrimaryDisplay).mockReturnValue(previousPrimary); + }), + ); + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = []; + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + createdWindowOptions, + desktopSettings: { + ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS, + mainWindowBounds: savedBounds, + }, + }); - yield* Effect.gen(function* () { - const desktopWindow = yield* DesktopWindow.DesktopWindow; - yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); - }).pipe(Effect.provide(layer)); + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + }).pipe(Effect.provide(layer)); - const options = createdWindowOptions[0]; - assert.equal(options?.width, 800); - assert.equal(options?.height, 574); - assert.equal(options?.y, 26); - assert.equal(options?.minWidth, 360); - assert.equal(options?.minHeight, 320); - }), + assert.equal(createdWindowOptions.length, 1); + assert.containsSubset(createdWindowOptions[0], expected); + }).pipe(Effect.scoped), ); it.effect("logs display lookup failures before falling back to the default size", () => diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 61f569c0d4865..577901d58258c 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -55,6 +55,7 @@ function syncMacosWindowButtons(window: Electron.BrowserWindow): void { const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937"; const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc"; +const MIN_MAIN_WINDOW_SIZE = { width: 360, height: 320 } as const; const MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS = 500; const DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const; const RENDERER_RECOVERY_RELOAD_DELAY_MS = 500; @@ -164,6 +165,7 @@ function windowBoundsEqual( export function resolveInitialMainWindowBounds( persistedBounds: DesktopAppSettings.DesktopWindowBounds | null, displays: readonly DisplayBounds[], + primaryWorkArea: DisplayBounds | null = null, ): DesktopAppSettings.DesktopWindowBounds | typeof DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE { if ( persistedBounds !== null && @@ -171,7 +173,7 @@ export function resolveInitialMainWindowBounds( ) { return persistedBounds; } - const display = displays[0]; + const display = primaryWorkArea; const defaults = DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE; if (!display || (defaults.width <= display.width && defaults.height <= display.height)) { return defaults; @@ -371,7 +373,8 @@ export const make = Effect.gen(function* () { try { return { _tag: "Success" as const, - bounds: Electron.screen.getAllDisplays().map((display) => display.workArea), + bounds: Electron.screen.getAllDisplays().map((display) => display.bounds), + primaryWorkArea: Electron.screen.getPrimaryDisplay().workArea, }; } catch (cause) { return { _tag: "Failure" as const, cause }; @@ -383,16 +386,19 @@ export const make = Effect.gen(function* () { : yield* logWindowWarning("failed to read connected displays; using defaults", { cause: displayBoundsResult.cause, }).pipe(Effect.as([])); - const initialBounds = resolveInitialMainWindowBounds(persistedBounds, displayBounds); + const initialBounds = resolveInitialMainWindowBounds( + persistedBounds, + displayBounds, + displayBoundsResult._tag === "Success" ? displayBoundsResult.primaryWorkArea : null, + ); const restoredPersistedBounds = persistedBounds !== null && initialBounds === persistedBounds; if (persistedBounds !== null && !restoredPersistedBounds) { yield* logWindowWarning("saved main window bounds could not be restored; using defaults"); } const window = yield* electronWindow.create({ ...initialBounds, - // Let small displays and tiling compositors reach the responsive layout. - minWidth: Math.min(DesktopAppSettings.MIN_MAIN_WINDOW_SIZE.width, initialBounds.width), - minHeight: Math.min(DesktopAppSettings.MIN_MAIN_WINDOW_SIZE.height, initialBounds.height), + minWidth: Math.min(MIN_MAIN_WINDOW_SIZE.width, initialBounds.width), + minHeight: Math.min(MIN_MAIN_WINDOW_SIZE.height, initialBounds.height), show: false, autoHideMenuBar: true, ...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}), From aff641ecb095136c41d8371cfefdf57ed30a2bd5 Mon Sep 17 00:00:00 2001 From: Matthew Feroz Date: Tue, 27 Oct 2026 15:31:07 -0400 Subject: [PATCH 4/4] fix(desktop): center the default window within the work area --- apps/desktop/src/window/DesktopWindow.test.ts | 6 ++++++ apps/desktop/src/window/DesktopWindow.ts | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 03c0e3a47a757..d5b0e5c057cfe 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -907,6 +907,12 @@ describe("DesktopWindow", () => { savedBounds: null, expected: { x: -1190, y: 26, width: 1100, height: 694, minWidth: 360, minHeight: 320 }, }, + { + scenario: "missing saved bounds and default size that just fits above the Dock", + workArea: { x: 0, y: 25, width: 1440, height: 800 }, + savedBounds: null, + expected: { x: 170, y: 35, width: 1100, height: 780, minWidth: 360, minHeight: 320 }, + }, { scenario: "offscreen saved bounds and a work area below the preferred minimum", workArea: { x: 40, y: 26, width: 280, height: 240 }, diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 62251bf9b4401..baf4aac3c48d0 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -174,9 +174,8 @@ export function resolveInitialMainWindowBounds( } const display = primaryWorkArea; const defaults = DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE; - if (!display || (defaults.width <= display.width && defaults.height <= display.height)) { - return defaults; - } + // Electron centers an unpositioned macOS window on the full screen, which can put it under the Dock. + if (!display) return defaults; const width = Math.min(defaults.width, display.width); const height = Math.min(defaults.height, display.height); return {