Skip to content

Fix session restore suppression on relaunch - #2469

Merged
austinywang merged 3 commits into
mainfrom
issue-2387-session-persistence-regression
Apr 1, 2026
Merged

austinywang merged 3 commits into
mainfrom
issue-2387-session-persistence-regression

Conversation

@austinywang

@austinywang austinywang commented Apr 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • ignore LaunchServices self-bundle opens during relaunch so they do not suppress session restore
  • add regression coverage for external open routing, including symlinked Finder aliases
  • preserve symlink aliases for workspace creation while still resolving symlinks for bundle exclusion comparisons

Verification

  • ./scripts/reload.sh --tag fix-finder-symlink
  • tests not run locally per repo policy

Summary by cubic

Fixes session restore being suppressed on relaunch by ignoring self-bundle URLs surfaced by LaunchServices. Preserves symlink alias paths while still excluding bundle paths. (Addresses issue-2387.)

  • Bug Fixes
    • Ignore the app bundle and its descendants when routing external opens; prevents false explicit opens on relaunch and restores previous session as expected.
    • Resolve symlinks only for excluded-root comparisons; keep alias paths when creating workspaces.
    • Added regression tests for bundle exclusions and symlinked Finder aliases.

Written for commit 8258459. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed issue where the app would incorrectly open folders on relaunch
    • Improved directory resolution to properly handle symlinked directory aliases
    • Enhanced path filtering to prevent app bundle resources from interfering with directory operations

@vercel

vercel Bot commented Apr 1, 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 Apr 1, 2026 8:38am

@coderabbitai

coderabbitai Bot commented Apr 1, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes enhance AppDelegate and FinderServicePathResolver to exclude bundle-relative paths from being opened as explicit directories, preventing LaunchServices-provided self paths from triggering folder-open handling or suppressing session restore on relaunch.

Changes

Cohort / File(s) Summary
Core Functionality
Sources/AppDelegate.swift
Added optional excludingDescendantsOf parameter to orderedUniqueDirectories(from:) with URL normalization helpers (normalizedComparisonURL, isSameOrDescendant) to filter bundle-relative paths. Updated externalOpenDirectories(from:) to exclude Bundle.main.bundleURL and adjusted control flow in openFromServicePasteboard to validate directories before preparing for explicit open intent.
Resolver Unit Tests
cmuxTests/OmnibarAndToolsTests.swift
Added withTemporaryDirectory test helper and four new test cases validating exclusion of bundle descendants, non-descendant path preservation, symlinked directory alias handling, and symlink resolution behavior during excluded root comparison.
Integration Test
cmuxTests/WindowAndDragTests.swift
Added testApplicationOpenURLsIgnoresBundleSelfPaths to verify that bundle executable paths provided via application(_:open:) do not create new workspaces/tabs.

Sequence Diagram(s)

sequenceDiagram
    participant NSApp as NSApplication
    participant AppDel as AppDelegate
    participant FSResolver as FinderServicePathResolver
    participant URLComp as URL Comparison
    
    NSApp->>AppDel: application(_:open:)
    AppDel->>FSResolver: orderedUniqueDirectories(from:excludingDescendantsOf:)
    FSResolver->>URLComp: normalizeAndCompare each URL
    URLComp->>URLComp: standardizedFileURL + resolvingSymlinksInPath
    alt Is descendant of excluded root?
        URLComp-->>FSResolver: skip (return nil)
    else Non-descendant path
        URLComp-->>FSResolver: include in results
    end
    FSResolver-->>AppDel: filtered [String] directories
    AppDel->>AppDel: prepareForExplicitOpenIntentAtStartup()
    AppDel-->>NSApp: open workspaces/windows
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Bundle paths? We hop right past!
Symlinks checked, normalization fast,
Descendants filtered, siblings stay,
Session restore won't go astray! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing session restore suppression on relaunch by ignoring LaunchServices self-bundle opens, which is the primary objective of this PR.
Description check ✅ Passed The description covers the core changes and includes verification details, but lacks a Testing section with local test verification and a Demo Video section as specified in the template.

✏️ 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-2387-session-persistence-regression

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.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 3 files

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/WindowAndDragTests.swift`:
- Around line 233-264: The test testApplicationOpenURLsIgnoresBundleSelfPaths
can falsely pass if first-run onboarding suppresses application(_:open:), so
before creating AppDelegate add the same onboarding/welcome-key setup used in
the dropped-folder test to mark onboarding as completed (i.e., set the same
UserDefaults key the dropped-folder test sets), placing that UserDefaults write
before instantiating app = AppDelegate() so application(_:open:) is exercised
normally and the bundle-self filtering logic in application(_:open:) is actually
tested.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e58d057-3ca4-45d0-855d-0aa251650257

📥 Commits

Reviewing files that changed from the base of the PR and between 7f22456 and 8258459.

📒 Files selected for processing (3)
  • Sources/AppDelegate.swift
  • cmuxTests/OmnibarAndToolsTests.swift
  • cmuxTests/WindowAndDragTests.swift

Comment on lines +233 to +264
func testApplicationOpenURLsIgnoresBundleSelfPaths() {
_ = NSApplication.shared
let app = AppDelegate()

let windowId = UUID()
let window = makeMainWindow(id: windowId)
defer { window.orderOut(nil) }

let manager = TabManager()
app.registerMainWindow(
window,
windowId: windowId,
tabManager: manager,
sidebarState: SidebarState(),
sidebarSelectionState: SidebarSelectionState()
)

window.makeKeyAndOrderFront(nil)
_ = app.synchronizeActiveMainWindowContext(preferredWindow: window)

let existingWorkspaceIds = Set(manager.tabs.map(\.id))
let embeddedExecutableURL = Bundle.main.bundleURL
.appendingPathComponent("Contents/MacOS/cmux", isDirectory: false)

app.application(
NSApplication.shared,
open: [embeddedExecutableURL]
)

let createdWorkspace = manager.tabs.first { !existingWorkspaceIds.contains($0.id) }
XCTAssertNil(createdWorkspace)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stabilize onboarding preconditions so this regression can’t pass for the wrong reason.

If first-run onboarding suppresses application(_:open:) routing, this test can pass even when bundle-self filtering regresses. Mirror the welcome-key setup used in the dropped-folder test to isolate the intended behavior.

Suggested patch
     func testApplicationOpenURLsIgnoresBundleSelfPaths() {
         _ = NSApplication.shared
         let app = AppDelegate()
@@
         window.makeKeyAndOrderFront(nil)
         _ = app.synchronizeActiveMainWindowContext(preferredWindow: window)

+        let defaults = UserDefaults.standard
+        let previousWelcomeShown = defaults.object(forKey: WelcomeSettings.shownKey)
+        defaults.set(true, forKey: WelcomeSettings.shownKey)
+        defer {
+            if let previousWelcomeShown {
+                defaults.set(previousWelcomeShown, forKey: WelcomeSettings.shownKey)
+            } else {
+                defaults.removeObject(forKey: WelcomeSettings.shownKey)
+            }
+        }
+
         let existingWorkspaceIds = Set(manager.tabs.map(\.id))
         let embeddedExecutableURL = Bundle.main.bundleURL
             .appendingPathComponent("Contents/MacOS/cmux", isDirectory: false)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/WindowAndDragTests.swift` around lines 233 - 264, The test
testApplicationOpenURLsIgnoresBundleSelfPaths can falsely pass if first-run
onboarding suppresses application(_:open:), so before creating AppDelegate add
the same onboarding/welcome-key setup used in the dropped-folder test to mark
onboarding as completed (i.e., set the same UserDefaults key the dropped-folder
test sets), placing that UserDefaults write before instantiating app =
AppDelegate() so application(_:open:) is exercised normally and the bundle-self
filtering logic in application(_:open:) is actually tested.

@greptile-apps

greptile-apps Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a session-restore suppression bug on relaunch: macOS LaunchServices was passing the running app bundle's URL to application(_:open:), causing prepareForExplicitOpenIntentAtStartup() to be called (which wiped startupSessionSnapshot) even though no real folder was being opened. The fix filters out self-bundle URLs before any decision about explicit opens is made.

Key changes:

  • FinderServicePathResolver gains two private helpers — normalizedComparisonURL (standardise + resolve symlinks) and isSameOrDescendant (path-component prefix match on the resolved forms) — and orderedUniqueDirectories gains an excludingDescendantsOf parameter that uses them.
  • externalOpenDirectories passes [Bundle.main.bundleURL] as the exclusion root, so bundle-interior paths are silently dropped before directories is evaluated.
  • In openFromServicePasteboard, prepareForExplicitOpenIntentAtStartup() is moved to after the guard !directories.isEmpty check, matching the existing pattern in application(_:open:) and preventing the state mutation when nothing would actually be opened.
  • Symlink aliases are intentionally preserved in the output path (only standardizedFileURL, no symlink resolution), while symlinks are resolved for the exclusion comparison via resolvingSymlinksInPath() — so a Finder alias to the bundle is correctly excluded without mangling the workspace path for legitimate external opens.
  • Four unit tests in OmnibarAndToolsTests.swift cover bundle exclusion, sibling non-exclusion, symlink alias preservation, and symlink-resolved-only comparison with real on-disk symlinks.
  • One integration test in WindowAndDragTests.swift verifies that opening a bundle-internal executable URL does not create a new workspace.

Confidence Score: 5/5

Safe to merge — fix is logically correct, follows the two-commit policy, and is well-covered by unit and integration tests.

All remaining findings are P2. No logic errors, data-loss risk, or broken contracts found. The core fix matches the pre-existing pattern in application(_:open:).

cmuxTests/WindowAndDragTests.swift — missing welcome-screen suppression boilerplate that could cause a false failure on a fresh CI runner.

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Adds isSameOrDescendant + normalizedComparisonURL helpers to FinderServicePathResolver and uses them to exclude self-bundle URLs from externalOpenDirectories; also moves prepareForExplicitOpenIntentAtStartup() in openFromServicePasteboard to after both non-empty guards.
cmuxTests/OmnibarAndToolsTests.swift Adds four unit tests for orderedUniqueDirectories covering bundle exclusion, sibling non-exclusion, symlink alias preservation, and symlink-resolved-only comparison using real temporary directories.
cmuxTests/WindowAndDragTests.swift Adds testApplicationOpenURLsIgnoresBundleSelfPaths integration test; missing UserDefaults welcome-screen suppression boilerplate present in the sister test, risking a false failure on a fresh CI runner.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["application(_:open: urls)"] --> B["externalOpenDirectories(from: urls)"]
    B --> C["Filter: isFileURL only"]
    C --> D["orderedUniqueDirectories(excludingDescendantsOf: [Bundle.main.bundleURL])"]
    D --> E{"For each URL"}
    E --> F["resolvedDirectoryURL(url)\n(standardizedFileURL only, symlinks preserved)"]
    F --> G{"isSameOrDescendant(directoryURL, of: bundleURL)?"}
    G -- "normalizedComparisonURL\n(standardize + resolvingSymlinks) on BOTH sides" --> H{"Prefix match of path components?"}
    H -- "Yes: bundle path" --> I["skip / continue"]
    H -- "No: external path" --> J["canonicalDirectoryPath (symlink alias preserved)"]
    J --> K["Append to directories"]
    I --> E
    K --> E
    E -- done --> L{"directories.isEmpty?"}
    L -- "Yes" --> M["return early — prepareForExplicitOpenIntentAtStartup NOT called → session restore unsuppressed"]
    L -- "No" --> N["prepareForExplicitOpenIntentAtStartup() then open workspace for each dir"]
Loading

Reviews (1): Last reviewed commit: "Preserve symlink aliases for external op..." | Re-trigger Greptile

Comment thread Sources/AppDelegate.swift
Comment on lines +429 to +438
private static func normalizedComparisonURL(_ url: URL) -> URL {
url.standardizedFileURL.resolvingSymlinksInPath()
}

private static func isSameOrDescendant(_ url: URL, of rootURL: URL) -> Bool {
let urlPathComponents = normalizedComparisonURL(url).pathComponents
let rootPathComponents = normalizedComparisonURL(rootURL).pathComponents
guard urlPathComponents.count >= rootPathComponents.count else { return false }
return Array(urlPathComponents.prefix(rootPathComponents.count)) == rootPathComponents
}

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.

P2 normalizedComparisonURL for excluded root recomputed per-input-URL

isSameOrDescendant(_:of:) is called inside an inner closure (excludedRootURLs.contains(where:)), so normalizedComparisonURL(rootURL) — which calls resolvingSymlinksInPath() (a synchronous filesystem stat chain) — is re-executed on the same excludedRootURL once for every URL in the input array.

For the typical launch-time use (excludedRootURLs = [Bundle.main.bundleURL], a handful of URLs), the overhead is negligible. But the current shape makes it easy for a future caller to pass a large pathURLs array and unknowingly amplify the I/O. Pre-computing the normalised roots before the loop would eliminate the redundancy at zero semantic cost.

Comment on lines +233 to +264
func testApplicationOpenURLsIgnoresBundleSelfPaths() {
_ = NSApplication.shared
let app = AppDelegate()

let windowId = UUID()
let window = makeMainWindow(id: windowId)
defer { window.orderOut(nil) }

let manager = TabManager()
app.registerMainWindow(
window,
windowId: windowId,
tabManager: manager,
sidebarState: SidebarState(),
sidebarSelectionState: SidebarSelectionState()
)

window.makeKeyAndOrderFront(nil)
_ = app.synchronizeActiveMainWindowContext(preferredWindow: window)

let existingWorkspaceIds = Set(manager.tabs.map(\.id))
let embeddedExecutableURL = Bundle.main.bundleURL
.appendingPathComponent("Contents/MacOS/cmux", isDirectory: false)

app.application(
NSApplication.shared,
open: [embeddedExecutableURL]
)

let createdWorkspace = manager.tabs.first { !existingWorkspaceIds.contains($0.id) }
XCTAssertNil(createdWorkspace)
}

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.

P2 Missing welcome-screen suppression guard

The existing testApplicationOpenURLsCreatesWorkspaceForDroppedDirectory test (lines 184–231 just above) explicitly suppresses the welcome-tab by writing to UserDefaults.standard before calling app.application(_:open:), then restores it in a defer. That guard was presumably added because, in a fresh CI environment where the welcome screen has never been shown, some AppDelegate init path can add an extra tab after the existingWorkspaceIds snapshot.

This new test skips that guard. Since the snapshot is captured before the call under test, any welcome-generated tab that appears after the snapshot would cause a false XCTAssertNil failure. Adding the same UserDefaults setup-and-restore block (before the existingWorkspaceIds snapshot) would make this test consistent with the established pattern and eliminate that potential flake.

This branch was successfully deployed

1 active deployment
Preview — 8258459a Deployed Apr 1, 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.

1 participant