cmux NEXT sidebar: an empty slot beside a workspace dismisses its hover card
Summary
Leo, on dismissing a sidebar workspace's hover card: it takes too much travel, like being trapped by the preview. The card sat top-aligned against its row, so everything right of the row was the card, and the only way out sideways was across its whole width.
The card's top edge now sits on the row's vertical middle. Right of the row's upper half is an empty escape slot that the card never covers, and moving the pointer there dismisses the card on that move. Straight right from the row's lower half still runs into the card, which touches the row with no seam, so a pointer that wants the card keeps it. Leaving both the row and the card still dismisses it at once, as before.
HoverCardEscapeGeometry(CmuxNextDesign) is the pure geometry: the card's frame, the slot, and what a point is (row, card, escape, outside). Near the screen's bottom the card flips above the row's middle (slot below), and near its right edge it goes left of the row, so the screen clamp never pushes the card over the slot.HoverCardPlacement.besideRowplaces the workspace card with it. Tab cards and the update card keep their placements.HoverCardCoordinator: while the pointer is in the slot, that row gives no hit and the slot bridges nothing, so the card hides at once. This covers the part of the slot inside the sidebar's own margin, where the row's hit test still finds the row. The slot is remembered after the card hides until the pointer leaves it or the row moves, so moving on inside it never brings the card back, and a card whose slot already holds the pointer when it would show goes at once.- The empty list below the last row no longer holds the card for the 500 ms reshow window; gaps and headers between rows still do, so moving row to row still slides the card.
Trade-off: a diagonal from the row's upper half down-right toward the card crosses the slot and dismisses it. The straight path from the lower half is the way in.
Files outside this PR: this doesn't touch the workspace-group sidebar code. #22294 (rows take hover only inside their pill) edits SidebarListView+HoverCard.swift, which this PR leaves alone; the two compose (with #22294 the margin beside a row stops hitting the row at all).
Testing
Regression first, on the fleet (cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=<suites>):
- Red at a7800bc73b92 (test only,
--red,SidebarHoverCardEscapeTests): failed as expected (fleet step 789be76ffdadd4330212b222):movingIntoTheEscapeSlotBesideTheRowDismissesTheCard(the card covered the slot point and stayed shown) andtheEscapeSlotInsideTheSidebarMarginDismissesAndStaysDismissed(the card stayed shown); the two guard tests passed - Green at efc6aa182d15 (
SidebarHoverCardEscapeTests,HoverCardEscapeGeometryTests,SidebarHoverCardDwellTests,WorkspaceHoverCardScrollTests,HoverCardFlushTests,HoverCardPanelMotionTests,UpdateCardHoverParityTests): all 7 suites passed (fleet step 87eab27991265e5e551a1b7a;SidebarHoverCardEscapeTests5 of 5). 94b8ff50885d then merges feat-cmux-next 43223883ff91, which drops a stale autofix ofschemas/defaults/cmux-next.json; it changes none of this PR's files
SidebarHoverCardEscapeTests drives a real SidebarListView and coordinator on a manual clock: the slot beside the row dismisses the card; the slot inside the sidebar's margin dismisses it and it stays dismissed while the pointer rests there; resting in the slot shows no card; the path from the row's lower half into the card keeps it at every step; leaving both dismisses it. HoverCardEscapeGeometryTests covers the same three rules on the pure geometry, plus the flips at the screen's bottom and right edges and pixel snapping at 1x, 2x and 3x.
python3 scripts/verify-local.py --affected origin/feat-cmux-next --swift-changed origin/feat-cmux-next: localization-defaults and feature-flags pass; package-groups fails identically on the base (6c0365c7453e, missing CmuxConversation and CmuxTabsKit workspace groups); Swift syntax is unsupported on this Linux host.
An independent review subagent found no blockers; its should-fix (the path test depended on the runner's screen height) and nits (a card resting in the slot, a stale slot after a scroll, the parity test still placing .beside, a settable anchor) are fixed in efc6aa182d15.
Observed / Not run
Observed: the fleet runs above and the static checks. Not run: the fleet suites on 5db097731d45, which merges feat-cmux-next 048c296cf710 and resolves its new card morph and leading-side placement into HoverCardPanel.place() (the morph's fixed corner now handles .besideRow). That step (fb4e647bbf16) stopped before the tests because the base's web bundles don't build (missing translations for settings.layout.focusPaneOnScroll), a base break outside this PR, and a tagged-build dogfood of the feel (the host is Linux), and the full CmuxNext suite.
Changelog
Changed: Moving the pointer right from the top half of a sidebar workspace now dismisses its hover card at once; the card sits a little lower beside the row, and moving straight across from the row's lower half still reaches it.
Proof
The media-tour label tours hover-card before and after (its last step shows a real workspace card beside its row).
🤖 Generated with Claude Code