Skip to content

Fix retained closed window contexts and PTY respawn - #8567

Merged
austinywang merged 153 commits into
mainfrom
issue-8349-ghost-window-contexts
Aug 25, 2026
Merged

austinywang merged 153 commits into
mainfrom
issue-8349-ghost-window-contexts

Conversation

@austinywang

@austinywang austinywang commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #8349

Summary

  • Centralize main-window closure in one idempotent, exact-NSWindow transaction shared by AppKit observation, controller delegation, socket close, tab close, and orphan cleanup.
  • Keep windowless recoverable owners in session persistence and quit-safety decisions while excluding them from visible/scriptable routing until their exact native window is live again.
  • Permanently retire closed TabManager, Workspace, Dock, terminal-registry, observer, remote-session, and background-work graphs so retained SwiftUI/AppKit models cannot respawn PTYs.
  • Guard every workspace and surface acquisition path after retirement; roll back an extension-created worktree safely if asynchronous workspace admission loses a window-close race.
  • Preserve exact route identity and O(1) single-route lookup, with route state explicitly owned by each AppDelegate.
  • Merge current origin/main and preserve the current last-terminal cancellation/recovery behavior.

The root cause was split authority: context removal, native-window visibility, terminal registration, and close callbacks could disagree about whether an ordered-out or retained window still owned runtime work. This change makes route ownership explicit and makes successful route removal the authority boundary before irreversible teardown.

Testing

  • git diff --check
  • ./scripts/check-pbxproj.sh
  • ./scripts/lint-pbxproj-test-wiring.sh — passed for 655 test files
  • python3 scripts/check-package-resolved-policy.py
  • python3 scripts/check-workspace-package-groups.py
  • python3 scripts/check-test-determinism.py — 0 findings
  • Behavior coverage includes exact-window close routing, windowless persistence, stale/duplicate native windows, retained-model retirement, terminal-registry cleanup, worktree rollback, and workspace-create idempotency recovery.
  • Regression provenance remains visible in the commit history, including test-first/fix-second pairs and the macOS 15/26 red proof linked in earlier PR history.
  • Localization audit: the final merge/cleanup introduces no new user-facing copy; the reused workspace-create error key already has English and Japanese translations.
  • Per the closeout instruction, no local build, local xcodebuild, or XCUITest was run. Required GitHub checks are the build/test authority for this pushed HEAD.

Demo Video

  • Video URL or attachment: Not recorded for the latest HEAD; build and dogfood are intentionally deferred until explicit user approval.

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally (non-build repository guards listed above)
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (not needed for this bug fix)
  • I requested bot reviews after my latest commit
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by CodeRabbit

  • Bug Fixes
    • Improved main-window closing, recovery, and session persistence, including safer handling of closed or temporarily windowless workspaces.
    • Prevented focus, workspace, panel, Dock, and terminal actions after their associated window or workspace has been closed.
    • Improved cleanup of terminal surfaces, remote sessions, panels, notifications, and Git tracking state.
    • Added safer rollback when extension worktree creation fails or changes unexpectedly.
    • Prevented duplicate or invalid workspace creation during shutdown and startup recovery.
  • Tests
    • Expanded coverage for window routing, teardown, workspace recovery, Dock retirement, and worktree rollback.

Note

High Risk
Touches core window routing, session persistence, terminal registry lifetime, and irreversible teardown on close—bugs could leak PTYs, drop session state, or close the wrong window.

Overview
Centralizes main-window close in one idempotent path keyed to the exact NSWindow, with committed-close tombstones so SwiftUI/AppKit cannot re-register or focus a window already tearing down.

Recoverable routes now keep sidebar snapshots and optional Dock transfer through transient context replacement; session autosave and persistence use registered plus recoverable owners, while visible routing still requires a live exact window. Route retirement no longer depends on “any terminal surface still registered”—inactive/finalized TabManagers are pruned instead.

