Skip to content

Add mux: decoupled terminal-multiplexer backend with tmux-like TUI - #7180

Merged
lawrencecchen merged 31 commits into
mainfrom
feat-rust-tui-backend
Jul 6, 2026
Merged

lawrencecchen merged 31 commits into
mainfrom
feat-rust-tui-backend

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

What

New Rust workspace under mux/: a decoupled multiplexer backend plus a bundled tmux-like TUI (cmux-mux). The core owns workspaces → tabs → panes; each pane is a real PTY whose output feeds libghostty-vt, the terminal engine extracted from Ghostty and compiled from this repo's ghostty/ submodule. Frontends only read render snapshots and send encoded input, so the same session state renders in any terminal via Ratatui today and can be attached to real Ghostty surfaces in the cmux app.

Crates

  • ghostty-vt-sys: build.rs compiles libghostty-vt.a from ../ghostty with zig (-Demit-lib-vt=true, ReleaseFast) and generates bindings from include/ghostty/vt.h with bindgen.
  • ghostty-vt: safe wrapper. Terminal (vt parsing, modes, pty-write/title/bell callbacks, plain-text dump, vt_replay() full-state serialization via the formatter), RenderState (dirty-tracked viewport snapshots with per-cell text/style/color), KeyEncoder (legacy + kitty keyboard protocol, options synced from the terminal each keystroke).
  • mux-core: session model, PTY runtime (portable-pty, one reader thread per pane, coalesced dirty events), layout math shared by frontends (rects, separators, directional focus), and the JSON-lines control socket.
  • mux-tui: the cmux-mux binary. Prefix Ctrl-b keybindings (tabs, splits, focus, workspaces, scrollback), mouse focus + wheel scroll with alternate-screen arrow fallback, bracketed paste, --headless mode, and cmux-mux attach (below).

Attach protocol (how cmux adopts a pane)

The socket is full-duplex. subscribe streams mux events as JSON lines. attach-pane sends a vt-state event carrying a base64 VT replay of the pane's complete state (screen, styles, cursor, modes, palette, kitty keyboard state, charsets — produced by ghostty's VT formatter), then streams every subsequent pty byte as output events. The replay snapshot and the stream tap are taken under the same terminal lock, so an attaching frontend sees exactly the bytes applied after its snapshot — no gap, no duplication. A real Ghostty surface in the cmux app can adopt a pane by replaying vt-state and following the stream, since both sides run the same VT engine. Other commands: identify, list-workspaces (includes each tab's split-tree layout), send, read-screen, vt-state, new-tab, new-workspace, split, kill-pane, resize-pane, focus-pane, select-tab, select-workspace, scroll-pane. The socket path is exported to panes as CMUX_MUX_SOCKET.

Detach/reattach

cmux-mux --headless --session x runs the backend alone; cmux-mux attach --session x runs the TUI against it over the socket, mirroring each pane into a client-local ghostty terminal fed by the replay + stream. Rendering, key encoding, and mode queries work identically to local mode. Prefix-d detaches; the session keeps running and reattach restores the full screen state. The TUI is structured around a Session/PaneHandle abstraction (Local | Remote) with focus/tab/workspace mutations in mux-core, so the socket and the local TUI share one mutation path.

Testing

  • cargo test (23 tests): wrapper behavior (render snapshots, reflow, callbacks, wide chars, DECCKM-sensitive key encoding), layout/tree invariants incl. degenerate-area and empty-headless-session regressions, PTY integration (spawn → output event → screen, exit events, socket round trip), and attach-stream atomicity (replay + live stream with no duplication).
  • mux/scripts/smoke-tui.py: the real binary in a scripted PTY — keystrokes → shell → ghostty screen verified over the socket, prefix tab/split, socket-driven split while attached, clean quit.
  • mux/scripts/smoke-attach.py: headless server + attach client — marker typed, detach (server survives with state), reattach renders the marker from VT replay alone, live path still works.
  • Path-filtered CI workflow .github/workflows/mux.yml runs all of the above on the macOS 15 runner.

Limitations (follow-ups)

Pre-attach scrollback is not replayed (screen + state only; mirrors accumulate their own), no mouse-event forwarding to apps, kitty graphics tracked but not drawn by the TUI, split ratios fixed at 50%.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added cmux-mux terminal multiplexer backend with workspace/pane layout, tab support, and focus/split controls.
    • Introduced a full-screen TUI with sidebar navigation, mouse interactions (selection/scroll/context menus), and VT-backed rendering.
    • Added JSON-lines Unix-socket control API, including attach/reattach with VT replay and streamed output.
  • Tests
    • Added PTY/control-socket integration tests plus TUI and attach/reattach smoke tests.
  • Chores
    • Added CI workflow to validate mux/** and run the workspace test suite.

Note

High Risk
Very large greenfield addition spanning PTY I/O, session attach protocol, Ghostty FFI builds, and optional Chrome CDP; regressions would affect multiplexer correctness and future cmux integration rather than existing app paths alone.

Overview
Introduces a new mux/ Rust workspace: a headless multiplexer core plus the cmux-mux TUI, built on libghostty-vt compiled from the vendored ghostty submodule (ghostty-vt-sys / ghostty-vt).

mux-core owns workspaces → screens → split panes → tabs (PTY or browser via mux-cdp). PTY tabs use portable-pty and Ghostty’s VT engine for render snapshots, key encoding, vt_replay(), and host default-color probing. A JSON-lines Unix socket exposes tree control, subscribe events, and attach-surface (base64 VT state + streamed output) so cmux-mux attach and future cmux app surfaces can mirror live sessions. mux-tui adds Ratatui/crossterm UI (splits, workspaces, mouse, detach/reattach).

Also adds path-filtered .github/workflows/mux.yml (fmt, clippy, tests, PTY smoke scripts) and documents a ghostty fork delta for OSC 4/10/11/12 query replies on the lib-vt stream path.

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

New Rust workspace under mux/ implementing a multiplexer core that owns
workspaces -> tabs -> panes, where each pane is a PTY feeding
libghostty-vt (built from the ghostty submodule via zig + bindgen).
Frontends read render-state snapshots and send encoded input, so the
same session runs as a standalone Ratatui TUI today and can attach to
real Ghostty surfaces in the app later.

- ghostty-vt-sys: zig-built static libghostty-vt + bindgen bindings
- ghostty-vt: safe Terminal / RenderState / KeyEncoder wrapper
- mux-core: session model, PTY runtime, layout math, JSON control socket
- mux-tui (bin cmux-mux): crossterm/ratatui frontend, tmux-style prefix
  keys, mouse focus/scroll, kitty-aware key encoding, --headless mode
- tests: wrapper unit tests, PTY + socket integration tests, and a
  scripted-pty smoke test driving the real binary end to end
- .github/workflows/mux.yml: path-filtered macOS CI job
@vercel

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

@coderabbitai

coderabbitai Bot commented Jul 2, 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

This PR adds a new mux Rust workspace with Ghostty VT bindings, a mux core for layout/state/server/surfaces, a TUI frontend, JSON socket support for local and remote sessions, smoke tests, docs, and CI wiring.

Changes

Mux Workspace Implementation

Layer / File(s) Summary
Workspace scaffolding and docs
mux/Cargo.toml, mux/.gitignore, mux/rustfmt.toml, mux/README.md, .github/workflows/mux.yml
Defines the workspace manifest, formatting and ignore rules, architecture/protocol docs, and CI workflow with fmt, clippy, tests, and smoke checks.
ghostty-vt FFI bindings
mux/crates/ghostty-vt-sys/Cargo.toml, mux/crates/ghostty-vt-sys/build.rs, mux/crates/ghostty-vt-sys/src/lib.rs
Builds libghostty-vt via Zig, generates bindgen bindings, and adds the raw FFI crate entrypoint.
ghostty-vt safe wrapper crate
mux/crates/ghostty-vt/Cargo.toml, mux/crates/ghostty-vt/src/lib.rs, mux/crates/ghostty-vt/src/terminal.rs, mux/crates/ghostty-vt/src/render.rs, mux/crates/ghostty-vt/src/key.rs, mux/crates/ghostty-vt/tests/terminal.rs
Adds safe terminal, render, and key encoding wrappers plus unit tests around rendering, callbacks, selection, scrollback, and encoding.
mux-core layout and state
mux/crates/mux-core/Cargo.toml, mux/crates/mux-core/src/lib.rs, mux/crates/mux-core/src/layout.rs, mux/crates/mux-core/src/model.rs
Defines split-tree layout math and the in-memory workspace, pane, and surface state model.
mux-core orchestration and server
mux/crates/mux-core/src/mux.rs, mux/crates/mux-core/src/surface.rs, mux/crates/mux-core/src/server.rs, mux/crates/mux-core/tests/pty.rs
Implements mux orchestration, PTY-backed surfaces, the JSON-lines control socket, and integration tests for lifecycle, RPC, and attach streaming.
mux-tui frontend application
mux/crates/mux-tui/Cargo.toml, mux/crates/mux-tui/src/main.rs, mux/crates/mux-tui/src/app.rs, mux/crates/mux-tui/src/keys.rs, mux/crates/mux-tui/src/session/*, mux/crates/mux-tui/src/ui/*
Implements the CLI entrypoint, local/remote session abstraction, event loop, input handling, and rendering for sidebar, panes, overlays, and status bar.
Smoke test scripts
mux/scripts/smoke-tui.py, mux/scripts/smoke-attach.py
Adds end-to-end Python smoke tests for TUI interaction and attach/detach behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error close_pane/close_workspace call remove_surface per tab, and remove_surface rescans panes/workspaces each time, making batch closes quadratic+. Track pane/workspace ids during removal or batch-remove in one pass with indexed lookups so closes don’t rescan state per surface.
Cmux User-Facing Error Privacy ❌ Error The new user-facing USAGE/usage_exit copy exposes libghostty-vt, an upstream component name, in command output. Rewrite help/recovery text to use generic product terms like “terminal engine” and avoid naming internal/upstream libraries.
✅ Passed checks (23 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 The diff only touches Rust, Python, and docs; no Swift files or SwiftUI/actor-isolation changes are present.
Cmux Swift Blocking Runtime ✅ Passed No Swift files were changed in this PR, so the Swift blocking-runtime rule is not applicable.
Cmux Browser Automation Off-Main ✅ Passed PR changes are confined to the new mux workspace; no browser-automation commands or target files are modified.
Cmux Expensive Synchronous Load ✅ Passed The diff only touches Rust and Python files under mux/; no Swift main-actor or interactive load paths were added or moved.
Cmux Cache Substitution Correctness ✅ Passed No Swift/TypeScript/JavaScript files changed, so the cache-substitution rule is not applicable to this Rust/Python-only PR.
Cmux No Hacky Sleeps ✅ Passed Only waits added are bounded polling/sleeps in the two smoke-test scripts; runtime crates add no ad hoc timing, and YAML CI waits are out of scope.
Cmux Swift Concurrency ✅ Passed The diff only touches Rust/Python/CI files; no Swift files were changed, so the Swift concurrency rule isn’t implicated.
Cmux Swift @Concurrent ✅ Passed The PR diff contains no Swift files or @concurrent-related changes, so the Swift concurrency rule is not applicable.
Cmux Swift File And Package Boundaries ✅ Passed No Swift files are changed in this PR; the diff is Rust/Python under mux/, so the Swift package-boundary rule is not applicable.
Cmux Swiftpm Lockfiles ✅ Passed PR changes only Rust files; no Package.swift, Package.resolved, or xcodeproj diffs. mux/.gitignore ignores target/ only, so the SwiftPM policy isn’t violated.
Cmux Swift Logging ✅ Passed No Swift app/runtime files were changed; the PR only touches Rust/Python under mux/, so the Swift logging rule is not triggered.
Cmux Full Internationalization ✅ Passed Diff only adds Rust workspace code, tests, scripts, and mux docs; no Swift, xcstrings, web/i18n, or locale-message files were touched.
Cmux Swiftui State Layout ✅ Passed No SwiftUI/AppKit files are in the PR diff; it only touches Rust, Python, and docs under mux/.
Cmux Architecture Rethink ✅ Passed No Swift files changed; the Rust workspace uses required locks/channels/threads as core ownership, not a symptom patch.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No Swift files or window-controller changes are in the diff; the auxiliary-window-close-shortcuts rule isn’t triggered.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/docs/tests/config; no logs, caches, build outputs, temp dirs, or other artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No Swift files or Sources/ paths were changed, so the production test/debug-seam rule is not applicable here.
Cmux No Ambient Global State ✅ Passed The PR only bumps the ghostty submodule and changes Zig/C files; no Swift sources were added or modified, so the Swift global-state rule isn’t triggered.
Title check ✅ Passed The title clearly summarizes the main change: adding a decoupled mux backend with a tmux-like TUI.
Description check ✅ Passed It covers the summary and testing well, but the demo video, review trigger block, and checklist from the template are missing.
✨ 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-rust-tui-backend

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-tui/src/main.rs
Comment thread mux/crates/mux-core/src/lib.rs Outdated
Comment thread mux/crates/mux-core/src/lib.rs Outdated
Comment thread mux/crates/mux-core/src/layout.rs Outdated
@socket-security

socket-security Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

@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: 9920f6549e

ℹ️ 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 thread mux/crates/mux-core/src/layout.rs Outdated
b,
Rect {
x: area.x + a_w + 1,
width: usable - a_w,

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 Guard split layout below divider size

When a split subtree is laid out with area.width <= 1 (for example after repeated vertical splits or shrinking the terminal), usable is 0, a_w is clamped back to 1, and this subtraction underflows. Debug builds panic during layout_tab, while release builds wrap the second pane to a huge width; the same pattern exists for vertical splits with height <= 1. Bail out or allocate zero-size children without subtracting past zero.

Useful? React with 👍 / 👎.

Comment thread mux/crates/mux-core/src/lib.rs Outdated
.ok_or_else(|| anyhow::anyhow!("unknown workspace {id}"))?,
None => state.active_workspace,
};
let ws = &mut state.workspaces[ws_idx];

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 Seed or reject new-tab before indexing workspaces

--headless goes through run_headless instead of app::run, so no initial workspace is created. If the first socket command is new-tab without an explicit workspace, ws_idx is state.active_workspace (0) and this index panics on the empty workspaces vector, poisoning the mux state for the headless backend the README says the socket should drive.

Useful? React with 👍 / 👎.

Comment thread mux/crates/mux-core/src/lib.rs Outdated
workspace: Option<WorkspaceId>,
cwd: Option<String>,
) -> anyhow::Result<Arc<Pane>> {
let pane = self.spawn_pane(cwd)?;

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 Validate workspace IDs before spawning panes

For socket new-tab requests that pass a stale/nonexistent workspace, this spawns a PTY and inserts it into state.panes before the workspace lookup can return unknown workspace; the error path then exits without removing or killing that pane, leaving an orphan shell that is not in any tab and cannot be discovered from list-workspaces. Validate the workspace first or clean up the spawned pane on failure.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces mux, a decoupled terminal-multiplexer backend with a tmux-like TUI, built in Rust with a VT engine extracted from Ghostty (libghostty-vt), a JSON-lines Unix domain socket control protocol, and a Ratatui/crossterm frontend.

  • Core architecture: Workspaces → screens → split panes → PTY surfaces, with an attach protocol that snapshots VT state and registers a tap under the same terminal lock to guarantee no-gap/no-duplicate replay, plus a Session enum abstracting local vs. remote pane mutations over the control socket.
  • New findings: A pending-sender leak in RemoteSession::request() (inserted before write, never removed on early-return or timeout) and a brief world-accessible window between UnixListener::bind and set_permissions(0o600) in the socket server.

Confidence Score: 3/5

Needs fixes before merge: multiple P1s flagged in prior threads (orphaned pane, dropped cwd, headless park, path traversal) plus two new P2s in this pass.

Four pre-existing P1-level issues (dropped cwd, orphaned pane, indefinite park with no cleanup, path traversal via socket path) cap the score below 4; the two new P2 findings (pending-sender leak, bind→chmod race) add further surface area. Core locking and attach atomicity are correct, which prevents a lower score.

mux/crates/mux-core/src/mux.rs (orphaned pane + dropped cwd), mux/crates/mux-tui/src/main.rs (headless park), mux/crates/mux-core/src/server.rs (path traversal + chmod race), mux/crates/mux-tui/src/session/remote.rs (pending sender leak)

Important Files Changed

Filename Overview
mux/crates/ghostty-vt-sys/build.rs Compiles libghostty-vt via zig with ReleaseFast and bindgen; always uses ReleaseFast regardless of Cargo profile (intentional for hot VT parsing).
mux/crates/mux-core/src/surface.rs PtySurface with correct term→taps lock ordering across reader thread, attach_stream(), and resize(); attach atomicity invariant confirmed.
mux/crates/mux-core/src/mux.rs Core Mux state machine; new_tab drops cwd on empty workspace (pre-existing flag) and has orphaned-pane risk on workspace lookup failure (pre-existing flag).
mux/crates/mux-core/src/server.rs JSON-lines Unix socket server; bind→chmod race creates brief world-accessible window; path traversal via session name in socket path (pre-existing flag).
mux/crates/mux-tui/src/main.rs TUI entry point; run_headless parks indefinitely on channel disconnect without cleanup (pre-existing flag).
mux/crates/mux-tui/src/session/remote.rs RemoteSession over control socket; request() leaks pending senders on write error or recv_timeout expiry.
mux/scripts/smoke-tui.py 649-line scripted PTY smoke test; uses select-based polling drain(); timing-based but acceptable as test scaffolding.
.github/workflows/mux.yml CI workflow for the mux crate; path-filtered, runs fmt/clippy/test/smoke scripts; rustup fallback via curl

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Server as mux-core/server
    participant Surface as PtySurface
    participant PTY as PTY child

    Client->>Server: "attach-surface {id}"
    Server->>Surface: attach_stream()
    Note over Surface: acquire term lock
    Surface->>Surface: snapshot VT state (base64 replay)
    Surface->>Surface: register tap Sender under same lock
    Note over Surface: release term lock
    Surface-->>Server: AttachFrame::Snapshot(bytes)
    Server-->>Client: "{type:vt-state, data:...}"

    loop PTY reader thread
        PTY-->>Surface: raw bytes
        Note over Surface: acquire term lock
        Surface->>Surface: feed bytes to Terminal
        Surface->>Surface: broadcast to all taps (term still held)
        Note over Surface: release term lock
        Surface-->>Server: AttachFrame::Output(bytes)
        Server-->>Client: "{type:output, data:...}"
    end

    Client->>Server: "send {id, data}"
    Server->>Surface: write to PTY writer
    Surface-->>PTY: bytes
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"}}}%%
sequenceDiagram
    participant Client
    participant Server as mux-core/server
    participant Surface as PtySurface
    participant PTY as PTY child

    Client->>Server: "attach-surface {id}"
    Server->>Surface: attach_stream()
    Note over Surface: acquire term lock
    Surface->>Surface: snapshot VT state (base64 replay)
    Surface->>Surface: register tap Sender under same lock
    Note over Surface: release term lock
    Surface-->>Server: AttachFrame::Snapshot(bytes)
    Server-->>Client: "{type:vt-state, data:...}"

    loop PTY reader thread
        PTY-->>Surface: raw bytes
        Note over Surface: acquire term lock
        Surface->>Surface: feed bytes to Terminal
        Surface->>Surface: broadcast to all taps (term still held)
        Note over Surface: release term lock
        Surface-->>Server: AttachFrame::Output(bytes)
        Server-->>Client: "{type:output, data:...}"
    end

    Client->>Server: "send {id, data}"
    Server->>Surface: write to PTY writer
    Surface-->>PTY: bytes
Loading

Reviews (24): Last reviewed commit: "Merge main into feat-rust-tui-backend" | Re-trigger Greptile

Comment thread mux/crates/mux-core/src/lib.rs Outdated
Comment on lines +219 to +246
let tab_id = self.next_id();
{
let mut state = self.state.lock().unwrap();
let ws_idx = match workspace {
Some(id) => state
.workspaces
.iter()
.position(|w| w.id == id)
.ok_or_else(|| anyhow::anyhow!("unknown workspace {id}"))?,
None => state.active_workspace,
};
let ws = &mut state.workspaces[ws_idx];
ws.tabs.push(Tab {
id: tab_id,
root: Node::Leaf(pane.id),
active_pane: pane.id,
});
ws.active_tab = ws.tabs.len() - 1;
}
self.emit(MuxEvent::TreeChanged);
Ok(pane)
}

/// Split the tab containing `target`, putting a new pane after it.
pub fn split(self: &Arc<Self>, target: PaneId, dir: SplitDir) -> anyhow::Result<Arc<Pane>> {
let cwd = self.pane(target).and_then(|p| p.pwd());
let pane = self.spawn_pane(cwd)?;
let mut done = false;

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 Orphaned pane when new_tab fails workspace lookup

spawn_pane inserts the new pane into state.panes before the workspace-ID check runs. When the ? propagates "unknown workspace {id}", the lock drops but the pane remains in the map with no tab referencing it. A live PTY process runs, consuming file descriptors and memory, and cannot be reached by any frontend or socket command. It is only removed from the map when it naturally exits and the app processes the resulting PaneExited event. The sibling split function handles this failure pattern correctly (lines 264–270): it removes the pane from the map and calls pane.kill() before returning the error.

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

🤖 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 1-24: Add an explicit permissions block in the mux workflow so the
job only gets the minimum GITHUB_TOKEN scope needed, such as read-only contents
access, and update the checkout step to use a pinned commit SHA instead of the
floating actions/checkout@v4 reference. Make the change in the workflow job
definition under the test job, keeping the existing behavior intact while
tightening security.

In `@mux/crates/ghostty-vt/src/key.rs`:
- Around line 102-107: The UTF-8 pointer casts in the key event FFI calls assume
signed `c_char` by using literal `i8`, which is non-portable. Update the
`ghostty_key_event_set_utf8` call sites in `key.rs` (and the related buffer
path) to cast to the `c_char` type expected by the `ghostty-vt-sys` bindings
instead of hardcoding `*const i8`/`*mut i8`. Keep the change localized to the
`input.utf8`/`buf` pointer conversions so the ABI matches the generated FFI
types across targets.

In `@mux/crates/mux-core/src/layout.rs`:
- Around line 93-118: Handle zero- or one-cell split regions in the layout
splitter to avoid underflow. Update the split logic in walk for SplitDir::Right
and SplitDir::Down so that when area.width <= 1 or area.height <= 1, it
short-circuits instead of computing usable - a_w or usable - a_h. Use the
existing walk, Separator, and layout_tab flow as the entry points, and make sure
the child Rects remain valid for tiny terminal sizes.

In `@mux/crates/mux-core/src/lib.rs`:
- Around line 218-230: The new_tab flow in mux-core::Mux::new_tab spawns a pane
before validating the target workspace, so failed attachment can leak a running
pane in state.panes and an empty active workspace can still panic on indexing.
Update the attach path to validate the workspace first or mirror the rollback
behavior used by split: if workspace lookup fails or state.workspaces is empty,
destroy/remove the spawned pane before returning the error, and ensure the ws
selection via state.active_workspace is guarded against an empty workspaces
list.

In `@mux/crates/mux-core/src/pane.rs`:
- Around line 79-83: The pane command handling in Pane::new should reject an
explicitly empty PaneOptions.command instead of indexing argv[0] and panicking.
Add a validation check around the command extraction logic so that when
opts.command is Some(vec![]) you return an anyhow::Error through the existing
Result path, while keeping the current fallback to SHELL for None and the
CommandBuilder::new/args setup for non-empty argv.

In `@mux/crates/mux-core/src/server.rs`:
- Around line 27-30: The socket path handling in default_socket_path and serve
is too permissive because session can become an arbitrary path component and
later be chmod’d or unlinked outside the cmux-owned directory. Sanitize or
reject session values so they can only produce a safe filename under the cmux
temp dir, and in serve only chmod the directory you created for this socket plus
only remove an existing path if it is a Unix socket owned by cmux. Ensure any
path validation is applied before calling default_socket_path, chmod, or
remove_file, using the existing default_socket_path and serve flow as the entry
points.
- Around line 234-236: The `Command::KillPane` branch in `server.rs` currently
always returns success after calling `mux.close_pane(pane)`, so update it to
detect when the pane is missing and return the same `unknown pane` error
behavior used by `send`, `read_screen`, and `resize_pane`. Use the existing pane
lookup/close path in `mux.close_pane` (or a pre-check in the `KillPane` match
arm) so that `kill-pane` only returns `Ok(json!({}))` when a pane was actually
found and closed, and otherwise propagates the unknown-pane error consistently.

In `@mux/crates/mux-core/tests/pty.rs`:
- Line 123: The test is cleaning up the socket while the pane’s child process
may still be alive because the server thread retains an Arc<Mux>. Update the
test around the existing cleanup call to explicitly close/kill the test pane
first, then call mux_core::server::cleanup(&sock_path); use the pane/session
handles in this test to ensure the “sleep 30” child is terminated before socket
removal.

In `@mux/crates/mux-tui/src/app.rs`:
- Around line 75-88: Terminal setup can fail after enable_raw_mode() and leave
the shell in raw/alternate-screen mode because restore_terminal() is only
guaranteed later. Update the initialization path in app.rs so any failure from
EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, or
RatatuiTerminal::new triggers cleanup before returning, using restore_terminal()
or an equivalent early-rollback path. Keep the panic hook behavior, but also
ensure the pre-hook setup steps are wrapped so they cannot exit without undoing
the terminal state.

In `@mux/crates/mux-tui/src/ui.rs`:
- Around line 149-167: The render path in `RenderState::walk_rows` is ignoring
the per-row dirty flag and repainting every row on every draw. Update the
`rs.walk_rows` callback in `ui.rs` to use the dirty state so clean rows are
skipped after they’ve been drawn, while still handling resize/blanking behavior
for rows that need it. Keep the logic localized around `walk_rows`,
`apply_cell`, and the blank-fill loop so only dirty rows incur redraw work.

In `@mux/scripts/smoke-tui.py`:
- Around line 41-42: The socket readiness check in the smoke test is flaky
because `drain(2.0)` uses a fixed delay before a single `os.path.exists(SOCK)`
assertion. Update the wait logic in `smoke-tui.py` to poll for the socket’s
existence with a timeout instead of sleeping once, so it can tolerate slower CI
runners. Keep the check tied to the existing `SOCK` path and the socket startup
flow around `cmux-mux`, and fail only after the polling timeout expires.
- Around line 7-17: The rpc() helper in smoke-tui.py can block forever on socket
operations, causing CI hangs when cmux-mux stops responding. Add a timeout on
the Unix socket before connect/recv so s.recv() fails instead of waiting
indefinitely, and make sure rpc() surfaces that timeout cleanly while preserving
its current request/response flow.
🪄 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: 8ab38009-9c76-4cad-9afa-39d01d0077cd

📥 Commits

Reviewing files that changed from the base of the PR and between 70de366 and 9920f65.

⛔ Files ignored due to path filters (1)
  • mux/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .github/workflows/mux.yml
  • mux/.gitignore
  • mux/Cargo.toml
  • mux/README.md
  • mux/crates/ghostty-vt-sys/Cargo.toml
  • mux/crates/ghostty-vt-sys/build.rs
  • mux/crates/ghostty-vt-sys/src/lib.rs
  • mux/crates/ghostty-vt/Cargo.toml
  • mux/crates/ghostty-vt/src/key.rs
  • mux/crates/ghostty-vt/src/lib.rs
  • mux/crates/ghostty-vt/src/render.rs
  • mux/crates/ghostty-vt/src/terminal.rs
  • mux/crates/ghostty-vt/tests/terminal.rs
  • mux/crates/mux-core/Cargo.toml
  • mux/crates/mux-core/src/layout.rs
  • mux/crates/mux-core/src/lib.rs
  • mux/crates/mux-core/src/pane.rs
  • mux/crates/mux-core/src/server.rs
  • mux/crates/mux-core/tests/pty.rs
  • mux/crates/mux-tui/Cargo.toml
  • mux/crates/mux-tui/src/app.rs
  • mux/crates/mux-tui/src/keys.rs
  • mux/crates/mux-tui/src/main.rs
  • mux/crates/mux-tui/src/ui.rs
  • mux/scripts/smoke-tui.py

Comment thread .github/workflows/mux.yml
Comment on lines +1 to +24
name: mux

on:
pull_request:
paths:
- "mux/**"
- "ghostty"
- ".github/workflows/mux.yml"
push:
branches: [main]
paths:
- "mux/**"
- "ghostty"
- ".github/workflows/mux.yml"

concurrency:
group: mux-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
timeout-minutes: 40
steps:

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

Add an explicit permissions block and pin actions/checkout.

Static analysis flags default (broad) GITHUB_TOKEN permissions and an unpinned actions/checkout@v4 reference. Recommend scoping permissions to the minimum needed (e.g. contents: read) and pinning the action to a commit SHA per repo policy.

🔒 Proposed fix
 name: mux
 
+permissions:
+  contents: read
+
 on:
   pull_request:
     paths:
       - "mux/**"
       - "ghostty"
       - ".github/workflows/mux.yml"
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<pinned-sha>  # actions/checkout@v4
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 1-50: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[info] 21-21: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)

🤖 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 1 - 24, Add an explicit permissions
block in the mux workflow so the job only gets the minimum GITHUB_TOKEN scope
needed, such as read-only contents access, and update the checkout step to use a
pinned commit SHA instead of the floating actions/checkout@v4 reference. Make
the change in the workflow job definition under the test job, keeping the
existing behavior intact while tightening security.

Source: Linters/SAST tools

Comment thread mux/crates/ghostty-vt/src/key.rs
Comment thread mux/crates/mux-core/src/layout.rs Outdated
Comment thread mux/crates/mux-core/src/lib.rs Outdated
Comment thread mux/crates/mux-core/src/pane.rs Outdated
Comment on lines +79 to +83
let argv = opts.command.clone().unwrap_or_else(|| {
vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into())]
});
let mut cmd = CommandBuilder::new(&argv[0]);
cmd.args(&argv[1..]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject an empty pane command instead of indexing it.

PaneOptions.command = Some(vec![]) makes Line 82 panic on argv[0], bypassing the anyhow::Result error path.

Proposed fix
         let argv = opts.command.clone().unwrap_or_else(|| {
             vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into())]
         });
-        let mut cmd = CommandBuilder::new(&argv[0]);
+        let Some(program) = argv.first() else {
+            anyhow::bail!("pane command cannot be empty");
+        };
+        let mut cmd = CommandBuilder::new(program);
         cmd.args(&argv[1..]);
📝 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
let argv = opts.command.clone().unwrap_or_else(|| {
vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into())]
});
let mut cmd = CommandBuilder::new(&argv[0]);
cmd.args(&argv[1..]);
let argv = opts.command.clone().unwrap_or_else(|| {
vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into())]
});
let Some(program) = argv.first() else {
anyhow::bail!("pane command cannot be empty");
};
let mut cmd = CommandBuilder::new(program);
cmd.args(&argv[1..]);
🤖 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/pane.rs` around lines 79 - 83, The pane command
handling in Pane::new should reject an explicitly empty PaneOptions.command
instead of indexing argv[0] and panicking. Add a validation check around the
command extraction logic so that when opts.command is Some(vec![]) you return an
anyhow::Error through the existing Result path, while keeping the current
fallback to SHELL for None and the CommandBuilder::new/args setup for non-empty
argv.

Comment thread mux/crates/mux-core/tests/pty.rs
Comment thread mux/crates/mux-tui/src/app.rs Outdated
Comment thread mux/crates/mux-tui/src/ui.rs Outdated
Comment on lines +149 to +167
rs.walk_rows(|row, _dirty, cells| {
if row >= max_rows {
return;
}
let y = rect.y + row as u16;
for (col, cell) in cells.iter().enumerate() {
if col >= max_cols {
break;
}
let x = rect.x + col as u16;
let target = &mut buf[(x, y)];
apply_cell(target, cell);
}
// Pane narrower than the rect (during resize races): blank the rest.
for col in cells.len()..max_cols {
let x = rect.x + col as u16;
buf[(x, y)].set_symbol(" ").set_style(Style::default());
}
})

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

Unused per-row dirty flag in the render hot path.

rs.walk_rows(|row, _dirty, cells| {...}) discards the dirty flag and rewrites every cell every draw. Since RenderState already tracks per-row dirtiness (this API exists for exactly this purpose), skipping untouched rows here would reduce CPU work in the redraw path, which matters for busy panes with frequent pty output.

♻️ Sketch: skip clean rows once already drawn once
-    rs.walk_rows(|row, _dirty, cells| {
+    rs.walk_rows(|row, dirty, cells| {
         if row >= max_rows {
             return;
         }
+        if !dirty && !first_draw_for_pane {
+            return;
+        }
         let y = rect.y + row as u16;
         ...
🤖 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-tui/src/ui.rs` around lines 149 - 167, The render path in
`RenderState::walk_rows` is ignoring the per-row dirty flag and repainting every
row on every draw. Update the `rs.walk_rows` callback in `ui.rs` to use the
dirty state so clean rows are skipped after they’ve been drawn, while still
handling resize/blanking behavior for rows that need it. Keep the logic
localized around `walk_rows`, `apply_cell`, and the blank-fill loop so only
dirty rows incur redraw work.

Comment thread mux/scripts/smoke-tui.py
Comment thread mux/scripts/smoke-tui.py Outdated

@cubic-dev-ai cubic-dev-ai 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.

14 issues found across 26 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mux/crates/mux-core/src/server.rs">

<violation number="1" location="mux/crates/mux-core/src/server.rs:30">
P1: Default socket path can escape the intended per-user temp directory when session names contain path separators. Sanitizing `session` before joining would keep control-socket files confined to `$TMPDIR/cmux-mux-<uid>` and avoid unintended file operations.</violation>

<violation number="2" location="mux/crates/mux-core/src/server.rs:110">
P2: The stale-socket cleanup currently deletes the path on every connect error, which can remove non-socket files or mask real access errors. Restrict deletion to the expected stale-socket error and return other errors unchanged.</violation>
</file>

<file name="mux/crates/mux-tui/src/app.rs">

<violation number="1" location="mux/crates/mux-tui/src/app.rs:88">
P2: A startup initialization error can leave the user's terminal stuck in raw mode/alternate screen. The fallible setup after `enable_raw_mode()` should run under a cleanup guard or explicitly call `restore_terminal()` on every early-return path before the main event loop starts.</violation>
</file>

<file name="mux/crates/mux-core/tests/pty.rs">

<violation number="1" location="mux/crates/mux-core/tests/pty.rs:93">
P2: The polling loop in `control_socket_round_trip` calls `reader.read_line(&mut line).unwrap()` which blocks indefinitely on a `UnixStream` with no timeout set. The deadline assertion after the read is unreachable if the server doesn't respond — the test hangs instead of failing fast. Set a read timeout on the stream via `stream.set_read_timeout(Some(Duration::from_secs(1)))` so that `read_line` returns an error under timeout, or restructure the loop to use a non-blocking read pattern similar to the `wait_for` helper used in the other tests.</violation>
</file>

<file name="mux/crates/mux-core/src/lib.rs">

<violation number="1" location="mux/crates/mux-core/src/lib.rs:218">
P1: A failed `new-tab` request can leave an orphan PTY running, and `new-tab` before any workspace can panic the backend. `new_tab` creates/inserts the pane before validating the target workspace/active workspace; validating first or cleaning up on every error path would keep the tree and pane map consistent.</violation>
</file>

<file name="mux/crates/mux-core/src/pane.rs">

<violation number="1" location="mux/crates/mux-core/src/pane.rs:79">
P2: Creating a pane with an empty `PaneOptions::command` panics instead of returning a spawn error. Since `PaneOptions` is public and cloned into `Mux::new`, validating the argv before indexing keeps bad client configuration from crashing the mux process.</violation>
</file>

<file name="mux/crates/mux-tui/Cargo.toml">

<violation number="1" location="mux/crates/mux-tui/Cargo.toml:19">
P3: `unicode-width` is declared as a dependency but never imported or used anywhere in `mux-tui` source files (src/). Remove it to keep dependencies clean.</violation>
</file>

<file name="mux/crates/mux-tui/src/main.rs">

<violation number="1" location="mux/crates/mux-tui/src/main.rs:80">
P2: Headless sessions start with no workspace or pane, so the socket API can return an empty session and `new-tab` without an explicit workspace can panic inside `Mux::new_tab`. Initializing the first workspace before entering `run_headless` (or making `new_tab` create/validate the workspace) would make headless behave like the TUI startup path.</violation>
</file>

<file name="mux/scripts/smoke-tui.py">

<violation number="1" location="mux/scripts/smoke-tui.py:100">
P2: On timeout, only the specific PID is killed, not the process group. If cmux-mux has spawned child processes (shells in panes via PTY), they survive as orphans. Use `os.killpg` or negative-PID kill to terminate the whole process group, then escalate to SIGKILL for stragglers.</violation>
</file>

<file name="mux/crates/mux-core/src/layout.rs">

<violation number="1" location="mux/crates/mux-core/src/layout.rs:61">
P2: Directional focus can jump to panes that are only diagonally in that direction. The current `neighbor` filter checks center direction but never enforces the documented perpendicular span overlap, so focus selection can ignore same-row/same-column intent.</violation>

<violation number="2" location="mux/crates/mux-core/src/layout.rs:109">
P1: Tiny terminal sizes can panic layout or wrap pane dimensions when a split is present. `usable` can be 0, but `a_w`/`a_h` is clamped to 1 before subtracting it for the second pane; handle unsplittable width/height before creating the separator and child rects.</violation>
</file>

<file name="mux/crates/mux-tui/src/keys.rs">

<violation number="1" location="mux/crates/mux-tui/src/keys.rs:99">
P2: Apps using kitty keyboard/disambiguation modes can receive incorrect key identity for shifted punctuation because `%`, `"`, etc. are encoded as `GHOSTTY_KEY_UNIDENTIFIED` with the shifted character as `unshifted_codepoint`. Consider mapping common shifted punctuation back to their physical keys/unshifted codepoints before calling the Ghostty encoder.</violation>
</file>

<file name="mux/crates/ghostty-vt/src/terminal.rs">

<violation number="1" location="mux/crates/ghostty-vt/src/terminal.rs:183">
P2: Repeated title reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_TITLE is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</violation>

<violation number="2" location="mux/crates/ghostty-vt/src/terminal.rs:193">
P2: Repeated working-directory reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_PWD is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

pub fn default_socket_path(session: &str) -> PathBuf {
let uid = unsafe { libc::getuid() };
let dir = std::env::temp_dir().join(format!("cmux-mux-{uid}"));
dir.join(format!("{session}.sock"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Default socket path can escape the intended per-user temp directory when session names contain path separators. Sanitizing session before joining would keep control-socket files confined to $TMPDIR/cmux-mux-<uid> and avoid unintended file operations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-core/src/server.rs, line 30:

<comment>Default socket path can escape the intended per-user temp directory when session names contain path separators. Sanitizing `session` before joining would keep control-socket files confined to `$TMPDIR/cmux-mux-<uid>` and avoid unintended file operations.</comment>

<file context>
@@ -0,0 +1,249 @@
+pub fn default_socket_path(session: &str) -> PathBuf {
+    let uid = unsafe { libc::getuid() };
+    let dir = std::env::temp_dir().join(format!("cmux-mux-{uid}"));
+    dir.join(format!("{session}.sock"))
+}
+
</file context>

Comment thread mux/crates/mux-core/src/lib.rs Outdated
Comment thread mux/crates/mux-core/src/layout.rs Outdated
"session socket {} is already in use (another instance running?)",
path.display()
),
Err(_) => std::fs::remove_file(&path)?,

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: The stale-socket cleanup currently deletes the path on every connect error, which can remove non-socket files or mask real access errors. Restrict deletion to the expected stale-socket error and return other errors unchanged.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-core/src/server.rs, line 110:

<comment>The stale-socket cleanup currently deletes the path on every connect error, which can remove non-socket files or mask real access errors. Restrict deletion to the expected stale-socket error and return other errors unchanged.</comment>

<file context>
@@ -0,0 +1,249 @@
+                "session socket {} is already in use (another instance running?)",
+                path.display()
+            ),
+            Err(_) => std::fs::remove_file(&path)?,
+        }
+    }
</file context>

Comment thread mux/crates/mux-tui/src/app.rs Outdated
Comment thread mux/crates/mux-core/src/layout.rs Outdated

match event.code {
KeyCode::Char(c) => {
input.key = physical_key_for_char(c);

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: Apps using kitty keyboard/disambiguation modes can receive incorrect key identity for shifted punctuation because %, ", etc. are encoded as GHOSTTY_KEY_UNIDENTIFIED with the shifted character as unshifted_codepoint. Consider mapping common shifted punctuation back to their physical keys/unshifted codepoints before calling the Ghostty encoder.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-tui/src/keys.rs, line 99:

<comment>Apps using kitty keyboard/disambiguation modes can receive incorrect key identity for shifted punctuation because `%`, `"`, etc. are encoded as `GHOSTTY_KEY_UNIDENTIFIED` with the shifted character as `unshifted_codepoint`. Consider mapping common shifted punctuation back to their physical keys/unshifted codepoints before calling the Ghostty encoder.</comment>

<file context>
@@ -0,0 +1,134 @@
+
+    match event.code {
+        KeyCode::Char(c) => {
+            input.key = physical_key_for_char(c);
+            input.unshifted_codepoint = c.to_ascii_lowercase() as u32;
+            // The encoder derives Ctrl-modified bytes from key+mods; text
</file context>


/// Terminal title as set by OSC 0/2, if any.
pub fn title(&self) -> Option<String> {
let s: sys::GhosttyString = self.get(sys::GHOSTTY_TERMINAL_DATA_TITLE).ok()?;

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: Repeated title reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_TITLE is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/ghostty-vt/src/terminal.rs, line 183:

<comment>Repeated title reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_TITLE is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</comment>

<file context>
@@ -0,0 +1,280 @@
+
+    /// Terminal title as set by OSC 0/2, if any.
+    pub fn title(&self) -> Option<String> {
+        let s: sys::GhosttyString = self.get(sys::GHOSTTY_TERMINAL_DATA_TITLE).ok()?;
+        if s.len == 0 || s.ptr.is_null() {
+            return None;
</file context>


/// Working directory reported via OSC 7, if any.
pub fn pwd(&self) -> Option<String> {
let s: sys::GhosttyString = self.get(sys::GHOSTTY_TERMINAL_DATA_PWD).ok()?;

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: Repeated working-directory reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_PWD is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/ghostty-vt/src/terminal.rs, line 193:

<comment>Repeated working-directory reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_PWD is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</comment>

<file context>
@@ -0,0 +1,280 @@
+
+    /// Working directory reported via OSC 7, if any.
+    pub fn pwd(&self) -> Option<String> {
+        let s: sys::GhosttyString = self.get(sys::GHOSTTY_TERMINAL_DATA_PWD).ok()?;
+        if s.len == 0 || s.ptr.is_null() {
+            return None;
</file context>

ratatui.workspace = true
crossterm.workspace = true
anyhow.workspace = true
unicode-width.workspace = 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.

P3: unicode-width is declared as a dependency but never imported or used anywhere in mux-tui source files (src/). Remove it to keep dependencies clean.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-tui/Cargo.toml, line 19:

<comment>`unicode-width` is declared as a dependency but never imported or used anywhere in `mux-tui` source files (src/). Remove it to keep dependencies clean.</comment>

<file context>
@@ -0,0 +1,19 @@
+ratatui.workspace = true
+crossterm.workspace = true
+anyhow.workspace = true
+unicode-width.workspace = true
</file context>

Comment thread mux/crates/mux-core/src/layout.rs Outdated

@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 @.github/workflows/mux.yml:
- Line 25: The checkout step in this workflow still leaves the GITHUB_TOKEN in
git config by default, which is unnecessary for the later rustup, cargo, and
smoke steps. Update the existing actions/checkout usage in the mux workflow to
explicitly disable credential persistence by setting persist-credentials to
false on that checkout step.
🪄 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: 5ec2a7a6-82a1-4186-b273-4a6a70fc48d0

📥 Commits

Reviewing files that changed from the base of the PR and between 9920f65 and 3bd3d34.

📒 Files selected for processing (1)
  • .github/workflows/mux.yml

Comment thread .github/workflows/mux.yml
runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
timeout-minutes: 40
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 | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on checkout.

Prior pinning-to-SHA feedback is addressed, but the checkout step still defaults to persisting the GITHUB_TOKEN in git config, exposing it to every later step (rustup curl|sh install, cargo test, smoke script) even though this workflow doesn't need to push.

🔒 Proposed fix
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+        with:
+          persist-credentials: false
📝 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
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 25-25: 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 25, The checkout step in this workflow
still leaves the GITHUB_TOKEN in git config by default, which is unnecessary for
the later rustup, cargo, and smoke steps. Update the existing actions/checkout
usage in the mux workflow to explicitly disable credential persistence by
setting persist-credentials to false on that checkout step.

Source: Linters/SAST tools

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mux/crates/mux-core/src/server.rs">

<violation number="1" location="mux/crates/mux-core/src/server.rs:30">
P1: Default socket path can escape the intended per-user temp directory when session names contain path separators. Sanitizing `session` before joining would keep control-socket files confined to `$TMPDIR/cmux-mux-<uid>` and avoid unintended file operations.</violation>

<violation number="2" location="mux/crates/mux-core/src/server.rs:110">
P2: The stale-socket cleanup currently deletes the path on every connect error, which can remove non-socket files or mask real access errors. Restrict deletion to the expected stale-socket error and return other errors unchanged.</violation>
</file>

<file name="mux/scripts/smoke-tui.py">

<violation number="1" location="mux/scripts/smoke-tui.py:100">
P2: On timeout, only the specific PID is killed, not the process group. If cmux-mux has spawned child processes (shells in panes via PTY), they survive as orphans. Use `os.killpg` or negative-PID kill to terminate the whole process group, then escalate to SIGKILL for stragglers.</violation>
</file>

<file name="mux/crates/mux-core/tests/pty.rs">

<violation number="1" location="mux/crates/mux-core/tests/pty.rs:93">
P2: The polling loop in `control_socket_round_trip` calls `reader.read_line(&mut line).unwrap()` which blocks indefinitely on a `UnixStream` with no timeout set. The deadline assertion after the read is unreachable if the server doesn't respond — the test hangs instead of failing fast. Set a read timeout on the stream via `stream.set_read_timeout(Some(Duration::from_secs(1)))` so that `read_line` returns an error under timeout, or restructure the loop to use a non-blocking read pattern similar to the `wait_for` helper used in the other tests.</violation>
</file>

<file name="mux/crates/mux-tui/Cargo.toml">

<violation number="1" location="mux/crates/mux-tui/Cargo.toml:19">
P3: `unicode-width` is declared as a dependency but never imported or used anywhere in `mux-tui` source files (src/). Remove it to keep dependencies clean.</violation>
</file>

<file name="mux/crates/mux-tui/src/main.rs">

<violation number="1" location="mux/crates/mux-tui/src/main.rs:80">
P2: Headless sessions start with no workspace or pane, so the socket API can return an empty session and `new-tab` without an explicit workspace can panic inside `Mux::new_tab`. Initializing the first workspace before entering `run_headless` (or making `new_tab` create/validate the workspace) would make headless behave like the TUI startup path.</violation>
</file>

<file name="mux/crates/mux-core/src/layout.rs">

<violation number="1" location="mux/crates/mux-core/src/layout.rs:61">
P2: Directional focus can jump to panes that are only diagonally in that direction. The current `neighbor` filter checks center direction but never enforces the documented perpendicular span overlap, so focus selection can ignore same-row/same-column intent.</violation>
</file>

<file name="mux/crates/mux-tui/src/keys.rs">

<violation number="1" location="mux/crates/mux-tui/src/keys.rs:99">
P2: Apps using kitty keyboard/disambiguation modes can receive incorrect key identity for shifted punctuation because `%`, `"`, etc. are encoded as `GHOSTTY_KEY_UNIDENTIFIED` with the shifted character as `unshifted_codepoint`. Consider mapping common shifted punctuation back to their physical keys/unshifted codepoints before calling the Ghostty encoder.</violation>
</file>

<file name="mux/crates/ghostty-vt/src/terminal.rs">

<violation number="1" location="mux/crates/ghostty-vt/src/terminal.rs:183">
P2: Repeated title reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_TITLE is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</violation>

<violation number="2" location="mux/crates/ghostty-vt/src/terminal.rs:193">
P2: Repeated working-directory reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_PWD is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</violation>
</file>

<file name=".github/workflows/mux.yml">

<violation number="1" location=".github/workflows/mux.yml:25">
P2: This checkout step still keeps the workflow token available to later git commands by default. Since this job only needs repository reads, adding `persist-credentials: false` here would reduce credential exposure to subsequent install/test steps.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/workflows/mux.yml
runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
timeout-minutes: 40
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.

P2: This checkout step still keeps the workflow token available to later git commands by default. Since this job only needs repository reads, adding persist-credentials: false here would reduce credential exposure to subsequent install/test steps.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/mux.yml, line 25:

<comment>This checkout step still keeps the workflow token available to later git commands by default. Since this job only needs repository reads, adding `persist-credentials: false` here would reduce credential exposure to subsequent install/test steps.</comment>

<file context>
@@ -22,7 +22,7 @@ jobs:
     timeout-minutes: 40
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
 
       - name: Init ghostty submodule
</file context>
Suggested change
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- new-tab on an empty headless session creates a workspace instead of
  panicking on workspaces[0]; unknown workspace ids error before a pane
  is spawned (no orphan pane leak)
- layout: degenerate split areas (<3 cells) no longer underflow u16;
  the second side gets a zero-size rect
- headless mode reaps exited panes from the tree
- kill-pane on an unknown pane returns an error instead of ok
- guard empty command argv; c_char casts for non-macOS portability
- TUI restores the host terminal when setup fails partway
- smoke script: socket timeout + poll for socket instead of fixed sleep
- workflow: explicit read-only permissions block
pub fn default_socket_path(session: &str) -> PathBuf {
let uid = unsafe { libc::getuid() };
let dir = std::env::temp_dir().join(format!("cmux-mux-{uid}"));
dir.join(format!("{session}.sock"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Session name escapes socket directory

Medium Severity

The default control socket path joins the raw --session string into a path under $TMPDIR/cmux-mux-<uid>/. Session values containing .. or absolute-style segments resolve outside that directory, so serve may create directories and bind the socket elsewhere while still exporting CMUX_MUX_SOCKET to pane children.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9ea830e. Configure here.

@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: 9ea830e7b1

ℹ️ 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 path = path.unwrap_or_else(|| default_socket_path(&mux.session));
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid chmoding caller-supplied socket parents

When cmux-mux --socket /tmp/foo.sock or any socket path under an existing shared directory reaches serve, this unconditional set_permissions(dir, 0o700) chmods the parent directory itself; for /tmp that makes the system temp directory private to the current user and can break other processes. The 0700 hardening should only apply to the dedicated default cmux-mux directory, or to a directory this call just created, not arbitrary parents supplied via --socket.

Useful? React with 👍 / 👎.

Comment thread mux/crates/mux-core/src/lib.rs Outdated
_ => Ok(()),
})?;
if self.with_tree(|workspaces, _| workspaces.is_empty()) {
return self.new_workspace(None);

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 cwd when seeding the first headless tab

When a headless session has no workspaces and a client sends new-tab with cwd, this branch creates the initial workspace via new_workspace(None) and drops the requested cwd; Pane::spawn then falls back to HOME, so the first tab does not start in the directory the socket client requested. Pass the cwd into the initial pane creation path or add a workspace helper that accepts it.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mux/crates/mux-core/src/lib.rs">

<violation number="1" location="mux/crates/mux-core/src/lib.rs:228">
P2: Creating the first tab in an empty session now ignores the requested working directory, so `new-tab` can start in the wrong directory in headless startup flows. The early return calls `new_workspace(None)` (which spawns with `None` cwd) instead of creating the initial workspace/tab with the provided `cwd`.</violation>
</file>

<file name="mux/crates/mux-core/src/layout.rs">

<violation number="1" location="mux/crates/mux-core/src/layout.rs:102">
P2: Focus navigation can jump to panes that are not renderable after a small resize because zero-size panes are still kept in `layout.panes` and considered by `neighbor`. It would be safer to exclude empty rects from neighbor selection (or otherwise mark collapsed panes non-focusable) so movement only lands on visible panes.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread mux/crates/mux-core/src/lib.rs Outdated
_ => Ok(()),
})?;
if self.with_tree(|workspaces, _| workspaces.is_empty()) {
return self.new_workspace(None);

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: Creating the first tab in an empty session now ignores the requested working directory, so new-tab can start in the wrong directory in headless startup flows. The early return calls new_workspace(None) (which spawns with None cwd) instead of creating the initial workspace/tab with the provided cwd.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-core/src/lib.rs, line 228:

<comment>Creating the first tab in an empty session now ignores the requested working directory, so `new-tab` can start in the wrong directory in headless startup flows. The early return calls `new_workspace(None)` (which spawns with `None` cwd) instead of creating the initial workspace/tab with the provided `cwd`.</comment>

<file context>
@@ -209,31 +209,53 @@ impl Mux {
+            _ => Ok(()),
+        })?;
+        if self.with_tree(|workspaces, _| workspaces.is_empty()) {
+            return self.new_workspace(None);
+        }
+
</file context>

Comment thread mux/crates/mux-core/src/layout.rs
Comment thread mux/crates/mux-core/src/lib.rs Outdated
Comment on lines +227 to +228
if self.with_tree(|workspaces, _| workspaces.is_empty()) {
return self.new_workspace(None);

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 cwd silently dropped on empty session in new_tab

When new_tab(workspace=None, cwd=Some("/path")) is called on a session with no workspaces yet, the early-return path delegates to new_workspace(None), which calls spawn_pane(None) with no cwd. The caller-supplied working directory is silently discarded. In headless mode the session starts empty, so the first new-tab socket command with a cwd field will always produce a pane starting in $HOME instead of the requested directory.

Comment on lines +99 to +106
let events = mux.subscribe();
loop {
match events.recv() {
Ok(mux_core::MuxEvent::PaneExited(id)) => mux.close_pane(id),
Ok(_) => {}
Err(_) => std::thread::park(),
}
}

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 run_headless parks on channel disconnect instead of returning

When events.recv() returns Err (channel disconnected), calling std::thread::park() causes the thread to block indefinitely on any spurious wakeup, loop back into recv, get Err again, and park again. In practice this path is unreachable today (the Mux is alive for the duration), but if reached for any reason, main never runs server::cleanup(&socket_path), leaving the socket file on disk. A break is the correct response to a disconnected channel.

Suggested change
let events = mux.subscribe();
loop {
match events.recv() {
Ok(mux_core::MuxEvent::PaneExited(id)) => mux.close_pane(id),
Ok(_) => {}
Err(_) => std::thread::park(),
}
}
let events = mux.subscribe();
loop {
match events.recv() {
Ok(mux_core::MuxEvent::PaneExited(id)) => mux.close_pane(id),
Ok(_) => {}
Err(_) => break,
}
}
Ok(())

The control socket is now full-duplex. 'subscribe' streams mux events
(tree-changed, pane-output, pane-exited, title-changed, bell) as JSON
lines interleaved with responses. 'attach-pane' sends a vt-state event
carrying a base64 VT replay of the pane's complete state (screen,
styles, cursor, modes, palette, kitty keyboard, charsets — via
ghostty's VT formatter) and then streams every subsequent pty byte as
output events. The replay snapshot and the stream tap are taken under
the same terminal lock, so an attaching frontend sees exactly the bytes
applied after its snapshot: no gap, no duplication. New commands:
vt-state, focus-pane, select-tab, select-workspace, scroll-pane;
list-workspaces now includes each tab's split-tree layout.

'cmux-mux attach --session <name>' runs the same TUI against a remote
session: panes are mirrored into client-local ghostty terminals fed by
vt-state + output streams, so rendering, key encoding, and mode queries
work identically to local mode. prefix-d detaches; the (headless)
session keeps running and reattach restores the full screen state.
The TUI is refactored onto a Session/PaneHandle abstraction (Local |
Remote) with focus/tab/workspace selection moved into mux-core so the
socket and the local TUI share one mutation path.

Tests: attach_stream atomicity (replay + stream, no duplication) and a
scripted-pty detach/reattach smoke (headless server survives detach,
reattach renders from replay, live path still works), wired into CI.
Comment thread mux/crates/mux-tui/src/session.rs Outdated
})
.collect(),
}),
Session::Remote(remote) => remote.tree().unwrap_or_default(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Attach tree cleared on RPC fail

Medium Severity

When an attach client refreshes the session tree, a failed list-workspaces RPC is turned into an empty TreeView via unwrap_or_default, even though RemoteSession still holds the last good tree and marks the snapshot stale for retry. sync_layout replaces app.tree every draw, so a timeout or error briefly wipes tabs, layout, and active-pane input until the next successful fetch.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6eab672. Configure here.

@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: 6eab672f37

ℹ️ 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 thread mux/crates/mux-core/src/server.rs Outdated
}
Command::ScrollPane { pane, delta } => {
let pane = mux.pane(pane).ok_or_else(|| anyhow::anyhow!("unknown pane {pane}"))?;
pane.with_terminal(|t| t.scroll_delta(delta));

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 Notify subscribers after scroll-pane

For subscribed socket frontends, scroll-pane against a quiet pane mutates the terminal viewport and returns ok without setting the pane dirty or emitting PaneOutput, so an attached UI relying on the socket event stream has no reason to redraw until unrelated PTY output arrives. Emit a pane-output/dirty notification after the scroll so the advertised socket command is visible immediately.

Useful? React with 👍 / 👎.

&self,
) -> ghostty_vt::Result<(u16, u16, Vec<u8>, std::sync::mpsc::Receiver<Vec<u8>>)> {
let mut term = self.term.lock().unwrap();
let (tx, rx) = std::sync::mpsc::channel();

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 Bound attach output queues

When an attach-pane client stops reading while the pane is producing output, the mux-attach-out thread can block in writer.send, but this std::sync::mpsc::channel() is unbounded and the PTY reader keeps enqueueing a fresh Vec for every chunk through the tap. A stalled frontend can therefore grow the mux process without limit; use a bounded queue or detach/drop policy for slow taps.

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: 4

Caution

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

⚠️ Outside diff range comments (1)
mux/crates/mux-core/src/pane.rs (1)

191-195: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close attach streams when the pane reader exits.

taps keeps each stream Sender forever unless a later output send fails. When the PTY reaches EOF, no later send happens, so attach-pane output threads can block on stream.recv() and never emit detached.

Proposed fix
-                    pane.dead.store(true, Ordering::Release);
+                    {
+                        // Synchronize with attach_stream's snapshot/register critical section.
+                        let _term = pane.term.lock().unwrap();
+                        pane.dead.store(true, Ordering::Release);
+                        pane.taps.lock().unwrap().clear();
+                    }
                     if let Some(mux) = mux.upgrade() {
                         mux.pane_exited(pane.id);
                     }
         let replay = term.vt_replay()?;
         let (cols, rows) = (term.cols(), term.rows());
-        self.taps.lock().unwrap().push(tx);
+        if !self.dead.load(Ordering::Acquire) {
+            self.taps.lock().unwrap().push(tx);
+        }
         Ok((cols, rows, replay, rx))

Also applies to: 274-284

🤖 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/pane.rs` around lines 191 - 195, The pane reader exit
path in pane.rs leaves attach streams open, so `attach-pane` can keep waiting on
`stream.recv()` instead of detaching. Update the pane-exit handling around
`pane.dead.store(...)` and `mux.pane_exited(...)` to explicitly close/clear the
active stream senders in `taps` when the PTY reader reaches EOF. Make the same
cleanup in the other attach-stream handling block referenced by the related diff
so all `Sender`s are dropped as soon as the pane exits.
🤖 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 `@mux/crates/mux-core/src/lib.rs`:
- Around line 227-228: The empty-session fallback in new_tab currently loses the
requested cwd by calling new_workspace(None), so update this path to preserve
the caller-provided cwd. Introduce or reuse a helper around new_workspace and
spawn_pane that accepts both name and cwd, then have the empty-workspace branch
call it with the incoming cwd instead of hardcoding None. Use the existing
new_tab and new_workspace symbols to keep the behavior consistent in both code
paths.

In `@mux/crates/mux-tui/src/session.rs`:
- Around line 396-411: The request handling in session::request leaves stale
entries in pending when serde_json::to_vec, writer.write_all, or rx.recv_timeout
fails after inserting the sender. Update request to clean up the pending map on
every early-return failure path, using the request id and the pending lock
around the insert/remove logic, so timed-out or failed requests are removed
immediately and do not leak forever.

In `@mux/README.md`:
- Line 24: The README text uses a compound adjective that should be hyphenated
for correct attribution. Update the wording in the `mux/README.md` description
so that the phrase describing the reconnected session uses “full-screen state”
instead of “full screen state,” keeping the rest of the attach/reconnect
explanation unchanged.

In `@mux/scripts/smoke-attach.py`:
- Around line 101-106: The attach smoke test is masking failures by
unconditionally passing the status-bar render check and by accepting any detach
status. In smoke-attach.py, update the first attach flow around the wait_output
assertion and the detach call so it actually fails when the session output does
not render or when the client exits non-zero; use the existing c1.wait_output,
c1.detach, and MARKER/SESSION checks to assert the expected render and a zero
exit status.

---

Outside diff comments:
In `@mux/crates/mux-core/src/pane.rs`:
- Around line 191-195: The pane reader exit path in pane.rs leaves attach
streams open, so `attach-pane` can keep waiting on `stream.recv()` instead of
detaching. Update the pane-exit handling around `pane.dead.store(...)` and
`mux.pane_exited(...)` to explicitly close/clear the active stream senders in
`taps` when the PTY reader reaches EOF. Make the same cleanup in the other
attach-stream handling block referenced by the related diff so all `Sender`s are
dropped as soon as the pane exits.
🪄 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: 975b79a3-7704-4e46-853f-5e4aa484e813

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd3d34 and 6eab672.

⛔ Files ignored due to path filters (1)
  • mux/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • .github/workflows/mux.yml
  • mux/README.md
  • mux/crates/ghostty-vt/src/key.rs
  • mux/crates/ghostty-vt/src/terminal.rs
  • mux/crates/mux-core/src/layout.rs
  • mux/crates/mux-core/src/lib.rs
  • mux/crates/mux-core/src/pane.rs
  • mux/crates/mux-core/src/server.rs
  • mux/crates/mux-core/tests/pty.rs
  • mux/crates/mux-tui/Cargo.toml
  • mux/crates/mux-tui/src/app.rs
  • mux/crates/mux-tui/src/main.rs
  • mux/crates/mux-tui/src/session.rs
  • mux/crates/mux-tui/src/ui.rs
  • mux/scripts/smoke-attach.py
  • mux/scripts/smoke-tui.py

Comment thread mux/crates/mux-core/src/lib.rs Outdated
Comment on lines +227 to +228
if self.with_tree(|workspaces, _| workspaces.is_empty()) {
return self.new_workspace(None);

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 | 🟡 Minor | ⚡ Quick win

Preserve cwd in the empty-session fallback.

new_tab(..., cwd) drops the requested cwd when no workspace exists because it delegates to new_workspace(None), which spawns with None. A headless new-tab RPC with cwd will start in the default directory instead of the requested one.

Proposed direction
-        if self.with_tree(|workspaces, _| workspaces.is_empty()) {
-            return self.new_workspace(None);
+        if workspace.is_none() && self.with_tree(|workspaces, _| workspaces.is_empty()) {
+            return self.new_workspace_with_cwd(None, cwd);
         }

Add a small helper used by both new_workspace() and this fallback:

fn new_workspace_with_cwd(
    self: &Arc<Self>,
    name: Option<String>,
    cwd: Option<String>,
) -> anyhow::Result<Arc<Pane>> {
    let pane = self.spawn_pane(cwd)?;
    // existing new_workspace body...
}
🤖 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/lib.rs` around lines 227 - 228, The empty-session
fallback in new_tab currently loses the requested cwd by calling
new_workspace(None), so update this path to preserve the caller-provided cwd.
Introduce or reuse a helper around new_workspace and spawn_pane that accepts
both name and cwd, then have the empty-workspace branch call it with the
incoming cwd instead of hardcoding None. Use the existing new_tab and
new_workspace symbols to keep the behavior consistent in both code paths.

Comment thread mux/crates/mux-tui/src/session.rs Outdated
Comment on lines +396 to +411
pub fn request(&self, mut cmd: Value) -> anyhow::Result<Value> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
cmd["id"] = json!(id);
let (tx, rx) = channel();
self.pending.lock().unwrap().insert(id, tx);

let mut line = serde_json::to_vec(&cmd)?;
line.push(b'\n');
{
let mut writer = self.writer.lock().unwrap();
writer.write_all(&line)?;
}

let response = rx
.recv_timeout(Duration::from_secs(10))
.map_err(|_| anyhow::anyhow!("session did not respond"))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove timed-out or failed requests from pending.

If JSON serialization, write_all, or recv_timeout fails after Line 400 inserts the sender, the entry remains in pending until a response arrives — or forever if the server is gone.

🛠️ Proposed fix
     pub fn request(&self, mut cmd: Value) -> anyhow::Result<Value> {
         let id = self.next_id.fetch_add(1, Ordering::Relaxed);
         cmd["id"] = json!(id);
         let (tx, rx) = channel();
         self.pending.lock().unwrap().insert(id, tx);
 
-        let mut line = serde_json::to_vec(&cmd)?;
+        let mut line = match serde_json::to_vec(&cmd) {
+            Ok(line) => line,
+            Err(e) => {
+                self.pending.lock().unwrap().remove(&id);
+                return Err(e.into());
+            }
+        };
         line.push(b'\n');
         {
             let mut writer = self.writer.lock().unwrap();
-            writer.write_all(&line)?;
+            if let Err(e) = writer.write_all(&line) {
+                self.pending.lock().unwrap().remove(&id);
+                return Err(e.into());
+            }
         }
 
-        let response = rx
-            .recv_timeout(Duration::from_secs(10))
-            .map_err(|_| anyhow::anyhow!("session did not respond"))?;
+        let response = match rx.recv_timeout(Duration::from_secs(10)) {
+            Ok(response) => response,
+            Err(_) => {
+                self.pending.lock().unwrap().remove(&id);
+                anyhow::bail!("session did not respond");
+            }
+        };
📝 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
pub fn request(&self, mut cmd: Value) -> anyhow::Result<Value> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
cmd["id"] = json!(id);
let (tx, rx) = channel();
self.pending.lock().unwrap().insert(id, tx);
let mut line = serde_json::to_vec(&cmd)?;
line.push(b'\n');
{
let mut writer = self.writer.lock().unwrap();
writer.write_all(&line)?;
}
let response = rx
.recv_timeout(Duration::from_secs(10))
.map_err(|_| anyhow::anyhow!("session did not respond"))?;
pub fn request(&self, mut cmd: Value) -> anyhow::Result<Value> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
cmd["id"] = json!(id);
let (tx, rx) = channel();
self.pending.lock().unwrap().insert(id, tx);
let mut line = match serde_json::to_vec(&cmd) {
Ok(line) => line,
Err(e) => {
self.pending.lock().unwrap().remove(&id);
return Err(e.into());
}
};
line.push(b'\n');
{
let mut writer = self.writer.lock().unwrap();
if let Err(e) = writer.write_all(&line) {
self.pending.lock().unwrap().remove(&id);
return Err(e.into());
}
}
let response = match rx.recv_timeout(Duration::from_secs(10)) {
Ok(response) => response,
Err(_) => {
self.pending.lock().unwrap().remove(&id);
anyhow::bail!("session did not respond");
}
};
🤖 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-tui/src/session.rs` around lines 396 - 411, The request
handling in session::request leaves stale entries in pending when
serde_json::to_vec, writer.write_all, or rx.recv_timeout fails after inserting
the sender. Update request to clean up the pending map on every early-return
failure path, using the request id and the pending lock around the insert/remove
logic, so timed-out or failed requests are removed immediately and do not leak
forever.

Comment thread mux/README.md
Comment thread mux/scripts/smoke-attach.py Outdated
Comment on lines +101 to +106
assert c1.wait_output("[" + SESSION.split("-")[0], 15) or True # status bar paints
c1.drain(1.5)
c1.send(f"printf '{MARKER}\\n'\r".encode())
assert c1.wait_output(MARKER, 15), "marker never rendered on first attach"
status = c1.detach()
print("first attach + detach ok, status", status)

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 | 🟡 Minor | ⚡ Quick win

Make the attach smoke fail on real client failures.

Line 101 always passes because of or True, and Line 105 accepts any wait status. Assert the expected render and zero exit status so CI catches regressions.

🛠️ Proposed fix
-    assert c1.wait_output("[" + SESSION.split("-")[0], 15) or True  # status bar paints
+    assert c1.wait_output("[" + SESSION.split("-")[0], 15), "status bar never rendered"
@@
     status = c1.detach()
+    assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, f"attach exited with {status}"
     print("first attach + detach ok, status", status)
📝 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
assert c1.wait_output("[" + SESSION.split("-")[0], 15) or True # status bar paints
c1.drain(1.5)
c1.send(f"printf '{MARKER}\\n'\r".encode())
assert c1.wait_output(MARKER, 15), "marker never rendered on first attach"
status = c1.detach()
print("first attach + detach ok, status", status)
assert c1.wait_output("[" + SESSION.split("-")[0], 15), "status bar never rendered"
c1.drain(1.5)
c1.send(f"printf '{MARKER}\\n'\r".encode())
assert c1.wait_output(MARKER, 15), "marker never rendered on first attach"
status = c1.detach()
assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, f"attach exited with {status}"
print("first attach + detach ok, status", status)
🤖 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 101 - 106, The attach smoke test is
masking failures by unconditionally passing the status-bar render check and by
accepting any detach status. In smoke-attach.py, update the first attach flow
around the wait_output assertion and the detach call so it actually fails when
the session output does not render or when the client exits non-zero; use the
existing c1.wait_output, c1.detach, and MARKER/SESSION checks to assert the
expected render and a zero exit status.

@cubic-dev-ai cubic-dev-ai 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.

10 issues found across 14 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mux/crates/mux-tui/src/session.rs">

<violation number="1" location="mux/crates/mux-tui/src/session.rs:111">
P2: Falling back to `TreeView::default()` on `remote.tree()` errors clears workspaces/tabs during transient RPC failures. Returning the last known tree instead preserves UI state while the next refresh retries.</violation>

<violation number="2" location="mux/crates/mux-tui/src/session.rs:406">
P2: Failed or timed-out requests can accumulate stale entries in `pending`, which can grow memory usage over a flaky/disconnected socket. This happens because the pending id is inserted before I/O, but error paths at send/timeout return without `remove(&id)` cleanup.</violation>
</file>

<file name="mux/crates/mux-core/src/pane.rs">

<violation number="1" location="mux/crates/mux-core/src/pane.rs:278">
P1: A slow attach client can cause unbounded buffering of pane output, which can grow process memory under sustained PTY output. This comes from using an unbounded channel for taps while downstream socket writes are blocking; a bounded channel with a drop/disconnect policy would keep memory bounded.</violation>

<violation number="2" location="mux/crates/mux-core/src/pane.rs:283">
P1: Attach streams may never terminate after pane output stops, so clients can hang waiting for a detach/end-of-stream signal. The tap sender is registered but not cleared when the reader thread exits, so `stream.recv()` can block indefinitely unless the pane object is dropped elsewhere.</violation>
</file>

<file name="mux/scripts/smoke-attach.py">

<violation number="1" location="mux/scripts/smoke-attach.py:101">
P2: This smoke test no longer verifies initial attach rendering because the assertion is forced true. Removing `or True` restores the guard so later steps run only after expected UI output appears.</violation>

<violation number="2" location="mux/scripts/smoke-attach.py:127">
P2: Teardown can hang/fail unreliably when the headless server does not exit on SIGTERM within 10s. Adding a SIGKILL fallback keeps CI cleanup deterministic and avoids leaked processes.</violation>
</file>

<file name="mux/crates/mux-tui/src/app.rs">

<violation number="1" location="mux/crates/mux-tui/src/app.rs:226">
P2: Rapid consecutive input can be routed to the wrong pane/tab because active-pane lookup uses a stale snapshot (`self.tree`) until the next draw pass. Reading the current active tab from `Session` when handling input would keep routing consistent within the same drained event batch.</violation>
</file>

<file name=".github/workflows/mux.yml">

<violation number="1" location=".github/workflows/mux.yml:54">
P3: The detach/reattach step depends on the binary built by the previous TUI smoke test step, but doesn't build it itself. If the prior step is ever reordered, skipped, or modified (e.g., conditional run), this step will fail with a binary-not-found error. Consider adding `cargo build -p mux-tui` before the script call, matching the pattern used in the TUI smoke test step.</violation>
</file>

<file name="mux/crates/mux-core/src/server.rs">

<violation number="1" location="mux/crates/mux-core/src/server.rs:335">
P2: `scroll-pane` mutates the pane viewport but does not emit any pane update event, so subscribed clients have no redraw trigger and can stay visually stale until unrelated PTY output arrives. Emitting a pane-output notification after the scroll keeps subscribers in sync.</violation>
</file>

<file name="mux/README.md">

<violation number="1" location="mux/README.md:24">
P3: Use “full-screen state” here; as a compound adjective before “state”, it should be hyphenated for readability.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread mux/crates/mux-core/src/surface.rs Outdated
// the reader thread cannot apply bytes between the two.
let replay = term.vt_replay()?;
let (cols, rows) = (term.cols(), term.rows());
self.taps.lock().unwrap().push(tx);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Attach streams may never terminate after pane output stops, so clients can hang waiting for a detach/end-of-stream signal. The tap sender is registered but not cleared when the reader thread exits, so stream.recv() can block indefinitely unless the pane object is dropped elsewhere.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-core/src/pane.rs, line 283:

<comment>Attach streams may never terminate after pane output stops, so clients can hang waiting for a detach/end-of-stream signal. The tap sender is registered but not cleared when the reader thread exits, so `stream.recv()` can block indefinitely unless the pane object is dropped elsewhere.</comment>

<file context>
@@ -252,6 +265,25 @@ impl Pane {
+        // the reader thread cannot apply bytes between the two.
+        let replay = term.vt_replay()?;
+        let (cols, rows) = (term.cols(), term.rows());
+        self.taps.lock().unwrap().push(tx);
+        Ok((cols, rows, replay, rx))
+    }
</file context>

&self,
) -> ghostty_vt::Result<(u16, u16, Vec<u8>, std::sync::mpsc::Receiver<Vec<u8>>)> {
let mut term = self.term.lock().unwrap();
let (tx, rx) = std::sync::mpsc::channel();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: A slow attach client can cause unbounded buffering of pane output, which can grow process memory under sustained PTY output. This comes from using an unbounded channel for taps while downstream socket writes are blocking; a bounded channel with a drop/disconnect policy would keep memory bounded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-core/src/pane.rs, line 278:

<comment>A slow attach client can cause unbounded buffering of pane output, which can grow process memory under sustained PTY output. This comes from using an unbounded channel for taps while downstream socket writes are blocking; a bounded channel with a drop/disconnect policy would keep memory bounded.</comment>

<file context>
@@ -252,6 +265,25 @@ impl Pane {
+        &self,
+    ) -> ghostty_vt::Result<(u16, u16, Vec<u8>, std::sync::mpsc::Receiver<Vec<u8>>)> {
+        let mut term = self.term.lock().unwrap();
+        let (tx, rx) = std::sync::mpsc::channel();
+        // Snapshot and tap registration under the same terminal lock:
+        // the reader thread cannot apply bytes between the two.
</file context>

Comment thread mux/crates/mux-tui/src/session.rs Outdated
Comment thread mux/scripts/smoke-attach.py Outdated
print("reattach replay + live stream ok")
finally:
server.terminate()
server.wait(timeout=10)

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: Teardown can hang/fail unreliably when the headless server does not exit on SIGTERM within 10s. Adding a SIGKILL fallback keeps CI cleanup deterministic and avoids leaked processes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/scripts/smoke-attach.py, line 127:

<comment>Teardown can hang/fail unreliably when the headless server does not exit on SIGTERM within 10s. Adding a SIGKILL fallback keeps CI cleanup deterministic and avoids leaked processes.</comment>

<file context>
@@ -0,0 +1,129 @@
+    print("reattach replay + live stream ok")
+finally:
+    server.terminate()
+    server.wait(timeout=10)
+
+print("ATTACH SMOKE OK")
</file context>

Comment thread mux/crates/mux-tui/src/app.rs Outdated
}

fn active_pane(&self) -> Option<PaneId> {
self.tree.active_tab().map(|tab| tab.active_pane)

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: Rapid consecutive input can be routed to the wrong pane/tab because active-pane lookup uses a stale snapshot (self.tree) until the next draw pass. Reading the current active tab from Session when handling input would keep routing consistent within the same drained event batch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mux/crates/mux-tui/src/app.rs, line 226:

<comment>Rapid consecutive input can be routed to the wrong pane/tab because active-pane lookup uses a stale snapshot (`self.tree`) until the next draw pass. Reading the current active tab from `Session` when handling input would keep routing consistent within the same drained event batch.</comment>

<file context>
@@ -218,26 +223,7 @@ impl App {
-                }
-            }
-        });
+        self.tree.active_tab().map(|tab| tab.active_pane)
     }
 
</file context>
Suggested change
self.tree.active_tab().map(|tab| tab.active_pane)
self.session.tree().active_tab().map(|tab| tab.active_pane)

Comment thread mux/crates/mux-core/src/server.rs Outdated
Comment thread mux/crates/mux-tui/src/session.rs Outdated
Comment thread .github/workflows/mux.yml
Comment on lines +54 to +56
- name: Detach/reattach smoke test
working-directory: mux
run: python3 scripts/smoke-attach.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The detach/reattach step depends on the binary built by the previous TUI smoke test step, but doesn't build it itself. If the prior step is ever reordered, skipped, or modified (e.g., conditional run), this step will fail with a binary-not-found error. Consider adding cargo build -p mux-tui before the script call, matching the pattern used in the TUI smoke test step.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/mux.yml, line 54:

<comment>The detach/reattach step depends on the binary built by the previous TUI smoke test step, but doesn't build it itself. If the prior step is ever reordered, skipped, or modified (e.g., conditional run), this step will fail with a binary-not-found error. Consider adding `cargo build -p mux-tui` before the script call, matching the pattern used in the TUI smoke test step.</comment>

<file context>
@@ -50,3 +50,7 @@ jobs:
           cargo build -p mux-tui
           python3 scripts/smoke-tui.py
+
+      - name: Detach/reattach smoke test
+        working-directory: mux
+        run: python3 scripts/smoke-attach.py
</file context>
Suggested change
- name: Detach/reattach smoke test
working-directory: mux
run: python3 scripts/smoke-attach.py
- name: Detach/reattach smoke test
working-directory: mux
run: |
cargo build -p mux-tui
python3 scripts/smoke-attach.py

Comment thread mux/README.md
Vertical workspace list on the left (cmux-style vertical tabs): one
entry per workspace showing its name and the active tab's title, active
workspace highlighted, plus a clickable '+ new workspace' row. Click an
entry to switch workspaces; prefix-s toggles the sidebar; it hides
automatically under 70 columns. Pane layout shifts right accordingly.
Session::select_workspace now takes index or delta (server side already
did). Smoke test drives a real SGR mouse click on the sidebar and
verifies the workspace switch over the control socket.
Comment thread mux/crates/mux-core/src/pane.rs Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mux/crates/mux-core/src/server.rs">

<violation number="1" location="mux/crates/mux-core/src/server.rs:30">
P1: Default socket path can escape the intended per-user temp directory when session names contain path separators. Sanitizing `session` before joining would keep control-socket files confined to `$TMPDIR/cmux-mux-<uid>` and avoid unintended file operations.</violation>

<violation number="2" location="mux/crates/mux-core/src/server.rs:110">
P2: The stale-socket cleanup currently deletes the path on every connect error, which can remove non-socket files or mask real access errors. Restrict deletion to the expected stale-socket error and return other errors unchanged.</violation>

<violation number="3" location="mux/crates/mux-core/src/server.rs:335">
P2: `scroll-pane` mutates the pane viewport but does not emit any pane update event, so subscribed clients have no redraw trigger and can stay visually stale until unrelated PTY output arrives. Emitting a pane-output notification after the scroll keeps subscribers in sync.</violation>
</file>

<file name="mux/scripts/smoke-tui.py">

<violation number="1" location="mux/scripts/smoke-tui.py:100">
P2: On timeout, only the specific PID is killed, not the process group. If cmux-mux has spawned child processes (shells in panes via PTY), they survive as orphans. Use `os.killpg` or negative-PID kill to terminate the whole process group, then escalate to SIGKILL for stragglers.</violation>
</file>

<file name="mux/crates/mux-core/tests/pty.rs">

<violation number="1" location="mux/crates/mux-core/tests/pty.rs:93">
P2: The polling loop in `control_socket_round_trip` calls `reader.read_line(&mut line).unwrap()` which blocks indefinitely on a `UnixStream` with no timeout set. The deadline assertion after the read is unreachable if the server doesn't respond — the test hangs instead of failing fast. Set a read timeout on the stream via `stream.set_read_timeout(Some(Duration::from_secs(1)))` so that `read_line` returns an error under timeout, or restructure the loop to use a non-blocking read pattern similar to the `wait_for` helper used in the other tests.</violation>
</file>

<file name="mux/crates/mux-tui/Cargo.toml">

<violation number="1" location="mux/crates/mux-tui/Cargo.toml:19">
P3: `unicode-width` is declared as a dependency but never imported or used anywhere in `mux-tui` source files (src/). Remove it to keep dependencies clean.</violation>
</file>

<file name="mux/crates/mux-tui/src/main.rs">

<violation number="1" location="mux/crates/mux-tui/src/main.rs:80">
P2: Headless sessions start with no workspace or pane, so the socket API can return an empty session and `new-tab` without an explicit workspace can panic inside `Mux::new_tab`. Initializing the first workspace before entering `run_headless` (or making `new_tab` create/validate the workspace) would make headless behave like the TUI startup path.</violation>
</file>

<file name="mux/crates/mux-core/src/layout.rs">

<violation number="1" location="mux/crates/mux-core/src/layout.rs:61">
P2: Directional focus can jump to panes that are only diagonally in that direction. The current `neighbor` filter checks center direction but never enforces the documented perpendicular span overlap, so focus selection can ignore same-row/same-column intent.</violation>

<violation number="2" location="mux/crates/mux-core/src/layout.rs:102">
P2: Focus navigation can jump to panes that are not renderable after a small resize because zero-size panes are still kept in `layout.panes` and considered by `neighbor`. It would be safer to exclude empty rects from neighbor selection (or otherwise mark collapsed panes non-focusable) so movement only lands on visible panes.</violation>
</file>

<file name="mux/crates/mux-tui/src/keys.rs">

<violation number="1" location="mux/crates/mux-tui/src/keys.rs:99">
P2: Apps using kitty keyboard/disambiguation modes can receive incorrect key identity for shifted punctuation because `%`, `"`, etc. are encoded as `GHOSTTY_KEY_UNIDENTIFIED` with the shifted character as `unshifted_codepoint`. Consider mapping common shifted punctuation back to their physical keys/unshifted codepoints before calling the Ghostty encoder.</violation>
</file>

<file name="mux/crates/ghostty-vt/src/terminal.rs">

<violation number="1" location="mux/crates/ghostty-vt/src/terminal.rs:183">
P2: Repeated title reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_TITLE is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</violation>

<violation number="2" location="mux/crates/ghostty-vt/src/terminal.rs:193">
P2: Repeated working-directory reads can leak memory because the GhosttyString returned by GHOSTTY_TERMINAL_DATA_PWD is copied into a Rust String but never released. Consider freeing the GhosttyString after copying the bytes, including the empty/null early-return path.</violation>
</file>

<file name=".github/workflows/mux.yml">

<violation number="1" location=".github/workflows/mux.yml:25">
P2: This checkout step still keeps the workflow token available to later git commands by default. Since this job only needs repository reads, adding `persist-credentials: false` here would reduce credential exposure to subsequent install/test steps.</violation>

<violation number="2" location=".github/workflows/mux.yml:54">
P3: The detach/reattach step depends on the binary built by the previous TUI smoke test step, but doesn't build it itself. If the prior step is ever reordered, skipped, or modified (e.g., conditional run), this step will fail with a binary-not-found error. Consider adding `cargo build -p mux-tui` before the script call, matching the pattern used in the TUI smoke test step.</violation>
</file>

<file name="mux/crates/mux-core/src/lib.rs">

<violation number="1" location="mux/crates/mux-core/src/lib.rs:228">
P2: Creating the first tab in an empty session now ignores the requested working directory, so `new-tab` can start in the wrong directory in headless startup flows. The early return calls `new_workspace(None)` (which spawns with `None` cwd) instead of creating the initial workspace/tab with the provided `cwd`.</violation>
</file>

<file name="mux/crates/mux-core/src/pane.rs">

<violation number="1" location="mux/crates/mux-core/src/pane.rs:278">
P1: A slow attach client can cause unbounded buffering of pane output, which can grow process memory under sustained PTY output. This comes from using an unbounded channel for taps while downstream socket writes are blocking; a bounded channel with a drop/disconnect policy would keep memory bounded.</violation>

<violation number="2" location="mux/crates/mux-core/src/pane.rs:283">
P1: Attach streams may never terminate after pane output stops, so clients can hang waiting for a detach/end-of-stream signal. The tap sender is registered but not cleared when the reader thread exits, so `stream.recv()` can block indefinitely unless the pane object is dropped elsewhere.</violation>
</file>

<file name="mux/scripts/smoke-attach.py">

<violation number="1" location="mux/scripts/smoke-attach.py:101">
P2: This smoke test no longer verifies initial attach rendering because the assertion is forced true. Removing `or True` restores the guard so later steps run only after expected UI output appears.</violation>

<violation number="2" location="mux/scripts/smoke-attach.py:127">
P2: Teardown can hang/fail unreliably when the headless server does not exit on SIGTERM within 10s. Adding a SIGKILL fallback keeps CI cleanup deterministic and avoids leaked processes.</violation>
</file>

<file name="mux/crates/mux-tui/src/app.rs">

<violation number="1" location="mux/crates/mux-tui/src/app.rs:226">
P2: Rapid consecutive input can be routed to the wrong pane/tab because active-pane lookup uses a stale snapshot (`self.tree`) until the next draw pass. Reading the current active tab from `Session` when handling input would keep routing consistent within the same drained event batch.</violation>

<violation number="2" location="mux/crates/mux-tui/src/app.rs:411">
P2: Mouse hit-testing in `handle_mouse` can use stale `sidebar_width` and `sidebar_hits` when multiple input events are processed in the same batch before `sync_layout` and `draw` run.

`sidebar_width` is only recomputed in `sync_layout`, and `sidebar_hits` is only rebuilt during `draw`. But the event loop drains up to 256 queued events first, calling `handle` on each without refreshing layout. If an earlier event toggles `sidebar_visible` (e.g., prefix `s`) or changes the workspace list (e.g., clicking `NewWorkspace`), a later mouse click still sees the old `sidebar_width` and `sidebar_hits`. This can misroute a click to the wrong workspace, spawn an unintended workspace, or drop a pane focus.

Consider calling `sync_layout` immediately after any event that changes layout-affecting state, so subsequent events in the batch see fresh geometry.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread mux/crates/mux-tui/src/app.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: 0320aa1e8d

ℹ️ 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 +96 to +97
if let Some(cwd) = opts.cwd.as_deref() {
cmd.cwd(cwd);

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 Set PWD when spawning in a requested cwd

When a split or socket-created tab uses a cwd that is a symlink, this only chdirs the child and leaves PWD inherited from the mux process. Shells such as bash/zsh treat a missing or mismatched PWD as a physical cwd and report the resolved path, so the new pane does not preserve the user's logical working directory. Set PWD to cwd alongside cmd.cwd(cwd) so inherited-directory splits keep the same path.

Useful? React with 👍 / 👎.

Comment on lines +153 to +159
if path.exists() {
match UnixStream::connect(&path) {
Ok(_) => anyhow::bail!(
"session socket {} is already in use (another instance running?)",
path.display()
),
Err(_) => std::fs::remove_file(&path)?,

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 Refuse to remove non-socket paths

When --socket points at an existing regular file or symlink that is not a listening Unix socket, UnixStream::connect fails and this branch deletes the path before binding. That turns a socket-path typo into data loss; stale cleanup should verify the path is actually a socket (or only unlink sockets in a directory this process owns) before calling remove_file.

Useful? React with 👍 / 👎.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

workflow-guard-tests is failing here because the ghostty bump to a78fe53 has no pinned GhosttyKit checksum. The xcframework release now exists (built with the build-ghosttykit.yml recipe on the AWS builder since the Warp macOS queue was wedged; asset sha256 verified after upload): https://github.com/manaflow-ai/ghostty/releases/tag/xcframework-a78fe53efaaea56b80d47569d85e0d7b76512aa7-crashsubdir-cmux-crash-v1

Append this line to scripts/ghosttykit-checksums.txt to green the guard (already done on the stacked #7325):

a78fe53efaaea56b80d47569d85e0d7b76512aa7 b35ee6a7b9a42c9ec1a1d43932ae6b00afd67de57afee67340da6221f2ac699c

🤖 Generated with Claude Code

…l-stable selection with auto-scroll

Coded by GPT 5.5 via the fable loop (round + revision, judge-approved);
smoke deflake per the judge's root-cause note.

Drag a tab chip to reorder within its bar or drop it on another pane's
bar to move the surface there (a pane emptied by the move collapses
without killing the moved surface); drag sidebar workspace entries to
reorder. Click-without-move keeps its old meaning; drags arm only when
the pointer leaves the press cell, drop targets resolve by surface and
workspace ID at release time, and drop indicators render from real
geometry. New additive protocol: move-tab {surface, pane, index} and
move-workspace {workspace, index}.

Selections are scroll-stable: anchor and head are absolute rows, the
highlight survives scrolling, and dragging at the content's top or
bottom edge auto-scrolls to extend the selection. Copy uses one
SCREEN-tagged absolute-range read (Terminal::selection_text_absolute),
preserving soft-wrap unwrapping with no viewport mutation; a same-pane
no-op move leaves active_tab untouched and emits nothing.

The status bar no longer shows the session label. The smoke's OSC 11
step gets an 8s deadline (the 2s race was the 'PY' flake).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: cargo zerocopy is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ? → cargo/tungstenite@0.24.0 → cargo/zerocopy@0.8.52

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/zerocopy@0.8.52. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

if self.request(json!({"cmd": "attach-surface", "surface": id})).is_err() {
self.surfaces.lock().unwrap().remove(&id);
return None;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ignored pre-attach resize

Medium Severity

When creating a remote mirror, ensure_surface issues resize-surface before attach-surface but discards errors from that resize. Attach and VT replay can proceed while the server PTY still has the old geometry, so the mirror terminal size and replay content can disagree with the layout size the TUI requested.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3b03faa. Configure here.

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

ℹ️ 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".

pub fn ensure_initial(&self, size: Option<(u16, u16)>) -> anyhow::Result<()> {
match self {
Session::Local(mux) => {
mux.new_workspace(None, size)?;

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 Check for existing local workspaces before seeding

In the non-headless startup path, main.rs starts the control socket before calling run_tui, so a fast socket client can create a workspace/tab before this runs. This branch then unconditionally creates another workspace and switches the active workspace to it, making the client-created session unexpectedly inactive; mirror the remote-side emptiness check or seed before serving the socket.

Useful? React with 👍 / 👎.

Comment on lines +396 to +397
*size = (cols, rows);
*pixels = (pixel_w, pixel_h);

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 Cache browser sizes only after CDP resize succeeds

When a CDP resize command fails after these assignments, the mux records the new cell/pixel size even though Chrome did not apply it. A later resize to the same dimensions is treated as unchanged and skips the CDP calls, while Mux::resize_surface may already have emitted the new size, leaving that browser pane stuck until a different size is requested; update these cached values only after the CDP resize succeeds or roll them back on error.

Useful? React with 👍 / 👎.

The host-colors round bumped the ghostty submodule without publishing
the GhosttyKit artifact or its pin, which failed the checksum guard and
every GhosttyKit-consuming CI job on branches based here. Artifact
built via build-ghosttykit.yml run 28722157154; sha256 computed from
the published release tarball.

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

pub fn send_bytes(&self, surface: SurfaceId, bytes: &[u8]) {
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
let _ = self.request(json!({"cmd": "send", "surface": surface, "bytes": encoded}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Attach input drops silently

Medium Severity

Remote attach input goes through send_bytes, which discards the result of the synchronous send JSON-RPC call. Timeouts, socket errors, or server ok:false responses drop keystrokes and pasted data with no feedback to the TUI or user.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 287ef64. Configure here.

…-exact alt+n, MRU close focus, overlay style reset

Coded by GPT 5.5 via the fable loop (4 rounds; the extra round restored
the original design after the judge disproved its own 8b prescription
against ghostty's formatter source).

The % artifact was mirror desync: attach clients learned about resizes
from an unordered subscribe event, so post-WINCH redraw bytes applied
at stale geometry. Resize markers now ride IN the attach stream,
broadcast under the same terminal lock as the server's own resize
(bytes -> marker -> bytes in exact server order), carrying a fresh full
VT replay; the client swaps in a fresh mirror terminal from that replay
(vt_replay covers scrollback + screen, so nothing is lost), invalidates
render state on resize, and PROTOCOL_VERSION bumps to 6 with clients
refusing older servers instead of running dual authority. The attach
smoke runs a rapid split storm, waits for quiescence, and asserts the
client's rendered cells match the server's screen with no reverse-video
% cells; a pty test pins output->resized->output ordering and a mirror
test asserts server-truth equality without scrollback duplication.

alt+n now follows zellij exactly (rows*ratio > cols with the real cell
pixel ratio, 20-row/60-col minimums, largest-pane fallback). Closing
the active pane focuses the most recently used survivor (monotonic
active_at, matching zellij/tmux). Menu/dialog cells reset before
styling, fixing sidebar bold/dim residue bleeding through overlays
(ratatui set_style merges modifiers). Same-position moves are ok:true
no-ops, move_workspace gets tab-parity gap correction, and the smoke
helpers are idempotent with modifier-aware style tracking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
let surface = get_surface(mux, surface)?;
require_pty(&surface)?;
surface.try_with_terminal(|t| t.scroll_delta(delta))?;
Ok(json!({}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Scroll socket skips redraw events

Medium Severity

The scroll-surface handler updates the PTY terminal viewport via scroll_delta but never marks the surface dirty or emits a surface-output (or related) mux event. Subscribers and attach streams only learn about PTY reader output and explicit resize paths, so a client that scrolls over the socket can change server state while attached UIs keep showing the old viewport until unrelated output arrives.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f5327e. Configure here.

…t, short ids, mirror-dump instrumentation

Coded by GPT 5.5 via the fable loop (items judge-approved; the
% investigation continues on top of this landing).

Directional focus is a faithful port of the macOS app's bonsplit
findBestNeighbor: pure geometry shared by alt and prefix arrows,
half-plane candidates ranked by perpendicular overlap then gap
distance, no memory, no wraparound; 11 unit cases include the reported
layout (left pane, tall top-right over two bottom panes: alt+l goes
top-right). Tab drags now drop past the last chip (end insertion) and
onto a pane's content area (append, same or cross pane). Copies show a
bottom-right toast driven by an event-loop deadline. Every
workspace/screen/pane/tab carries a stable 6-char base36 short id
(collision-probed, additive short_id JSON fields) with right-click
copy menu items.

The attach smoke asserts stranded reverse-video % BEFORE the ^L
repaint (which was masking it) and polls convergence to tolerate
in-flight output. CMUX_MUX_DEBUG_MIRROR_DUMP=<dir> dumps every mirror
with inverse runs marked on client drop; the storm repro now proves
the % artifact lives IN the mirror (row 0 holds a lone reverse-video
% where the server row 0 is the prompt: a one-row mirror offset), the
evidence the next round's fix is built on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
if !taps.is_empty() {
taps.retain(|tap| {
tap.send(AttachFrame::Resized { cols, rows, replay: replay.clone() }).is_ok()
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Silent empty resize replay

Medium Severity

On PTY resize, attach subscribers receive a Resized frame whose replay comes from vt_replay() with unwrap_or_default(). If replay formatting fails, an empty payload is still broadcast. Attach clients that rebuild their mirror from that marker lose screen state instead of surfacing an error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0d96308. Configure here.

The pre-attach resize-surface request raced ahead of the mirror tap:
the server resized and the shell's WINCH redraw could land in the gap
before the ordered attach stream started, stranding cells (visible as
% and other glyphs frozen at old positions). Size now flows through
the same ordered stream as everything else, so the resize marker and
any redraw bytes stay in sequence.

Also adds CMUX_MUX_DEBUG_MIRROR_DUMP-gated per-surface frame logging
(opt-in, no effect unless the env var is set) used to diagnose this.
# Conflicts:
#	scripts/ghosttykit-checksums.txt

@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.

There are 18 total unresolved issues (including 17 from previous reviews).

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 f4966ae. Configure here.

trimmed.to_string()
} else {
format!("https://{trimmed}")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Protocol-relative URLs mis-normalized

Medium Severity

normalize_url prepends https:// to any input without an explicit scheme, but protocol-relative URLs like //host/path do not contain :// and are not exempt. They become https:////host/path, which Chrome treats as an invalid or unintended target instead of the intended host.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f4966ae. Configure here.

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

ℹ️ 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".

// Compute the socket path up front so surface children inherit it.
let socket_path =
args.socket.unwrap_or_else(|| mux_core::server::default_socket_path(&args.session));
surface_options.extra_env.push(("CMUX_MUX_SOCKET".into(), socket_path.display().to_string()));

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 Export an absolute socket path to child panes

When the server is started with a relative --socket path (for example cmux-mux --socket mux.sock), the listener binds relative to the launch directory, but panes spawned without an explicit cwd are moved to $HOME in Surface::spawn; exporting the same relative string here makes CMUX_MUX_SOCKET=mux.sock point at $HOME/mux.sock inside the shell instead of the live control socket. Resolve the explicit socket path to an absolute path before storing it in extra_env so child-side socket clients can reliably reconnect.

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
Preview – cmux — f4966ae8 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