Mirrored tmux tabs follow window reorders made on the host

Merged
EJ
1d ago
+524-29

Summary

When something on the host reorders a session's windows with move-window or swap-window, the mirrored workspace's tabs keep the old order. A move-window also closes the moved window's tab and opens a new one at the end of the strip, so its terminal is rebuilt from scratch.

To reproduce, mirror a session work with three windows, then run one of these on the host:

tmux swap-window -s '=work:@1' -t '=work:@3'
tmux move-window -s '=work:@1' -t '=work:9'

tmux list-windows -t '=work' shows the new order and the tab bar doesn't.

There are three causes. I measured the notifications on tmux 3.7b, with a control client attached to the session and with one attached to a session that has the windows linked in.

  1. tmux reports a move-window inside a session as %window-add, %session-window-changed and %window-close, all for the moved window. tmux sends %window-close only when the window is still in the client's session after the unlink. A window that left the session (killed, or unlinked from it) arrives as %unlinked-window-close. cmux treated both as a close. When a script moves every window of a session, the workspace runs out of tabs for a moment, cmux opens a local terminal to fill it, and the windows come back in the order they arrived.
  2. swap-window sends no notification to any client, so nothing told cmux to read the window list again.
  3. Once the strip held a tab that no tmux window owns (that local terminal, for example), the reorder stopped running at all, because it required the strip to match the window list one for one.

The fix:

  • %window-close now re-reads the window list and keeps the tab. The list says where the window is. %unlinked-window-close still closes the tab right away.
  • The window order now comes from a tmux format subscription. tmux re-evaluates it about once a second and reports it only when the value changes. The shared view stream's session digest now includes each session's window ids in index order. A stream attached to one session subscribes to its own window order, and re-reads the window list when the reported order differs from the one it has.
  • Tabs that no tmux window owns keep their positions, and the mirrored tabs are reordered around them.

A reorder no longer closes or recreates any tab or surface.

Testing

The focused command is xcodebuild test -scheme cmux-unit -only-testing:cmuxTests/RemoteTmuxTabOrderFollowsTmuxTests.

  • On the test commit (6068b438655, main plus the new tests), it fails: Test run with 4 tests in 1 suite failed ... with 4 issues. The move test sees the moved window's tab replaced. The local-tab test sees the order stay [1, 2, 3] instead of [3, 2, 1]. Both swap tests get nothing from tmux after the swap.
  • On the fix commit, Test run with 4 tests in 1 suite passed.
  • I broke each part of the fix in turn: mapping %window-close back to a close, dropping the window ids from the view digest, skipping the per-session subscription, and ignoring tabs without a window. Each one fails exactly the test written for it, and the other three pass.

The two swap tests start a tmux server on its own socket, replay what a stream writes on attach into a real tmux -C client, swap two windows, and feed tmux's output back into the stream. They run only when a tmux binary is installed.

I also ran all 63 RemoteTmux* suites (586 tests) in six batches. Two existing fixtures expected the attach to queue only the window list, so RemoteTmuxAuthTests and RemoteTmuxPaneSeedTransportTests now account for the subscription after it. Five RemoteTmuxMirrorCloseDetachTests tests fail locally with the same nine issues on the test commit and on the fix commit. CI runs that suite in a console session of its own.

python3 scripts/verify-local.py passed 17 of 17, and python3 scripts/swift_file_length_budget.py passed.

Observed / Not run

Observed: the focused command red on 6068b438655 and green on the fix commit, the four mutations above, the suite batches, and the raw tmux notifications listed in the summary, captured from tmux -C clients on an isolated server.

Main doesn't build cmux-cli right now (topLevelCommandNames is missing its (), fixed in #22523). I ran every local test with that one-character fix applied and not committed.

Not run: CI, and a live check against a real host in a built app (the reattach in my dev build stalled at login before tmux answered).

Changelog

Fixed: Mirrored tmux tabs follow window reorders made on the host with move-window or swap-window, and a move no longer rebuilds the moved tab

Proof

Protocol-level and real-tmux tests only; no screenshots, because the live reattach didn't complete.

Checklist

  • Behavior changes have added or updated tests, or Testing says why not

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Mirrored tmux tabs now follow window reorders made on the host with move-window or swap-window. Previously tabs kept the old order, and a move closed the moved tab and rebuilt its terminal.

  • %window-close no longer closes the tab; it re-reads the window list, since a move inside a session sends %window-close while the window remains linked. Only %unlinked-window-close closes a tab.
  • swap-window sends no client notification, so the window order is tracked via a tmux format subscription; the list is re-read when the reported order differs.
  • Tabs no tmux window owns keep their positions, and mirrored tabs reorder around them.
  • Real-tmux tests run on an isolated server on its own socket, locate tmux on PATH, require the subscription baseline to arrive before the swap, and assert the reply queue drains.

Summary by CodeRabbit

  • Bug Fixes
    • Mirrored tabs now follow window moves and swaps in tmux while preserving each tab’s identity.
    • Tabs not managed by tmux stay in place as tmux-managed tabs change order.
    • Window-order changes are reflected in both shared and directly attached views.
    • Windows unlinked elsewhere are distinguished from windows removed from the attached session, keeping mirrored window lists up to date.
    • Attached views refresh their window lists when the session’s window order changes.