Skip to content

mux: platform module + Linux CI (phase 1 of Windows/Linux support) - #7346

Merged
lawrencecchen merged 6 commits into
mainfrom
feat-mux-platforms
Jul 6, 2026
Merged

lawrencecchen merged 6 commits into
mainfrom
feat-mux-platforms

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Phase 1 of cross-platform support: a mux_core::platform module centralizing runtime dir/config path/shell/Chrome/ghostty-config decisions (XDG-correct on Linux, provably unchanged on macOS), a transport seam isolating socket construction for the Windows phase, zero-pixel winsize hardening, and a macos+ubuntu CI matrix running the full gate (tests, clippy -D warnings, fmt, both pty smoke scripts). The ubuntu job is the actual Linux verification for this PR. Phase 2 (Windows: ConPTY via portable-pty, transport swap, pwsh>powershell>cmd defaults, %APPDATA% config, libghostty-vt zig windows-target risk gate) follows on top of these seams. Targets the feature branch.

🤖 Generated with Claude Code


Note

Medium Risk
Touches control-socket paths, Chrome launch contracts, and CDP concurrency; Linux CI is new coverage but macOS behavior is intended unchanged via centralized platform logic.

Overview
Phase 1 cross-platform support for cmux-mux: new mux_core::platform centralizes runtime dirs (XDG on Linux), config paths, default shell, Chrome discovery/profile dirs, Ghostty config candidates, and socket permission helpers. platform::transport abstracts the control socket (Unix on Unix, uds_windows on Windows); server, remote attach, and tests use it instead of raw UnixStream.

Chrome binary resolution moves from mux-cdp into mux-core via platform; Chrome::launch now requires a resolved PathBuf binary and a user_data_dir when not ephemeral. ghostty-vt-sys cross-compiles with zig targets on Windows and links ghostty-vt-static.

CDP client switches the shared WebSocket to nonblocking I/O with bounded send retries and reader backoff to avoid mutex stalls in tests.

CI: mux workflow runs the full gate on macOS and Ubuntu (Linux apt deps for bindgen); install-zig-ci.sh supports Linux; optional continue-on-error Windows job cross-builds mux-tui for x86_64-pc-windows-gnu. Smoke scripts parse the server’s “control socket at …” line and pass --socket, with XDG-aware fallbacks.

TUI: Unix-only host color/kitty probes with safe defaults elsewhere; zero-pixel winsize falls back to 8×16. README documents macOS/Linux support and path behavior.

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


Summary by cubic

Adds a cross‑platform mux_core::platform and a transport seam so Linux uses XDG‑correct runtime/config paths while macOS stays unchanged, plus Windows‑ready implementations and an experimental Windows GNU build in CI. Also fixes a CDP stall with a nonblocking WebSocket and bounded retries, and points smokes/tests at the server‑advertised control socket.

  • New Features

    • Centralized logic in mux_core::platform: runtime dir, config path, default shell; Chrome discovery/profile dir; Ghostty config candidates. Adds Windows variants and platform::transport (Unix sockets on Unix; uds_windows on Windows).
    • Browser launch moved into platform: mux_cdp::Chrome::launch now takes a resolved binary PathBuf; non‑ephemeral launches require user_data_dir.
    • CI: macOS+Ubuntu matrix with Linux build deps; Windows GNU lane cross‑builds ghostty-vt-static and mux-tui; Zig installer picks OS/arch.
  • Bug Fixes

    • CDP client: nonblocking WebSocket with bounded send retries and idle backoff to prevent mutex stalls.
    • Handle zero‑pixel TIOCGWINSZ by falling back to 8×16; host color and kitty graphics probes are cfg(unix) with safe fallbacks.
    • Smokes parse “control socket at ” and pass --socket; browser runtime test is gated on a discoverable Chrome and runs under a watchdog.

Written for commit 71e1415. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added broader platform support for control sockets, config discovery, and browser/profile path handling across Linux, macOS, and Windows.
    • Introduced an experimental Windows build job to validate GNU-target support.
    • Updated smoke scripts to discover the control socket dynamically at startup.
  • Bug Fixes

    • Improved browser launch reliability by requiring an explicit browser binary and clearer profile-directory handling.
    • Reduced WebSocket busy looping and added send timeouts for more stable remote sessions.

@vercel

vercel Bot commented Jul 4, 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 6, 2026 9:08am
cmux-staging Building Building Preview, Comment Jul 6, 2026 9:08am

@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds cross-platform (Windows) support to the cmux-mux crate: a new platform module abstracts sockets, filesystem paths, shell/browser discovery; server/surface/TUI/remote session code is wired to it; Chrome launch API now requires an explicit binary path; CI workflows, build scripts, and smoke tests are updated accordingly.

Changes

Cross-platform support and Windows enablement

Layer / File(s) Summary
Platform abstraction module
mux/crates/mux-core/src/platform.rs, mux/crates/mux-core/src/lib.rs, mux/Cargo.toml, mux/crates/mux-core/Cargo.toml
New module provides a transport abstraction (Unix/Windows sockets), runtime/config/home directory resolution, default shell selection, Chrome/Ghostty path discovery, and permission helpers, with uds_windows added as a dependency.
Server and surface wiring
mux/crates/mux-core/src/server.rs, mux/crates/mux-core/src/surface.rs
Control socket server and PTY surface spawning now use platform transport, runtime dir, default shell, and home dir helpers instead of hard-coded Unix logic.
TUI/remote session wiring and Unix-gated probing
mux/crates/mux-tui/src/config.rs, mux/crates/mux-tui/src/host_colors.rs, mux/crates/mux-tui/src/ui/graphics.rs, mux/crates/mux-tui/src/session/remote.rs, mux/crates/mux-core/tests/pty.rs
Config path/color resolution use platform helpers; terminal color/graphics probing is gated behind cfg(unix) with non-Unix fallbacks; remote session and tests use the boxed transport stream type.
Chrome launch API and resolution
mux/crates/mux-cdp/src/chrome.rs, mux/crates/mux-cdp/src/lib.rs, mux/crates/mux-cdp/tests/chrome_smoke.rs, mux/crates/mux-core/src/browser.rs
ChromeLaunchOptions.binary is now a required PathBuf; binary/profile discovery moved into mux-core via new resolver helpers; smoke test gated on an env var.
CDP client retry logic
mux/crates/mux-cdp/src/client.rs
Connection uses nonblocking sockets; send and reader loops retry on WouldBlock/TimedOut with sleeps and a send deadline.
CI matrix and Windows build job
.github/workflows/mux.yml, mux/crates/ghostty-vt-sys/build.rs, scripts/install-zig-ci.sh, tests/test_install_zig_ci_no_sudo.sh
Test job runs on a macos/linux matrix; a new experimental Windows GNU build job is added; Zig build/install scripts support Windows/Linux OS detection and cross-target builds.
Smoke script socket discovery
mux/scripts/smoke-attach.py, mux/scripts/smoke-tui.py
Scripts discover the control socket dynamically from server output rather than a fixed path, and clean up the socket on exit.
Documentation
mux/README.md
Adds Platforms section and updates configuration/control-socket documentation with new discovery precedence and examples.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server
  participant transport
  participant platform

  Client->>Server: connect to control socket
  Server->>platform: runtime_dir()
  Server->>transport: listen(path)
  transport-->>Server: Listener
  loop accept loop
    Server->>transport: accept()
    transport-->>Server: Box<dyn Stream>
    Server->>platform: restrict_directory/restrict_file
    Server->>Server: spawn handle_connection(stream)
  end
Loading
sequenceDiagram
  participant Browser as browser.rs
  participant Resolver as resolve_chrome_binary/dir
  participant Platform as platform
  participant Chrome

  Browser->>Resolver: resolve_chrome_binary(opts)
  Resolver->>Platform: chrome_candidates()
  Platform-->>Resolver: candidate paths
  Resolver-->>Browser: executable path or error
  Browser->>Resolver: resolve_chrome_user_data_dir(opts)
  Resolver->>Platform: chrome_user_data_dir()
  Platform-->>Resolver: profile dir or error
  Browser->>Chrome: launch(binary)
  Chrome->>Chrome: launch_with(options)
Loading

Poem

A rabbit hops from mac to win,
new sockets born where none had been,
Chrome's binary must now be true,
no shortcuts left, just paths anew,
Zig builds cross oceans, CI wide —
cmux-mux now spans every side. 🐇🪟

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary well, but it omits the template's Testing, Demo Video, Review Trigger, and Checklist sections. Add the missing template sections, especially Testing and Checklist, and include the review-trigger comment block.
✅ Passed checks (24 passed)
Check name Status Explanation
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.
Cmux Swift Actor Isolation ✅ Passed PR changes only Rust/workflow/docs; no Swift files or actor-isolation-sensitive Swift declarations were touched.
Cmux Swift Blocking Runtime ✅ Passed No Swift files are touched in this PR; the diff is workflow, Rust, and scripts only, so the Swift blocking-runtime rule is not applicable.
Cmux Browser Automation Off-Main ✅ Passed Diff only touches Rust/browser-launch plumbing and CI; no changes to Swift router files or socketWorkerMethods/processV2Command/mainActor routing, so no off-main browser automation violation introd...
Cmux Expensive Synchronous Load ✅ Passed PR changes are Rust/Python/CI/docs only; no Swift files or expensive-agent-history load paths were touched.
Cmux Cache Substitution Correctness ✅ Passed The diff has no Swift, TypeScript, or JavaScript changes, so this cache-substitution rule is not applicable here.
Cmux No Hacky Sleeps ✅ Passed Only new sleep/polling is in smoke-test scaffolding; changed shell/build scripts add no hacky lifecycle waits, and install-zig-ci only adds a network retry-delay.
Cmux Algorithmic Complexity ✅ Passed Added loops are over fixed-size candidate lists or test harnesses; no new nested scans over scalable collections or hot-path rescans were introduced.
Cmux Swift Concurrency ✅ Passed No .swift files were changed in this PR, so the Swift concurrency modernization rule isn’t implicated.
Cmux Swift @Concurrent ✅ Passed No Swift files are changed in this PR, so the @concurrent/nonisolated-async rule does not apply.
Cmux Swift File And Package Boundaries ✅ Passed The diff touches only workflow/Cargo/Rust files; no Swift files were changed, so the Swift boundary rule is not applicable.
Cmux Swiftpm Lockfiles ✅ Passed The commit changes only Rust and workflow files; no SwiftPM Package.swift/Package.resolved, Xcode project, or cmux-owned .gitignore files were touched.
Cmux Swift Logging ✅ Passed The PR diff touches no .swift files, so it adds no Swift logging to evaluate against the rule.
Cmux User-Facing Error Privacy ✅ Passed PASS: The diff adds only generic runtime/build diagnostics; no new user-facing errors expose vendor/internal names or raw upstream details.
Cmux Full Internationalization ✅ Passed Diff only touches Rust/CI/scripts; no Swift UI text, xcstrings/Info.plist edits, or web i18n files/messages are changed, so no full-internationalization violation.
Cmux Swiftui State Layout ✅ Passed PASS: The diff touches only GitHub Actions, Rust, and shell/README files; no SwiftUI views/state were added or modified, so the rule is not applicable.
Cmux Architecture Rethink ✅ Passed PR changes are non-Swift only; the Swift architectural rethink rule is not applicable here.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR diff contains no Swift/Xcode files, so it introduces no new cmux-owned windows or shortcut-registration changes.
Cmux Source Artifacts ✅ Passed Touched paths are intentional source/docs/tests/config/CI and a Cargo.lock update; no temp dirs, build outputs, logs, caches, or other artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No changed files are Swift production Sources files; this PR only touches Rust/workflow code, so the Swift no-test-debug-seam rule is not applicable.
Cmux No Ambient Global State ✅ Passed No Swift production files were changed in this commit, so the no-ambient-global-state Swift rule is not applicable.
Title check ✅ Passed The title accurately summarizes the main change: a platform module plus Linux CI support for phase 1 cross-platform work.
✨ 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 feat-mux-platforms

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.

Comment thread mux/crates/mux-core/src/platform.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f036e579d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +182 to +184
push_path_candidates(
&mut candidates,
&["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Linux Brave/Edge discovery

On Linux this new platform-specific list no longer includes brave-browser or microsoft-edge, although the previous find_chrome_binary checked both PATH names and /usr/bin paths. In a Linux install where Brave or Edge is the only Chrome-family browser, resolve_chrome_binary() now returns “no Chrome/Chromium binary found” and browser tab creation fails unless users add explicit config; keep those existing candidates in the Linux branch.

Useful? React with 👍 / 👎.

Comment on lines +276 to +277
env_path("XDG_RUNTIME_DIR")
.or_else(|| env_path("TMPDIR"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align smoke scripts with XDG sockets

When XDG_RUNTIME_DIR is set, this moves the default socket to $XDG_RUNTIME_DIR/cmux-mux-<uid>, but I checked mux/scripts/smoke-tui.py and mux/scripts/smoke-attach.py: both still wait/connect to ${TMPDIR:-/tmp}/cmux-mux-<uid>. Under normal Linux/systemd sessions, the server starts at the new path while the smoke tests poll the old one, so update the scripts or pass an explicit --socket.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces mux_core::platform to centralise OS-specific decisions (runtime dir, config path, default shell, Chrome/Ghostty discovery, home dir) and a platform::transport seam isolating Unix socket construction for a future Windows phase. It also fixes a CDP mutex-stall bug by switching the WebSocket to nonblocking I/O, adds a zero-pixel winsize fallback, and wires CI to run the full gate on both macOS and Ubuntu.

  • mux_core::platform — new module replaces scattered $HOME/$TMPDIR/getuid() calls with XDG-correct, Windows-ready helpers; cfg blocks cover macOS, Linux, Windows, and a generic Unix fallback.
  • Transport seam — platform::transport::{listen, connect, Stream} wraps UnixListener/UnixStream on Unix and uds_windows on Windows; server, tests, and the remote session now use this abstraction rather than std::os::unix::net directly.
  • CDP nonblocking fix — CdpClient switches to set_nonblocking(true) and adds a bounded send-retry loop; a logic error in the retry calls ws.send() instead of ws.flush() on WouldBlock, which can duplicate the message in tungstenite's internal write buffer.

Confidence Score: 4/5

Safe to merge on all paths where the CDP nonblocking path is not exercised; the tungstenite send-retry bug requires a rare TCP back-pressure event to trigger, but the fix is straightforward before shipping.

The platform module, transport seam, shell/home-dir refactor, zero-pixel winsize fix, and CI matrix changes are all clean and well-tested. The one real defect is in the new CDP send-retry loop in client.rs: after tungstenite's send() returns WouldBlock the frame is already encoded into its internal BufWriter, so the retry calls send() again and enqueues a duplicate frame. In practice a TCP write-buffer stall is essentially impossible for small CDP messages, but the logic is wrong and would corrupt the protocol if it did trigger.

mux/crates/mux-cdp/src/client.rs — the send_text retry loop

Important Files Changed

Filename Overview
mux/crates/mux-cdp/src/client.rs Switches WebSocket to nonblocking mode to reduce mutex hold time; introduces a bounded send-retry loop that incorrectly calls ws.send() on WouldBlock instead of ws.flush(), potentially duplicating CDP frames in tungstenite's write buffer.
mux/crates/mux-core/src/platform.rs New module centralising OS-specific paths (runtime dir, config, shell, Chrome candidates, Ghostty config); well-structured with unix/windows/fallback cfg blocks. Previously flagged compile gap for non-unix/non-windows targets applies here too but is the same pre-flagged pattern.
mux/crates/mux-cdp/src/chrome.rs Chrome::launch now requires a resolved PathBuf binary; binary discovery moved to mux-core/browser.rs. Callers correctly resolve both binary and user_data_dir before calling launch_with.
mux/crates/mux-core/src/browser.rs resolve_chrome_binary and resolve_chrome_user_data_dir added, delegating to platform helpers. Logic is correct; user_data_dir is only resolved when not ephemeral.
mux/crates/mux-core/src/server.rs Control socket now uses platform::transport abstraction; directory/file permission helpers replace direct unix-only calls. Server accept loop cleanly ported to Listener::accept().
.github/workflows/mux.yml Adds macOS+Ubuntu matrix and experimental Windows GNU job. macOS leg correctly preserves the Blacksmith runner via the conditional expression.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[send_text called] --> B{mutex acquired?}
    B -- yes --> C[ws.send Message::Text text.clone]
    C --> D{Result?}
    D -- Ok --> E[return Ok]
    D -- WouldBlock/TimedOut --> F[drop mutex, sleep 10ms]
    F --> G{deadline passed?}
    G -- no --> B
    G -- yes --> H[bail: CDP send timed out]
    D -- other error --> I[return Err]
    style C fill:#f96,stroke:#c00
    style F fill:#f96,stroke:#c00
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[send_text called] --> B{mutex acquired?}
    B -- yes --> C[ws.send Message::Text text.clone]
    C --> D{Result?}
    D -- Ok --> E[return Ok]
    D -- WouldBlock/TimedOut --> F[drop mutex, sleep 10ms]
    F --> G{deadline passed?}
    G -- no --> B
    G -- yes --> H[bail: CDP send timed out]
    D -- other error --> I[return Err]
    style C fill:#f96,stroke:#c00
    style F fill:#f96,stroke:#c00
Loading

Reviews (8): Last reviewed commit: "mux: Windows phase 2 - libghostty-vt cro..." | Re-trigger Greptile

Comment on lines +127 to +146
pub fn default_shell() -> String {
if let Some(shell) = env_string("SHELL") {
return shell;
}

#[cfg(unix)]
{
if Path::new("/bin/bash").is_file() {
"/bin/bash".to_string()
} else {
"/bin/sh".to_string()
}
}

#[cfg(windows)]
{
// Phase 2 Windows order: pwsh > powershell > cmd.
"cmd".to_string()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing return path for non-Unix, non-Windows targets

When both #[cfg(unix)] and #[cfg(windows)] are inactive (e.g. WASM, Redox, Fuchsia) and SHELL is not set, the function falls off the end without returning a String, which is a compile error. The Windows seam (#[cfg(windows)]) was already added as a Phase 2 forward-look, so this function is explicitly intended to be compiled on other platforms eventually. A simple #[cfg(not(any(unix, windows)))] fallback block or unreachable!() would close this gap before Phase 2 work widens the target matrix.

Comment on lines +304 to +315
fn push_path_candidates(candidates: &mut Vec<PathBuf>, names: &[&str]) {
let Some(path) = std::env::var_os("PATH") else { return };
for name in names {
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if is_executable_file(&candidate) {
push_unique(candidates, candidate);
break;
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Redundant is_executable_file stat for PATH-discovered candidates

push_path_candidates already calls is_executable_file for every (name, dir) pair and only pushes entries that pass. resolve_chrome_binary in browser.rs then calls is_executable_file again on each entry from chrome_candidates(). For PATH-discovered candidates this double stat can never return a different result. A doc comment documenting the pre-verification contract would make the redundancy intentional and visible.

Suggested change
fn push_path_candidates(candidates: &mut Vec<PathBuf>, names: &[&str]) {
let Some(path) = std::env::var_os("PATH") else { return };
for name in names {
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if is_executable_file(&candidate) {
push_unique(candidates, candidate);
break;
}
}
}
}
/// PATH candidates are pre-verified by `is_executable_file`; fixed paths are
/// not (callers are expected to stat them). All entries are de-duplicated.
fn push_path_candidates(candidates: &mut Vec<PathBuf>, names: &[&str]) {
let Some(path) = std::env::var_os("PATH") else { return };
for name in names {
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if is_executable_file(&candidate) {
push_unique(candidates, candidate);
break;
}
}
}
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@blacksmith-sh

This comment has been minimized.

Comment thread mux/crates/mux-core/tests/browser_runtime.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 526b2e7f3e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


#[test]
fn two_browser_surfaces_share_external_runtime_and_demux_frames() {
let Some(_binary) = browser_test_binary() else { return };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove the Chrome gate from the external CDP test

This test body connects to the in-process fake CDP websocket via cdp_url and sets browser_discover: false, so it never launches or needs a real Chrome binary. With this new early return, any runner or developer machine without one of the discovered Chrome candidates silently skips the demux regression instead of exercising the external-runtime path; remove this precondition or limit it to tests that actually launch Chrome.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18de8f6049

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ws.get_ref().set_read_timeout(Some(Duration::from_millis(100)))?;
// The reader and writers share tungstenite's synchronous WebSocket.
// Nonblocking reads keep the reader from holding the mutex while idle.
ws.get_ref().set_nonblocking(true)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle nonblocking CDP writes before enabling them

This makes the shared TcpStream nonblocking for writes as well as reads. The screencast ACK path (ack_screencast_frame) still does a single send and ignores the result, so if the local CDP socket ever reports WouldBlock under backpressure, the Page.screencastFrameAck is dropped and Chrome can stop sending frames, freezing browser panes. Please either keep writes blocking or route all CDP writes/acks through a helper that retries/flushes WouldBlock safely.

Useful? React with 👍 / 👎.

@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 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 147aef3. Configure here.

Comment thread mux/crates/mux-core/src/server.rs
@socket-security

socket-security Bot commented Jul 5, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​uds_windows@​1.2.110010094100100

View full report

lawrencecchen and others added 6 commits July 6, 2026 01:11
…matrix

Coded by GPT 5.5 via the fable loop (2 rounds, judge-reviewed).

All platform decisions route through mux_core::platform: runtime dir
(XDG_RUNTIME_DIR then TMPDIR then /tmp), config path (CMUX_MUX_CONFIG
then XDG_CONFIG_HOME then ~/.config), default shell (SHELL then bash
then sh), per-OS Chrome discovery + profile dirs, and ghostty config
candidates (Linux XDG paths join the macOS Application Support path).
macOS resolution is unchanged under default env. Socket construction
sits behind platform::transport::{listen, connect} so the Windows
phase swaps transports in one place; 0700/0600 perms preserved.

Zero-pixel TIOCGWINSZ degrades to the 8x16 default with a test, and
the cell-pixel probe keeps its lazy fallback: the CSI 14 t query only
runs when the ioctl reports nothing (the judge caught an eager-query
regression that would have stalled macOS startup 120ms and eaten
type-ahead).

CI runs the full gate on macos-latest and ubuntu-latest;
install-zig-ci.sh resolves per-OS archives (Darwin path unchanged for
existing cmux CI consumers) and ubuntu installs clang/libclang/pkg-config
for bindgen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…OS-aware

The matrix had moved the macOS lane to GH-hosted macos-latest, where
the zig build of libghostty-vt fails linking libSystem; the lane now
routes through vars.MACOS_RUNNER_15 (Blacksmith fallback) as before,
with ubuntu-latest as the Linux lane. The install-zig guard test's
fixtures hardcoded macOS archive naming, which only matched because
the script used to hardcode it too; the test now derives ZIG_OS from
uname like the script and computes checksums via shasum-else-sha256sum.
Verified on macOS and on an ubuntu VM (both PASS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ubuntu lane hung >60s in
two_browser_surfaces_share_external_runtime_and_demux_frames because
the runner has no Chrome; the test now skips in milliseconds with a
printed reason when neither CMUX_MUX_BROWSER_TEST_BINARY nor a
platform chrome candidate is executable, and the runnable path sits
under a bounded watchdog so a broken Chrome can never wedge CI.
Verified with and without a discoverable binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on CI

Root cause of both CI hangs (ubuntu >60s cancel, Blacksmith macOS 300s
watchdog): CdpClient kept one synchronous websocket behind a Mutex and
the reader thread's idle ws.read() could hold that lock while the next
CDP send waited, so Target.createTarget never reached the fake server.
Chrome was never involved; the round-4 availability gate only masked
ubuntu by skipping. The websocket is now nonblocking after handshake
(brief idle sleep on WouldBlock) and sends retry under a 5s bound, so
a dead endpoint fails fast instead of wedging. The fake-CDP test drops
its Chrome gating (it is hermetic) and its watchdog tightens to 60s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ubuntu lane failed because XDG_RUNTIME_DIR is set there, so the
server places its socket per the platform module while the smokes still
computed ${TMPDIR:-/tmp}/cmux-mux-<uid>. Both scripts now parse
'control socket at <path>' from headless startup output (bounded wait,
XDG/TMP fallback only if the line is unavailable), so there is one
resolution authority. Verified on macOS with and without a simulated
XDG_RUNTIME_DIR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_windows transport, experimental CI lane

Coded by GPT 5.5 via the fable loop (judge-reviewed; one dropped XDG
fallback restored by the orchestrator per the verdict).

The risk gate passed: libghostty-vt builds for x86_64-windows-gnu via
zig (valid COFF static archive, verified with zig ar + file); MSVC is
blocked upstream in ghostty's C/C++ deps (simdutf/highway include
errors) and documented. build.rs maps windows triples to zig targets
and the native macOS/Linux invocations stay argument-identical.
platform.rs gains windows implementations (CMUX_MUX_CONFIG >
XDG_CONFIG_HOME > %APPDATA% config, pwsh > powershell > cmd shells,
%TEMP% runtime dir, Program Files chrome candidates); transport uses
uds_windows AF_UNIX target-scoped so unix builds never compile it;
host_colors and kitty-graphics ioctls are cfg(unix) with clean windows
fallbacks. cargo check --target x86_64-pc-windows-gnu passes for the
whole workspace; an experimental continue-on-error windows-latest lane
builds it in CI. P3: run on a real Windows machine (EC2), ConPTY
behavior validation, windows smoke coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen
lawrencecchen changed the base branch from feat-rust-tui-backend to main July 6, 2026 08:13

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 71e1415e05

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


{
let mut ws = self.inner.ws.lock().unwrap();
match ws.send(Message::Text(text.clone())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid retrying WebSocket sends by re-enqueuing the message

When the nonblocking socket hits backpressure, ws.send(...) may have already queued the frame in tungstenite before flush returns WouldBlock; this retry loop then calls send again with the same JSON-RPC id, so commands such as Target.createTarget can be delivered twice once the socket drains, creating extra Chrome targets while only the first response is matched. In the backpressure path, flush the existing buffered frame (or keep writes blocking) instead of re-enqueuing Message::Text(text.clone()).

Useful? React with 👍 / 👎.

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

🤖 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 @.github/workflows/mux.yml:
- Around line 99-108: The standalone “Build libghostty-vt for Windows GNU” step
is redundant with the `ghostty-vt-sys/build.rs` path used by `cargo build -p
mux-tui --target x86_64-pc-windows-gnu`. Remove the explicit `zig build`/archive
inspection step from the workflow, or fold it into a single smoke-check if
needed, so the Windows GNU `mux-tui` job relies on the existing
`build.rs`-driven `ghostty-vt` build only once.
- Line 33: Add persist-credentials: false to every actions/checkout step in the
workflow, including the checkout entries in the test and windows-experimental
jobs. Update the checkout configuration itself rather than any later step, so
the Actions token is not left on disk after checkout since no subsequent step
needs git push/authenticated access.

In `@mux/crates/ghostty-vt-sys/build.rs`:
- Around line 44-48: The cross-compilation path in build.rs silently falls back
to the host target when zig_target_for_rust_target(&target) returns None, which
can misbuild libghostty-vt for the wrong platform. Update the target handling in
the build command setup so the non-host branch explicitly errors out when no Zig
mapping exists instead of skipping -Dtarget; use the existing
zig_target_for_rust_target and the command construction around target != host to
fail loudly for unmapped targets.

In `@mux/crates/mux-core/src/platform.rs`:
- Around line 33-109: The unix and windows transport imp modules are duplicating
the same Listener, listen, connect, accept, and Stream implementation logic, so
consolidate the shared behavior to avoid future drift. Extract the common
transport code into a single helper or macro-driven implementation and keep only
the platform-specific UnixListener/UnixStream imports inside the cfg blocks. Use
the existing imp module, Listener, listen, connect, accept, and Stream for
UnixStream symbols to preserve the public shape while removing byte-for-byte
duplicated bodies.

In `@mux/scripts/smoke-attach.py`:
- Around line 25-76: The socket-discovery logic in fallback_socket_path() and
wait_for_control_socket() is duplicated and should be centralized to avoid
drift. Move both helpers into a shared mux/scripts/smoke_common.py module, then
update smoke-attach.py and smoke-tui.py to import and use the shared functions.
Keep the existing behavior, including CONTROL_SOCKET_RE handling, fallback path
resolution, and timeout/exit checks, so both scripts stay in sync.
🪄 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: 36891dd8-73cd-4aca-ba8c-6d412b2db25f

📥 Commits

Reviewing files that changed from the base of the PR and between 02f18cd and 71e1415.

⛔ Files ignored due to path filters (1)
  • mux/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • .github/workflows/mux.yml
  • mux/Cargo.toml
  • mux/README.md
  • mux/crates/ghostty-vt-sys/build.rs
  • mux/crates/mux-cdp/src/chrome.rs
  • mux/crates/mux-cdp/src/client.rs
  • mux/crates/mux-cdp/src/lib.rs
  • mux/crates/mux-cdp/tests/chrome_smoke.rs
  • mux/crates/mux-core/Cargo.toml
  • mux/crates/mux-core/src/browser.rs
  • mux/crates/mux-core/src/lib.rs
  • mux/crates/mux-core/src/platform.rs
  • mux/crates/mux-core/src/server.rs
  • mux/crates/mux-core/src/surface.rs
  • mux/crates/mux-core/tests/browser_runtime.rs
  • mux/crates/mux-core/tests/pty.rs
  • mux/crates/mux-tui/src/config.rs
  • mux/crates/mux-tui/src/host_colors.rs
  • mux/crates/mux-tui/src/session/remote.rs
  • mux/crates/mux-tui/src/ui/graphics.rs
  • mux/scripts/smoke-attach.py
  • mux/scripts/smoke-tui.py
  • scripts/install-zig-ci.sh
  • tests/test_install_zig_ci_no_sudo.sh

Comment thread .github/workflows/mux.yml
matrix:
os: [macos, linux]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set persist-credentials: false on checkout steps.

Static analysis flags both checkout steps for credential persistence (artipacked). The checked-out git credentials remain on disk for the rest of the job unless explicitly disabled, which is unnecessary here since no step needs to push/authenticate as the checkout token.

🔒 Proposed fix
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+        with:
+          persist-credentials: false

Apply to both the test job (line 33) and windows-experimental job (line 83).

Also applies to: 83-83

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 33-33: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 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 @.github/workflows/mux.yml at line 33, Add persist-credentials: false to
every actions/checkout step in the workflow, including the checkout entries in
the test and windows-experimental jobs. Update the checkout configuration itself
rather than any later step, so the Actions token is not left on disk after
checkout since no subsequent step needs git push/authenticated access.

Source: Linters/SAST tools

Comment thread .github/workflows/mux.yml
Comment on lines +99 to +108
- name: Build libghostty-vt for Windows GNU
shell: bash
working-directory: ghostty
run: |
zig build -Demit-lib-vt=true -Demit-xcframework=false -Doptimize=ReleaseFast -Dtarget=x86_64-windows-gnu --prefix "$RUNNER_TEMP/ghostty-vt-win-gnu"
zig ar t "$RUNNER_TEMP/ghostty-vt-win-gnu/lib/ghostty-vt-static.lib" | head

- name: cargo build mux-tui for Windows GNU
working-directory: mux
run: cargo build -p mux-tui --target x86_64-pc-windows-gnu --locked

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Manual zig build step for ghostty-vt looks redundant with build.rs.

The "Build libghostty-vt for Windows GNU" step builds into $RUNNER_TEMP/ghostty-vt-win-gnu purely as a standalone check, but ghostty-vt-sys/build.rs invokes its own zig build (with the same -Dtarget=x86_64-windows-gnu cross-target logic, see build.rs lines 44-51) into OUT_DIR when cargo build -p mux-tui --target x86_64-pc-windows-gnu runs at line 108. That means the Ghostty VT static library is built twice, doubling the slowest part of this job for no functional purpose (the manually built archive at $RUNNER_TEMP is never consumed by the cargo build). If it's meant purely as an early smoke-check, consider that this experimental job already tolerates failures (continue-on-error: true), so the extra build mainly costs CI time.

🤖 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 @.github/workflows/mux.yml around lines 99 - 108, The standalone “Build
libghostty-vt for Windows GNU” step is redundant with the
`ghostty-vt-sys/build.rs` path used by `cargo build -p mux-tui --target
x86_64-pc-windows-gnu`. Remove the explicit `zig build`/archive inspection step
from the workflow, or fold it into a single smoke-check if needed, so the
Windows GNU `mux-tui` job relies on the existing `build.rs`-driven `ghostty-vt`
build only once.

Comment on lines +44 to +48
if target != host {
if let Some(zig_target) = zig_target_for_rust_target(&target) {
command.arg(format!("-Dtarget={zig_target}"));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Silent no-op when cross-compiling to an unmapped target.

When target != host and zig_target_for_rust_target returns None (any target other than the three Windows triples), the code silently skips -Dtarget=..., so zig build proceeds using the host's native target instead of the requested cross target. The resulting libghostty-vt archive would then be built for the wrong architecture/OS, and linking it into the cross-compiled Rust binary would likely fail at link time — or worse, produce a corrupt binary if ABI-compatible enough to link but not run. This should fail loudly rather than silently mis-target the build.

🛠️ Proposed fix
     if target != host {
-        if let Some(zig_target) = zig_target_for_rust_target(&target) {
-            command.arg(format!("-Dtarget={zig_target}"));
-        }
+        match zig_target_for_rust_target(&target) {
+            Some(zig_target) => {
+                command.arg(format!("-Dtarget={zig_target}"));
+            }
+            None => {
+                panic!(
+                    "cross-compiling to unsupported target {target} from host {host}: \
+                     add a mapping in zig_target_for_rust_target"
+                );
+            }
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if target != host {
if let Some(zig_target) = zig_target_for_rust_target(&target) {
command.arg(format!("-Dtarget={zig_target}"));
}
}
if target != host {
match zig_target_for_rust_target(&target) {
Some(zig_target) => {
command.arg(format!("-Dtarget={zig_target}"));
}
None => {
panic!(
"cross-compiling to unsupported target {target} from host {host}: \
add a mapping in zig_target_for_rust_target"
);
}
}
}
🤖 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 `@mux/crates/ghostty-vt-sys/build.rs` around lines 44 - 48, The
cross-compilation path in build.rs silently falls back to the host target when
zig_target_for_rust_target(&target) returns None, which can misbuild
libghostty-vt for the wrong platform. Update the target handling in the build
command setup so the non-host branch explicitly errors out when no Zig mapping
exists instead of skipping -Dtarget; use the existing zig_target_for_rust_target
and the command construction around target != host to fail loudly for unmapped
targets.

Comment on lines +33 to +109
#[cfg(unix)]
mod imp {
use std::io;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::Path;
use std::time::Duration;

use super::Stream;

pub(super) struct Listener {
inner: UnixListener,
}

pub(super) fn listen(path: &Path) -> io::Result<Listener> {
UnixListener::bind(path).map(|inner| Listener { inner })
}

pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(UnixStream::connect(path)?))
}

impl Listener {
pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
let (stream, _) = self.inner.accept()?;
Ok(Box::new(stream))
}
}

impl Stream for UnixStream {
fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(self.try_clone()?))
}

fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
UnixStream::set_read_timeout(self, timeout)
}
}
}

#[cfg(windows)]
mod imp {
use std::io;
use std::path::Path;
use std::time::Duration;

use super::Stream;
use uds_windows::{UnixListener, UnixStream};

pub(super) struct Listener {
inner: UnixListener,
}

pub(super) fn listen(path: &Path) -> io::Result<Listener> {
UnixListener::bind(path).map(|inner| Listener { inner })
}

pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(UnixStream::connect(path)?))
}

impl Listener {
pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
let (stream, _) = self.inner.accept()?;
Ok(Box::new(stream))
}
}

impl Stream for UnixStream {
fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(self.try_clone()?))
}

fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
UnixStream::set_read_timeout(self, timeout)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unix/Windows transport::imp are byte-for-byte duplicate logic.

The #[cfg(unix)] and #[cfg(windows)] imp modules define identical Listener, listen, connect, accept, and Stream for UnixStream bodies — only the underlying UnixListener/UnixStream types differ (std::os::unix::net vs uds_windows). This duplication will need to be kept in sync by hand for every future change to the transport contract.

♻️ Suggested consolidation via macro
-    #[cfg(unix)]
-    mod imp {
-        use std::io;
-        use std::os::unix::net::{UnixListener, UnixStream};
-        use std::path::Path;
-        use std::time::Duration;
-
-        use super::Stream;
-
-        pub(super) struct Listener {
-            inner: UnixListener,
-        }
-
-        pub(super) fn listen(path: &Path) -> io::Result<Listener> {
-            UnixListener::bind(path).map(|inner| Listener { inner })
-        }
-
-        pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
-            Ok(Box::new(UnixStream::connect(path)?))
-        }
-
-        impl Listener {
-            pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
-                let (stream, _) = self.inner.accept()?;
-                Ok(Box::new(stream))
-            }
-        }
-
-        impl Stream for UnixStream {
-            fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
-                Ok(Box::new(self.try_clone()?))
-            }
-
-            fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
-                UnixStream::set_read_timeout(self, timeout)
-            }
-        }
-    }
-
-    #[cfg(windows)]
-    mod imp {
-        use std::io;
-        use std::path::Path;
-        use std::time::Duration;
-
-        use super::Stream;
-        use uds_windows::{UnixListener, UnixStream};
-
-        pub(super) struct Listener {
-            inner: UnixListener,
-        }
-
-        pub(super) fn listen(path: &Path) -> io::Result<Listener> {
-            UnixListener::bind(path).map(|inner| Listener { inner })
-        }
-
-        pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
-            Ok(Box::new(UnixStream::connect(path)?))
-        }
-
-        impl Listener {
-            pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
-                let (stream, _) = self.inner.accept()?;
-                Ok(Box::new(stream))
-            }
-        }
-
-        impl Stream for UnixStream {
-            fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
-                Ok(Box::new(self.try_clone()?))
-            }
-
-            fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
-                UnixStream::set_read_timeout(self, timeout)
-            }
-        }
-    }
+    macro_rules! impl_uds_transport {
+        ($UnixListener:ty, $UnixStream:ty) => {
+            use std::io;
+            use std::path::Path;
+            use std::time::Duration;
+            use super::Stream;
+
+            pub(super) struct Listener {
+                inner: $UnixListener,
+            }
+
+            pub(super) fn listen(path: &Path) -> io::Result<Listener> {
+                <$UnixListener>::bind(path).map(|inner| Listener { inner })
+            }
+
+            pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
+                Ok(Box::new(<$UnixStream>::connect(path)?))
+            }
+
+            impl Listener {
+                pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
+                    let (stream, _) = self.inner.accept()?;
+                    Ok(Box::new(stream))
+                }
+            }
+
+            impl Stream for $UnixStream {
+                fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
+                    Ok(Box::new(self.try_clone()?))
+                }
+
+                fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
+                    <$UnixStream>::set_read_timeout(self, timeout)
+                }
+            }
+        };
+    }
+
+    #[cfg(unix)]
+    mod imp {
+        impl_uds_transport!(std::os::unix::net::UnixListener, std::os::unix::net::UnixStream);
+    }
+
+    #[cfg(windows)]
+    mod imp {
+        impl_uds_transport!(uds_windows::UnixListener, uds_windows::UnixStream);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[cfg(unix)]
mod imp {
use std::io;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::Path;
use std::time::Duration;
use super::Stream;
pub(super) struct Listener {
inner: UnixListener,
}
pub(super) fn listen(path: &Path) -> io::Result<Listener> {
UnixListener::bind(path).map(|inner| Listener { inner })
}
pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(UnixStream::connect(path)?))
}
impl Listener {
pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
let (stream, _) = self.inner.accept()?;
Ok(Box::new(stream))
}
}
impl Stream for UnixStream {
fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(self.try_clone()?))
}
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
UnixStream::set_read_timeout(self, timeout)
}
}
}
#[cfg(windows)]
mod imp {
use std::io;
use std::path::Path;
use std::time::Duration;
use super::Stream;
use uds_windows::{UnixListener, UnixStream};
pub(super) struct Listener {
inner: UnixListener,
}
pub(super) fn listen(path: &Path) -> io::Result<Listener> {
UnixListener::bind(path).map(|inner| Listener { inner })
}
pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(UnixStream::connect(path)?))
}
impl Listener {
pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
let (stream, _) = self.inner.accept()?;
Ok(Box::new(stream))
}
}
impl Stream for UnixStream {
fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(self.try_clone()?))
}
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
UnixStream::set_read_timeout(self, timeout)
}
}
}
macro_rules! impl_uds_transport {
($UnixListener:ty, $UnixStream:ty) => {
use std::io;
use std::path::Path;
use std::time::Duration;
use super::Stream;
pub(super) struct Listener {
inner: $UnixListener,
}
pub(super) fn listen(path: &Path) -> io::Result<Listener> {
<$UnixListener>::bind(path).map(|inner| Listener { inner })
}
pub(super) fn connect(path: &Path) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(<$UnixStream>::connect(path)?))
}
impl Listener {
pub(super) fn accept(&self) -> io::Result<Box<dyn Stream>> {
let (stream, _) = self.inner.accept()?;
Ok(Box::new(stream))
}
}
impl Stream for $UnixStream {
fn try_clone_box(&self) -> io::Result<Box<dyn Stream>> {
Ok(Box::new(self.try_clone()?))
}
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
<$UnixStream>::set_read_timeout(self, timeout)
}
}
};
}
#[cfg(unix)]
mod imp {
impl_uds_transport!(std::os::unix::net::UnixListener, std::os::unix::net::UnixStream);
}
#[cfg(windows)]
mod imp {
impl_uds_transport!(uds_windows::UnixListener, uds_windows::UnixStream);
}
🤖 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 `@mux/crates/mux-core/src/platform.rs` around lines 33 - 109, The unix and
windows transport imp modules are duplicating the same Listener, listen,
connect, accept, and Stream implementation logic, so consolidate the shared
behavior to avoid future drift. Extract the common transport code into a single
helper or macro-driven implementation and keep only the platform-specific
UnixListener/UnixStream imports inside the cfg blocks. Use the existing imp
module, Listener, listen, connect, accept, and Stream for UnixStream symbols to
preserve the public shape while removing byte-for-byte duplicated bodies.

Comment on lines +25 to +76
SOCK = None
CONTROL_SOCKET_RE = re.compile(r"control socket at (.+)$")
MARKER = f"reattach-marker-{os.getpid()}"


def fallback_socket_path():
base = os.environ.get("XDG_RUNTIME_DIR") or os.environ.get("TMPDIR") or "/tmp"
return os.path.join(base, f"cmux-mux-{os.getuid()}", f"{SESSION}.sock")


def wait_for_control_socket(server, seconds=15):
deadline = time.time() + seconds
output = []
assert server.stdout is not None
while time.time() < deadline:
if server.poll() is not None:
rest = server.stdout.read() or ""
if rest:
output.append(rest)
break
wait = min(0.1, max(0.0, deadline - time.time()))
readable, _, _ = select.select([server.stdout], [], [], wait)
if not readable:
continue
line = server.stdout.readline()
if not line:
continue
output.append(line)
match = CONTROL_SOCKET_RE.search(line.strip())
if match:
path = match.group(1)
socket_deadline = time.time() + 5
while time.time() < socket_deadline:
if os.path.exists(path):
return path
if server.poll() is not None:
break
time.sleep(0.05)
raise AssertionError(f"control socket line found but socket missing at {path}")

fallback = fallback_socket_path()
if os.path.exists(fallback):
print("control socket line not seen; using fallback", fallback)
return fallback
raise AssertionError(
"headless server socket missing; expected startup line or fallback at "
+ fallback
+ "; output:\n"
+ "".join(output)[-2000:]
)


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract duplicated socket-discovery helpers into a shared module.

fallback_socket_path() and wait_for_control_socket() are duplicated near-verbatim in mux/scripts/smoke-tui.py. A future fix to the regex, fallback ordering, or timeout handling would need to be applied in both places, risking drift.

♻️ Suggested consolidation

Move fallback_socket_path() and wait_for_control_socket() into a new mux/scripts/smoke_common.py and import it from both smoke-attach.py and smoke-tui.py.

Also applies to: 404-409

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 30-30: Do not hardcode temporary file or directory names
Context: "/tmp"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🪛 Ruff (0.15.20)

[error] 31-31: Probable insecure usage of temporary file or directory: "/tmp"

(S108)


[warning] 63-63: Avoid specifying long messages outside the exception class

(TRY003)

🤖 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 `@mux/scripts/smoke-attach.py` around lines 25 - 76, The socket-discovery logic
in fallback_socket_path() and wait_for_control_socket() is duplicated and should
be centralized to avoid drift. Move both helpers into a shared
mux/scripts/smoke_common.py module, then update smoke-attach.py and smoke-tui.py
to import and use the shared functions. Keep the existing behavior, including
CONTROL_SOCKET_RE handling, fallback path resolution, and timeout/exit checks,
so both scripts stay in sync.

lawrencecchen added a commit that referenced this pull request Jul 10, 2026
…ommitted mux/target (#7815)

* Remove accidentally committed mux/target build artifacts

f38b303 merged 959 files of
cargo build output under mux/target/. The mux/ dir was renamed to
cmux-tui/ in the rebrand (#7710), which moved mux/.gitignore away and
left old checkouts' untracked mux/target unprotected. Delete the
artifacts and ignore /mux/ so stale local dirs can't be committed.

* cmux-tui: fix ghostty-vt static lib name for windows-gnu

The windows experimental CI job has failed on every run since it was
added in #7346: rustc's *-windows-gnu targets search for
native static libs only as lib<name>.a, but zig installs the Windows
archive as ghostty-vt-static.lib, so linking died with 'could not find
native static library ghostty-vt-static'. Copy the archive to
libghostty-vt-static.a in the build script before emitting the link
directive. Also unbreaks the windows-gnu lane of
cmux-tui-build-package.yml, which links the same crate.

* cmux-tui: cfg-gate unix-only signal and stdin-probe paths

With the ghostty-vt-static link fixed, the windows-gnu build surfaced
the next layer: raw libc calls in the TUI binary. Follow the existing
host_colors.rs pattern: SIGTERM/SIGINT/SIGHUP handlers and the
poll(2)-based stdin reads behind the kitty-graphics and cell-size
probes are #[cfg(unix)], with windows stubs (no signal handlers,
probes report no response so callers fall back to defaults).

This branch was successfully deployed

1 active deployment
Preview – cmux — 71e1415e Deployed Jul 6, 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.

1 participant