Skip to content

Fix intermittent warm workspace switch latency - #9244

Merged
austinywang merged 68 commits into
mainfrom
issue-9231-workspace-switch-latency
Sep 6, 2026
Merged

austinywang merged 68 commits into
mainfrom
issue-9231-workspace-switch-latency

Conversation

@austinywang

@austinywang austinywang commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Ship permanent, recording-gated switch diagnostics: ws.switch.selection-commit, ws.switch.portal-show, ws.switch.portal-hide, ws.switch.renderer-realization, ws.switch.first-frame, ws.switch.interactive, ws.switch.window-focus, and ws.switch.window-become-key.
  • Make mounted-workspace reconciliation the sole owner of switch handoff. Portal, frame, and focus callbacks measure presentation and interaction; they never retain the source workspace or block the selected destination.
  • Reconcile the current model selection when SwiftUI delivers a stale/coalesced value, so rapid A → B → C changes cannot remount B or strand C's transaction.
  • Protect a terminal renderer from selection commit until mount reconciliation retires the source. Protection is request-scoped, cancels on mismatch/rapid reselection, and self-expires through a weak owner before any reclamation pass.
  • Tie the switch-time frame observer and rendered-frame notification demand to an RAII owner, so deallocation is a complete cleanup path even without an explicit window callback.

Root cause

The 0.64.20 handoff path split lifecycle ownership between model selection and presentation/focus callbacks. It retained the old workspace behind hasLoadedTerminalSurface()—a native-pointer proxy rather than a presentation guarantee—or a 150 ms timeout. During rapid or delayed SwiftUI delivery, an earlier selection callback could also reconcile after the model had already advanced.

That made mount ownership depend on timing: the destination was selected, but the old and new view graphs and portals could remain in a handoff state until a callback or timeout happened. PR #7231 deduplicated repeated portal-hide work and PR #7236 stopped hibernation presentation from restoring unrelated portals, but neither removed this split ownership.

The reported 0.64.20 build predates renderer reclamation, so reclamation is not claimed as the original cause. Current main does reclaim renderers, however, so the switch transaction protects its destination through the authoritative mount transition.

The new invariant is:

  1. The model commits selection and starts one request-scoped diagnostic transaction.
  2. Any SwiftUI selection callback reconciles the current model value once; an intermediate delivered value never becomes a second source of truth.
  3. Mount reconciliation selects the destination and retires the source synchronously; it never waits for portal, frame, focus, or elapsed time.
  4. The destination renderer remains protected only through that mount transition: the unconditional selection callback calls sourceDidRetire immediately after reconciliation, even when the source workspace model was already deleted. A transaction mismatch cancels immediately, and a dead coordinator's weak owner makes any missed release ineligible for protection.
  5. Portal, first-frame, and interaction signals finish signpost intervals only. A missing signal cannot strand an old workspace or block another switch.

No sleep, polling loop, loaded-pointer proxy, callback-owned mount decision, or unbounded protection lease remains.

Cold destinations

The live source workspace is intentionally not retained as a loading cover for an already-reclaimed destination. Doing so would recreate the bug's timing-dependent dual-mounted view graph. A cold destination may expose the normal window background during its measured ws.switch.renderer-realization / ws.switch.first-frame interval; warm destinations stay on the immediate path.

The former cycle-hot 220 ms mount-pair timer and its now-dead two-workspace budget are removed; rapid cycling uses the same single authoritative mount path as every other switch.

The required tagged dogfood after CI should include an intentionally reclaimed destination and use these intervals to judge that visual interval. This PR does not claim an unmeasured p99 bound, and it does not trade the warm-switch ownership fix for another timer or callback-gated mount.

Diagnostics

The intervals use the existing DynamicTracingSignposts / OSSignposter path. Names and messages are not constructed unless a tracing tool is recording, and the phase names are readable directly in a Points of Interest trace.

Terminal realization ends on an observed drawable rather than when a nonblocking rebuild request is accepted. noteFirstFrame releases the frame observer and its notification-demand lease immediately, independent of source retirement, portal readiness, or interaction readiness; if no frame is produced, there is no per-frame notification work. The transaction owns at most one target-frame observer and one renderer lease per window; a later switch cancels and releases the previous transaction. The observer/demand pair also tears itself down on deallocation. Protected renderers are pinned inside, and count toward, the configured warm-renderer budget.

Regression coverage

The commits preserve a failing-test-before-fix boundary:

  1. 1a69a65869 adds the behavioral requirement that source retirement release switch-time renderer protection independently of presentation readiness.
  2. cfdaa39d10 makes mount reconciliation authoritative, rejects stale callbacks, rebases coalesced selection sources, and moves renderer-lease release to source retirement.
  3. e919239040 removes the dead live-source presentation path and adds request-scoped frame delivery and teardown coverage.
  4. 307bca49d9 makes protection self-expiring, reconciles authoritative stale deliveries, and covers presentation-target mismatch cancellation.
  5. 76a24646ae covers coalesced round trips and portal rebind presentation.
  6. 5f4b6931cb keeps selection side effects unconditional, makes disabled tracing allocation-free, and removes the obsolete source-cover policy.
  7. 8be3f7a6c0 makes frame observation RAII-owned, counts protected renderers inside the warm budget, and removes dead cycle-hot timer state.
  8. f1a50c3e79 splits the coordinator state model into one-type-per-file definitions.
  9. 5564407b5b makes the terminal portal registry the single source of presentation readiness.
  10. dacf39d15a adds the matching browser predicate and gates process-wide portal diagnostics to active, matching-window switches.

The test-only full CI run is pinned to 1a69a65869: https://github.com/manaflow-ai/cmux/actions/runs/30599009307

Additional focused coverage exercises portal/frame ordering, stale frame callbacks, reclaimed-renderer frame sequencing, rapid-switch cancellation, stale source callbacks, mismatch cancellation, owner expiration, deallocation cleanup, warm-budget accounting, and both idle and memory-pressure reclamation protection. I did not add a fake p99 UI assertion: intermittent SwiftUI/AppKit/Ghostty scheduling still requires a live trace, which is why permanent phase signposts are part of the fix.

Validation

  • swiftc -parse for all changed Swift and test files
  • ./scripts/check-pbxproj.sh
  • ./scripts/lint-pbxproj-test-wiring.sh
  • python3 scripts/check-test-determinism.py
  • python3 scripts/lint-stored-dispatch-work-items.py
  • bundled cmux policy check
  • git diff --check
  • Current-HEAD GitHub CI and bundled autoreview are the remaining gates

No local app build, local xcodebuild test, or XCUITest was run, per task constraints. Current main no longer contains .github/swift-file-length-budget.tsv or scripts/swift_file_length_budget.py; the new coordinator and its test file both remain below 500 lines, and .github/swift-warning-budget.tsv is unchanged.

Localization audit: no user-facing strings were added or changed, so Resources/Localizable.xcstrings and web message catalogs are unchanged.

Closes #9231

Closeout notes (2026-09-01)

  • The feature branch is rebased onto current main (2422b69) and the feature trees and messages are unchanged.
  • The current CLA policy could not resolve the historical cmux reload-cloud commit identity. I normalized author and committer metadata for the 49 feature commits to the PR opener austinywang; the test-before-fix commit sequence and all source trees remain intact. This is metadata-only and is required by the current CLA gate.
  • Renderer protection now ends only after source retirement and presentation readiness. For terminals that means both the portal and a post-selection drawable; browsers use portal readiness as their visual boundary.
  • Portal presentation notifications are deferred through a portal-owned, cancellable scheduler; hide, rebind, detach, and teardown cancel stale edges.
  • Deferred source unfocus is owned by WorkspaceHandoffRetirementGate and is flushed after the matching queued focus pass, so a coalesced callback cannot leave the old panel focused.
  • The coordinator remains in the app target because its production seam directly coordinates AppKit, WebKit, Ghostty, and the renderer controller. Extracting a package-only state machine would duplicate those lifecycle boundaries and is intentionally deferred as a separate architectural migration. The existing AppKit lifecycle test suite remains XCTest because its suite-level teardown owns global portal and window registries; moving one test to parallel Swift Testing would weaken that isolation.
  • Local macOS builds and tests were not run. Static parsing and repository checks passed; the required build and behavior verification are being run through the AWS and CI paths below.

@cursor

cursor Bot commented Jul 31, 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.

@coderabbitai

coderabbitai Bot commented Jul 31, 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

Workspace switching now uses a transaction coordinator that waits for destination presentation and source retirement, protects destination renderers from reclamation, reports profiling milestones, and validates terminal/browser readiness with tests.

Changes

Workspace switch coordination

Layer / File(s) Summary
Renderer protection during presentation
Sources/App/RendererRealizationController.swift, Sources/App/RendererRealizationPlanner.swift
Request-scoped protected surfaces are excluded from normal and memory-pressure renderer reclamation.
Presentation and retirement coordinator
Sources/WorkspaceSwitchCoordinator.swift, Packages/macOS/CmuxTerminal/.../GhosttyMetalLayer.swift, Sources/GhosttyNotificationScrollRuntimeBridge.swift
A main-actor coordinator tracks target readiness, source retirement, renderer protection, frame notifications, cancellation, completion, and profiling intervals; rendered frame sequences are exposed for handoff validation.
Selection and view handoff integration
Sources/App/TabManager.swift, Sources/ContentView.swift, Sources/App/CmuxMainWindow.swift, Sources/App/MainWindowVisibilityController.swift, Sources/ProfilingSignposts.swift, Sources/Workspace.swift, Sources/AppDelegate.swift, Sources/AppDelegate+WindowDock.swift
Selection, focus, portal, frame, interaction, cleanup, and retirement events are routed through coordinated workspace handoffs with signpost instrumentation.
Tests and project wiring
cmuxTests/WorkspaceSwitchCoordinatorTests.swift, cmux.xcodeproj/project.pbxproj
Readiness and renderer-protection tests are added, and coordinator files are included in Xcode targets.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TabManager
  participant WorkspaceSwitchCoordinator
  participant RendererRealizationController
  participant ContentView
  TabManager->>WorkspaceSwitchCoordinator: Begin workspace selection
  WorkspaceSwitchCoordinator->>RendererRealizationController: Protect destination surface
  ContentView->>WorkspaceSwitchCoordinator: Report portal, frame, and interaction readiness
  WorkspaceSwitchCoordinator->>RendererRealizationController: Release presentation protection
  ContentView->>WorkspaceSwitchCoordinator: Report source retirement
  WorkspaceSwitchCoordinator-->>ContentView: Complete workspace handoff
Loading

Possibly related PRs

  • manaflow-ai/cmux#8540: Uses the renderer-presentation lifecycle and presentation state extended by this change.

Suggested reviewers: azooz2003-bit, lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error WorkspaceSwitchCoordinator adds a 497-line, independently tested readiness/transaction state machine under root Sources and app tests; no SwiftPM boundary was added. Extract the pure handoff state machine into a small CmuxWorkspaceSwitch package exposing WorkspaceSwitchStateMachine; retain AppKit/WebKit/Ghostty observers and ContentView wiring in the app.
Cmux Architecture Rethink ❌ Error The new coordinator does not own the handoff end-to-end: ContentView retains retiringWorkspaceId and performs retirement, while the coordinator separately tracks active/sourceRetired and signals vi... Make one coordinator the source of truth for the switch state and retirement action; replace the custom notification with a direct callback/action, then remove duplicate ContentView transaction state.
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 (22 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation addresses issue #9231 through event-driven warm-switch readiness, renderer protection, cancellation, diagnostics, and deterministic regression tests.
Out of Scope Changes check ✅ Passed The changes are focused on workspace and window switch latency, presentation readiness, renderer protection, diagnostics, cleanup, and related tests.
Cmux Swift Actor Isolation ✅ Passed Production changes use explicit MainActor isolation for UI coordinator/controller paths; observer callbacks hop to MainActor, while planner inputs remain pure Sendable values. No new service protoc...
Cmux Swift Blocking Runtime ✅ Passed PR removes the 150ms Task.sleep fallback and adds no new sleeps, delayed dispatch, blocking waits, sync calls, polling loops, semaphores, or locks; handoff readiness is driven by MainActor state an...
Cmux Browser Automation Off-Main ✅ Passed The PR changes only workspace/window handoff and renderer coordination; it does not modify socket browser commands, worker/main routing, WebKit waits, or policy tests.
Cmux Expensive Synchronous Load ✅ Passed PR adds no synchronous agent-history I/O or parsing; the existing closeWorkspace fallback is unchanged and remains cache-first via SharedLiveAgentIndex.shared.
Cmux Cache Substitution Correctness ✅ Passed The diff makes no persistence/history/undo/snapshot read substitution; session snapshot and closed-history code are unchanged. Added frame/portal state is transient UI handoff coordination.
Cmux No Hacky Sleeps ✅ Passed PR is Swift-only; it removes the 150 ms Task.sleep handoff fallback and adds no sleep, asyncAfter, timer, or polling primitive. Existing renderer coalescing timing is unchanged and cancellation-aware.
Cmux Algorithmic Complexity ✅ Passed New production work is linear per switch/evaluation; protection uses a Set. The existing O(n log n) renderer sort and multi-pass scans are unchanged, and no nested or per-target rescans were introd...
Cmux Swift Concurrency ✅ Passed The PR adds no new background queues, fire-and-forget Tasks, Combine state, or internal completion APIs; added onReceive/NotificationCenter hooks are SwiftUI/OS callback boundaries.
Cmux Swift @Concurrent ✅ Passed PR adds no async/nonisolated/@Concurrent work; new coordinator and callbacks are @MainActor, and the prior fallback Task was removed. Remaining scheduling is intentionally UI-bound.
Cmux Swiftpm Lockfiles ✅ Passed PR adds Swift sources and Xcode file wiring only; no Package.swift, Package.resolved, or .gitignore changes, and no SwiftPM package-reference entries changed.
Cmux Swift Logging ✅ Passed Cumulative diff adds only OSLog OSSignposter dynamic-tracing intervals with truncated UUIDs, window IDs, and enum reasons; no prohibited print/NSLog/file logging or unsafe Logger change.
Cmux User-Facing Error Privacy ✅ Passed The production diff adds no user-facing errors, alerts, command output, API bodies, or recovery copy; additions are internal signposts, notifications, comments, and transition diagnostics.
Cmux Full Internationalization ✅ Passed Full diff adds no user-facing copy or localization/catalog/web-message changes; new strings are profiling labels, notification/protocol tokens, debug reasons, comments, and tests, all allowed by th...
Cmux Swiftui State Layout ✅ Passed PR adds no new SwiftUI observation state, GeometryReader, lazy-row store references, or render-time state writes; it removes fallback @State and adds event-driven callbacks around a plain coordinator.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes main workspace window focus/teardown and switch coordination; no auxiliary window is added or materially changed. scripts/lint_auxiliary_window_close_shortcuts.py passes.
Cmux Source Artifacts ✅ Passed PR diff has only tracked Swift source, one test, and Xcode project wiring; no forbidden artifact directories, generated outputs, caches, logs, screenshots, recordings, or binary additions.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Cumulative diff adds no DEBUG/TEST guards or test/debug-named members; renderedFrameSequence has production handoff callers, and debugSnapshot predates the PR.
Cmux No Ambient Global State ✅ Passed Switch state is owned by constructable per-TabManager WorkspaceSwitchCoordinator with injected seams; the only new file-scope value is immutable signpost configuration, with no new mutable global o...
Title check ✅ Passed The title clearly identifies the primary change: fixing intermittent latency during warm workspace switches.
Description check ✅ Passed The description thoroughly explains the change, root cause, testing, validation, and issue link, but omits the requested demo video and checklist.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-9231-workspace-switch-latency

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.

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

🤖 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/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 14-80: Add behavior-level tests that drive the actual
WorkspaceSwitchCoordinator lifecycle from selectionWillCommit through
beginPresentation, readiness milestones, and sourceDidRetire, rather than
testing Readiness or RendererRealizationPlanner directly. Verify readiness
remains false until all required milestones arrive, active clears only after
sourceDidRetire, and renderer leases are released exactly once on portal
presentation, cancel(), targetSurfaceID replacement, and rapid reselection. Use
the injectable RendererRealizationController dependency instead of the shared
singleton so lease assertions remain isolated.

In `@Sources/ContentView.swift`:
- Around line 3704-3716: Extract the shared terminal interaction-readiness
predicate from selectedWorkspaceInputIsReady and
workspaceSwitchPresentationTarget into a single helper, preserving the
hosted-view-first-responder check followed by the text-box input view fallback.
Update both call sites to use that helper so snapshot and notification-driven
readiness remain consistent.
- Line 3664: Update the presentation setup around firstFramePresented so it is
scoped to the current presentation rather than using the lifetime-based
terminalHasRenderedFrame result. Capture and compare the frame signal at
beginPresentation, or initialize firstFramePresented to false and let
ghosttyDidRenderFrame set it after the destination renders, ensuring
WorkspaceSwitchCoordinator waits for a real destination frame.

In `@Sources/WorkspaceSwitchCoordinator.swift`:
- Around line 95-97: Remove the readinessForTesting computed property from
WorkspaceSwitchCoordinator. Do not replace it with another test-only accessor;
tests should access the existing active state through `@testable` import if
needed.
- Around line 36-46: Update the workspace-switch coordinator’s readiness flow
around Readiness and renderTarget so key-window state is handled dynamically
rather than captured in immutable requiresInteraction. Observe
NSWindow.didResignKeyNotification directly in the coordinator and clear or
bypass the interaction gate for the affected target when its window resigns key,
allowing active to become ready and source portalRenderingEnabled to be
released.
🪄 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 Plus

Run ID: 52fc980c-fe88-43cd-9215-25b08be6e707

📥 Commits

Reviewing files that changed from the base of the PR and between befd3ca and 75ea6f7.

📒 Files selected for processing (10)
  • Sources/App/CmuxMainWindow.swift
  • Sources/App/MainWindowVisibilityController.swift
  • Sources/App/RendererRealizationController.swift
  • Sources/App/RendererRealizationPlanner.swift
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceSwitchCoordinator.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceSwitchCoordinatorTests.swift
💤 Files with no reviewable changes (1)
  • Sources/Workspace.swift

Comment thread cmuxTests/WorkspaceSwitchCoordinatorTests.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift
Comment thread Sources/WorkspaceSwitchCoordinator.swift Outdated
Comment thread Sources/WorkspaceSwitchCoordinator.swift Outdated
@cursor

cursor Bot commented Jul 31, 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.

@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 (1)
cmuxTests/WorkspaceSwitchCoordinatorTests.swift (1)

145-165: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that the eligible warm renderer is reclaimed.

Line 165 passes if the planner returns an empty set, so this does not verify normal reclamation continues while the protected target is excluded. Assert the complete result includes warmSurfaceID and excludes switchTargetID.

As per coding guidelines, “When a reported bug was missed by tests, add behavior-level coverage for the exact reproduction path.”

🤖 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 `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift` around lines 145 - 165,
Update the test around RendererRealizationPlanner.selectedSurfaceIds to assert
the complete selected set: require warmSurfaceID to be included and
switchTargetID to be excluded. Preserve the existing protected-target scenario
while ensuring the test verifies eligible warm renderer reclamation rather than
passing on an empty result.

Source: Coding guidelines

🤖 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/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 49-59: Update sourceRetirementDoesNotWaitForTerminalFocusTransfer
in WorkspaceSwitchCoordinatorTests so it no longer expects readiness when
requiresInteraction is true and interactionReady is false. Align the assertion
with the input-readiness contract and existing terminal readiness behavior, and
add behavior-level coverage for the reproduction path where source retirement
must wait until interactionReady is true; do not model focus transfer through
Readiness.
- Around line 93-97: Update WorkspaceSwitchCoordinator’s
noteFirstFrame(surfaceID:) flow to retain a matching frame notification received
before portalPresented, using pending transaction state as needed. When
noteTerminalPortalPresented(surfaceID:) marks the portal available, consume the
preserved frame so firstFramePresented and isReadyForSourceRetirement reflect
both events; keep the existing regression expectation unchanged.

---

Outside diff comments:
In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 145-165: Update the test around
RendererRealizationPlanner.selectedSurfaceIds to assert the complete selected
set: require warmSurfaceID to be included and switchTargetID to be excluded.
Preserve the existing protected-target scenario while ensuring the test verifies
eligible warm renderer reclamation rather than passing on an empty result.
🪄 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 Plus

Run ID: 2f25e2c3-d19a-4dd8-8609-238708b00e3b

📥 Commits

Reviewing files that changed from the base of the PR and between 75ea6f7 and f263bb4.

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

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

cursor Bot commented Jul 31, 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.

@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

Caution

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

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

314-335: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Key the retirement callbacks to retiringWorkspaceId.

sourceWillRetire()/sourceDidRetire() currently only guard that “some” transaction is active. Since selectionWillCommit() cancels and installs a new transaction, a late retirement callback for the previous source can mark the new active transaction as retired and complete it before its real source has retired. Pass retiringWorkspaceId from completeWorkspaceHandoff() through these callbacks and return when the active transaction’s sourceWorkspaceID does not match.

🤖 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/WorkspaceSwitchCoordinator.swift` around lines 314 - 335, Update
completeWorkspaceHandoff() and the sourceWillRetire()/sourceDidRetire()
callbacks to accept and propagate retiringWorkspaceId. Before mutating the
active transaction, require its sourceWorkspaceID to match retiringWorkspaceId;
otherwise return without recording retirement or completing the transaction.

Source: Coding guidelines

♻️ Duplicate comments (1)
cmuxTests/WorkspaceSwitchCoordinatorTests.swift (1)

231-233: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that cancellation does not release the lease twice.

The assertion precedes cancel(), so a duplicate endRendererProtection during cancellation still passes. Reassert after cancellation to preserve the exactly-once teardown contract.

Proposed test addition
         `#expect`(releasedRequestIDs == protectedRequestIDs)
         coordinator.cancel()
+        `#expect`(releasedRequestIDs == protectedRequestIDs)

As per coding guidelines, “When a reported bug was missed by tests, add behavior-level coverage for the exact reproduction path.”

🤖 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 `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift` around lines 231 - 233,
Update the test around WorkspaceSwitchCoordinator cancellation to assert
releasedRequestIDs equals protectedRequestIDs after coordinator.cancel(), not
only before it. Preserve the pre-cancellation assertion if useful, and ensure
the post-cancellation assertion verifies that cancellation does not invoke
endRendererProtection twice.

Source: Coding guidelines

🤖 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/ContentView.swift`:
- Around line 2984-2989: Update the NSWindow.didResignKeyNotification handler in
the view’s onReceive registration to avoid subscribing globally when
observedWindow is nil; only attach or process the resign-key event when
observedWindow identifies the relevant window, preserving
noteInteractionNoLongerRequired for that window’s notification.

---

Outside diff comments:
In `@Sources/WorkspaceSwitchCoordinator.swift`:
- Around line 314-335: Update completeWorkspaceHandoff() and the
sourceWillRetire()/sourceDidRetire() callbacks to accept and propagate
retiringWorkspaceId. Before mutating the active transaction, require its
sourceWorkspaceID to match retiringWorkspaceId; otherwise return without
recording retirement or completing the transaction.

---

Duplicate comments:
In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 231-233: Update the test around WorkspaceSwitchCoordinator
cancellation to assert releasedRequestIDs equals protectedRequestIDs after
coordinator.cancel(), not only before it. Preserve the pre-cancellation
assertion if useful, and ensure the post-cancellation assertion verifies that
cancellation does not invoke endRendererProtection twice.
🪄 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 Plus

Run ID: 732c5b95-34d8-4290-bbe4-04cf52142a7d

📥 Commits

Reviewing files that changed from the base of the PR and between f263bb4 and 54646c5.

📒 Files selected for processing (8)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Engine/Rendering/GhosttyMetalLayer.swift
  • Sources/ContentView.swift
  • Sources/GhosttyNotificationScrollRuntimeBridge.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceSwitchCoordinator.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceSwitchCoordinatorTests.swift

Comment thread Sources/ContentView.swift
@cursor

cursor Bot commented Jul 31, 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.

@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 `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 289-306: Update the test around
WorkspaceSwitchCoordinator.cancel() to start a replacement transaction by
invoking selectionWillCommit again before beginPresentation. Keep the passive
presentation setup unchanged, then assert isReadyForSourceRetirement to verify a
subsequent handoff is not blocked.
🪄 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 Plus

Run ID: fe79d998-8471-4026-a8d8-ae3f997c1b8a

📥 Commits

Reviewing files that changed from the base of the PR and between 54646c5 and da885d1.

📒 Files selected for processing (5)
  • Sources/App/CmuxMainWindow.swift
  • Sources/App/MainWindowVisibilityController.swift
  • Sources/ProfilingSignposts.swift
  • Sources/WorkspaceSwitchCoordinator.swift
  • cmuxTests/WorkspaceSwitchCoordinatorTests.swift

Comment thread cmuxTests/WorkspaceSwitchCoordinatorTests.swift Outdated
@cursor

cursor Bot commented Jul 31, 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.

@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.

Caution

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

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

204-211: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep renderer protection until first-frame readiness.

These paths release the request-scoped renderer lease as soon as portalPresented is true, even when firstFramePresented is still false. finishIfPossible explicitly waits for the first drawable frame, so the renderer can be reclaimed during that gap and recreate the intermittent presentation delay this transaction is meant to prevent. Let finishIfPossible release the lease only after visual readiness is complete.

Also applies to: 231-244

🤖 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/WorkspaceSwitchCoordinator.swift` around lines 204 - 211, Update the
portal presentation handling around the transaction branches and the
corresponding path at the noted secondary location so renderer protection is not
released merely when portalPresented is true. Remove or defer the
releaseRendererProtection call and let finishIfPossible release the
request-scoped lease only after firstFramePresented indicates visual readiness.

301-334: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Scope source-retirement callbacks to the active workspace request.

selectionWillCommit() calls cancel() and then installs a new transaction, so an in-flight delayed source-retirement callback can currently mutate that new transaction: sourceDidRetire() sets sourceRetired = true and noteInteractionNoLongerRequired() clears requiresInteraction and ends the interactive interval. Have these callbacks carry and validate the retiring source/target IDs, or make them no-ops once the current transaction has moved on.

🤖 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/WorkspaceSwitchCoordinator.swift` around lines 301 - 334, Scope
source-retirement callbacks to the transaction that initiated them: update
sourceWillRetire, sourceDidRetire, and noteInteractionNoLongerRequired to carry
or validate the retiring source/target workspace IDs against the active
transaction, and return without mutation when they no longer match. Ensure stale
callbacks cannot alter readiness, interactive intervals, portal-hide intervals,
or sourceRetired on a newly installed transaction.
🤖 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.

Outside diff comments:
In `@Sources/WorkspaceSwitchCoordinator.swift`:
- Around line 204-211: Update the portal presentation handling around the
transaction branches and the corresponding path at the noted secondary location
so renderer protection is not released merely when portalPresented is true.
Remove or defer the releaseRendererProtection call and let finishIfPossible
release the request-scoped lease only after firstFramePresented indicates visual
readiness.
- Around line 301-334: Scope source-retirement callbacks to the transaction that
initiated them: update sourceWillRetire, sourceDidRetire, and
noteInteractionNoLongerRequired to carry or validate the retiring source/target
workspace IDs against the active transaction, and return without mutation when
they no longer match. Ensure stale callbacks cannot alter readiness, interactive
intervals, portal-hide intervals, or sourceRetired on a newly installed
transaction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 01113c65-e1c0-4795-be60-ea9c1bd761e5

📥 Commits

Reviewing files that changed from the base of the PR and between da885d1 and 9d2a236.

📒 Files selected for processing (4)
  • Sources/AppDelegate+WindowDock.swift
  • Sources/AppDelegate.swift
  • Sources/TabManager.swift
  • Sources/WorkspaceSwitchCoordinator.swift
💤 Files with no reviewable changes (1)
  • Sources/TabManager.swift

@cursor

cursor Bot commented Jul 31, 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.

1 similar comment
@cursor

cursor Bot commented Aug 1, 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.

@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.

All reported issues were addressed across 29 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread Sources/TerminalWindowPortal.swift Outdated
Comment thread Sources/ProfilingSignposts.swift Outdated
@cursor

cursor Bot commented Aug 1, 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.

@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.

Review completed against the latest diff

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread Sources/ContentView.swift
Comment thread Sources/WorkspaceSwitchCoordinator.swift
Comment thread cmuxTests/WorkspaceSwitchFrameObservationTests.swift
Comment thread cmuxTests/TerminalAndGhosttyTests.swift

@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.

All reported issues were addressed across 30 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread Sources/WorkspaceSwitchCoordinator.swift Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/WorkspaceSwitchCoordinator.swift

@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.

All reported issues were addressed across 30 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread Sources/TerminalWindowPortal.swift Outdated
Comment thread Sources/TerminalWindowPortal.swift Outdated
Comment thread Sources/ContentView.swift
Comment thread Sources/WorkspaceSwitchSignposts.swift Outdated
Comment thread cmuxTests/WorkspaceSwitchFrameObservationTests.swift
Comment thread Sources/WorkspaceSwitchCoordinator.swift Outdated
Comment thread Sources/ContentView.swift Outdated
@austinywang

Copy link
Copy Markdown
Contributor Author

CI status note: the only red check, app-host unit tests (4/4) → "Run remote tmux mirror detach and placement regressions", is a pre-existing repo-wide flaky host crash, now tracked in #9348. The crash point moves between attempts (ordinaryCloseOfLiveMirrorDetachesWithoutKillingSession on attempt 1, dedicatedWindowSocketDefaultsToFocusNeutral on rerun) and the identical step fails on unrelated branches (runs 30680581107, 30679561158). All other jobs on this HEAD are green, including shards 1–3, swift-package-tests, and tests-build-and-lag.

@cursor

cursor Bot commented Aug 26, 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.

@vercel

vercel Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
cmux166 Ready Ready Preview Sep 6, 2026 6:46pm UTC
cmux41 Ready Ready Preview Sep 6, 2026 6:46pm UTC

@cursor

cursor Bot commented Sep 4, 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 Sep 4, 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.

@austinywang
austinywang merged commit 4e1c41e into main Sep 6, 2026
19 checks passed
@austinywang
austinywang deleted the issue-9231-workspace-switch-latency branch September 6, 2026 19:22
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 6, 2026
4e1c41e Fix intermittent warm workspace switch latency (manaflow-ai#9244)
aerickson pushed a commit to aerickson/cmux that referenced this pull request Sep 13, 2026
* Add workspace switch phase signposts

* test: cover workspace switch readiness

* Wait for workspace presentation readiness

* test: cover workspace switch signal ordering

* Fix workspace switch readiness signal ordering

* Deduplicate workspace interaction readiness

* Fix shared workspace switch signpost owner

* Test canceled workspace switch handoff

* Retire handoff without an active switch transaction

* Cancel workspace switch at window teardown

* Test portal readiness after layout

* Signal workspace presentation from portal layout

* Test source retirement independent of presentation

* Make mount reconciliation own workspace handoff

* Close workspace switch lifecycle review gaps

* Make workspace switch leases self-expiring

* Handle coalesced switch and portal rebind lifecycle

* Close workspace switch review findings

* Tighten workspace switch resource ownership

* Split workspace switch state types

* Use portal-owned presentation readiness

* Scope portal readiness diagnostics

* Avoid workspace lookup outside switch diagnostics

* test: cover coalesced portal visibility edge

* Fix coalesced terminal portal presentation edges

* Scope workspace switch signposts to an owner

* Move workspace switch signposts to their own file

* test: cover destination pane focus changes

* End switch interaction measurement at workspace boundary

* fix: retire workspace handoff after focus pass

* fix: bind handoff retirement to authoritative source

* perf: scope workspace switch diagnostics to owners

* fix: complete browser workspace readiness

* perf: observe only active workspace portals

* fix: retain scoped portal observers through handoff

* fix: publish workspace switch completion

* fix: retain renderer protection through portal presentation

* fix: bound unresolved workspace switch handoff

* fix: bound post-retirement workspace switches

* fix: harden workspace switch presentation lifecycle

* perf: avoid disabled switch signpost formatting

* fix: scope workspace switch callbacks to their target

* fix: capture workspace switch request before expiry scheduling

* fix: restore workspace switch build under Swift 6

* fix: keep merged test target compiling

* fix: remove duplicate exact session lookup

* fix: preserve hibernation liveness in record initialization

* fix: initialize hibernation process liveness explicitly

* fix: close workspace switch lifecycle findings

* test: keep cloud port regression target compiling

* test: wire cloud machine resize action fixture
lucasr1b added a commit that referenced this pull request Oct 7, 2026
fbbe32a raised the mount lru to 4 (and 4 while cycling) so a switch
back to a recent workspace was a visibility flip instead of a cold mount.
that predates #9244 (warm switch latency fix, 2026-09-06), which fixed the
switch lag at the source with readiness signals and dropped the cycle pair
on main. the merge brought maxMountedWorkspacesDuringCycle back as an
unused constant.

4 is not worth it:
- every mounted workspace keeps portal rendering on
  (WorkspacePortalRenderingPlan -> setPortalRenderingEnabled(true)), so its
  terminal and browser portal views keep their geometry follow-ups and stay
  in the layer tree the window walks on every frame and typing update
- each kept terminal keeps its surface render targets in memory, times 4
- the switch handoff is already covered: the retiring workspace stays
  pinned through mountedBackgroundWorkspaceLoadIds while the new one loads
- WorkspaceMountPolicyTests.testDefaultPolicyMountsOnlySelectedWorkspace
  asserts the budget of 1 and fails at 4

so the file goes back to main byte for byte.

This branch was successfully deployed

2 active deployments
Preview – cmux41 — f89a2a94 Deployed Sep 6, 2026 by vercel[bot]
Preview – cmux166 — f89a2a94 Deployed Sep 6, 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.

0.64.20 regression: intermittent delay switching windows/workspaces

1 participant