Skip to content

Avoid MainActor executor crashes in tab hit testing - #190

Open
austinywang wants to merge 1 commit into
mainfrom
fix-macos26-main-actor-crash-v2
Open

austinywang wants to merge 1 commit into
mainfrom
fix-macos26-main-actor-crash-v2

Conversation

@austinywang

@austinywang austinywang commented Jul 17, 2026 •

Copy link
Copy Markdown

Summary

  • make tab hit-region queries explicitly main-actor isolated instead of calling MainActor.assumeIsolated on every mouse event
  • deliver geometry notification updates through main-actor tasks
  • remove the unsafe nonisolated hit-bounds storage

Why

On macOS 26.4.x, MainActor.assumeIsolated can fault inside the Swift executor runtime even when AppKit invokes the callback on the main thread. The tab hit-test path runs for ordinary pointer events, making tagged cmux builds crash during normal use.

Verification

  • swift build
  • parent cmux tagged dogfood build and focus/tab stress will follow before merge

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


Summary by cubic

Fixes crashes in tab hit testing on macOS 26.4 by running hit-region queries and geometry updates on the main actor. This removes reliance on MainActor.assumeIsolated and prevents executor faults during pointer events.

  • Bug Fixes
    • Marked BonsplitTabItemHitRegionProviding.containsBonsplitTabItemHit and BonsplitTabItemHitRegionRegistry.containsWindowPoint as @MainActor.
    • Replaced MainActor.assumeIsolated with Task { @MainActor ... } for scroll and geometry notifications.
    • Removed nonisolated(unsafe) hit-bounds storage; access is now main-actor isolated.
    • Simplified tab-lane hit testing to read geometry directly on the main actor.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved tab bar hit testing and notification handling for more reliable UI interactions.
    • Ensured geometry updates and hit-testing operations execute safely on the main thread.
    • Reduced the risk of inconsistent tab selection or interaction results during UI updates.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR updates tab-bar geometry observers to dispatch through the main actor and aligns hit-region protocols, registry entry points, and view implementations with main-actor isolation.

Changes

Tab-bar concurrency alignment

Layer / File(s) Summary
Main-actor geometry observer dispatch
Sources/Bonsplit/Internal/Views/TabBarItemGeometryRegistry.swift
Scroll-view, document, and container geometry observer callbacks now perform registry updates inside Task { @mainactor ... } closures.
Main-actor hit-testing contracts and implementations
Sources/Bonsplit/Internal/Views/TabBarView.swift, Sources/Bonsplit/Internal/Views/TabBarItemGeometryRegistry.swift
Hit-testing protocol and registry APIs are marked @MainActor; related view storage and methods no longer use nonisolated isolation overrides.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: lawrencecchen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing tab hit testing to avoid MainActor executor crashes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 fix-macos26-main-actor-crash-v2

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 17, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR fixes a macOS 26.4.x crash where MainActor.assumeIsolated could fault in the Swift executor runtime even when AppKit called the notification callback on the main thread. The fix replaces all assumeIsolated usages in notification closures with Task { @MainActor }, promotes containsBonsplitTabItemHit to explicit @MainActor on both the protocol and conforming types, and removes the nonisolated(unsafe) storage that was only needed to serve the previously-nonisolated hit-test path.

  • All four NotificationCenter callbacks (scroll bounds, live scroll, document geometry, container geometry) now enqueue an async MainActor task instead of asserting isolation synchronously — avoiding the executor fault at the cost of one run-loop turn of latency, which is imperceptible for geometry/scroll updates.
  • BonsplitTabItemHitRegionProviding.containsBonsplitTabItemHit and BonsplitTabItemHitRegionRegistry.containsWindowPoint are now explicitly @MainActor, making the hit-test isolation contract enforced at compile time rather than at runtime.
  • hitBounds in RegionNSView is promoted from nonisolated(unsafe) to a plain stored property; one leftover nonisolated(unsafe) on tabIds in TabBarBackgroundNSView was not cleaned up.

Confidence Score: 4/5

Safe to merge after dogfood stress validation; the async dispatch introduces a one-run-loop delay that is benign for geometry updates and the FIFO MainActor queue preserves notification ordering in almost all cases.

The core fix (swapping assumeIsolated for Task { @mainactor }) is idiomatic and removes a real crash on macOS 26.4.x. The protocol-level @mainactor annotations are a clean long-term improvement. The leftover nonisolated(unsafe) on tabIds is a minor inconsistency, and there is a narrow theoretical ordering window around willStartLiveScrollNotification and boundsDidChangeNotification that deserves a clarifying comment but is unlikely to cause visible regressions in practice.

TabBarItemGeometryRegistry.swift — the four async notification callbacks near the willStartLiveScroll observer are the most sensitive part of the change and would benefit from a brief inline comment explaining why FIFO MainActor task ordering is sufficient.

Important Files Changed