Closed owners are permanently finalized: TabManager/Workspace graphs retire via finalizeAllWorkspacesForWindowClose, workspace creation goes through addWorkspaceIfActive (and related guards), and Dock stores honor isRetired on mutations. Terminal surface registry is reworked with weak registration ledgers, dead-entry sweeps, and coalesced retireInactiveRecoverableMainWindowRoutes callbacks; surfaces unregister on explicit teardown before deinit.

Smaller fixes: sidebar Git probe reset consolidation, workspace-group creation when host creation fails, Iroh host lifecycle test clock alignment, and extension worktree rollback when workspace admission fails after async creation.

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

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change coordinates main-window close cleanup, recoverable route ownership, workspace and Dock retirement, terminal-surface registry ownership, guarded worktree rollback, and Sidebar Git reset behavior. Tests cover routing, teardown, weak registrations, rollback, and idempotency.

Changes

Lifecycle retirement

Layer / File(s) Summary
Surface registry retirement
Packages/macOS/CmuxTerminal/..., Packages/macOS/CmuxTerminal/Tests/...
Terminal surfaces now retire registry ownership once. The registry uses weak registrations, identity indexes, dead-entry sweeps, and coalesced route-retirement scheduling.
Recoverable windowless routing
Sources/AppDelegate+RecoverableMainWindowRoutes.swift, Sources/AppDelegate+WindowIdentity.swift, Sources/AppDelegate.swift
Recoverable routes retain sidebar snapshots and remain addressable without live windows while their managers are not finalized. Window ownership checks require exact identity and live registration.
Main-window close commit
Sources/AppDelegate.swift, Sources/App/MainWindowVisibilityController.swift, Sources/AppDelegate+NonInteractiveWindowClose.swift
Close requests commit a tombstone, validate the exact owner, record route and history data, finalize workspaces, remove contexts, clean references, and publish close state.
Workspace and Dock retirement
Sources/TabManager.swift, Sources/Workspace.swift, Sources/DockSplitStore.swift, Sources/ContentView.swift
Finalized managers reject new workspaces. Retired workspaces and Dock stores reject later panel, surface, focus, restore, and configuration operations.
Workspace creation and API updates
Sources/TerminalController+WorkspaceCreate.swift, Sources/TerminalController+WorkspaceCreateIdempotencyCache.swift, Sources/CmuxConfigExecutor+WorkspaceLaunch.swift, Sources/RemoteTmuxController.swift, Packages/macOS/CmuxWorkspaces/...
Workspace creation uses active-manager acquisition and optional results. Idempotency reservations retain rollback metadata and retry persistence after concurrent changes.
Extension worktree rollback
Sources/ExtensionWorktreePrototype.swift, Sources/ContentView.swift, cmuxTests/ExtensionWorktreeSpawnArgsTests.swift
Worktree creation records repository, branch, HEAD, and artifact metadata. Rollback validates ownership and content before removing or restoring worktree state.

Sidebar Git tracking reset

Layer / File(s) Summary
Bulk probe reset
Packages/macOS/CmuxSidebarGit/Sources/..., Packages/macOS/CmuxSidebarGit/Tests/...
Polling shutdown now uses centralized bulk cleanup for watchers, tasks, probe state, directory and signature caches, and pull-request probing.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AppKit
  participant AppDelegate
  participant MainWindowVisibilityController
  participant TabManager
  participant Workspace
  participant TerminalSurfaceRegistry
  AppKit->>AppDelegate: close window
  AppDelegate->>MainWindowVisibilityController: commitClose(window)
  AppDelegate->>TabManager: commitMainWindowClose(window)
  TabManager->>Workspace: finalizeAllWorkspacesForWindowClose()
  Workspace->>TerminalSurfaceRegistry: retire surface registrations
  AppDelegate->>AppDelegate: remove context and recoverable route
Loading

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#9261 — Related terminal-surface teardown and lifecycle tests.
  • manaflow-ai/cmux-dev-artifacts#9313 — Related Dock and terminal-surface lifecycle behavior.

Possibly related PRs

Suggested reviewers: lawrencecchen, azooz2003-bit


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (5 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Cache Substitution Correctness ❌ Error The fresh MainWindowContext sidebar read is replaced by immutable route.sidebarSnapshot in session persistence, autosave, and close-history snapshots; stale handling is absent. Refresh sidebarSnapshot from an event/version-checked source while live, or document why the close-time snapshot remains authoritative when the context is gone.
Cmux Algorithmic Complexity ❌ Error Sources/AppDelegate.swift:6243 scans repaired.values for every context, making repairMismatchedMainWindowContextIndex O(C²) over unbounded mainWindowContexts; it is called by window identity routing. Track repaired context ObjectIdentifiers in a Set while building the index, then test Set membership in the second pass instead of scanning repaired.values.
Cmux Swift Concurrency ❌ Error TerminalSurfaceRegistry broadens fire-and-forget Task { @mainactor ... } route-retirement work to many registry paths; its handle is not stored or cancellable. Store and cancel or await the scheduled task, or move coalescing into an actor/structured async API while retaining only the required main-actor boundary.
Cmux Swift Package Boundaries ❌ Error The PR materially expands a Foundation-only idempotency cache in Sources/ from 243 to 447 lines, with persistence protocols and isolated fakes, but keeps it nested in TerminalController. Extract the cache, persistence protocol, and stores into a small CmuxWorkspaceCreate SwiftPM target. Expose WorkspaceCreateIdempotencyCache and its persistence protocol; retain only App lifecycle wiring in Sources/.
Cmux User-Facing Error Privacy ❌ Error New rollback recovery errors store backup paths and raw restore/cleanup error text in CmuxExtensionWorktreePrototypeDetails, exposing internal filesystem and upstream details. Keep user-visible recovery text generic and move sanitized diagnostics to private logs; do not include paths or raw error descriptions in the error details payload.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address [#8349] by centralizing window closure, retiring retained runtime graphs, and preventing terminal and PTY respawn.
Out of Scope Changes check ✅ Passed The broad lifecycle, rollback, and workspace changes support the stated window-close, retirement, and race-prevention objectives.
Cmux Swift Actor Isolation ✅ Passed No new actor-isolation violation found: UI stores and tasks remain @MainActor, value additions are Sendable, and registry shared state uses a documented NSLock boundary.
Cmux Swift Blocking Runtime ✅ Passed The PR adds no semaphore, sleep, delayed dispatch, timer, polling wait, or main-queue sync. Its existing registry NSLock remains documented for synchronous deinit/native-pointer access; retries awa...
Cmux Browser Automation Off-Main ✅ Passed The PR only adds a retirement guard to Workspace.openDockBrowserLinkInNewTab; it does not change browser socket routing, WebKit waits, policy sets, worker routers, or policy tests.
Cmux Expensive Synchronous Load ✅ Passed The diff adds no direct RestorableAgentSessionIndex.load(); close code uses SharedLiveAgentIndex.shared, and new artifact reads run inside Task.detached with bounded worktree validation.
Cmux No Hacky Sleeps ✅ Passed The PR delta contains only Swift sources/tests plus Xcode project wiring; it introduces no covered TypeScript, JavaScript, shell, or build/runtime sleep or timer changes.
Cmux Swift @Concurrent ✅ Passed The new rollback is async, non-actor-isolated, and guarded by @concurrent plus Task.detached; UI worktree calls also hop detached, while persistence I/O crosses a dedicated actor.
Cmux Swiftpm Lockfiles ✅ Passed PR changes no Package.swift, Package.resolved, .gitignore, or workflow files; the Xcode project diff only wires a test file and adds no package reference.
Cmux Swift Logging ✅ Passed The diff adds no production print/debugPrint/dump/NSLog or file/stdout diagnostics; new focus logs use existing #if DEBUG cmuxDebugLog, and the only FileHandle changes are test process sinks.
Cmux Full Internationalization ✅ Passed No catalog or web locale files changed; the new socket failure uses String(localized:defaultValue:) with an existing catalog key, while added rollback text is internal/debug-only and generated HTML...
Cmux Swiftui State Layout ✅ Passed The diff only changes workspace-acquisition calls and lifecycle guards in existing views; it adds no SwiftUI state, geometry measurement, lazy-row store references, or render-time state writes.
Cmux Architecture Rethink ✅ Passed Production changes use explicit AppDelegate, TabManager, Workspace, Dock, and registry ownership with monotonic retirement guards; the only new main-queue hop is test synchronization.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR changes main workspace close routing and test fixtures only; it adds no standalone auxiliary window, and scripts/lint_auxiliary_window_close_shortcuts.py passes for 35 identifiers.
Cmux Source Artifacts ✅ Passed The full PR diff adds only two intentional Swift source/test files; all other changes are source, tests, or project wiring, with no logs, caches, temp folders, binaries, or build artifacts.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The production diff adds no ForTesting/TestHook/testOnly/debug accessor or visibility-widened test wrapper; its new #if DEBUG blocks only emit lifecycle diagnostics, and test seams remain in test/d...
Cmux No Ambient Global State ✅ Passed Production diff adds no top-level API functions or mutable globals; no new singleton or static-only namespace. MainWindowRouteLedger is constructable per-AppDelegate state replacing file-scope asso...
Title check ✅ Passed The title clearly summarizes the primary changes: fixing retained closed window contexts and preventing PTY respawn.
Description check ✅ Passed The description covers the change, rationale, testing, review status, checklist, and explicitly documents that no demo video or local build was performed.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-8349-ghost-window-contexts

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.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Centralizes exact-window close handling and permanently retires closed workspace, Dock, terminal, and remote-session ownership while preserving recoverable windowless routes.

  • Adds guarded workspace and surface admission after owner retirement.
  • Reworks terminal-surface registration and cleanup around weak ownership and coalesced route retirement.
  • Preserves recoverable owners in persistence while excluding them from visible routing.
  • Adds rollback and lifecycle coverage for asynchronous workspace creation and window-close races.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported transient-window, stale-duplicate, finalized-manager revival, duplicate unregister, and windowless remote-teardown issues are addressed at the current head.

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Consolidates exact-window and explicit windowless-owner close transactions without leaving the previously reported transient routing or stale-duplicate close defects outstanding.
Sources/AppDelegate+RecoverableMainWindowRoutes.swift Retains windowless owners for recovery, removes routes transactionally, and now forwards their workspace identities into remote teardown.
Sources/AppDelegate+WindowIdentity.swift Validates recoverable native windows by exact identity, application membership, route identifier, and committed-close state.
Sources/TabManager.swift Makes window-close finalization authoritative and rejects workspace admission after the manager is finalized.
Sources/Workspace.swift Retires workspace-owned runtime graphs and prevents lifecycle work from reviving an owner after closure.
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Engine/SurfaceRegistry/TerminalSurfaceRegistry.swift Replaces legacy weak-table indexing with explicit weak registration ledgers, lifecycle indexes, dead-entry pruning, and coalesced route-retirement callbacks.
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift Makes surface teardown the ownership-aware registry-retirement boundary, resolving the previously reported duplicate unregister path.
Sources/AppDelegate+WindowDock.swift Aligns Dock lifetime and mutation routing with recoverable-window ownership and permanent retirement.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Close[Exact native-window close or explicit windowless-owner close] --> Remove[Remove authoritative route]
  Remove --> Finalize[Finalize TabManager and workspaces]
  Finalize --> Surfaces[Teardown terminal and Dock surfaces]
  Surfaces --> Registry[Unregister runtime ownership]
  Registry --> Remote[Detach remote sessions]
  Registry --> Sweep[Retire inactive recoverable routes]
  WindowLoss[Transient weak-window loss] --> Recoverable[Retain recoverable owner]
  Recoverable --> Persist[Include in persistence and quit safety]
  Recoverable --> Hidden[Exclude from visible and scriptable routing]
  Recoverable --> Reattach[Register exact replacement window]
Loading

Reviews (68): Last reviewed commit: "fix: allow windowless noninteractive clo..." | Re-trigger Greptile

Comment thread Sources/AppDelegate.swift Outdated

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift`:
- Around line 293-297: Restrict the ownsSurfaceRegistryRegistration property in
TerminalSurface to private visibility, preserving its existing one-shot
lifecycle behavior and internal reads or mutations within TerminalSurface.swift.
🪄 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: 79a13355-7dae-4500-8141-d2132a231881

📥 Commits

Reviewing files that changed from the base of the PR and between 1de3327 and b71813a.

📒 Files selected for processing (8)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
  • Packages/macOS/CmuxTerminal/Tests/CmuxTerminalTests/TerminalSurfaceTeardownCallbackLifetimeTests.swift
  • Sources/App/MainWindowVisibilityController.swift
  • Sources/AppDelegate+NonInteractiveWindowClose.swift
  • Sources/AppDelegate.swift
  • Sources/TabManager+NonInteractiveClose.swift
  • cmuxTests/ClosedMainWindowRoutingTests.swift
💤 Files with no reviewable changes (1)
  • Sources/TabManager+NonInteractiveClose.swift

Comment thread Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift Outdated
Comment thread Sources/AppDelegate.swift

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

🤖 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 `@cmuxTests/ClosedMainWindowRoutingTests.swift`:
- Around line 267-268: Remove the unsupported manager.tabs.isEmpty assertion
from the commitMainWindowClose test, and retain the workspace.owningTabManager
== nil assertion to verify teardown state without requiring TabManager.tabs to
be emptied.
- Around line 187-191: Update the closed-window test around
toggleSidebarInActiveMainWindow() to assert the missing-window close contract:
after the call, verify the context, recoverableMainWindowRoute, and
terminalSurfaceRegistry no longer retain the manager, route, or terminal
surface. Do not preserve the existing expectations unless the production close
behavior is intentionally changed.
🪄 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: 1de92757-66cc-4dfd-a8e2-fc840ae9da29

📥 Commits

Reviewing files that changed from the base of the PR and between 5a6bfc4 and eec5d64.

📒 Files selected for processing (1)
  • cmuxTests/ClosedMainWindowRoutingTests.swift

Comment thread cmuxTests/ClosedMainWindowRoutingTests.swift
Comment thread cmuxTests/ClosedMainWindowRoutingTests.swift Outdated
@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/TabManager.swift

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/AppDelegate.swift (1)

16163-16172: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail closed when no exact window owner matches.

tabManagerFor(windowId:) can return the manager from the same recoverable route subsequently checked by the private overload, making that manager comparison tautological. A duplicate NSWindow with the owner’s identifier can therefore finalize the live/windowless owner and retire its terminal surfaces.

Require recoverableMainWindowRoute(windowId:)?.window === window before committing this fallback; when the route has no live window, return false.

Proposed fix
-        guard let windowId = mainWindowId(from: window),
-              let manager = tabManagerFor(windowId: windowId) else {
+        guard let windowId = mainWindowId(from: window),
+              let route = recoverableMainWindowRoute(windowId: windowId),
+              let routedWindow = route.window,
+              routedWindow === window,
+              let manager = route.tabManager else {
             return false
         }

As per path instructions, close ownership must use one authoritative identity and fail closed when it is absent. Based on learnings, a missing weak window is recoverable rather than authoritative.

🤖 Prompt for 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.

In `@Sources/AppDelegate.swift` around lines 16163 - 16172, Update the fallback
close path around mainWindowId, tabManagerFor(windowId:), and
commitMainWindowClose so it only proceeds when
recoverableMainWindowRoute(windowId:) exists and its live window is identical to
window (using object identity). Return false when the route is missing or has no
live window, preventing a manager lookup from authorizing a duplicate window.

Sources: Path instructions, Learnings

Sources/TabManager.swift (1)

2012-2044: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

finalizeWorkspaceForRemoval tears down panels/remote connection unconditionally, before confirming workspace is actually a member of tabs.

closeWorkspace only guards tabs.count > 1; it calls finalizeWorkspaceForRemoval(workspace) (line 2042) before the membership check at line 2044, which is only used for array removal. If a caller ever passed a workspace not owned by this TabManager (e.g. one belonging to another window), this path would still tear down its panels, remote connection, and null out owningTabManager — the exact class of cross-window lifecycle corruption this PR is fixing in the other direction. No current call site violates this today, but the shared helper now has a wider blast radius (used by two call sites), so it's worth failing closed here.

🛡️ Suggested guard
     func closeWorkspace(_ workspace: Workspace, recordHistory: Bool = true) {
         guard tabs.count > 1 else { return }
+        guard tabs.contains(where: { $0.id == workspace.id }) else { return }
         panelTitleUpdateCoalescer.flushNow()

As per path instructions ("**/*.{swift,ts,tsx,js,jsx,mjs,cjs}: Apply .github/review-bot-rules/reliability-single-source-of-truth.md"), correctness-critical lifecycle/teardown decisions should not rely on an implicit assumption that always happens to hold at every call site.

🤖 Prompt for 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.

In `@Sources/TabManager.swift` around lines 2012 - 2044, Update closeWorkspace to
verify the supplied workspace is a member of tabs before performing any history
handling or calling finalizeWorkspaceForRemoval. Return immediately when no
matching tab exists, then reuse the validated tab index for removal and history
logic so finalizeWorkspaceForRemoval only runs on workspaces owned by this
TabManager.

Source: Path instructions

🤖 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 `@cmuxTests/ClosedMainWindowRoutingTests.swift`:
- Around line 262-264: Update recoverEmptyWorkspaceAfterStartupIfNeeded() to
detect that finalizeAllWorkspacesForWindowClose() has finalized the manager and
return false without calling addWorkspace(). Preserve recovery for genuinely
empty, non-finalized managers, and ensure finalization remains authoritative
after tabs are cleared.

In `@Sources/TabManager.swift`:
- Around line 1308-1315: Update detachWorkspace to call
recoverEmptyWorkspaceAfterStartupIfNeeded() instead of performing its own
tabs.isEmpty check and addWorkspace recovery, preserving the existing return
behavior while centralizing the empty-workspace invariant in the helper.

---

Outside diff comments:
In `@Sources/AppDelegate.swift`:
- Around line 16163-16172: Update the fallback close path around mainWindowId,
tabManagerFor(windowId:), and commitMainWindowClose so it only proceeds when
recoverableMainWindowRoute(windowId:) exists and its live window is identical to
window (using object identity). Return false when the route is missing or has no
live window, preventing a manager lookup from authorizing a duplicate window.

In `@Sources/TabManager.swift`:
- Around line 2012-2044: Update closeWorkspace to verify the supplied workspace
is a member of tabs before performing any history handling or calling
finalizeWorkspaceForRemoval. Return immediately when no matching tab exists,
then reuse the validated tab index for removal and history logic so
finalizeWorkspaceForRemoval only runs on workspaces owned by this TabManager.
🪄 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: 556407af-b743-4b29-8819-f639e7b50aa5

📥 Commits

Reviewing files that changed from the base of the PR and between eec5d64 and 7cc2fcd.

📒 Files selected for processing (7)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
  • Sources/AppDelegate+RecoverableMainWindowRoutes.swift
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • cmuxTests/ClosedMainWindowRoutingTests.swift
💤 Files with no reviewable changes (1)
  • Sources/Workspace.swift

Comment thread cmuxTests/ClosedMainWindowRoutingTests.swift
Comment thread Sources/TabManager.swift
Comment thread Sources/TabManager.swift
@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

Comment on lines +547 to +556
private func recoverableRouteWorkspaceIdsForRemoteTeardown(
_ route: RecoverableMainWindowRoute
) -> [UUID] {
return route.workspaceIds.filter { workspaceId in
guard let currentOwner = tabManagerFor(tabId: workspaceId) else {
return route.tabManager == nil
}
return currentOwner === route.tabManager
}
}

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.

P1 Windowless route skips remote teardown

When a windowless recoverable route is retired while its manager and remote-tmux mirror remain retained, tabManagerFor(tabId:) excludes that route because it has no live native window. The ownership filter consequently omits the route's own workspace IDs from handleWindowWorkspacesClosed, leaving the remote session or ControlMaster connection active after retirement.

Knowledge Base Used: Desktop workspace experience

@austinywang
austinywang merged commit 6cf5630 into main Aug 25, 2026
7 checks passed
austinywang added a commit that referenced this pull request Aug 26, 2026
Two constructs in today's #8567 never satisfy the Swift compiler (seen
on Swift 6.3.3 / Xcode 26.5): the worktree identity fields were declared
'let ... = nil', which excludes them from the memberwise initializer the
creation path calls with real values (extra arguments at #8/#9), and the
rollback guard compared an optional filesystem-identity tuple against a
non-optional one, which tuples do not support. Make the fields plain
memberwise 'let's (the only production call site provides them; the
spawn-args test now passes nil explicitly) and unwrap the current
identity before comparing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang pushed a commit that referenced this pull request Aug 26, 2026
… fields (#10777)

* fix: restore app target compilation broken by worktree identity fields

Commit 6cf5630 (#8567) declared worktreeDeviceID/worktreeFileID as
'let ... = nil', which removes them from the synthesized memberwise
initializer, so the createWorktree call passing both labels failed to
compile. Drop the defaults so the fields enter the memberwise init and
the captured identity keeps flowing to rollback.

Also unwrap the optional identity tuple in
bestEffortCleanupFailedWorktree before comparing, since == is not
lifted over optional tuples.

* fix: add explicit return in task-group closure in worktree rollback test

A multi-statement closure gets no implicit return, so the group.addTask
closure returning Int32? failed to compile. Found while running the
touched test class on the remote builder.

---------

Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 14, 2026
`mobilePerformFailureReleasesAcceptedOperationID` uses a `TabManager` subclass
whose `addWorkspaceIfActive` always returns nil, to simulate a failed
workspace creation. It built that manager with the default initializer, which
creates an initial workspace and, since manaflow-ai#8567, treats a nil result as a
programmer error with `preconditionFailure("Initial workspace creation failed
for an active window manager")`. So the test host died before the test body
ran. xcodebuild relaunched the host and reran the test, which killed it again.

Build the rejecting manager with `createInitialWorkspace: false`.
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 20, 2026
…e routing tests

Since manaflow-ai#8567 a registered main-window context resolves its window from the
context itself, and the lookup by window identifier is gone. These tests
registered a windowless context and then named an NSWindow by identifier, so
performNewWorkspaceAction found no window, dropped the context, and the
assertions that follow saw nothing happen. Set the window on the context, as
registerMainWindow does.
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 20, 2026
… tests

Since manaflow-ai#8567 a registered main-window context resolves its window from the
context itself, and the lookup by window identifier is gone.
registerWindowedContext registered a windowless context and then named an
NSWindow by identifier, so performNewWorkspaceAction found no window, dropped
the context, and the routed request returned before it could report its
failure. Set the window on the context, as registerMainWindow does.
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 20, 2026
… tests

Since manaflow-ai#8567 a registered main-window context resolves its window from the
context itself, and the lookup by window identifier is gone.
registerWindowedContext registered a windowless context and then named an
NSWindow by identifier, so performNewWorkspaceAction found no window, dropped
the context, and the routed request returned before it could report its
failure. Set the window on the context, as registerMainWindow does.
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 20, 2026
… tests

Since manaflow-ai#8567 a registered main-window context resolves its window from the
context itself, and the lookup by window identifier is gone.
registerWindowedContext registered a windowless context and then named an
NSWindow by identifier, so performNewWorkspaceAction found no window, dropped
the context, and the routed request returned before it could report its
failure. Set the window on the context, as registerMainWindow does.
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 26, 2026
…e routing tests

Since manaflow-ai#8567 a registered main-window context resolves its window from the
context itself, and the lookup by window identifier is gone. These tests
registered a windowless context and then named an NSWindow by identifier, so
performNewWorkspaceAction found no window, dropped the context, and the
assertions that follow saw nothing happen. Set the window on the context, as
registerMainWindow does.
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.

Closed macOS windows remain as ghost MainWindowContexts and respawn login/zsh PTYs

1 participant