fix: a new workspace's sidebar row lands with its content
Summary
On Cmd-N, the new workspace's content appeared in one frame while the sidebar still showed the old workspace selected and had no row for the new one. The row and selection arrived one or two frames later. The perceived-speed ranking (#20516) recorded this as the "old sidebar" flash.
Cause: the window swaps content in on its own observation hop (WindowController.observeWorkspace → show(_:on:)). SidebarBridge updates SidebarModel on a later hop, and SidebarView draws the model on the hop after that.
Fix: WindowController.show(_:on:) now runs the sidebar's passes and draws the sidebar just before the content swap, so both land in the same frame.
SidebarSameTurn(new, besideSidebarBridge) holds the bridge's rows and selection passes. Each pass keeps itsObservationStreamand can also be run on demand withsyncNow. The streams' later values are equal, so they are skipped (the model's setters, the selection hold, andSidebarView.render'slastState).SidebarView.renderNow()draws the current model in the caller's turn.- Cost: only a workspace shown for the first time pays the rows pass, which walks every workspace. A switch to a parked workspace (its row is already listed) runs only the selection pass.
Testing
- New test:
SidebarSameTurnTests. A second workspace appears and the window shows it. With no await, the drawn sidebar must list the new row and have it selected. After a short sleep, the settled state must also list and select it, so a broken fixture shows up as a separate failure. - Red (test commit, fleet
run-swift-testing-suites.sh Packages/macOS/CmuxNext, job 565fd84781b6ce95fafe0836):(firstListed → [.workspace(k1)]).contains(new → .workspace(k2))(firstSelected → .workspace(k1)) == (new → .workspace(k2))- The settled checks passed.
- Green at this head (same command, job b7a7ded77329f5ec8a538c30): exit 0, the same 317 suites, no failures.
- Base feat-cmux-next doesn't compile
ChannelDaemonSessionTestsuntil #20690 lands. So red and green both ran with #20690's one test-only commit on top, on temporary branches that have since been deleted. This branch does not contain that commit. - An earlier green run of the fix commit had one unrelated
PinnedPageTeststimeout. Its rerun was clean. - Checks passed (full output read):
check-no-godfiles.sh(WindowController.swift stays at its base 430 lines),check-concurrency.sh,check-system-blue.py,crash_ratchet.py. - Not yet done:
- workspace-switch latency, before vs after (
bench-tab-switch.sh); - a before/after recording in the real app;
- a Release compile of the head.
- workspace-switch latency, before vs after (
Changelog
Fixed: A new workspace's sidebar row now appears and is selected in the same frame as its content
Proof
Before/after recording to follow, from a fleet-mini DEV build.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes the perceived "old sidebar" flash when a new workspace is shown (Cmd-N): its content landed one or two frames before the sidebar listed its row and selected it. The window now renders the sidebar in the same turn as the content swap.
Bug Fixes
WindowController.show(_:on:)runs the sidebar's rows and selection passes and draws it before swapping the content in.- New
SidebarSameTurnholds those passes, observed as before and also run on demand viasyncNow;SidebarView.renderNow()draws the current model immediately. - Later observation values are equal to what was already applied, so no unnecessary re-renders happen.
Performance
- Only a workspace shown for the first time pays the rows pass (a walk of every workspace); switching to a parked workspace runs only the selection pass.
- Added
SidebarSameTurnTests, which asserts the very first frame already lists and selects the new workspace's row.
Showcase


Full quality: