Skip to content

feat(desktop): persist window size and position across app restarts#550

Merged
Kitenite merged 2 commits intosuperset-sh:mainfrom
andreasasprou:persist-window-size
Jan 4, 2026
Merged

feat(desktop): persist window size and position across app restarts#550
Kitenite merged 2 commits intosuperset-sh:mainfrom
andreasasprou:persist-window-size

Conversation

@andreasasprou
Copy link
Copy Markdown
Contributor

@andreasasprou andreasasprou commented Dec 30, 2025

Summary

This PR adds window size and position persistence so the app remembers its bounds across restarts.

Details

Architecture

  • Separate persistence module: Window state stored in ~/.superset/window-state.json, separate from UI state (tabs/theme) in app-state.json
  • Sync file I/O: Read synchronously at startup (before window creation), write synchronously on close
  • Atomic writes: Temp file + rename pattern prevents corruption on interrupted writes
  • Multi-monitor aware: Validates saved bounds against connected displays, falls back gracefully

Why separate from app-state?

Aspect UI State (tabs, theme) Window State
Written by Renderer (via tRPC) Main process only
Access pattern Async LowDB Sync fs read/write
Read timing After window created Before window creation
Failure impact Lose tabs = bad UX Lose position = minor

Different ownership, lifecycle, and access patterns → separate persistence mechanisms. This also avoids concurrent write issues with LowDB.

How it works

  1. On startup: Load window-state.json → validate bounds visible on connected displays → restore position/size
  2. If maximized was saved: Restore bounds first, then maximize (preserves un-maximized position)
  3. On close: Save current bounds atomically before cleanup
  4. Monitor disconnected: Use saved dimensions, but center on primary display

Design Principles

  1. Separation of Concerns — Window state is main-process-only with sync I/O; UI state is renderer-driven with async LowDB
  2. Fail-Safe by Default — Atomic writes, parse errors → graceful fallback, Number.isFinite() validation
  3. Defensive Multi-Monitor — 50px overlap threshold, clamp to workArea for DPI changes
  4. Minimal Coupling — Self-contained module, zero imports from app-state/
  5. Keep Orchestrators ThinMainWindow() calls 3 helper functions, all logic in dedicated module

Manual Test Checklist

Happy path

https://www.loom.com/share/534ed26be7ba43edac81557415a34d6a

Core Functionality

  • Fresh install: Window opens at primary display size, centered
  • Resize + restart: Resize window → quit → reopen → exact size and position restored
  • Move + restart: Move window → quit → reopen → exact position restored

Multi-Monitor

  • Secondary monitor: Move to secondary monitor → quit → reopen → restores on secondary
  • Monitor disconnect: Move to secondary → disconnect monitor → reopen → centered on primary with saved dimensions

Edge Cases

  • Delete state file: Delete ~/.superset/window-state.json → reopen → falls back to defaults
  • Corrupt state file: Write invalid JSON to file → reopen → falls back to defaults (no crash)
  • Resolution change: Change display resolution → reopen → dimensions clamped to new workArea

Files Changed

New Files

  • apps/desktop/src/main/lib/window-state/window-state.ts — Load/save with atomic writes, validation
  • apps/desktop/src/main/lib/window-state/bounds-validation.ts — Multi-monitor validation, bounds calculation
  • apps/desktop/src/main/lib/window-state/index.ts — Barrel export

Modified Files

  • apps/desktop/src/main/lib/app-environment.ts — Added WINDOW_STATE_PATH constant
  • apps/desktop/src/main/windows/main.ts — Integrated window state persistence

Summary by CodeRabbit

  • New Features

    • Desktop app now persists window position, size, and maximized state across launches.
    • Improved initial window placement that adapts to display changes and recentries windows when prior positions are unavailable.
  • Bug Fixes / Reliability

    • Safer, corruption-resistant saving/loading of window state to disk.
    • Ensures windows reopen visible on current displays and restores maximized state reliably.
  • Tests

    • Comprehensive unit tests for bounds/visibility logic and window-state validation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 30, 2025

📝 Walkthrough

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding window size and position persistence across app restarts, which is the core feature introduced in this PR.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering architecture, design rationale, implementation details, manual testing, and file changes. All required template sections are addressed.
✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

@andreasasprou andreasasprou marked this pull request as draft December 30, 2025 11:19
@andreasasprou andreasasprou marked this pull request as ready for review December 30, 2025 12:02
@Kitenite
Copy link
Copy Markdown
Collaborator

Kitenite commented Jan 4, 2026

This is gonna help a ton with testing as well. Thanks @andreasasprou !

@Kitenite Kitenite merged commit 319fb77 into superset-sh:main Jan 4, 2026
4 of 5 checks passed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/desktop/src/main/lib/window-state/bounds-validation.test.ts (1)

8-9: Consider importing constants from implementation.

The magic numbers MIN_VISIBLE_OVERLAP and MIN_WINDOW_SIZE are duplicated from the implementation. If these values change in the source, tests could pass with incorrect assumptions.

🔎 Proposed refactor to import constants

If the implementation exports these constants (or can be refactored to do so), import them instead:

+import {
+	getInitialWindowBounds,
+	isVisibleOnAnyDisplay,
+	MIN_VISIBLE_OVERLAP,
+	MIN_WINDOW_SIZE,
+} from "./bounds-validation";
-
-const MIN_VISIBLE_OVERLAP = 50;
-const MIN_WINDOW_SIZE = 400;

Otherwise, add a comment documenting the source of these values:

+// Must match constants in bounds-validation.ts
 const MIN_VISIBLE_OVERLAP = 50;
 const MIN_WINDOW_SIZE = 400;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a82d646 and 35fc869.

📒 Files selected for processing (5)
  • apps/desktop/src/main/lib/window-state/bounds-validation.test.ts
  • apps/desktop/src/main/lib/window-state/index.ts
  • apps/desktop/src/main/lib/window-state/window-state.test.ts
  • apps/desktop/src/main/lib/window-state/window-state.ts
  • apps/desktop/test-setup.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/main/lib/window-state/index.ts
  • apps/desktop/src/main/lib/window-state/window-state.ts
🧰 Additional context used
📓 Path-based instructions (5)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/main/lib/window-state/window-state.test.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/main/lib/window-state/bounds-validation.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/main/lib/window-state/window-state.test.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/main/lib/window-state/bounds-validation.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Co-locate tests with implementation files using .test.ts or .test.tsx suffix

Files:

  • apps/desktop/src/main/lib/window-state/window-state.test.ts
  • apps/desktop/src/main/lib/window-state/bounds-validation.test.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/main/lib/window-state/window-state.test.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/main/lib/window-state/bounds-validation.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/main/lib/window-state/window-state.test.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/main/lib/window-state/bounds-validation.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (4)
apps/desktop/test-setup.ts (1)

102-109: LGTM! Mock setup supports window-state testing.

The extended screen mock provides the necessary display bounds for window visibility and positioning tests. The single-display configuration is appropriate as a default—individual tests can override these mocks for multi-monitor scenarios.

apps/desktop/src/main/lib/window-state/window-state.test.ts (1)

1-288: LGTM! Comprehensive type guard test coverage.

The test suite thoroughly exercises isValidWindowState across valid inputs, boundary conditions, type errors, and edge cases. The inclusion of forward-compatibility testing (line 66-77) and extreme values (MAX_SAFE_INTEGER at line 79-89) demonstrates defensive thinking.

apps/desktop/src/main/lib/window-state/bounds-validation.test.ts (2)

11-180: LGTM! Thorough visibility logic testing.

The test suite for isVisibleOnAnyDisplay covers single/multi-display configurations, edge boundaries (line 53-62), insufficient overlap (line 88-97), negative coordinates, display gaps, and edge cases including no displays (line 164-169). The boundary testing at exactly MIN_VISIBLE_OVERLAP demonstrates careful validation of strict inequality.


182-377: LGTM! Comprehensive initial bounds calculation testing.

The test suite for getInitialWindowBounds covers:

  • Centering behavior when no saved state or display disconnected
  • Exact position restoration when visible
  • Dimension clamping to work area (lines 274-318)
  • Edge case where work area < MIN_WINDOW_SIZE (lines 338-353)
  • isMaximized state preservation across scenarios
  • Multi-monitor positioning

The coverage is thorough and demonstrates proper handling of real-world scenarios like resolution changes and display disconnection.

@andreasasprou
Copy link
Copy Markdown
Contributor Author

This is gonna help a ton with testing as well. Thanks @andreasasprou !

Why I did it in the first place 😄 @Kitenite

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants