Fix tab moves while terminals reconnect after restart

Merged
AU
6d ago
+90-8

Terminal tabs can be moved or reordered immediately after a daemon restart, while their terminal hosts are still being adopted. Both tab.move and the move-tab socket command used by dragging now require a committed placement rather than a runtime surface. Layout validation includes the tab's durable content identity before that surface exists.

Fixes #25999.

Verification

The regression covers same-pane reorder and cross-pane movement without a runtime surface, checks for invariant rejection, and compares durable layout with memory. A second regression exercises the move-tab wire command and retains rejection of unknown IDs.

The same focused fleet command failed before the fix at f3b9eb6f3d1a566ac2939208613ac258bca1244b (1 failed) and passed after the fix at 23ff5996d35c42bbcfdb1ba2f924357ae70cc569 (1 passed):

cmux-ci run --class isolated --script scripts/ci/cmux-tui-rust-check.sh \
  --ref SHA --arg test \
  --arg moves_of_a_tab_without_a_runtime_surface_follow_the_reducer \
  --key issue25999-proof-SHA --no-supersede --timeout 3600

Observed / Not run

Verified head: fde79e2fac12ca95e12e24844e3b4a254254cc5e, with base 3cf858d6e06c599c88b45182b81d014906cd5ea8 merged. GitHub reports it mergeable. All pushes were ordinary pushes, with no force or verification bypass.

  • 29 focused tests passed on rbx at this exact head, with Ghostty checked out at its committed pin. They include both regressions, layout properties and actual daemon restart/adoption recovery:

    rbx --lane issue-25999-tab-move-adoption -- \
      cargo nextest run --locked --workspace --exclude acpmux --no-fail-fast \
      -E 'test(layout_invariants::tests) | test(cmux_next_move_tab_moves_a_tab_without_a_runtime_surface) | test(adoption)'
    

    The unrelated acpmux package is excluded because its test-list harness requires Node on rbx.

  • The exact head passed the executed CI static checks, fleet Release compiler and app compile gateway, same-tree daemon tests, and fast gates. No current failing check remains. Skipped jobs are not counted as executed coverage.

  • Fresh independent correctness review of this exact head found no blockers. It covered both move entrypoints, durable staging/rollback, current adoption insertion, locking, reaping and undo. The reviewer performed source review, not independent runtime testing.

  • Tagged fleet build succeeded and is published: job 6f3d5455f597185cb87e5098, tag issue-25999-tab-move-adoption9, explicit local backend. Artifact SHA-256 34e0eab2e4ee6728534a4eada33863bb82d9313838975b73e161ed9f984a0425; workspace cleanup, Tag Opener health, tag registration and matching artifact manifest verified.

  • git diff --check and the scoped prepush gate passed. The CI static job above also executed on this head. No native build or heavy test ran on the person's Mac.

  • The regressions simulate pending adoption by removing the runtime surface; the recovery suite separately exercises actual daemon restarts. The tagged app was not launched: no GUI interaction, live GUI restart or shared Cloud backend verification is claimed. No UI strings or localization catalogs changed.

Try it

The rbx command above deterministically exercises the adoption window and real restart recovery. The tagged build issue-25999-tab-move-adoption9 is available for local daemon/layout dogfood; shared Cloud behavior is unverified. Use CMUX_TAG=issue-25999-tab-move-adoption9 scripts/cmux-debug-cli.sh for isolated automation. Immediately after a daemon restart, move or reorder a terminal tab: the move should commit while the terminal reconnects, without layout-conservation-violation.

Changelog

Fixed: Terminal tabs can be moved immediately after reconnecting to a restarted daemon.

No capture: daemon layout behavior with no native UI changes.


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


Summary by cubic

Fixes #25999: terminal tabs can now be moved or reordered immediately after a daemon restart, while their terminals are still being adopted.

Two causes made the live move disagree with the layout reducer: move_tab_in_state returned early for a tab without a runtime surface, and the layout projection built tab content only from runtime surfaces. The move path — both tab.move and the move-tab socket command the app's drag sends — now requires only a placement, and the projection describes unmaterialized tabs by their durable content identity with no runtime.

  • Adds regression tests covering same-pane reorder, cross-pane move, and the move-tab wire command for a tab without a runtime surface.
  • Adds release notes linking the fix to CLI instructions.