Skip to content

Fix Cmd+N window size after last window closes - #3611

Merged
austinywang merged 8 commits into
mainfrom
issue-3509-cmd-n-window-size
May 7, 2026
Merged

austinywang merged 8 commits into
mainfrom
issue-3509-cmd-n-window-size

Conversation

@austinywang

@austinywang austinywang commented May 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Testing

  • git diff --check
  • Not run locally: XCTest/build, per repo policy to use CI.

Note

Medium Risk
Changes main-window creation geometry selection to pull from persisted state and clamp to screen bounds, which could affect window placement/sizing across restore and multi-display scenarios.

Overview
Fixes new main windows (e.g., Cmd+N after the last window closes) opening at a tiny fallback size by reusing the persisted last-window frame when there is no live sourceWindow.

Replaces the hardcoded 460x360 fallback with a 1000x700 default content size clamped to the current visible display, and moves MainWindowHostingView plus sizing helpers into CmuxMainWindow.

Adds a regression test that seeds persisted window geometry in UserDefaults and asserts createMainWindow(sourceWindow: nil) uses that saved frame (via a DEBUG-only hook to access the defaults key).

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


Summary by cubic

Fixes the tiny window created by Cmd+N after the last window closes by reusing the last saved window geometry. Falls back to a centered 1000x700 default clamped to the visible display when no geometry is saved.

  • Bug Fixes

    • Apply initial frame in this order—even with a malformed snapshot: session snapshot → source window → persisted geometry → default 1000x700 (clamped).
    • Centralize geometry selection in AppDelegate.createMainWindow to cover Cmd+N, Dock, and service entrypoints.
    • Add a regression test that seeds persisted geometry and verifies the created window frame.
  • Refactors

    • Move default-window sizing/clamping and MainWindowHostingView into CmuxMainWindow.
    • Keep persisted-geometry helpers private in AppDelegate with a DEBUG-only defaults-key hook; restore normal formatting while staying within file-length budgets.

Written for commit 474fbb3. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Refactor

    • Improved window geometry persistence and frame restoration for more reliable window positioning.
    • Enhanced window layout constraint handling and safe-area management.
  • Tests

    • Added test coverage for window geometry persistence functionality.

The no-source main-window path currently falls back to a tiny built-in frame instead of the last persisted geometry, so add a regression test that seeds the runtime defaults payload and creates a real main window through AppDelegate.

Constraint: Regression policy requires the failing test to land before the fix

Confidence: high

Scope-risk: narrow

Directive: Keep this test behavior-level; do not replace it with source-text assertions

Tested: git diff --check

Not-tested: XCTest not run locally per repo policy
Cmd+N after the last window closes reaches createMainWindow without a live source window. That path now resolves initial geometry in one AppDelegate-owned order: session snapshot, live source window, persisted last geometry, then a centered 1000x700 fallback clamped to the visible display.

Constraint: New main-window creation has multiple entrypoints, so the geometry policy belongs in createMainWindow rather than a menu-only handler

Rejected: Add only a SwiftUI defaultSize | main windows are AppKit-created here and the bug also affects Dock/socket/service entrypoints

Confidence: high

Scope-risk: narrow

Directive: Keep new-main-window geometry fallback centralized in AppDelegate.createMainWindow

Tested: git diff --check

Not-tested: XCTest not run locally per repo policy
@vercel

vercel Bot commented May 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled May 7, 2026 0:07am
cmux-staging Building Building Preview, Comment May 7, 2026 0:07am

@coderabbitai

coderabbitai Bot commented May 6, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change addresses the Cmd+N new-window issue by introducing a custom MainWindowHostingView that manages safe-area behavior and layout guides, adding persisted-geometry resolution helpers to AppDelegate, and updating window creation to apply resolved persisted geometry. A test validates the persisted-geometry path for new windows without source context.

Changes

New-Window Geometry Restoration

Layer / File(s) Summary
Hosting View & Geometry Helpers
Sources/App/CmuxMainWindow.swift
New MainWindowHostingView<Content: View> overrides safe-area insets and installs a layout guide. Extension adds defaultContentSize, defaultContentRect(styleMask:), and clampedFrame(_:within:) static helpers to compute and constrain window geometry.
Persistence & Resolution Logic
Sources/AppDelegate.swift
Adds debug property debugPersistedWindowGeometryDefaultsKey. Refactors persistedWindowGeometry(defaults:) formatting. Introduces resolvedPersistedWindowGeometryFrame() helper to combine current display geometries with persisted geometry, routing through existing frame-resolution logic.
Window Creation Integration
Sources/AppDelegate.swift
createMainWindow(...) now computes explicitInitialFrame from either session-restored frame or resolved persisted geometry, applies it early with setFrame(..., display: false), and logs debug context based on restoration source (session vs. persisted).
Test Validation
cmuxTests/AppDelegateBareSpaceShortcutRoutingTests.swift
New test testCreateMainWindowUsesPersistedGeometryWhenNoSourceWindow seeds UserDefaults with persisted geometry, creates a window with no source context, and asserts the created window frame matches the persisted dimensions/position. Helper restoreDefaultsValue(_:forKey:defaults:) manages cleanup.

Possibly Related PRs

  • manaflow-ai/cmux#2129: Both PRs refactor AppDelegate's window-geometry restoration logic and frame-resolution helpers for persisted/restored window frames.
  • manaflow-ai/cmux#2306: Both PRs modify AppDelegate's window-geometry persistence/restore logic and frame-resolution helpers.
  • manaflow-ai/cmux#2345: Both PRs adjust AppDelegate.createMainWindow's geometry/source-resolution and window-initialization logic.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~23 minutes

Poem

🐰 A window that knew not its frame, now remembers with grace—
No more tiny squares where grand vistas should embrace!
Cmd+N whispers to geometry long-saved,
Safe-areas bow, and the layout guides paved.
Hop toward the last window you loved, brave and true!

🚥 Pre-merge checks | ✅ 13
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main fix: resolving the issue where Cmd+N creates a small window after the last window closes by restoring proper window sizing behavior.
Linked Issues check ✅ Passed All code changes directly address #3509: reuses persisted geometry when Cmd+N creates windows with no source window, implements geometry-selection priority order, adds regression test with DEBUG hook, and centralizes window sizing logic in AppDelegate and CmuxMainWindow.
Out of Scope Changes check ✅ Passed All changes are in scope: window geometry logic refactoring in AppDelegate and CmuxMainWindow, regression test setup, and DEBUG-only test helpers. No unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation issues introduced. CmuxMainWindow and AppDelegate are @MainActor. New static methods are pure utilities. State objects created in @MainActor context.
Cmux Swift Blocking Runtime ✅ Passed Geometry helpers are synchronous with no blocking primitives. No DispatchSemaphore, Task.sleep, asyncAfter, locks, or main-queue sync in production. Test uses allowed RunLoop for deterministic timing.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. All new code uses synchronous geometry calculations appropriate for UI initialization with proper AppKit boundaries.
Cmux Swift @Concurrent ✅ Passed PR introduces only synchronous Swift code. No async/await patterns, no @concurrent annotations added or needed. All functions properly isolated within @MainActor contexts.
Cmux Swift File And Package Boundaries ✅ Passed CmuxMainWindow.swift (88 lines) is pure AppKit code below 400-line threshold. AppDelegate adds 29 lines, below 250-line limit. No mixed responsibilities. Focused bug fix.
Cmux Swift Logging ✅ Passed Single debug log added in AppDelegate.swift, guarded by #if DEBUG, uses cmuxDebugLog. No print/NSLog/dump. Allowed by rules as debug-only logging. No secrets exposed.
Cmux Swiftui State Layout ✅ Passed PR changes do not violate SwiftUI state layout rules. All modifications are AppKit bridge classes, AppKit window management, or refactored existing code—no new SwiftUI state patterns introduced.
Cmux Architecture Rethink ✅ Passed Sound correctness fix with clear owner, documented invariant, no timing patches, no new mutable state, unified entrypoints. Small fix, required platform bridge code, test-only synchronization.
Description check ✅ Passed The pull request description covers the summary, testing approach, and includes detailed context from code review bots, but the standard template sections (Demo Video and Checklist) are missing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-3509-cmd-n-window-size

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.

The guard budget rejects growth in AppDelegate and AppDelegateShortcutRoutingTests, so move the default-window geometry helper onto the existing CmuxMainWindow type and place the regression in a smaller AppDelegate test file while preserving the same behavior.

Constraint: CI enforces per-file Swift line budgets

Rejected: Raise the file length budget | this fix can stay within the existing limits

Confidence: high

Scope-risk: narrow

Directive: Keep future AppDelegate window-placement growth out of the large AppDelegate file

Tested: git diff --check

Tested: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Not-tested: XCTest not run locally per repo policy
@greptile-apps

greptile-apps Bot commented May 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the tiny 460×360 Cmd+N window that appeared after the last window closed by inserting persisted last-window geometry as a third priority in createMainWindow's frame selection chain (session snapshot → source window → persisted geometry → 1000×700 clamped default).

  • Frame selection rewrite: resolvedPersistedWindowGeometryFrame() is added as a private AppDelegate helper; createMainWindow evaluates it only when there is no restored session frame and no source window, keeping the new code on a single execution path that covers Cmd+N, Dock, and service entrypoints.
  • MainWindowHostingView relocation: Moved from AppDelegate.swift to CmuxMainWindow.swift alongside a new geometry extension; an explicit deinit {} is added to satisfy Swift 6 actor-isolation requirements for NSHostingView subclasses.
  • Regression test: Seeds UserDefaults with encoded PersistedWindowGeometry, calls createMainWindow with no source window, and asserts the created window frame matches within 1pt; a DEBUG-only debugPersistedWindowGeometryDefaultsKey property lets the test reference the production key without duplicating its string literal.

Confidence Score: 5/5

Safe to merge; the change is a well-scoped frame-priority fix with a matching regression test and no correctness issues in the new geometry logic.

The frame selection chain is correct in all four branches (session snapshot, source window, persisted geometry, hardcoded default). The private helper resolvedPersistedWindowGeometryFrame lives in AppDelegate.swift alongside the two private methods it calls, so no access-control widening is needed. The clampedFrame helper only operates on already-validated NSRect values from NSScreen.visibleFrame and has a clean guard for zero-size displays. The empty deinit {} in MainWindowHostingView is the correct Swift 6 pattern for NSHostingView subclasses. The test correctly saves and restores UserDefaults in a defer block and asserts frame dimensions within a 1pt tolerance.

No files require special attention.

Important Files Changed

Filename Overview
Sources/App/CmuxMainWindow.swift Receives MainWindowHostingView (moved from AppDelegate.swift) and a new CmuxMainWindow extension with defaultContentRect/clampedFrame geometry helpers; adds an empty deinit {} for Swift 6 actor isolation correctness.
Sources/AppDelegate.swift Adds resolvedPersistedWindowGeometryFrame() private helper and wires persisted geometry as the third priority in createMainWindow's frame selection chain; exposes the defaults key under a DEBUG-only accessor for testing.
cmuxTests/AppDelegateBareSpaceShortcutRoutingTests.swift Adds a regression test that seeds UserDefaults with persisted geometry and asserts the created window frame matches; cleanup is handled with defer blocks and a new restoreDefaultsValue helper.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[createMainWindow called] --> B{sessionWindowSnapshot?}
    B -- yes --> C[resolvedWindowFrame returns restoredFrame]
    B -- no --> D{sourceWindow?}
    D -- yes --> E[existingFrame from sourceWindow.frame]
    D -- no --> F[resolvedPersistedWindowGeometryFrame]
    F --> G{persistedGeometry in UserDefaults?}
    G -- yes --> H[Decode and map to current display - persistedGeometryFrame]
    G -- no --> I[CmuxMainWindow.defaultContentRect 1000x700 clamped to visibleFrame]
    C --> J[initialRect = contentRect for restoredFrame, setFrame restoredFrame]
    E --> K[initialRect = contentRect for existingFrame, positionNewMainWindow relative to source]
    H --> L[initialRect = contentRect for persistedGeometryFrame, setFrame persistedGeometryFrame]
    I --> M[initialRect = defaultContentRect, window.center + cascade]
    J --> N[Window shown]
    K --> N
    L --> N
    M --> N
    N --> O{explicitInitialFrame set?}
    O -- yes --> P[setFrame again with display true + DEBUG log]
    O -- no --> Q[done]
    P --> Q
Loading

Reviews (5): Last reviewed commit: "Preserve window geometry fallback order" | Re-trigger Greptile

Comment thread Sources/App/CmuxMainWindow.swift Outdated
Comment thread cmuxTests/AppDelegateBareSpaceShortcutRoutingTests.swift
The Cmd+N geometry fix should not widen AppDelegate internals just to satisfy tests or a helper placement. Move the persisted geometry resolver back into AppDelegate's implementation file, keep the helper methods private, and expose only a DEBUG test hook for the defaults key so the regression test follows the production key without duplicating the literal.

Constraint: Swift file length budget blocks unbounded AppDelegate growth

Rejected: Leave the resolver in CmuxMainWindow.swift | forces unrelated AppDelegate helpers to become internal

Rejected: Keep the test's duplicated defaults key | can drift from production persistence schema

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Tested: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Not-tested: XCTest per repository policy to prefer CI and user instruction forbidding direct xcodebuild
The previous review fix preserved the Swift file budget by compressing the persisted-geometry helper, but that made the window sizing path harder to read. Move MainWindowHostingView beside CmuxMainWindow in the existing main-window source file, then restore the AppDelegate geometry helper to normal Swift formatting while keeping the file under budget.

Constraint: Swift file length budget must remain green

Rejected: Keep the semicolon-packed helper | non-idiomatic and called out by review

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Tested: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Not-tested: XCTest per repository policy to prefer CI and user instruction forbidding direct xcodebuild

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/App/CmuxMainWindow.swift`:
- Around line 4-26: Add an explicit empty deinit to the MainWindowHostingView
class to satisfy the required_deinit lint rule: inside final class
MainWindowHostingView<Content: View> { ... } add a deinit { } implementation (no
teardown needed) so the class defines a deinitializer while leaving existing
properties and init(coder:) unchanged.
- Around line 63-74: defaultContentRect currently builds a content rect at
origin .zero then clamps the frame but leaves the rect bottom-left positioned,
which is confusing; update CmuxMainWindow.defaultContentRect to explicitly
return a clamped content rect without attempting to center (i.e., compute
frameRect via NSWindow.frameRect(forContentRect: defaultContentSize, styleMask:
styleMask), clamp it with clampedFrame(..., within: visibleFrame), and return
NSWindow.contentRect(forFrameRect: clamped, styleMask: styleMask)), and add a
brief doc comment to defaultContentRect stating it returns an
unpositioned/clamped rect so callers (window.center(), positionNewMainWindow(),
etc.) are responsible for final placement.

In `@Sources/AppDelegate.swift`:
- Around line 6865-6874: The current logic gates sourceWindow and
persisted-geometry fallbacks on sessionWindowSnapshot == nil, so if session
snapshot exists but resolvedWindowFrame(from:) returns nil you skip to default
sizing; change the selection order to explicitly try the snapshot-derived frame
first, then sourceWindow, then persisted geometry, then default. Concretely,
call resolvedWindowFrame(from: sessionWindowSnapshot) (or equivalent) into a
snapshotFrame variable and use it if non-nil; otherwise if let existingFrame
(from sourceWindow) use that; otherwise use restoredFrame ??
resolvedPersistedWindowGeometryFrame(); for whichever frame you pick convert to
content rect via NSWindow.contentRect(forFrameRect: styleMask:) and assign to
initialRect. Ensure persistedGeometryFrame is computed but not conditionally
hidden behind sessionWindowSnapshot == nil so the fallback order is: session
snapshot → sourceWindow → persisted → default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a886d8d1-a017-4bd1-9381-02dd756ca21c

📥 Commits

Reviewing files that changed from the base of the PR and between e00fbfc and af8048e.

📒 Files selected for processing (3)
  • Sources/App/CmuxMainWindow.swift
  • Sources/AppDelegate.swift
  • cmuxTests/AppDelegateBareSpaceShortcutRoutingTests.swift

Comment thread Sources/App/CmuxMainWindow.swift
Comment thread Sources/App/CmuxMainWindow.swift
Comment thread Sources/AppDelegate.swift Outdated
CodeRabbit caught that a malformed session snapshot could skip the source-window and persisted-geometry fallbacks. Keep the intended priority explicit: snapshot frame first, then source window, then persisted geometry, then the clamped default. Also add the required empty deinit for MainWindowHostingView and document that defaultContentRect only supplies a clamped, unpositioned content rect.

Constraint: Do not edit CI workflow YAML files while updating the PR

Rejected: Keep gating source-window fallback on sessionWindowSnapshot == nil | invalid snapshots would jump straight to default sizing

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Tested: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Not-tested: Local build or XCTest; per repo policy and user instruction, no xcodebuild was run

This branch was successfully deployed

1 active deployment
Preview – cmux — 474fbb39 Deployed May 7, 2026 by vercel[bot]
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.

New window from Cmd+N ignores saved frame and opens at SwiftUI default size (~460×360)

1 participant