Filename Overview
Sources/Bonsplit/Internal/Views/TabBarItemGeometryRegistry.swift Replaces four MainActor.assumeIsolated calls in notification callbacks with Task { @mainactor } to avoid executor crashes; removes nonisolated(unsafe) from hitBounds now that containsBonsplitTabItemHit is @mainactor.
Sources/Bonsplit/Internal/Views/TabBarView.swift Adds @mainactor to the BonsplitTabItemHitRegionProviding protocol method and containsWindowPoint; removes nonisolated + assumeIsolated from TabBarBackgroundNSView.containsBonsplitTabItemHit, but leaves nonisolated(unsafe) on tabIds which is now unnecessary.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant NC as NotificationCenter
    participant CB as Closure (main queue)
    participant MA as MainActor (Task)
    participant Reg as TabBarItemGeometryRegistry
    participant HT as containsBonsplitTabItemHit (@MainActor)

    Note over NC,Reg: Old path (crashy on macOS 26.4.x)
    NC->>CB: boundsDidChangeNotification (queue: .main)
    CB->>MA: MainActor.assumeIsolated
    Note over MA: Runtime fault if executor state is inconsistent
    MA->>Reg: scrollBoundsDidChange()

    Note over NC,Reg: New path (this PR)
    NC->>CB: boundsDidChangeNotification (queue: .main)
    CB->>MA: "Task { @MainActor [weak self] in }"
    Note over MA: Async enqueue — no runtime executor check
    MA->>Reg: scrollBoundsDidChange()

    Note over HT: Hit-test path (this PR)
    HT->>HT: containsBonsplitTabItemHit(localPoint:)
    Note over HT: Now @MainActor on protocol, hitBounds is plain stored property
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant NC as NotificationCenter
    participant CB as Closure (main queue)
    participant MA as MainActor (Task)
    participant Reg as TabBarItemGeometryRegistry
    participant HT as containsBonsplitTabItemHit (@MainActor)

    Note over NC,Reg: Old path (crashy on macOS 26.4.x)
    NC->>CB: boundsDidChangeNotification (queue: .main)
    CB->>MA: MainActor.assumeIsolated
    Note over MA: Runtime fault if executor state is inconsistent
    MA->>Reg: scrollBoundsDidChange()

    Note over NC,Reg: New path (this PR)
    NC->>CB: boundsDidChangeNotification (queue: .main)
    CB->>MA: "Task { @MainActor [weak self] in }"
    Note over MA: Async enqueue — no runtime executor check
    MA->>Reg: scrollBoundsDidChange()

    Note over HT: Hit-test path (this PR)
    HT->>HT: containsBonsplitTabItemHit(localPoint:)
    Note over HT: Now @MainActor on protocol, hitBounds is plain stored property
Loading

Comments Outside Diff (1)

  1. Sources/Bonsplit/Internal/Views/TabBarView.swift, line 2273 (link)

    P2 The nonisolated(unsafe) annotation on tabIds was required only because containsBonsplitTabItemHit was previously nonisolated. Now that the protocol method carries @MainActor, all accesses to tabIds happen on the MainActor and the unsafe annotation is unnecessary — it should be removed to eliminate the false impression of a cross-isolation data hazard.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "Avoid MainActor executor checks in tab h..." | Re-trigger Greptile

Comment on lines 113 to 126
@@ -120,7 +120,7 @@ final class TabBarItemGeometryRegistry {
object: scrollView,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated {
Task { @MainActor [weak self] in
self?.userWillScroll()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Async delivery changes willStartLiveScroll ordering guarantee

The willStartLiveScrollNotification is documented to fire synchronously before the first scroll delta is applied. Replacing assumeIsolated with Task { @MainActor } means userWillScroll() (which clears expectedProgrammaticOffset and pendingScrollIntent) now runs one run-loop turn after the notification, while boundsDidChangeNotification tasks from that same scroll gesture may already be enqueued. Although Task enqueues are FIFO on the MainActor and the willStartLiveScroll task should arrive first, any notification delivered before that task drains — e.g., a re-entrant layout triggered by AppKit — can create a task that observes stale expectedProgrammaticOffset and issues a spurious programmatic offset correction mid-gesture. The window is tiny in practice, but worth a comment explaining why ordering is still safe here.

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

🧹 Nitpick comments (1)
Sources/Bonsplit/Internal/Views/TabBarView.swift (1)

2273-2273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove unnecessary nonisolated(unsafe) from tabIds.

Since containsBonsplitTabItemHit is now explicitly @MainActor, tabIds is exclusively accessed and mutated on the main actor. Removing nonisolated(unsafe) aligns with the PR objective to clean up unsafe nonisolated hit-testing storage.

♻️ Proposed refactor
-        nonisolated(unsafe) var tabIds: [UUID] = []
+        var tabIds: [UUID] = []
🤖 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/Bonsplit/Internal/Views/TabBarView.swift` at line 2273, Remove the
unnecessary nonisolated(unsafe) annotation from tabIds, leaving it as a regular
variable because containsBonsplitTabItemHit now accesses and mutates it
exclusively on the MainActor.
🤖 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.

Nitpick comments:
In `@Sources/Bonsplit/Internal/Views/TabBarView.swift`:
- Line 2273: Remove the unnecessary nonisolated(unsafe) annotation from tabIds,
leaving it as a regular variable because containsBonsplitTabItemHit now accesses
and mutates it exclusively on the MainActor.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1321fb49-3f09-4677-8715-56fa5c9972ff

📥 Commits

Reviewing files that changed from the base of the PR and between d27b4c6 and bb022b4.

📒 Files selected for processing (2)
  • Sources/Bonsplit/Internal/Views/TabBarItemGeometryRegistry.swift
  • Sources/Bonsplit/Internal/Views/TabBarView.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant