cmux NEXT spaces: the switcher shows which window shows each space (Go to Window / Move Here)

TE
2d ago
+1189-9

Leo, 2026-10-29: "there's also other ergo wrt new windows being like new spaces, incognito spaces, etc... this could legit make our thing more useful than chrome by having windows be switchable in this way"

Spec: leo-workspace design/spaces.md, "Windows and spaces" → "The switcher (PR 3)" (plus design/sidebar.md, design/foundations.md). This PR is step 3 of that plan.

What changes

The space strip now shows which spaces another open window is showing.

  • Picked: the ring (version 1). A space another window shows gets a thin 1 pt ring around its dot. The dots strip is otherwise unchanged: no dot moves and no existing default changes.
  • Click on a ringed space opens a menu right away (no fade-in) with Go to Window and Move Here. Move Here runs the existing space.bringHere from #28049 for this window. Option-click goes straight to that window. Clicking a space no other window shows still switches in place, as before.
  • Incognito never mixes: Move Here is left out when either window is incognito. Go to Window is still offered (SpaceSwitcherLogic.choices).
  • While the menu is open the clicked dot stays lit and the sidebar's hover chrome stays revealed (chromeMenuOpenChanged). The highlight covers only that dot's slot.
  • The tooltip and VoiceOver label say "in another window".
  • One shared path: a click, Option-click or menu row asks the bar's SpaceWindowMarks.perform. In the App, SpaceWindowMarksWiring turns that into the window registry's bring-to-front, space.bringHere for this window, or space.newIncognito. The same wiring fills the marks from the window registry and each window's space. The rules are pure code in CmuxNextSidebar/Logic/SpaceSwitcherLogic.swift. The state lives in its own type, so SidebarModel, SidebarBridge and WindowManager don't grow (the god-file guard).

Gallery (sidebar.spaceSwitcher, Debug Settings)

StyleWhat it shows
ring (default for the new marker)a ring on another window's space; click offers Go to Window / Move Here; Option-click goes there
glypha small window glyph at the dot's corner instead of the ring
menuArc-style space menu: clicking the current space lists every space (current one checked), Go to Window / Move Here in a submenu on another window's space, then New Space / New Incognito Space
plaintoday's strip, no marker, click switches

Why the ring:

  • It adds the least to a footer row that already holds the avatar, Search and the +.
  • It still reads on the compact dots, where the glyph drops out.
  • It keeps every dot's position and click target.

Not built: named chips (version 2) and the rail (version 4).

  • Chips need a variable-width strip, and the footer row has no room for one beside the avatar and Search.
  • The rail costs a column of width on every row.

Both are written up in the spec as open if Leo wants them.

SpaceSwitcherGalleryTests renders every style in the real sidebar: light and dark, at rest and hovered, at 2x, into spaces-switcher/. It needs a GUI session (CMUX_TEST_REQUIRE_GUI=1). Captures are a follow-up and don't block this PR. Also a follow-up: a real-app capture scene spaces-switcher in CaptureSceneRegistry, with two windows on different spaces.

Tests

  • Red first. Commit 1 (SpaceSwitcherWindowMarksTests) adds only the tests. They cover:
    • which spaces other windows show, with the frontmost window winning
    • Move Here is never offered across incognito
    • click outcomes for each style
    • the bar's menu titles and the intents it sends
    • Option-click
    • the dot stays lit and the chrome stays revealed while the menu is open
    • the space menu's rows
    • accessibility labels
  • Commit 2 adds the implementation.
  • Fleet run: package-test-lane.sh suite with SpaceSwitcherWindowMarksTests, ProfileBarDotTearOffTests, ProfileBarPlusPlacementTests, ProfileBarDotReorderTests. App compile: cmux-ci compile-check --app.

Localization audit

There are four new sidebar strings: sidebar.space.goToWindow, sidebar.space.moveHere, sidebar.space.newIncognito and sidebar.space.inAnotherWindow. Each has every locale the catalog carries (21), which covers the nine required macOS locales (en, de, fr, ar, es, zh-Hant, zh-Hans, ko, ja). New Incognito Space reuses the action.space.newIncognito translations. The Debug tunable titles are English, like every other tunable.

Risk

  • Structural sidebar change (a new interaction on the spaces strip). Per the rulebook it carries the hold label for dogfood builds.
  • New default: tunable.sidebar.spaceSwitcher = ring, a new key for the new marker only. defaults_guard needs the independent review at head.
  • New observers: the bar observes its marker inputs only while it is in a window. viewDidMoveToWindow cancels the observation, and the task holds the bar weakly. SpaceWindowMarksWiring observes the registry and window spaces. That is O(windows) per change with no per-token work, and it ends with the bar's marks (a weak reference).
  • CmuxNextApp size: about 60 lines, in one new file. The size cap is warn-only.
  • Follow-ups filed from review: #30874 (a lone space's actions on hover), #30875 (menu style and a shared current space), #30877 (captures and the capture scene), #30968 (the ring during a dot drag).

🤖 Generated with Claude Code

Agent session: agent session