Skip to content

feat (desktop): persist window size and restore macOS fullscreen exits cleanly#3597

Open
rushilrai wants to merge 1 commit into
pingdotgg:mainfrom
rushilrai:feat/desktop-window-state
Open

feat (desktop): persist window size and restore macOS fullscreen exits cleanly#3597
rushilrai wants to merge 1 commit into
pingdotgg:mainfrom
rushilrai:feat/desktop-window-state

Conversation

@rushilrai

@rushilrai rushilrai commented Jun 29, 2026

Copy link
Copy Markdown

What Changed

This PR enables persistence of window states for the desktop app across relaunches

It adds a DesktopWindowState service (apps/desktop/src/window/DesktopWindowState.ts) that persists the main window's bounds and restore mode to window-state.json and restores them on launch.

It restores:

  • normal window size and position
  • maximized state
  • windows closed from macOS true fullscreen back onto the current desktop, without the white startup flash

Keeps normal restore bounds separate from the fullscreen-origin visible frame, so standard maximize behaviour stays intact. Missing, corrupt, or off-screen state safely falls back to the centered default.

Implementation notes:

  • Geometry math are pure, unit-tested helpers; Electron's screen is read at a single spot.
  • Persistence uses the Effect FileSystem layer with atomic temp-file writes, a debounced interruptible-fiber save on resize/move, and an immediate save on maximize / fullscreen toggle / close.
  • Wired into DesktopWindow.createWindow (initial bounds + maximize-on-reveal), DesktopEnvironment (windowStatePath), and the desktop foundation layer in main.ts.

Why

Right now the desktop app always comes back at the default size, which is frustrating if you keep it in a specific layout.
This change makes relaunch behavior feel much closer to what users expect from native desktop apps.

UI Changes

Desktop window restore behaviour only.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Low Risk
Local desktop window UX and JSON persistence only; no auth, network, or shared backend changes, with fallbacks and unit tests for load/sanitize paths.

Overview
Adds DesktopWindowState, which reads and writes window-state.json under the app state directory so the main window reopens with the last size, position, and restore mode instead of fixed defaults.

DesktopWindow now loads those bounds when creating the main window, calls attach to save on move/resize (debounced) and on maximize/fullscreen/close, and maximizes before reveal when the saved mode was maximized. A fullscreen-origin mode stores the pre-fullscreen frame so relaunch after macOS true fullscreen can reopen on the current desktop without re-entering fullscreen.

Missing, invalid, version-mismatched, or off-screen saved state falls back to centered defaults on the primary display. DesktopEnvironment exposes windowStatePath; the service is registered in the desktop foundation layer in main.ts. Geometry helpers and load behavior are covered by dedicated tests; DesktopWindow tests stub the service to stay filesystem-free.

Reviewed by Cursor Bugbot for commit 1708db1. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Persist window size and restore maximized state on desktop window creation

  • Adds a new DesktopWindowState service (DesktopWindowState.ts) that loads and saves window geometry to a window-state.json file in the app state directory.
  • On window creation, restores previous position, size, and maximized state; defaults to centered primary display bounds and clamps to minimum dimensions.
  • Validates restored bounds against current display geometry, falling back to centered defaults if the window would be off-screen or too small.
  • State is persisted with debounced, atomic writes on resize, move, maximize, and fullscreen events; fullscreen exits restore the pre-fullscreen bounds cleanly on macOS.
  • Behavioral Change: windows now open at their last known position and size rather than at a fixed default.
📊 Macroscope summarized 1708db1. 4 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ec378d5d-c518-45b7-bec4-84485fa5c546

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 29, 2026
window.setAutoHideCursor(false);
}

yield* windowState.attach(window);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium window/DesktopWindow.ts:292

windowState.attach(window) is called immediately after window creation, before the reveal callback runs window.maximize(). If the window is closed before ready-to-show/did-finish-load fires (e.g. slow or failed renderer startup), the close handler persists window.isMaximized() as false, overwriting the saved maximized state. The next launch then restores a normal-sized window instead of the user's maximized state. Consider calling window.maximize() right after windowState.attach, or deferring windowState.attach until after the maximize is applied.

Also found in 1 other location(s)

apps/desktop/src/window/DesktopWindowState.ts:251

