feat(sidebar): show inner tabs in workspace hover

Closed
TE
5d ago
+1527-28

Summary

Workspace hover cards now open a compact, Rex-style inner-tab popover for every non-empty workspace. The header names the workspace; each tab row shows its icon, title, abbreviated cwd, existing status indicator and status word. Rows select the workspace and tab on mouse release, close the card, and support Up/Down plus Return from the card's keyboard monitor. The card stays interactive while the pointer crosses into it.

Leo asked for this directly on 2026-10-24:

"also when it comes to hovering over the sidebar workspaces, we should just do what rex does and it shows the inner tabs... if they exist. apparently we dont really like the existing situation it kinda sucks i agree the depiction is bad... tho wouldnt first prompt/input needing/whatever MAYBE be nice...? idk tho. we can tuck that behind toggle, but default just only show the inner tabs on hover and be able to go to and focus them when hovering ovre workspaces. let's go get some codex doing stuff"

The existing prompt, needs-input and workspace-facts card remains intact behind the new sidebar.hoverCards.richDetails setting, which is off by default. This matches the private Rex density while keeping cmux glass, colors and typography; the private reference image is not included here. The change does not touch Lawrence's rail placement or Chief sidebar code.

Scope: the change is one interaction plus its settings/export/test surfaces. The raw diff exceeds 800 lines because 262 lines are the two localized setting strings and 91 lines are fleet-generated exports; splitting the compact body from keyboard/click routing would leave a partial interaction.

Risk: the shared hover-card coordinator now permits mouse events for this one interactive body and consumes its arrow/Return keys. Events outside the card still dismiss and pass through, and the rich card continues through its existing path. Empty workspaces have no tab popover and therefore do not show a hover card in the default mode.

Stale-press repair — 2026-10-29

At 1ad21a369b, each reusable hover-card row captures its TabID on mouse-down, clears the press on configure, and only activates on release when the captured ID still matches the current row. aPressedRowCannotSelectAReusedRowAfterItsTabChanges presses the first tab, reconfigures the row as the replacement tab, releases, and proves no replacement tab is selected. Swift syntax and the prepush gate pass; no duplicate native job was dispatched.

Current integration — 2026-10-29

Current head 46fc445c34 merges feat-cmux-next at aff72f12bd into the stale-press repair at 1ad21a369b. The only conflict in this merge was schemas/defaults/cmux-next.json; regenerating the snapshot preserves every current-base default and adds the default-off sidebar.hoverCards.richDetails setting with fresh source fingerprints. Prepush gate, Swift syntax for the 20 changed files, defaults freshness and diff checks pass. No duplicate native job was dispatched.

Dependency: #26942 remains the only external dependency. Its drawn workspace-fill/pinned-pill anchors, inward snapping, no-gap geometry and both sidebar-owner notes are retained alongside the current base's anchor behavior. The per-tab hover content and stale-press fix are unchanged by this merge.

Prior native evidence is tied to its original head: at d1ee8995ea, the app compile passed, 38 tests passed across eight focused suites, and GUI retry 95a9bed132d925513c2f1773 passed all five keyboard/card tests. These receipts do not establish native validation of the new merge head.

Testing

  • python3 scripts/cmux-next/prepush-gate.py passed.
  • Both new settings strings cover all 21 locales used by the Settings web page; the native catalog strings and keys are preserved.
  • Fleet app compile at 71ce4d6c3b passed (job edb49ba8a54e6c042b549101); later changes only complete settings translations and strengthen the interaction capture tour.
  • Added WorkspaceTabHoverCardTests for tab line projection, single-tab behavior, arrow navigation, Return focus, release-click and pointer transfer; SidebarTabMappingTests covers per-tab cwd and status mapping. Six focused native suites ran at 85c2c9a2a8: mapping, dwell, scroll and pinned-row suites passed. The new keyboard fixture failed before establishing its synthetic window/hover target; repaired at 924d3e77ae and all 5 tests passed on the fleet (job 48d5375b7ce0bee70fd438ae, exit 0). Its test now requires the visible native window, a registered hover source, a shown target and an event associated with that same window before testing key consumption. The row escape tests now preserve S2’s flush anchor geometry.
  • Remote webviews full check passed on Linux (job 5096b7a0036307e3dbe2a4ec): formatting, lint, typecheck, browser-lane and protocol checks.
  • Rebased onto base e275db3f69, retaining Lawrence’s geometry and pinned-row hover lookup. The card reuses the current per-tab folder/state projection. Fleet export job 7d423c7c3850d3db326334ac passed all 9 settings/MDM export tests; the exact generated schema, MDM docs, settings reference and default snapshot are committed at 71ce4d6c3b. defaults_guard.py check-fresh passes. Repeating regeneration at 85c2c9a2a8 passed all 9 tests with exit 0 and no changed export; the empty patch upload failed separately with cache HTTP 400. Current base derives the source stamp instead of committing it, so the retired stamp is not restored.

Changelog

Added: Workspace hover cards show selectable inner tabs with per-tab status and cwd.

Proof

Real hover, after (cmux-ci capture at 4df66d7983, fleet build, capture mini at 2x, tour workspace-hover-tabs-card): a workspace with three terminal tabs, the pointer on its sidebar row, and the inner-tab card shown over the sidebar. Synthetic data only: the tabs are plain shells in /tmp, so each title repeats its path (the shell's own title). Cropped to the card with its row for context.

LightDark
light afterdark after

Same hover before the contrast fix (at e2a0a93d99, same tour). The paths drew in the tertiary text token (#808080 in this dark theme) and read as washed-out grey on the glass.

LightDark
light beforedark before

Interaction proof (earlier head 85c2c9a2a8, unchanged interaction code): hover, enter the card, click a different tab; the card closes after selection. GIF, .

Base (46e3205f42, no inner-tab card): light, dark.

Dark text contrast fix

Each inner-tab row's path now uses the theme's secondary text token, the same token the rich card uses for its facts. Titles stay primary. WorkspaceTabHoverCardContrastTests pins both the tokens and the rendered ink in a dark theme against the rich card's title:

  • Red at 473667b61c on the fleet (gui-session-v1): path token #808080 instead of #A1A1A1, and path ink 0.289 against a floor of 0.323. The titles already matched the rich card's title (ink 0.717).
  • Green at 4df66d7983, together with the nine hover suites (10 of 10 passed, fleet step a67f5e08).

Checklist

  • Behavior changes have added or updated tests.
  • UI and settings localization audited with scripts/localize-changes.
  • Independent Codex gpt-6.1-sol xhigh source review: SHIP at 46fc445c34. Native receipts and captures above retain their original heads; no new native run or capture is claimed for this merge.

Visual review

Viewed after image: yes
Visual verdict: Ship, because the after stills show the real inner-tab card over the hovered sidebar row in light and dark with bright titles and paths that now read clearly in secondary grey on the dark glass, no repeated per-row controls and a card that fits beside the sidebar, so it sits fine next to ChatGPT or Claude.

Agent session: agent session