Skip to content

feat(ssh): support Windows tmux control sessions - #22

Open
prabirshrestha wants to merge 2 commits into
h3nock:mainfrom
prabirshrestha:feat/windows-tmux-control
Open

feat(ssh): support Windows tmux control sessions#22
prabirshrestha wants to merge 2 commits into
h3nock:mainfrom
prabirshrestha:feat/windows-tmux-control

Conversation

@prabirshrestha

@prabirshrestha prabirshrestha commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

This is an experimental PR. Windows/psmux support is based on the currently observed psmux control-mode protocol and may need follow-up as that implementation evolves.

Summary

Add support for Windows-hosted tmux-compatible sessions over SSH.

  • detect Windows by probing cmd.exe
  • launch tmux -CC through cmd.exe
  • unwrap the DCS control-mode envelope
  • adapt send-keys -H input to psmux's 0xNN format
  • adapt refresh-client sizing for psmux
  • preserve existing POSIX behavior

Design decisions

  • Probe only cmd.exe; do not probe for psmux.exe or tmux.exe.
  • Use the configured bare tmux command and let Windows resolve tmux.exe through PATH.
  • Apply protocol adaptations only after Windows is positively detected.
  • Keep the existing POSIX /bin/sh launch path unchanged.
  • Use a non-PTY SSH channel because psmux control mode expects plain pipes.
  • Translate only known protocol differences and preserve unrelated commands byte-for-byte.
  • Buffer newline-delimited commands so adaptations work across split SSH writes.
  • Treat inconclusive platform detection as an error instead of silently selecting the wrong launcher.
  • Keep this inside the existing SSH transport rather than introducing a larger backend abstraction.

Upstream context

psmux uses a slightly different control-mode protocol from upstream tmux. These issues document the relevant compatibility work:

Issue #261 specifically covers the -CC DCS envelope, 0xNN input encoding, and control-client resizing behavior adapted here.

Testing

  • 101 focused transport and client-size tests pass
  • verified connecting and typing in a Windows session over SSH
  • verified portrait → landscape → portrait resizing
  • verified the Codex TUI reflows to the current viewport

Screenshot

CleanShot 2026-07-30 at 21 29 49@2x

Written with Codex using GPT-5.6 Sol.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for connecting to and controlling tmux sessions on Windows hosts.
    • Remote platform detection automatically selects the appropriate control-session behavior.
  • Bug Fixes

    • Improved Windows handling for keyboard input, resizing, startup output, and command buffering.
    • Improved error handling when detecting or opening remote sessions.
    • Client-size refresh now remains accurate when switching between portrait and landscape orientations.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fc7e3ff-3124-489d-987f-3ff697290972

📥 Commits

Reviewing files that changed from the base of the PR and between addfb91 and 0e963e3.

📒 Files selected for processing (4)
  • RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift
  • RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift
  • RemuxAppTests/SSHTmuxControlTransportTests.swift
  • RemuxAppTests/TmuxSessionControllerClientSizeTests.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • RemuxAppTests/TmuxSessionControllerClientSizeTests.swift
  • RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift
  • RemuxAppTests/SSHTmuxControlTransportTests.swift
  • RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift

📝 Walkthrough

Walkthrough

The SSH tmux transport now detects POSIX or Windows remotes, builds platform-specific control commands, adapts Windows input and output streams, and validates detection, framing, resizing, quoting, and UTF-8 handling.

Changes

Windows SSH tmux control support

Layer / File(s) Summary
Platform detection and command construction
RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift, RemuxAppTests/SSHTmuxControlTransportTests.swift
The command builder detects remote platforms and creates POSIX or Windows control-session commands with Windows argument quoting and percent-sign validation.
Remote detection and startup lifecycle
RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift
The transport runs a timed remote probe, handles channel cleanup, selects platform-specific adapters, and passes the detected platform to session construction.
Windows control-stream adaptation
RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift
Windows input commands are buffered and transformed. Resize syntax and hexadecimal send-keys payloads are converted. Windows DCS output framing is removed.
Adapter, detection, and resize validation
RemuxAppTests/SSHTmuxControlTransportTests.swift, RemuxAppTests/TmuxSessionControllerClientSizeTests.swift
Tests cover Windows and POSIX stream behavior, detection outcomes, command construction, malformed input, invalid UTF-8, and separate client-size refreshes.

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

Sequence Diagram(s)

sequenceDiagram
  participant SSHTmuxControlTransport
  participant RemoteCommandRunner
  participant SSHChannel
  participant SSHTmuxControlCommandBuilder
  participant WindowsInputAdapter
  participant WindowsStartupOutputAdapter

  SSHTmuxControlTransport->>RemoteCommandRunner: run remote platform probe
  RemoteCommandRunner->>SSHChannel: execute probe
  SSHChannel-->>RemoteCommandRunner: return stdout and exit status
  RemoteCommandRunner-->>SSHTmuxControlTransport: return detected platform
  SSHTmuxControlTransport->>SSHTmuxControlCommandBuilder: build control-session command
  SSHTmuxControlCommandBuilder-->>SSHTmuxControlTransport: return POSIX or Windows command
  SSHTmuxControlTransport->>WindowsInputAdapter: adapt Windows input
  WindowsInputAdapter-->>SSHTmuxControlTransport: return transformed command
  SSHTmuxControlTransport->>SSHChannel: write control command
  SSHChannel-->>WindowsStartupOutputAdapter: return Windows control output
  WindowsStartupOutputAdapter-->>SSHTmuxControlTransport: return unwrapped output
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: Windows tmux control-session support over SSH.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 3

🧹 Nitpick comments (3)
RemuxAppTests/SSHTmuxControlTransportTests.swift (1)

1198-1216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for Windows output that has no DCS envelope.

SSHTmuxControlStartupOutputAdapter.adapt has a third branch: the platform is .windows and the first bytes do not match dcsOpener. The adapter then flushes the buffered prefix and passes all later data through unchanged. No test covers that branch, so a regression there would surface only against a real host.

🧪 Proposed test
func testWindowsStartupOutputAdapterPassesUnwrappedOutputThrough() {
    let adapter = SSHTmuxControlStartupOutputAdapter(platform: .windows)

    XCTAssertEqual(adapter.adapt(Data("%beg".utf8)), Data("%beg".utf8))
    XCTAssertEqual(
        adapter.adapt(Data("in 1 1 0\n\u{1b}\\".utf8)),
        Data("in 1 1 0\n\u{1b}\\".utf8)
    )
}

Note that the expected value on the second call depends on the intended behavior for unwrapped streams. Confirm whether ESC \ must be preserved when isDCSWrapped is false.

🤖 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 `@RemuxAppTests/SSHTmuxControlTransportTests.swift` around lines 1198 - 1216,
Add a test alongside testWindowsStartupOutputAdapterRemovesSplitDCSEnvelope for
the Windows unwrapped-output branch of SSHTmuxControlStartupOutputAdapter.adapt:
verify an initial prefix is returned unchanged and subsequent data, including
ESC-backslash, is also preserved when no DCS opener is detected.
RemuxAppTests/TmuxSessionControllerClientSizeTests.swift (1)

628-647: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten the first assertion and state the overlap with the existing round-trip test.

Two points:

  1. The first assertion reads only takeStrings().first and checks a prefix. Extra writes pass unnoticed. The second assertion checks the exact array. Make the first assertion equally strict, so the test detects an unexpected additional write.
  2. This test does not pump the response for the first refresh. The second resize is therefore admitted while the first pane refresh is still in flight. testInFlightGridRefreshFollowsViewportRevertExactlyOnce at Lines 649-693 already covers that path with different dimensions. State the added value in the test name or the assertion message, for example that the portrait-to-landscape swap of cols and rows must not be coalesced away.
♻️ Proposed assertion change
         harness.controller.setClientSize(cols: 120, rows: 32)
         await drain(harness.controller)
-        XCTAssertTrue(
-            try XCTUnwrap(harness.recorder.takeStrings().first)
-                .hasPrefix("refresh-client -C 120x32\n")
-        )
+        let landscapeWrites = harness.recorder.takeStrings()
+        XCTAssertEqual(landscapeWrites.count, 1)
+        XCTAssertTrue(
+            try XCTUnwrap(landscapeWrites.first)
+                .hasPrefix("refresh-client -C 120x32\ndisplay-message")
+        )
🤖 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 `@RemuxAppTests/TmuxSessionControllerClientSizeTests.swift` around lines 628 -
647, Update testClientSizeRefreshesAcrossPortraitLandscapeRoundTrip to assert
the first recorder output equals exactly ["refresh-client -C 120x32\n"], not
merely that its first entry has a prefix. Clarify in the test name or assertion
message that this specifically verifies a portrait-to-landscape cols/rows swap
is not coalesced away, distinguishing it from
testInFlightGridRefreshFollowsViewportRevertExactlyOnce.
RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift (1)

891-919: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Every session start now pays an extra channel round trip.

run opens a second session channel and waits for channelInactive before the control session opens. POSIX hosts, which are the common case, pay this cost on every transport start, including reconnects that share one RemuxSSHRoot.

Consider caching the detected platform per SSH root or per saved server, so repeated session opens reuse the first result.

🤖 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 `@RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift` around lines 891 - 919,
Update the platform-detection flow around SSHTmuxRemoteCommandRunner.run to
cache the detected platform per reusable SSH root or saved server. Reuse the
cached result on subsequent session starts and reconnects before opening another
session channel, while ensuring concurrent starts share the same detection
rather than issuing duplicate probes.
🤖 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 `@RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift`:
- Around line 129-132: Update windowsQuotedArgument and its callers for Windows
command arguments so percent signs cannot be expanded by cmd.exe: reject or
validate values containing % for sessionName and tmuxExecutable before building
the command, or explicitly document that these values are unsupported. Preserve
the existing embedded-quote escaping behavior.

In `@RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift`:
- Around line 982-1000: Update SSHTmuxControlTransport.start and
detectRemotePlatform to treat unusable remote probes—including rejected exec
requests, missing exit status, and forced-command output—as .posix, recording
the fallback reason through the existing trace APIs. Preserve a hard error only
for a successfully completed probe with intentionally ambiguous output, if that
distinction remains supported.
- Around line 681-714: Update removingDCSClosers to track whether it is inside
the DCS envelope, rather than removing every ESC-backslash pair. Strip only the
single ESC-backslash terminator that closes the envelope, preserve identical
sequences in payload data, and retain hasPendingEscape handling across
fragmented Data chunks.

---

Nitpick comments:
In `@RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift`:
- Around line 891-919: Update the platform-detection flow around
SSHTmuxRemoteCommandRunner.run to cache the detected platform per reusable SSH
root or saved server. Reuse the cached result on subsequent session starts and
reconnects before opening another session channel, while ensuring concurrent
starts share the same detection rather than issuing duplicate probes.

In `@RemuxAppTests/SSHTmuxControlTransportTests.swift`:
- Around line 1198-1216: Add a test alongside
testWindowsStartupOutputAdapterRemovesSplitDCSEnvelope for the Windows
unwrapped-output branch of SSHTmuxControlStartupOutputAdapter.adapt: verify an
initial prefix is returned unchanged and subsequent data, including
ESC-backslash, is also preserved when no DCS opener is detected.

In `@RemuxAppTests/TmuxSessionControllerClientSizeTests.swift`:
- Around line 628-647: Update
testClientSizeRefreshesAcrossPortraitLandscapeRoundTrip to assert the first
recorder output equals exactly ["refresh-client -C 120x32\n"], not merely that
its first entry has a prefix. Clarify in the test name or assertion message that
this specifically verifies a portrait-to-landscape cols/rows swap is not
coalesced away, distinguishing it from
testInFlightGridRefreshFollowsViewportRevertExactlyOnce.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 821dfba1-f1a3-49ee-8636-73a10b64a5c6

📥 Commits

Reviewing files that changed from the base of the PR and between 00c3fe3 and addfb91.

📒 Files selected for processing (4)
  • RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift
  • RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift
  • RemuxAppTests/SSHTmuxControlTransportTests.swift
  • RemuxAppTests/TmuxSessionControllerClientSizeTests.swift

Comment thread RemuxApp/Sources/Tmux/SSHTmuxControlCommandBuilder.swift
Comment thread RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift Outdated
Comment thread RemuxApp/Sources/Tmux/SSHTmuxControlTransport.swift
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.

1 participant