When resolveDocument sees window.isFullScreen(), it unconditionally writes restoreMode: "fullscreen-origin" and drops whether the pre-fullscreen window was maximized. If the user enters macOS fullscreen from a maximized window and quits there, the next launch restores a large normal window instead of re-maximizing, so the previous maximize state is silently lost.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/window/DesktopWindow.ts around line 292:

`windowState.attach(window)` is called immediately after window creation, before the reveal callback runs `window.maximize()`. If the window is closed before `ready-to-show`/`did-finish-load` fires (e.g. slow or failed renderer startup), the close handler persists `window.isMaximized()` as `false`, overwriting the saved `maximized` state. The next launch then restores a normal-sized window instead of the user's maximized state. Consider calling `window.maximize()` right after `windowState.attach`, or deferring `windowState.attach` until after the maximize is applied.

Also found in 1 other location(s):
- apps/desktop/src/window/DesktopWindowState.ts:251 -- When `resolveDocument` sees `window.isFullScreen()`, it unconditionally writes `restoreMode: "fullscreen-origin"` and drops whether the pre-fullscreen window was maximized. If the user enters macOS fullscreen from a maximized window and quits there, the next launch restores a large normal window instead of re-maximizing, so the previous maximize state is silently lost.

const persist = (document: PersistedWindowStateDocument): Effect.Effect<void> =>
Effect.gen(function* () {
const directory = path.dirname(windowStatePath);
const tempPath = `${windowStatePath}.${process.pid}.tmp`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium window/DesktopWindowState.ts:231

persist writes every save through the same fixed temp path `${windowStatePath}.${process.pid}.tmp`. Overlapping saves — e.g. a debounced resize persist racing with an immediate close or maximize persist — clobber each other's temp file, so the final rename can persist a stale document or one writeFileString/rename fails and is silently swallowed by the catch. Consider using a unique temp path per persist call (e.g. appending a random or counter suffix) to prevent concurrent writes from colliding.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/window/DesktopWindowState.ts around line 231:

`persist` writes every save through the same fixed temp path `` `${windowStatePath}.${process.pid}.tmp` ``. Overlapping saves — e.g. a debounced resize persist racing with an immediate `close` or `maximize` persist — clobber each other's temp file, so the final `rename` can persist a stale document or one `writeFileString`/`rename` fails and is silently swallowed by the `catch`. Consider using a unique temp path per persist call (e.g. appending a random or counter suffix) to prevent concurrent writes from colliding.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1708db1. Configure here.

if (!isWindowVisibleEnough(sanitizedOrigin, workAreas)) {
return fallback;
}
return { bounds: sanitizedOrigin, restoreMode: "fullscreen-origin" };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fullscreen restore blocked by normalBounds

Medium Severity

When restoreMode is fullscreen-origin, the load function prematurely rejects persisted window state if normalBounds fail the visibility check. This prevents evaluation of fullscreenOriginBounds, causing the window to reset to default positioning, particularly after display layout changes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1708db1. Configure here.

window.on("unmaximize", persistNow);
window.on("enter-full-screen", persistNow);
window.on("leave-full-screen", persistNow);
window.on("close", persistNow);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fullscreen quit loses origin state

High Severity

On macOS, quitting while in native fullscreen often emits leave-full-screen before close. Both handlers call persistNow, and resolveDocument only writes fullscreen-origin when window.isFullScreen() is still true, so the final save becomes normal/maximized and overwrites the earlier fullscreen snapshot—undermining the PR’s fullscreen exit restore behavior.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1708db1. Configure here.

// Re-maximize before showing so the window doesn't flash at normal bounds first.
if (initialWindowState.restoreMode === "maximized" && !window.isDestroyed()) {
window.maximize();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early quit drops maximized mode

Medium Severity

Restored maximized mode is applied only in the first-reveal callback, while attach persists on close from the live window via readRestorableState. If the user quits while the main window is still hidden (e.g. during the connecting splash), isMaximized() is false and the saved file drops back to normal, losing maximized layout on the next launch.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1708db1. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new window state persistence feature with ~300 lines of new logic. Multiple unresolved review comments identify substantive bugs, including a high-severity race condition where quitting in fullscreen loses the origin state. Human review is needed to address these issues and validate the new functionality.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant