Skip to content

Fix workspace color picker context menu regression - #4648

Closed
austinywang wants to merge 4 commits into
mainfrom
issue-4646-color-picker-regression
Closed

austinywang wants to merge 4 commits into
mainfrom
issue-4646-color-picker-regression

Conversation

@austinywang

@austinywang austinywang commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4646.

Regression context: #2560 was fixed by #2566 on 2026-04-07 by freezing TabItemView context-menu presentation and deferring workspace observation updates while the menu is open.

Summary

  • Restore full frozen sidebar row presentation while a workspace context menu is active so unread badge/preview and shortcut-hint publishes cannot change TabItemView equality and rebuild .contextMenu mid-submenu.
  • Replace the menu visibility boolean with a single row menu lifecycle that treats AppKit NSMenu tracking and SwiftUI .contextMenu content as the same active phase. This covers the macOS 26 case where SwiftUI .onAppear may be late or skipped, and the inverse .onDisappear-skipped case.
  • Start and stop the presentation freeze from the AppKit tracking callback too, so the freeze begins before SwiftUI menu content lifecycle callbacks are required.

Root Cause

#4084 made frozen sidebar presentations keep unread count and latest notification text live during a context-menu freeze. Those fields participate in TabItemView ==, so notification-store publishes could still re-evaluate the row and rebuild .contextMenu while the Workspace Color submenu was open. Separately, the deferral gate only trusted SwiftUI .contextMenu .onAppear, while the row already had a more reliable AppKit menu-tracking signal.

Regression Coverage

Two-commit red/green structure:

  1. e4f809c0d changes/adds failing runtime policy tests for frozen presentation stability and AppKit menu-tracking visibility.
  2. 515b8e45c implements the fix.

Verification

  • git diff --check

Not run locally per repo/task policy: local tests/builds and ./scripts/reload.sh.

Cloud Mac video: unavailable. cloud-mac preflight is currently blocked locally by unauthenticated macfleet, and gh auth status reports invalid keyring tokens. I still pushed the branch successfully over the repo remote, but cloud VM provisioning could not be started from this checkout.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touching sidebar row/menu interaction state and snapshot freezing logic could introduce subtle UI regressions (hover/close button visibility or deferred snapshot flushing), though changes are localized and backed by new tests.

Overview
Fixes a sidebar workspace context-menu regression by fully freezing SidebarTabItemPresentationSnapshot while a row menu is active, preventing live unread/notification updates from rebuilding TabItemView (and its .contextMenu) mid-interaction.

Unifies menu visibility handling by replacing the simple boolean with a SwiftUI + AppKit context-menu lifecycle in SidebarWorkspaceRowInteractionState, and starts/ends the freeze based on both SwiftUI appear/disappear and AppKit NSMenu tracking callbacks (via a new onContextMenuTrackingChanged hook in SidebarWorkspaceRowHoverTracker). Adds/updates tests to cover the new freeze semantics and menu lifecycle edge cases.

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


Summary by cubic

Fixes the workspace color picker context menu regression by fully freezing the sidebar row while any menu is active and deriving visibility from a unified SwiftUI/AppKit lifecycle. Stops unread/preview changes from rebuilding the menu mid-submenu and keeps hover suppression until AppKit tracking ends.

  • Bug Fixes

    • Return the frozen SidebarTabItemPresentationSnapshot during an active menu to stabilize unread count, preview text, and shortcut hints.
    • Start/end the freeze from AppKit NSMenu tracking and SwiftUI; ignore SwiftUI disappear while tracking and flush deferred workspace updates only after tracking ends; SidebarWorkspaceRowHoverTracker now reports tracking changes to drive the freeze.
  • Refactors

    • Replace the boolean menu flag with a lifecycle state (inactive, SwiftUI-visible, AppKit-tracking) in SidebarWorkspaceRowInteractionState to unify visibility and close-button suppression across edge cases.

Written for commit 6de0bbe. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Sidebar badges and notification text remain stable while context menus are open.
    • Context-menu interactions now preserve hover/close-button behavior and visibility across different menu lifecycles.
    • Sidebar presentation now consistently maintains state during menu operations.
  • Tests

    • Added tests covering context-menu visibility and notification-state stabilization across interaction patterns.

Review Change Stack

@vercel

vercel Bot commented May 23, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 23, 2026 9:32am
cmux-staging Building Building Preview, Comment May 23, 2026 9:32am

@coderabbitai

coderabbitai Bot commented May 23, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR centralizes context-menu visibility into a lifecycle, adds a hover-tracker callback to report AppKit menu tracking, returns the full frozen presentation snapshot for matched tab IDs, and replaces ad-hoc onAppear/onDisappear freezing with begin/end orchestration that conditionally clears frozen state and flushes deferred invalidation.

Changes

Context-Menu Lifecycle and Presentation Freezing

Layer / File(s) Summary
Context-menu lifecycle state machine foundation
Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift
SidebarWorkspaceRowInteractionState replaces stored contextMenuVisible with a computed property backed by a ContextMenuLifecycle and updates lifecycle handlers to transition that state while preserving suppression/deferral logic.
Row hover tracker callback integration
Sources/Sidebar/SidebarWorkspaceRowHoverTracker.swift
Adds onContextMenuTrackingChanged closure (no-op default) to the representable and coordinator; the closure is invoked when AppKit menu tracking begins/ends, alongside existing interaction callbacks.
Frozen presentation snapshot resolution
Sources/ContentView.swift
SidebarTabItemPresentationResolutionPolicy.resolved(...) now returns the full frozen snapshot when the tabId matches, preserving frozen unreadCount and latestNotificationText.
Context-menu freeze/unfreeze orchestration
Sources/ContentView.swift
SidebarTabItemContextMenuState converted to a value-type and TabItemView uses @State. TabItemView wires the hover tracker callback and replaces direct onAppear/onDisappear freezing with beginContextMenuPresentationFreeze() / endContextMenuPresentationFreeze() that conditionally clear the frozen presentation and may flush deferred invalidation.
Lifecycle and presentation test coverage
cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift
Updated frozen-presentation resolution test expectations and added tests for AppKit vs SwiftUI context-menu lifecycle transitions and hover suppression semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#1629: Changes stabilize frozen notification fields and may address flaky tests that trigger on notification-driven row layout changes.
  • manaflow-ai/cmux-dev-artifacts#1599: Modifies snapshot resolution and context-menu lifecycle in ways that align with the objectives described in that issue.

Poem

🐰 I nibbled at menus that blinked out of sight,
Now lifecycle holds them through hover and bite.
A tracker that listens, a freeze that won’t roam,
Badges stay steady while the rabbit paints home. 🎨🫶


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift @Concurrent ❌ Error submitFeedback() performs network-heavy (URLSession) and parsing-heavy operations but lacks @concurrent annotation when called from MainActor without explicit actor hop. Add @concurrent annotation to submitFeedback() or use Task.detached for network/parsing work per swift-concurrent-annotation.md rules.
Cmux Architecture Rethink ❌ Error Splits context menu lifecycle ownership between parent frozenPresentation and child rowInteractionState, allowing Row A to clear Row B's frozen state while Row B's menu is active. Consolidate ownership: move "which row is frozen" to single parent owner with atomic transitions, preventing endContextMenuPresentationFreeze from clearing a different row's frozen presentation.
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.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix workspace color picker context menu regression' clearly and concisely describes the main change—fixing a regression affecting the workspace color picker context menu.
Linked Issues check ✅ Passed The code changes directly address all primary objectives from #4646: fully freezing the presentation snapshot, unifying context-menu lifecycle, integrating AppKit tracking, and adding test coverage.
Out of Scope Changes check ✅ Passed All changes are in-scope: ContentView.swift, SidebarWorkspaceRowHoverTracker.swift, SidebarWorkspaceSnapshotRefreshPolicy.swift, and related test files directly target the context-menu regression fix.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no blocking/timing patterns (Task.sleep, NSLock, semaphores, polling, main-queue sync) in production code. Only structural refactoring of context menu lifecycle.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift files (.swift). The no-hacky-sleeps rule explicitly excludes Swift, delegating timing/blocking to swift-blocking-runtime.md. This check does not apply.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. Freeze/unfreeze methods are synchronous. @escaping callback appropriately used at AppKit/SwiftUI boundary per allowed cases.
Cmux Swift File And Package Boundaries ✅ Passed PR adds +36 lines to ContentView.swift (oversized but still under 250-line growth threshold). Changes are UI/AppKit glue for context menu freezing, allowed as focused bug fix per boundary rules.
Cmux Swift Logging ✅ Passed PR adds one NSLog call guarded by #if DEBUG in Sources/ContentView.swift. Per swift-logging.md, debug-only NSLog guarded by #if DEBUG is allowed. No other logging violations found.
Cmux User-Facing Error Privacy ✅ Passed All changes are internal: private functions, enums, and test code. No user-facing strings, error messages, alerts, vendor names, credentials, or implementation details are exposed.
Cmux Full Internationalization ✅ Passed PR contains only internal refactoring (state management, enums, logic) and test additions—no new user-facing strings, localization APIs, or string catalog modifications.
Cmux Swiftui State Layout ✅ Passed PR improves SwiftUI state: converts ObservableObject to value type with @State, replaces stored with computed property, uses callbacks instead of observation. No problematic patterns.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add new windows. Changes are context menu lifecycle refactoring and hover tracking callbacks within existing sidebar UI.
Description check ✅ Passed The PR description thoroughly covers all required template sections with detailed explanations of the bug fix, testing methodology, and regression coverage.
✨ 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-4646-color-picker-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.

@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 `@Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift`:
- Around line 115-121: In contextMenuDidDisappear(), avoid clearing the hover
suppression while AppKit tracking remains authoritative: only set
contextMenuTrackingSuppressesCloseButton = false and call
applyDeferredPointerHovering() when contextMenuLifecycle != .appKitTracking so
the suppression remains active until contextMenuTrackingDidEnd() clears it;
update the logic in the contextMenuDidDisappear() method to check
contextMenuLifecycle (and keep the existing contextMenuLifecycle = .inactive
assignment behavior) before releasing suppression.
🪄 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: 5359a749-9a99-4a51-82f7-46b5b311d31b

📥 Commits

Reviewing files that changed from the base of the PR and between 2647b9b and 515b8e4.

📒 Files selected for processing (4)
  • Sources/ContentView.swift
  • Sources/Sidebar/SidebarWorkspaceRowHoverTracker.swift
  • Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift
  • cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift

Comment thread Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift
@greptile-apps

greptile-apps Bot commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the workspace color picker context menu regression by fully freezing SidebarTabItemPresentationSnapshot while a menu is open (returning frozen entirely instead of partially-live unread/notification fields) and unifying SwiftUI and AppKit menu lifecycle into a ContextMenuLifecycle enum so AppKit tracking is the authoritative close signal.

  • Full presentation freeze: SidebarTabItemPresentationResolutionPolicy.resolved now returns the frozen snapshot unchanged, preventing unreadCount/latestNotificationText updates from flowing into .contextMenu content and triggering a mid-submenu rebuild.
  • Lifecycle state machine: boolean contextMenuVisible replaced with .inactive/.swiftUIVisible/.appKitTracking; contextMenuDidDisappear is a no-op during .appKitTracking, and contextMenuTrackingDidEnd is always authoritative — covering the macOS 26 case where SwiftUI .onAppear/.onDisappear may be late or skipped.
  • AppKit-driven freeze entrypoint: SidebarWorkspaceRowHoverTracker now forwards NSMenu tracking notifications to TabItemView via onContextMenuTrackingChanged, so beginContextMenuPresentationFreeze fires before SwiftUI content lifecycle callbacks are required; both entrypoints are guarded for idempotency.

Confidence Score: 5/5

Safe to merge; the lifecycle state machine changes are well-guarded and regression coverage is thorough.

The logic for the full presentation freeze, the dual-entrypoint idempotency guards, and the contextMenuLifecycle enum transitions are all correct and directly covered by the new tests. No functional regressions introduced.

No files require special attention; the @State vs @StateObject re-render behavior difference in ContentView.swift is worth awareness but does not affect correctness.

Important Files Changed

Filename Overview
Sources/ContentView.swift Core fix: SidebarTabItemPresentationResolutionPolicy.resolved now returns the fully frozen snapshot; SidebarTabItemContextMenuState migrated from @StateObject class to @State struct; beginContextMenuPresentationFreeze/endContextMenuPresentationFreeze helpers added with idempotency guards; onContextMenuTrackingChanged wired to SidebarWorkspaceRowHoverTracker.
Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift Replaces boolean contextMenuVisible with a ContextMenuLifecycle enum (inactive/swiftUIVisible/appKitTracking); contextMenuDidDisappear and contextMenuTrackingDidEnd guard correctly so AppKit tracking is the authoritative close signal when SwiftUI skips .onDisappear.
Sources/Sidebar/SidebarWorkspaceRowHoverTracker.swift Adds onContextMenuTrackingChanged: (Bool) -> Void callback; updateNSView keeps it current on every reconcile; menuTrackingChanged fires the callback after updating rowInteractionState, ensuring lifecycle and freeze state advance in a consistent order.
cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift Renames existing test to reflect the now-correct full-freeze semantics; adds lifecycle tests covering AppKit-before-SwiftUI ordering, SwiftUI-only path, and hover suppression surviving a SwiftUI .onDisappear during active AppKit tracking.

Sequence Diagram

sequenceDiagram
    participant AppKit as NSMenu (AppKit)
    participant Tracker as SidebarWorkspaceRowHoverTracker
    participant TabItemView
    participant LifecycleState as SidebarWorkspaceRowInteractionState
    participant FreezeState as frozenPresentation (Binding)

    AppKit->>Tracker: NSMenu.didBeginTrackingNotification
    Tracker->>LifecycleState: contextMenuTrackingDidBegin() → .appKitTracking
    Tracker->>TabItemView: onContextMenuTrackingChanged(true)
    TabItemView->>FreezeState: "beginContextMenuPresentationFreeze() → freeze = livePresentation"

    Note over TabItemView: SwiftUI .onAppear (may be late or skipped)
    TabItemView->>LifecycleState: contextMenuDidAppear() → stays .appKitTracking (guarded)
    TabItemView->>FreezeState: beginContextMenuPresentationFreeze() → guard: already frozen, no-op

    Note over TabItemView: Notification store publish (unread/preview update)
    TabItemView->>TabItemView: resolved() returns frozen → contextMenu content unchanged

    Note over TabItemView: SwiftUI .onDisappear (may be early or skipped)
    TabItemView->>LifecycleState: contextMenuDidDisappear() → guard: .appKitTracking, no-op
    Note over TabItemView: endContextMenuPresentationFreeze() NOT called (contextMenuVisible still true)

    AppKit->>Tracker: NSMenu.didEndTrackingNotification
    Tracker->>LifecycleState: contextMenuTrackingDidEnd() → .inactive
    Tracker->>TabItemView: onContextMenuTrackingChanged(false)
    TabItemView->>FreezeState: "endContextMenuPresentationFreeze() → frozen = nil, flush deferred"
Loading

Reviews (3): Last reviewed commit: "refactor: derive context menu freeze sta..." | Re-trigger Greptile

Comment on lines 115 to 121
mutating func contextMenuDidDisappear() {
contextMenuVisible = false
if contextMenuLifecycle != .appKitTracking {
contextMenuLifecycle = .inactive
}
contextMenuTrackingSuppressesCloseButton = false
applyDeferredPointerHovering()
}

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 contextMenuDidDisappear unconditionally clears contextMenuTrackingSuppressesCloseButton and calls applyDeferredPointerHovering() even when lifecycle is .appKitTracking. If SwiftUI fires .onDisappear while AppKit is still tracking — the exact rebuild scenario this PR is guarding against, which can still occur if the freeze fails to hold — the close button suppression is prematurely dropped and any pending deferred hover is drained. The lifecycle check already gates contextMenuLifecycle, but the same guard is not applied to the two side-effects below it.

Suggested change
mutating func contextMenuDidDisappear() {
contextMenuVisible = false
if contextMenuLifecycle != .appKitTracking {
contextMenuLifecycle = .inactive
}
contextMenuTrackingSuppressesCloseButton = false
applyDeferredPointerHovering()
}
mutating func contextMenuDidDisappear() {
guard contextMenuLifecycle != .appKitTracking else { return }
contextMenuLifecycle = .inactive
contextMenuTrackingSuppressesCloseButton = false
applyDeferredPointerHovering()
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 362343c with the same lifecycle guard, plus regression coverage for SwiftUI disappearance during active AppKit tracking.

— Claude Code

Comment thread Sources/ContentView.swift Outdated
Comment on lines 13260 to 13264
private final class SidebarTabItemContextMenuState: ObservableObject {
var presentationFreezeActive = false
var hasDeferredWorkspaceObservationInvalidation = false
var pendingWorkspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot?
}

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 SidebarTabItemContextMenuState is a mutable final class: ObservableObject without @MainActor. All its call sites are on the main actor (SwiftUI lifecycle callbacks and AppKit queue: .main notifications), but the absence of an explicit @MainActor annotation leaves that isolation implicit. In Swift 6 strict-concurrency mode, storing or accessing this class from a @MainActor-bound view without the annotation can produce isolation diagnostics, and the new presentationFreezeActive property deepens the pattern.

Suggested change
private final class SidebarTabItemContextMenuState: ObservableObject {
var presentationFreezeActive = false
var hasDeferredWorkspaceObservationInvalidation = false
var pendingWorkspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot?
}
@MainActor
private final class SidebarTabItemContextMenuState: ObservableObject {
var presentationFreezeActive = false
var hasDeferredWorkspaceObservationInvalidation = false
var pendingWorkspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot?
}

Rule Used: Flag new or materially worsened Swift 6 actor isol... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 362343c by marking SidebarTabItemContextMenuState as @mainactor; its SwiftUI/AppKit menu lifecycle accesses are main-actor UI state.

— Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 6de0bbe: instead of keeping this as an @mainactor ObservableObject, I removed the object entirely and made the non-published context-menu storage a plain @State value. The freeze-active bit is now derived from the frozen presentation binding.

— Claude Code

@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 `@Sources/ContentView.swift`:
- Around line 14033-14040: The guard in endContextMenuPresentationFreeze() is
too permissive and can clear a shared frozenPresentation for another row; update
the logic so you only clear frozenPresentation and call
flushDeferredWorkspaceObservationInvalidation() when the frozenPresentation
actually belongs to the current row (i.e. frozenPresentation?.tabId == tab.id)
AND the deferred invalidation flag applies to this same row
(contextMenuState.hasDeferredWorkspaceObservationInvalidation must be tied to
this row), rather than using OR; in practice change the condition so both checks
must be true (or add an explicit comparison between frozenPresentation's owner
identifier and this row's contextMenuState identifier) before nil-ing
frozenPresentation and flushing.
🪄 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: 442d3c3b-b8cd-45b6-9680-6adfcb141d72

📥 Commits

Reviewing files that changed from the base of the PR and between 362343c and 6de0bbe.

📒 Files selected for processing (1)
  • Sources/ContentView.swift

Comment thread Sources/ContentView.swift
Comment on lines +14033 to +14040
private func endContextMenuPresentationFreeze() {
guard frozenPresentation?.tabId == tab.id ||
contextMenuState.hasDeferredWorkspaceObservationInvalidation
else {
return
}
frozenPresentation = nil
flushDeferredWorkspaceObservationInvalidation()

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 | 🟠 Major | ⚡ Quick win

Don't clear another row's frozen snapshot during late teardown.

If row A still has deferred invalidation and row B has already started a new context menu, this guard still passes for A and Line 14039 clears the shared frozenPresentation for B. That reopens the same cross-row rebuild/dismissal race this PR is trying to close.

Suggested fix
 private func endContextMenuPresentationFreeze() {
-    guard frozenPresentation?.tabId == tab.id ||
+    let ownsFrozenPresentation = frozenPresentation?.tabId == tab.id
+    guard ownsFrozenPresentation ||
         contextMenuState.hasDeferredWorkspaceObservationInvalidation
     else {
         return
     }
-    frozenPresentation = nil
+    if ownsFrozenPresentation {
+        frozenPresentation = nil
+    }
     flushDeferredWorkspaceObservationInvalidation()
 }
🤖 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/ContentView.swift` around lines 14033 - 14040, The guard in
endContextMenuPresentationFreeze() is too permissive and can clear a shared
frozenPresentation for another row; update the logic so you only clear
frozenPresentation and call flushDeferredWorkspaceObservationInvalidation() when
the frozenPresentation actually belongs to the current row (i.e.
frozenPresentation?.tabId == tab.id) AND the deferred invalidation flag applies
to this same row (contextMenuState.hasDeferredWorkspaceObservationInvalidation
must be tied to this row), rather than using OR; in practice change the
condition so both checks must be true (or add an explicit comparison between
frozenPresentation's owner identifier and this row's contextMenuState
identifier) before nil-ing frozenPresentation and flushing.

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6de0bbe. Configure here.

Comment thread Sources/ContentView.swift
@Binding var frozenPresentation: SidebarTabItemPresentationSnapshot?
@State private var workspaceSnapshotStorage: SidebarWorkspaceSnapshotBuilder.Snapshot?
@StateObject private var contextMenuState = SidebarTabItemContextMenuState()
@State private var contextMenuState = SidebarTabItemContextMenuState()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@State mutations trigger body re-evaluations during menu freeze

Medium Severity

Changing contextMenuState from @StateObject (class with non-@Published properties) to @State (struct) means mutations to pendingWorkspaceSnapshot now trigger SwiftUI body re-evaluations during the freeze. Each debounced workspace observation arriving while the menu is open mutates @State, causing the body—including .contextMenu { workspaceContextMenu }—to be re-evaluated. With the prior @StateObject approach, these internal bookkeeping mutations were invisible to SwiftUI's update cycle. Since this entire PR exists to prevent .contextMenu rebuilds mid-submenu, the new @State re-evaluations work against that guarantee, risking the same submenu-dismissal regression on macOS versions where SwiftUI may diff and rebuild open context-menu content.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6de0bbe. Configure here.

This branch was successfully deployed

1 active deployment
Preview – cmux — 6de0bbea Deployed May 23, 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.

Workspace color picker dismisses before color can be selected (regression of #2560)

1 participant