fix: deep split chains no longer freeze the main thread on first mount
Summary
A deep split chain, such as an agent team's column of teammates, froze the main thread the first time it was shown or restored: about 28 s at depth 16 and about 2 minutes at depth 18 (#10311, #16393).
The cause is in Bonsplit: every split level nested another NSHostingView. SwiftUI's key-view-loop rebuild walks the focusable items of every nested host, and that walk roughly doubled with each level. manaflow-ai/bonsplit#284 hosts each pane once and builds nested splits as plain AppKit NSSplitViews, so pane hosts are siblings and the walk stays linear. The NSSplitView tree that cmux's portals inspect is unchanged.
This PR:
- Bumps
vendor/bonsplitfrombd94ed1tobc0d6dd, the bonsplit#284 commit only; #282 stays with #19593. - Fixes
tests_v2/test_deep_split_chain_stays_responsive.py. The old version passed 5/5 while the main thread was still pinned for about 25 s, because it checked withping, which is answered off the main actor. It now timespane.list, a main-actor request, after showing the 16-deep chain, after switching away and after switching back, and fails unless each answers within 1 s.
Credit to @Apptah: their diagnosis in manaflow-ai/cmux#10315 traced the hang to SwiftUI's key-view-loop focus walk over nested hosts, which is what this removes.
#19606's acceptsFirstResponder cache only trimmed about 10%, and bonsplit#284 removes it.
Testing
All runs were on capture minis, from fleet builds of this branch.
Regression test, recorded with the test commit before the bump:
| Build | Result | Main-actor stall after showing the chain |
|---|---|---|
Main build 30d1f5aa | Red, RESULT 3/4 | 27.92 s |
Main-equivalent build e71060cc, in the suite run | Red, RESULT 3/4 | 28.33 s |
This head (1cb93c77) | Green, RESULT 4/4 | 0.00 s |
Depth scan, main-actor stall after the first show:
| Depth | Before | This head |
|---|---|---|
| 12 | 1.1 s | 0.001 s |
| 14 | 6.2 s | 0.001 s |
| 16 | 28–29 s | 0.001–0.007 s |
| 18 | 119 s | 0.009 s |
| 24 | — | 0.02 s |
| 32 | — | 0.02 s |
| 40 | — | 0.26 s |
Session restore: launching onto a saved session with an 18-deep and four 16-deep chains answers immediately, and each restored workspace shows without a stall. Main left the app unanswering at launch on the same session.
Existing split e2e tests, each in a fresh workspace, give identical results on the main-equivalent build and this head:
- Pass on both:
focus_pane_by_numbernested_split_no_arranged_subview_underflownested_split_no_detach_during_updatenested_split_panel_routingnested_split_preserves_existing_split
- Fail the same way on both:
nested_split_does_not_disappear(no_space)pane_break_swap_preserve_focus(rate_limited)split_cmd_d_ctrl_d_geometry_fuzz(process_exited)split_cmd_d_ctrl_d_two_pane_frame_guard(process_exited)split_flash_and_layout(invalid_state)
Local checks: scripts/verify-local.py passes except swift-syntax on scripts/generate-computer-use-helper-icon.swift, which this PR doesn't touch. No review subagent was run.
Changelog
- Fixed: Workspaces with deeply nested splits no longer freeze cmux when first shown or restored.
🤖 Generated with Claude Code
Summary by CodeRabbit
- Tests
- Expanded regression coverage for responsiveness in deeply nested workspaces. Checks now verify that pane lists respond promptly after the workspace is shown and when switching away and back, while confirming the expected pane count. This improves detection of delays during common workspace interactions.