Skip to content

Preserve ssh-tmux pane identity across remote layout changes - #7838

Merged
austinywang merged 27 commits into
mainfrom
issue-7833-mirror-incremental-split
Jul 11, 2026
Merged

austinywang merged 27 commits into
mainfrom
issue-7833-mirror-incremental-split

Conversation

@austinywang

@austinywang austinywang commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Closes #7833

Root cause

The one-pane mirror path and multi-pane window renderer independently owned local pane surfaces. The first remote split crossed that representation boundary without transferring identity, so the multi-pane renderer recreated every pane and exported fresh surface and pane IDs. Bonsplit render-node IDs were also exposed as durable control-plane identities even though a fallback render-tree rebuild may replace them.

Fix

  • Adopt the existing single-pane TerminalPanel and its exported pane identity when a live window first becomes multi-pane, keyed by the authoritative tmux pane ID.
  • Reconcile pane panels by tmux pane ID: create only additions, close only removals, and retain surviving surfaces across every layout update.
  • Keep stable control-plane pane identities in a separate ledger from replaceable Bonsplit render-node IDs, so fallback tree rebuilds cannot churn automation handles.
  • Prune stale single-pane mappings when tmux removes a pane.

The reconciliation stays synchronously owned by the existing @MainActor mirror; it adds no tasks, locks, timers, or duplicate topology source of truth.

Tests

The first commit adds a failing behavior regression through the real control-mode parser, pane-rect publication, topology notification, and session/window reconcile path. It covers:

  • single pane to two panes, preserving the original panel, surface ID, and pane ID while creating exactly one new surface;
  • an additional split in an already-multi window, preserving both survivors;
  • remote pane close from three panes to two, removing control references and releasing the destroyed surface;
  • close back to one pane, preserving the original identities with no stale surface.

The second commit implements the fix so the same behavior path passes.


Note

Medium Risk
Touches remote tmux topology, window-close race handling, and control-plane identity routing; regressions could break automation handles or pane moves, but behavior is heavily covered by new identity tests and a dedicated CI gate.

Overview
Stable automation handles for ssh-tmux mirrors no longer reset when a tab goes from one pane to splits, when panes move between windows, or when tmux publishes layout out of order.

Session-scoped control identity replaces per-window-mirror pane IDs: RemoteTmuxSessionMirror owns a tmux-pane→PaneID ledger, reconciles it on topology rebuild (including panes retained while a closed window’s panes might still exist elsewhere), and routes focus/input/split/resize/kill through RemoteTmuxControlPaneLocation and RemoteTmuxControlPaneMutationOwner (session mirror in production; standalone window mirror only when no session is bound).

Window mirror lifecycle adopts the existing single-pane TerminalPanel when a window first becomes multi-pane (keyed by tmux pane id), creates/closes panels incrementally on layout changes, and notifies the session mirror on surface attach/detach instead of owning control cleanup locally.

Control connection tracks publishedWindowIdByPane, coalesces in-flight list-windows, tags each snapshot with retainedPaneIDs from overlapping %window-close events, releases retention only when that snapshot succeeds, and reconnects if a retention refresh fails—so panes aren’t pruned during move/close races. %window-close now triggers an immediate requestWindows() while tabs can drop early.

Control plane / workspace resolution goes through the session mirror when present (remoteTmuxSessionMirror, isRemoteTmuxControlContainer); socket handlers call location.requestSplit / requestKill etc. rather than talking only to RemoteTmuxWindowMirror.

CI: non-tolerant focused run for RemoteTmuxMirrorLayoutIdentityTests on the app-host regression shard, plus shard script exclusions for that suite.

Reviewed by Cursor Bugbot for commit bdcc92f. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added a dedicated remote tmux control-pane projection that routes focus, input, key sends, and pane actions through the controlling mirror.
  • Bug Fixes

    • Improved remote tmux mirroring reconciliation when display panels are newly created, including safer surface/callback handling and correct pane adoption.
    • Preserved stable, unique pane/control identities across incremental updates, fallback rebuilds, and window split/move scenarios.
    • Enhanced pruning and teardown cleanup for removed panes/surfaces and published pane ownership.
  • Tests

    • Added regression coverage for remote tmux mirror layout identity stability and pane lifecycle behavior.
  • CI

    • Added a focused, non-tolerant test gate and workflow checks for the macOS 15 helper-capable runner.

@vercel

vercel Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 11, 2026 9:08am
cmux-staging Building Building Preview, Comment Jul 11, 2026 9:08am

@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Remote tmux mirroring now preserves eligible pane panels and stable control identities during topology reconciliation, routes pane operations through session-owned locations, adds regression coverage for layout changes and splits, and runs the focused suite through dedicated CI validation.

Changes

Remote tmux identity reconciliation

Layer / File(s) Summary
Published pane ownership tracking
Sources/RemoteTmuxControlConnection*
Tracks pane-to-window ownership across initial publication, layout updates, pruning, and window closure.
Topology adoption and mirror lifecycle
Sources/RemoteTmuxSessionMirror*, Sources/RemoteTmuxWindowMirror*, Sources/Workspace.swift, cmux.xcodeproj/project.pbxproj
Reconciles stable pane identities, adopts existing panels, manages surface callbacks, and registers the new mirror components.
Control-pane projection and mutation routing
Sources/RemoteTmuxControlPane*, Sources/Workspace+RemoteTmuxControlTopology.swift, Sources/TerminalController+RemoteTmuxControl*
Introduces session-owned pane locations and routes focus, input, split, respawn, kill, projection, and cleanup operations through them.
Layout identity regression coverage and CI
cmuxTests/*, .github/workflows/ci.yml, scripts/ci/*, tests/test_ci_*
Tests incremental reconciliation, rebuilds, splits, cross-window moves, cleanup, uniqueness, and deallocation; the suite is registered as a focused non-tolerant CI gate and excluded from sharding.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#3185 — Concerns the same remote tmux pane/surface mapping and teardown paths.
  • manaflow-ai/cmux-dev-artifacts#3180 — Reports failures in the remote tmux mirror observability test area changed here.
  • manaflow-ai/cmux-dev-artifacts#3126 — Relates to the RemoteTmuxWindowMirror refactor and its control-mutation extension.

Possibly related PRs

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error Sources/RemoteTmuxSessionMirror+WindowReconciliation.swift:63 now does a full panelIdByPane.filter for each newly created window mirror, making rebuilds O(w·p). Replace the per-window full filter with targeted removals (or a reverse panel→pane index) so rebuild stays linear in the pane count.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers root cause, fix, and tests, but it does not follow the required template sections like Summary, Demo Video, Review Trigger, or Checklist. Add the missing template sections: Summary, Testing, Demo Video/URL, Review Trigger block, and Checklist items, or mark N/A where appropriate.
✅ Passed checks (22 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes match #7833 by preserving surviving pane/surface identities and applying incremental reconciliation instead of full recreation.
Out of Scope Changes check ✅ Passed The additional CI and test-sharding updates support the new regression coverage and do not appear unrelated to the fix.
Cmux Swift Actor Isolation ✅ Passed New production types are explicitly @MainActor; the protocol/struct stay actor-confined, and no new Sendable/shared-mutable background access was introduced.
Cmux Swift Blocking Runtime ✅ Passed No new blocking waits/locks/syncs were added in production diff; changes are state reconciliation/callback wiring, and tests use deterministic command-drain loops only.
Cmux Browser Automation Off-Main ✅ Passed The PR only changes remote-tmux session mirror files and tests; no browser.* commands, processV2Command, or socketWorkerMethods routing changed.
Cmux Expensive Synchronous Load ✅ Passed No added or moved RestorableAgentSessionIndex.load() or other heavy agent-history parsing appears in the changed main-actor or interactive tmux paths; changes are pane-mirroring only.
Cmux Cache Substitution Correctness ✅ Passed No persistence/history/undo/snapshot path was changed to trust a stale cache; the new pane/window maps are event-driven runtime topology state, not saved state.
Cmux No Hacky Sleeps ✅ Passed No new sleep/poll/timer logic was added in the covered shell/Python scripts; the only waits are existing workflow YAML, which is out of scope.
Cmux Swift Concurrency ✅ Passed PR diff adds only one Task at a manual input callback boundary; no new DispatchQueue, Combine, or completion-handler patterns appear in changed production code.
Cmux Swift @Concurrent ✅ Passed No new @concurrent/nonisolated-async misuse appears in the changed Swift files; the only added async hop is an explicit Task { @MainActor in ... }.
Cmux Swift File And Package Boundaries ✅ Passed PASS: New production Swift files are small (20–148 lines); the 435-line file is tests; oversized app files only got tiny incidental edits, so no boundary violation.
Cmux Swiftpm Lockfiles ✅ Passed PR changes only source/test/workflow files; project.pbxproj adds source refs only, no SwiftPM package refs, and no .gitignore or Package.resolved diffs.
Cmux Swift Logging ✅ Passed No added/changed production Swift logging was found; git diff had no print/debugPrint/dump/NSLog/Logger lines in the changed runtime files.
Cmux User-Facing Error Privacy ✅ Passed Diff only changes remote-tmux topology logic and tests; no new user-facing errors, alerts, or command/output text were added.
Cmux Full Internationalization ✅ Passed No new user-facing Swift/web copy or locale assets were introduced; changed strings are commands/debug logs/tests, which the rule allows.
Cmux Swiftui State Layout ✅ Passed Diff touches only mirror/controller logic and tests; no SwiftUI View, ObservableObject/@published, GeometryReader, or render-time state mutation appears in the changed files.
Cmux Architecture Rethink ✅ Passed Clear session-owned control topology with value/closure bridges; no sleeps, polling, locks, or split lifecycle ownership added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The diff only changes remote tmux mirror/session code and tests; it adds no NSWindow/NSPanel/WindowGroup code and no cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed Only source/test files changed; no logs, caches, build output, temp dirs, or other artifact paths appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Changed Sources files add production control-topology code; diff scans found no added #if DEBUG or ForTesting/TestHook seams in production source.
Cmux No Ambient Global State ✅ Passed No new ambient global state was added: the PR adds only constructable types/extensions and instance methods; no new file-scope API, mutable global var, or singleton.
Title check ✅ Passed The title clearly summarizes the main change: preserving pane identity across remote layout changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7833-mirror-incremental-split

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@austinywang
austinywang marked this pull request as ready for review July 10, 2026 09:57
@greptile-apps

greptile-apps Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR preserves remote tmux pane identity across layout changes. The main changes are:

  • Session-owned pane and surface identity tracking.
  • Incremental reconciliation for split, close, move, and rebuild paths.
  • Control routing through stable remote tmux pane locations.
  • Focused tests and CI coverage for layout identity preservation.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
Sources/RemoteTmuxSessionMirror.swift Adds session-owned pane, surface, window, and panel ledgers used during topology rebuilds.
Sources/RemoteTmuxSessionMirror+ControlTopology.swift Adds stable control-pane lookup, surface tracking, cleanup, and session-owned mutation routing.
Sources/RemoteTmuxSessionMirror+WindowReconciliation.swift Adopts existing single-pane panels when creating multi-pane window mirrors.
Sources/RemoteTmuxWindowMirror.swift Updates window mirrors to use session-owned control identities and report surface changes.
Sources/RemoteTmuxControlConnection.swift Tracks published pane ownership and retained pane IDs during remote window close gaps.
Sources/RemoteTmuxControlConnection+Commands.swift Coalesces window-list requests and snapshots retained pane IDs per request.
Sources/RemoteTmuxControlConnection+CommandResults.swift Releases retained pane IDs from successful window snapshots and reconnects on unsafe refresh failures.
Sources/Workspace+RemoteTmuxControlTopology.swift Routes remote tmux pane discovery through the session mirror when present.
Sources/TerminalController+RemoteTmuxControlMutations.swift Updates remote tmux control commands to use the new pane-location abstraction.
cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift Adds tests for identity preservation across split, close, and return-to-one-pane flows.

Reviews (21): Last reviewed commit: "Coalesce remote tmux topology refreshes" | Re-trigger Greptile

Comment thread Sources/RemoteTmuxWindowMirror+Bonsplit.swift Outdated
Comment thread Sources/RemoteTmuxSessionMirror.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift (1)

285-291: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

tearDown should be called reliably even on test failure.

If a test throws or records an issue before reaching tearDown(), the pipe handles and session mirror observer won't be cleaned up, potentially leaking resources across tests. Consider calling harness.tearDown() in a defer block at the top of each test, or conforming to a cleanup protocol if Swift Testing supports it in this codebase.

♻️ Suggested pattern
+ defer { harness.tearDown() }
 // ... test body ...
-harness.tearDown()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift` around lines 285 - 291,
Ensure cleanup runs even when tests fail by registering teardown immediately at
the start of each test in RemoteTmuxMirrorLayoutIdentityTests, preferably with
defer or the project’s supported cleanup mechanism. Reuse the existing
tearDown() cleanup for sessionMirror, workspace, panels, writer, and pipe
handles, and avoid relying on reaching tearDown() through normal test
completion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift`:
- Around line 239-249: Deduplicate the lookup logic shared by the windowMirror
property and windowMirror(windowID:) method. Consolidate the panel traversal
into a single helper or make the computed property delegate to
windowMirror(windowID:), applying the windowId filter only when provided while
preserving the current first-match behavior.

---

Outside diff comments:
In `@cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift`:
- Around line 285-291: Ensure cleanup runs even when tests fail by registering
teardown immediately at the start of each test in
RemoteTmuxMirrorLayoutIdentityTests, preferably with defer or the project’s
supported cleanup mechanism. Reuse the existing tearDown() cleanup for
sessionMirror, workspace, panels, writer, and pipe handles, and avoid relying on
reaching tearDown() through normal test completion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c821bebc-8b0b-4c63-afda-9756f55b87f3

📥 Commits

Reviewing files that changed from the base of the PR and between cce3649 and 9f4c728.

📒 Files selected for processing (1)
  • cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift

Comment thread cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/RemoteTmuxSessionMirror.swift`:
- Around line 204-223: Update the panel mapping logic in the loop over
panelIdByWindow to prefer the pane previously associated with that panel in
previousPanelIdByPane when it still belongs to the current window’s
paneIDsInOrder; only fall back to paneIDsInOrder.first when no prior association
exists, preserving existing panel identity and scrollback during reordering or
splits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 061d2408-af7a-44f4-b5e7-fe1830feaf69

📥 Commits

Reviewing files that changed from the base of the PR and between 24265ab and 1171508.

📒 Files selected for processing (22)
  • Sources/RemoteTmuxControlConnection+CommandResults.swift
  • Sources/RemoteTmuxControlConnection+LayoutPublication.swift
  • Sources/RemoteTmuxControlConnection.swift
  • Sources/RemoteTmuxControlPane.swift
  • Sources/RemoteTmuxControlPaneLocation.swift
  • Sources/RemoteTmuxControlPaneMutationOwner.swift
  • Sources/RemoteTmuxController.swift
  • Sources/RemoteTmuxSessionMirror+ControlTopology.swift
  • Sources/RemoteTmuxSessionMirror+WindowReconciliation.swift
  • Sources/RemoteTmuxSessionMirror.swift
  • Sources/RemoteTmuxWindowMirror+ControlMutations.swift
  • Sources/RemoteTmuxWindowMirror+ControlTopology.swift
  • Sources/RemoteTmuxWindowMirror.swift
  • Sources/TerminalController+ControlPaneContext.swift
  • Sources/TerminalController+RemoteTmuxControlMutations.swift
  • Sources/TerminalController+RemoteTmuxControlRefs.swift
  • Sources/TerminalController+RemoteTmuxControlTopology.swift
  • Sources/Workspace+RemoteTmuxControlTopology.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift
  • cmuxTests/RemoteTmuxMirrorLayoutIdentityTests.swift
💤 Files with no reviewable changes (1)
  • Sources/RemoteTmuxWindowMirror+ControlTopology.swift

Comment thread Sources/RemoteTmuxSessionMirror.swift Outdated
Comment thread Sources/RemoteTmuxSessionMirror.swift
# Conflicts:
#	.github/workflows/ci.yml

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit dc526f2. Configure here.

Comment thread Sources/RemoteTmuxControlConnection+CommandResults.swift
@austinywang
austinywang merged commit 90b462a into main Jul 11, 2026
31 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — bdcc92ff Deployed Jul 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ssh-tmux mirror: remote split destroys and recreates all of the window's surfaces instead of adding one split

1 participant