Skip to content

Forward mouse input to TUI applications - #7759

Merged
lawrencecchen merged 109 commits into
mainfrom
task-tui-mouse-passthrough
Jul 15, 2026
Merged

lawrencecchen merged 109 commits into
mainfrom
task-tui-mouse-passthrough

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • forward PTY click, release, motion, drag, and wheel input through Ghostty's mouse encoder
  • preserve cmux selection and pane menus behind the Shift modifier
  • document mouse-aware TUI behavior and protocol support

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • python3 scripts/smoke-tui.py
  • attached-client htop preflight: SGR wheel changed the htop screen and F10 click exited htop

Notes

  • python3 scripts/smoke-attach.py reaches the second attach, then fails its output-quiescence assertion; the same failure reproduces on unchanged origin/main

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touches core mux event delivery and a breaking-ish protocol bump (v7 wire shapes), which affects every attach/subscribe client; coalescing and overflow disconnect change observable event ordering for slow consumers.

Overview
Bumps the control socket to protocol v7 and replaces the mux’s simple broadcast channel with a per-subscriber event bus that coalesces high-churn title-changed and surface-output per surface, caps pending work (~4k slots), and disconnects slow readers with an overflow event (subscribe vs attach-scoped variants).

Wire/API changes: title-changed now includes the authoritative title string; attach resized events use a replay field (bindings still accept legacy v6 data). PTY/browser paths emit MuxEvent::TitleChanged { surface, title } instead of surface-only IDs.

Runtime hardening: browser resize is serialized (resize_transaction), state is committed after CDP round-trips without clearing frames delivered mid-resize, and Surface::resize propagates errors via anyhow::Result. Conformance runner gains attach streams, title/replay fixtures, and overflow parsing; all language bindings and e2e checks are updated for protocol ≤7.

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


Summary by cubic

Forwards mouse input to PTY apps and bumps the control socket to protocol v7 with a bounded, per-subscriber event bus. Improves attach, resize, and recovery for local, remote, and web clients with authoritative titles, scoped overflow, and safer, retried browser resizes.

  • New Features

    • PTY mouse passthrough via ghostty-vt::MouseEncoder/MouseEncoders (X10/UTF‑8/SGR/URxvt; SGR‑pixel) on an ordered, off-loop input lane with motion coalescing, backpressure, reserved release bytes, and remote release retries; preserves ordering across local/remote and keeps cmux selection/menus behind Shift.
    • Protocol v7 + event bus: bounded per‑subscriber queues (~4k) coalesce title-changed/surface-output; slow consumers receive overflow { error, scope, surface? } and only the affected stream closes; shared transports can multiplex attach plus one subscription (extra subs require a streamTransportFactory).
    • Wire/API: authoritative title-changed { surface, title }; attach resized { replay } (v6 data still accepted); resize-surface -> { accepted, reservation_id? } (legacy empty result defaults to accepted: true); surface-resized { ..., reservation_id? }; new surface-resize-failed { surface, cols, rows, error, retry_after_ms?, reservation_id? }.
    • Browser/web: queued cell resizes with bounded backoff and failure reporting; capture clamped to TRANSPORT_SAFE_CAPTURE_MEGAPIXELS (2.0 MP) with browser.capture_scale as a max; web requires v7 and recovers attach overflow with bounded retries; SDKs reconcile authoritative titles and treat overflow as terminal per stream.
  • Bug Fixes

    • Replace polling with condition-based wakeups; bound VT replays, purge stale pre‑ack/overflowed frames, and keep ordered draining across local/remote; reconcile remote size/title on recovery and reattach.
    • Guarantee mouse release delivery (including remote with retries), resolve ambiguous button/scroll (including horizontal), keep pointer motion off VT locks, and serialize PTY input vs session mutations with backpressure recovery; preserve drag after motion failure and clean up failed presses.
    • Bound remote failure handling, reserve transport capacity for responses, propagate resize errors (including reservation identity), and time out stalled WebSocket clients.

Written for commit f19ead0. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Enhanced PTY mouse handling: forward pointer press/drag/release, motion, and wheel (including horizontal) to PTY apps with pointer-accurate encoding when mouse tracking is enabled.
    • Added PTY input queuing with ordered delivery, motion coalescing, and backpressure handling.
  • Bug Fixes
    • Prevented PTY mouse state from interfering during resizes, splits, and menu/rename flows; hover/mouse routing now respects modifier context and cancels during overlays or focus loss.
    • Updated PTY scroll behavior to use wheel-style forwarding instead of prior fallbacks.
  • Documentation
    • Refined mouse documentation and updated the in-app MOUSE help text.
  • Tests
    • Expanded PTY mouse unit tests for click/release, wheel, Shift suppression, and overlay/focus-loss cancellation.

@vercel

vercel Bot commented Jul 9, 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 15, 2026 6:56pm
cmux-staging Building Building Preview, Comment Jul 15, 2026 6:56pm

@coderabbitai

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

Adds Ghostty mouse encoding, asynchronous PTY event dispatch, and PTY-pane forwarding for clicks, drags, motion, and wheel events while preserving Shift-based selection behavior. Updates mutation barriers, tests, usage text, mouse documentation, and plugin input specifications.

Changes

PTY mouse passthrough

Layer / File(s) Summary
Ghostty mouse encoder contract
cmux-tui/crates/ghostty-vt/src/mouse.rs, cmux-tui/crates/ghostty-vt/src/lib.rs
Adds mouse event types and FFI-backed encoding with terminal-mode synchronization, resource cleanup, buffer retry handling, and encoding tests.
Asynchronous PTY input dispatch
cmux-tui/crates/cmux-tui/src/pty_input.rs, cmux-tui/crates/cmux-tui/src/main.rs, cmux-tui/Cargo.toml, cmux-tui/crates/cmux-tui/Cargo.toml
Adds a bounded worker-thread dispatcher that coalesces motion events, reserves release capacity, writes encoded events to PTY surfaces, and is registered in the TUI.
PTY input forwarding
cmux-tui/crates/cmux-tui/src/app.rs
Integrates mouse encoding and dispatch into click, drag, motion, and vertical or horizontal wheel handling, with Shift, overlay, menu, and prompt gates plus shared PTY write routing.
PTY-safe state mutations
cmux-tui/crates/cmux-tui/src/app.rs, cmux-tui/crates/cmux-tui/src/session/*
Flushes or cancels PTY input before incompatible geometry, focus, surface, pane, menu, navigation, and workspace mutations, with surface existence and resize checks.
Mouse behavior documentation
cmux-tui/crates/cmux-tui/src/main.rs, cmux-tui/docs/mouse.md, cmux-tui/spec/plugins.md
Documents PTY mouse tracking, Shift overrides, tab and screen switching, and the plugin sidebar forwarding boundary.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant App
  participant MouseEncoder
  participant PtyInputDispatcher
  participant PTY
  User->>App: Mouse input
  App->>MouseEncoder: Encode MouseInput
  MouseEncoder->>App: Ghostty mouse bytes
  App->>PtyInputDispatcher: Enqueue PTY event
  PtyInputDispatcher->>PTY: Write encoded bytes
Loading

Possibly related PRs

  • manaflow-ai/cmux#7319: Both changes forward non-left PTY mouse drag events through Ghostty-related mouse handling.
🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
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 No Swift files are changed in this PR; the diff is Rust/docs only, so the Swift actor-isolation rule is not applicable.
Cmux Swift Blocking Runtime ✅ Passed HEAD^..HEAD changes only Rust files; no Swift diff means the Swift blocking-runtime rule is not implicated.
Cmux Browser Automation Off-Main ✅ Passed Diff is confined to cmux-tui PTY/session code; no browser-automation/WebKit/socket-worker routing changes or policy-test gaps appear in changed files.
Cmux Expensive Synchronous Load ✅ Passed PR touches only Rust/docs files; no Swift files or main-actor interactive paths were changed.
Cmux Cache Substitution Correctness ✅ Passed PR only touches Rust/TOML/docs; no Swift/TS/JS cache-substitution in persistence/history/undo/snapshot paths.
Cmux No Hacky Sleeps ✅ Passed PASS: the HEAD diff only touches Rust files; no TypeScript/JS/shell/build-runtime scripts were changed, so the no-hacky-sleeps rule is out of scope.
Cmux Algorithmic Complexity ✅ Passed No new nested scans or repeated sorts on scalable collections; the added PTY queue is fixed-bounded and lookups are O(1).
Cmux Swift Concurrency ✅ Passed PR diff only changes Rust files; no cmux-owned Swift code was added or expanded, so the Swift-concurrency rule is not applicable.
Cmux Swift @Concurrent ✅ Passed No Swift files changed, so the @concurrent/nonisolated-async rule is not applicable to this PR.
Cmux Swift File And Package Boundaries ✅ Passed No Swift files are touched in this PR; the diff is Rust cmux-tui code, so the Swift package-boundary rule doesn’t apply.
Cmux Swiftpm Lockfiles ✅ Passed PR diff only changes Rust files and cmux-tui/Cargo.lock; no SwiftPM/Xcode, .gitignore, or workflow files were touched, so the lockfile rule isn’t triggered.
Cmux Swift Logging ✅ Passed The PR only changes Rust/docs files; no Swift runtime files or logging statements were added or modified.
Cmux User-Facing Error Privacy ✅ Passed The only new user-facing error text is a generic PTY-input timeout message; no vendor/internal names, raw upstream errors, or secrets were added.
Cmux Full Internationalization ✅ Passed No locale-managed surface changed; the diff only edits Rust CLI help/docs, and no translation catalogs or routing files were touched.
Cmux Swiftui State Layout ✅ Passed The PR is Rust-only; no SwiftUI files or state/layout patterns covered by the rule were introduced.
Cmux Architecture Rethink ✅ Passed PASS: This Rust PR adds explicit PTY-input ownership and ordering barriers, with no sleeps/polling or split UI lifecycle ownership.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Full PR diff has no Swift files, so the auxiliary-window close-shortcut rule isn’t triggered.
Cmux Source Artifacts ✅ Passed Changed files are only source/docs/Cargo paths; none are logs, temp dirs, build output, or other artifacts prohibited by the rule.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR changes only Rust files under cmux-tui; no Swift production Sources/** files are modified, so this Swift-only rule is not applicable.
Cmux No Ambient Global State ✅ Passed Rule is Swift-only; this Rust PR adds no new ambient global state—mouse/PTy behavior is instance-owned, and main.rs only has the existing signal-boundary static.
Title check ✅ Passed The title clearly matches the main change: forwarding mouse input to TUI applications.
Description check ✅ Passed The summary and testing sections are filled with concrete details and cover the main change well.
✨ 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 task-tui-mouse-passthrough

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 cmux-tui/crates/cmux-tui/src/app.rs
Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
@greptile-apps

greptile-apps Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR forwards mouse input through the TUI PTY path and updates the related protocol surfaces. The main changes are:

  • PTY click, drag, release, motion, and wheel forwarding.
  • Ordered PTY input delivery with release reservation handling.
  • Protocol and binding updates for newer event shapes.
  • Documentation and tests for mouse-aware TUI behavior.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
cmux-tui/crates/cmux-tui/src/app.rs Updates PTY mouse routing so active-button releases, fallback release bytes, and local drag cleanup are handled in the mouse path.
cmux-tui/crates/cmux-tui/src/pty_input.rs Adds the ordered PTY input lane with bounded queues, motion coalescing, release reservations, and remote failure handling.
cmux-tui/crates/ghostty-vt/src/mouse.rs Adds the Ghostty mouse encoder wrapper used to encode PTY mouse protocol events.

Reviews (97): Last reviewed commit: "Always deliver the paired mouse release" | Re-trigger Greptile

Comment on lines +98 to +102
sys::ghostty_mouse_encoder_setopt(
self.encoder,
sys::GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED,
&input.any_button_pressed as *const _ as *const c_void,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Wheel Events Mark Button Held

Wheel forwarding passes any_button_pressed through this encoder option, and the app sends true for plain ScrollUp and ScrollDown events. If Ghostty applies this flag to wheel buttons the same way it applies it to motion, a normal wheel tick can be encoded as a button-held wheel event, so mouse-aware TUI apps can misread ordinary scrolling as a drag-wheel input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8b3de53. Wheel events now pass any_button_pressed=false; that flag is reserved for active-button motion outside the viewport.

— Claude Code

Comment on lines +2254 to +2255
(x as f32 - content.x as f32 + 0.5) * cell_width as f32,
(y as f32 - content.y as f32 + 0.5) * cell_height as f32,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Drag Uses Stale Pane Geometry

Drag::PtyMouse stores the content rect from the press and every later drag/release reuses it for coordinate conversion. If the pane is resized or the sidebar layout changes before release, the pointer is encoded against the old rect, so the TUI app can receive drag or release coordinates for the wrong cell.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8b3de53. Drag and release coordinate conversion now resolves the current pane content rect by surface, with a resize-during-drag regression assertion.

— Claude Code

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment thread cmux-tui/crates/cmux-tui/src/app.rs
Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
}
}
MouseEventKind::Up(MouseButton::Left) => {
if self.finish_pty_mouse_drag(mouse.column, mouse.row, mouse.modifiers) {

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 Match Active Button This release path can end the PTY drag for the wrong physical button. If right down starts Drag::PtyMouse, then left down is ignored by the new guard, but left up still calls finish_pty_mouse_drag; that sends a right-button release and clears self.drag while the right button is still held. The inner TUI sees the drag end early, and later motion or the actual right-up is no longer forwarded as part of that drag. Only the release for the stored active button should finish the PTY mouse drag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 71fc0ae. The drag tracks secondary presses separately, so a secondary release no longer ends the active button; the actual active-button release is required.

— Claude Code

@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: 8b3de53bc9

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

/// Queue an event without blocking the UI. A wedged remote endpoint may
/// fill the bounded queue, in which case new input is dropped.
pub fn enqueue(&self, event: PtyMouseInputEvent) {
let _ = self.tx.try_send(event);

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 release events when the mouse queue fills

For remote sessions, a slow or blocked control socket can fill this 512-slot channel with motion events while the worker is stuck in RemoteSession::send_bytes; because try_send drops every new event, the subsequent button release or wheel event can be discarded too. That leaves mouse-aware TUIs seeing a press/drag with no release even after the socket catches up, so only coalescible motion should be dropped or releases should be prioritized.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 71fc0ae. The bounded queue now evicts stale motion for ordered events and prioritizes releases under saturation. A regression test fills the queue and verifies the release displaces motion.

— Claude Code

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
}
}
MouseEventKind::Up(MouseButton::Left) => {
if self.finish_pty_mouse_drag(mouse.column, mouse.row, mouse.modifiers) {

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 Ignore releases from non-active mouse buttons

When a PTY drag is active, extra Down events are ignored, but any Up branch still calls finish_pty_mouse_drag, which always releases the stored active button. If a user presses right in a mouse-aware TUI, then taps/releases left before releasing right, this left release sends a right-button release and clears self.drag; the real right release is then no longer forwarded, leaving the inner app's button state out of sync.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 71fc0ae. Secondary down/up pairs are tracked and consumed independently from the active PTY drag, while ambiguous legacy releases remain supported when there was no observed secondary press.

— Claude Code

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +2238 to +2243
if reported_button != active_button && ignored_buttons & reported_bit != 0 {
if let Some(Drag::PtyMouse { ignored_buttons, .. }) = &mut self.drag {
*ignored_buttons &= !reported_bit;
}
return true;
}

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 Ignore non-active releases

A release for a non-active button can still end the active PTY drag when its matching press was not recorded in ignored_buttons. For example, if a right-button PTY drag is active and a left-button Up arrives after its Down was missed, this branch falls through, clears self.drag, and sends a right-button release to the inner app. That leaves the real right-button drag stopped early. Only the stored active button should finish the PTY drag; other button releases should be consumed after clearing any recorded ignored bit.

Suggested change
if reported_button != active_button && ignored_buttons & reported_bit != 0 {
if let Some(Drag::PtyMouse { ignored_buttons, .. }) = &mut self.drag {
*ignored_buttons &= !reported_bit;
}
return true;
}
if reported_button != active_button {
if let Some(Drag::PtyMouse { ignored_buttons, .. }) = &mut self.drag {
*ignored_buttons &= !reported_bit;
}
return true;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 982b45a. Only the stored active button can finish the PTY drag; every non-active release is consumed without relabeling or clearing it. The host requests SGR mouse reporting, which preserves release identity.

— Claude Code

Comment thread cmux-tui/crates/cmux-tui/src/app.rs
Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
if let Some(index) = events.iter().position(|queued| queued.kind == PtyInputKind::Motion) {
events.remove(index);
} else {
return 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 Release can drop

When the remote input worker is blocked and the queue reaches QUEUE_CAPACITY with only ordered input and prior releases, this branch returns false because there is no motion event to evict. finish_pty_mouse_drag and cancel_pty_mouse_drag ignore that enqueue failure, so a remote PTY app can receive the mouse press but never receive the release. That leaves the app in a stuck button state even though releases are meant to be protected by the reserve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ef5473d. Every accepted mouse press now increments an exact release reservation; its matching release consumes that reservation, so capacity is guaranteed without evicting ordered events.

— Claude Code

Comment thread cmux-tui/crates/cmux-tui/src/app.rs
Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment thread cmux-tui/crates/cmux-tui/src/app.rs
Comment thread cmux-tui/crates/cmux-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: 5939e784f0

ℹ️ 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 cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +2279 to +2280
if reported_button != active_button {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle ambiguous mouse releases during PTY drags

On terminals where crossterm cannot identify the button for MouseEventKind::Up (its docs say it returns MouseButton::Left in that case), releasing a right- or middle-button PTY drag reaches this branch and is consumed without sending the stored button's release or clearing self.drag. That leaves the inner mouse-aware app with a stuck button and cmux keeps ignoring new mouse downs until focus loss or another cancellation path; treat an unmatched Left release as ambiguous when no secondary button was observed.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs
Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
};
let (surface, handle, reservation_id, fallback, content, button) =
(*surface, handle.clone(), *reservation_id, release_bytes.clone(), *content, *button);
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {

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 Reject mismatched releases

This guard still lets an untracked non-active release finish the active PTY drag. If a secondary button was pressed before the drag started, or its Down was missed, remove(&reported_button) returns false and the code falls through to send the stored active button's release. The inner TUI can see the active drag end while the physical active button is still held. Reject every reported_button != button before encoding the active release.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5152 to +5153
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);

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 Send fallback release

This branch still drops the stored release after an accepted press. Drag::PtyMouse stores press-time release bytes, but mouse-up re-encodes against the terminal's current mouse mode. If the TUI disables or changes mouse reporting while the button is held, capture_pty_mouse_release can return NotReported; this branch then cancels the reservation without sending the stored bytes. The PTY app can receive button-down without button-up and remain stuck in a drag or selection state. The cancellation path needs the same fallback behavior.

@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: 30fa9c9808

ℹ️ 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 cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5134 to +5135
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle ambiguous secondary mouse releases

Fresh evidence: this version records secondary Down events, but the release gate still keys only off reported_button. On terminals where crossterm reports an unknown Up button as Left, releasing a secondary right/middle button during an active PTY drag arrives as Up(Left); when Left is the active button or is not the recorded secondary, this falls through, sends the active button's release, and clears self.drag. The real active release is then no longer forwarded, so mouse-aware apps can end up with mismatched button state; ambiguous Left releases should be consumed while any secondary button is outstanding.

Useful? React with 👍 / 👎.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5158 to +5159
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {
return true;

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 Reject Mismatched Releases This still lets an untracked non-active button-up finish the active PTY drag. The guard only consumes a mismatched release when that button was previously recorded in ignored_pty_mouse_buttons; if the secondary button was already down before the PTY drag started, its Down was missed, or the host only reports the Up, remove(&reported_button) returns false and execution falls through to encode the stored active button's release. That clears self.drag while the physical active button can still be held, so the inner TUI sees its drag or selection end early and the real active-button release is no longer forwarded. Every reported_button != button release should be consumed without sending the active release; the ignored set should only be bookkeeping.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5176 to +5178
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);
}

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 Send Stored Release This NotReported branch still drops the stored fallback release after an accepted press. A Drag::PtyMouse is only created after press-time release_bytes were captured and the press was queued with a reservation, but mouse-up re-encodes against the terminal's current mouse mode. If the TUI disables or changes mouse reporting while the button is held, capture_pty_mouse_release can return NotReported here; cancelling the reservation without enqueueing fallback leaves the PTY app with a button-down and no matching button-up. The cancel path below has the same NotReported behavior, so both paths need to send the stored fallback release before cancelling the reservation.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5161 to +5163
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {
return true;
}

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 Reject mismatched releases

This still only consumes a different button's release when that button was recorded in ignored_pty_mouse_buttons. If a left-up arrives while a right-button PTY drag is active, but the left-down was missed or happened outside this path, remove(&reported_button) returns false and the code falls through. That sends a right-button release and clears self.drag while the physical right button is still held, so the TUI can end its drag early and the real right-up is no longer forwarded.

Suggested change
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {
return true;
}
if reported_button != button {
self.ignored_pty_mouse_buttons.remove(&reported_button);
return true;
}

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5179 to +5180
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);

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 Send stored release

This NotReported branch still drops the stored fallback release after a press has already been accepted. A Drag::PtyMouse is only created after press-time release_bytes were captured and the press was queued with a release reservation, but mouse-up re-encodes against the terminal's current mouse mode. If the TUI disables or changes mouse reporting while the button is held, capture_pty_mouse_release() can return NotReported; cancelling the reservation without enqueueing the stored fallback leaves the PTY app with a button-down and no matching button-up. The cancellation path has the same branch, so focus loss or layout changes can hit the same missing release.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5161 to +5163
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {
return true;
}

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 Reject Mismatched Releases
This still lets a non-active Up finish the active PTY drag when that button was not recorded in ignored_pty_mouse_buttons. The guard only returns early for mismatched releases that had a prior in-drag Down; if a secondary button was already down before the PTY drag started, its Down was missed, or the host reports only the Up, remove(&reported_button) returns false and this falls through to encode the stored active button's release and clear self.drag. That can end the inner TUI drag while the physical active button is still held, and the real active-button release will no longer be forwarded. Every reported_button != button release should be consumed without sending the active release.

Suggested change
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {
return true;
}
if reported_button != button {
self.ignored_pty_mouse_buttons.remove(&reported_button);
return true;
}

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5179 to +5181
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);
}

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 Send Stored Release
This NotReported branch still drops the stored fallback release after an accepted press. A Drag::PtyMouse is only created after press-time release_bytes were captured, but mouse-up re-encodes against the terminal's current mouse mode. If the TUI disables or changes mouse reporting while the button is held, capture_pty_mouse_release() can return NotReported here. Canceling the reservation without enqueueing the stored fallback leaves the PTY app with a button-down and no matching button-up, so selections or drags can remain stuck after cmux clears local drag state.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5291 to +5293

fn enqueue_pty_release(

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 Cancel With Fallback
The cancel path can drop the same stored release. Focus loss, prompts, menus, or mutation barriers can call cancel_pty_mouse_drag after the PTY already received the press; if the app has since disabled or changed mouse reporting, the fresh release capture returns NotReported and this branch cancels the reservation without enqueueing the stored press-time fallback bytes. The inner TUI can remain stuck in a pressed or dragging state even though cmux clears its local drag.

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

ℹ️ 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 +521 to +523
return this.attachOnlyEvent(event.event)
|| event.event === "scroll-changed"
|| this.isSurfaceOverflow(event, surface);

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 Route browser attach events on shared transports

When a TypeScript client uses the default shared transport, attachSurface() only delivers events accepted by this predicate. Protocol v7 now has the server send browser-state as the initial attach event and frame updates for browser surfaces, but neither name is accepted here, so those events are discarded before the stream sees them; the attach request then resolves after the ok response and next() just times out. Add the browser attach event names (and frame decoding/types) to this path so the advertised browser attach stream works without requiring a dedicated transport.

Useful? React with 👍 / 👎.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5162 to +5164
if reported_button != button && self.ignored_pty_mouse_buttons.remove(&reported_button) {
return true;
}

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 Reject mismatched releases

This still lets an untracked non-active button-up finish the active PTY drag. The guard only consumes a mismatched release when that button was recorded in ignored_pty_mouse_buttons; if the secondary button was already down before the PTY drag started, its Down was missed, or the host reports only the Up, remove(&reported_button) returns false and execution falls through to encode the stored active button's release. That clears self.drag while the active button can still be held, so the inner TUI sees its drag or selection end early and the real active-button release is no longer forwarded.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5180 to +5182
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);
}

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 Send stored release

This branch still drops the stored release after an accepted press. A Drag::PtyMouse is only created after press-time release bytes were captured, but mouse-up re-encodes against the terminal's current mouse mode. If the TUI disables or changes mouse reporting while the button is held, capture_pty_mouse_release() can return NotReported; canceling the reservation without enqueueing the stored fallback leaves the PTY app with a button-down and no matching button-up, so selections or drags can remain stuck after cmux clears local drag state.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5292 to +5294
}

fn enque

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 Cancel with fallback

The cancellation path can drop the same stored release. Focus loss, prompts, menus, or mutation barriers can call cancel_pty_mouse_drag after the PTY already received the press; if the app has since disabled or changed mouse reporting, the fresh release capture returns NotReported and this branch cancels the reservation without enqueueing the stored press-time fallback bytes. The inner TUI can remain stuck in a pressed or dragging state even though cmux clears its local drag.

@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: 594215867d

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

cols: number;
rows: number;
}
export interface ResizeSurfaceResult { accepted: boolean }

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 Make accepted optional for legacy resize responses

When this SDK is used against a protocol 5/6 server, resize-surface still returns an empty {} data object; client.resizeSurface() compensates with result.accepted ?? true, but the generic request("resize-surface", ...) API infers this interface and tells callers accepted is always a boolean. Code using the typed generic request can therefore branch on undefined at runtime without any TypeScript warning; model accepted as optional or normalize the generic response too.

Useful? React with 👍 / 👎.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5193 to +5194
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);

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 Send fallback release

This branch still drops the paired release when the PTY stops reporting mouse before button-up. A Drag::PtyMouse only exists after press-time release_bytes were captured and the press was accepted with a reservation, but capture_pty_mouse_release() re-encodes against the terminal's current mouse mode. If the TUI disables or changes mouse reporting while the button is held, this path cancels the reservation without enqueueing the stored fallback, so the PTY app can receive a button-down without the matching button-up and keep a selection or drag stuck. The cancel path below has the same NotReported handling, so both paths need to enqueue the stored fallback before canceling the reservation.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5190 to +5192
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);
}

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 Stored Release Skipped

When a PTY app accepts a mouse press and then disables or changes mouse reporting before mouse-up, capture_pty_mouse_release can return NotReported. At this point the drag already has the press-time release_bytes, but this branch only cancels the reservation. The inner app can receive the button-down without the matching button-up and stay stuck in a drag or selection state after cmux clears its local drag. The cancel path has the same branch, so focus loss, prompts, menus, or layout changes can hit the same missing release.

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

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

protocol = &info.Protocol
}
if *protocol > 6 || (*protocol > 5 && !c.allowProtocolV6Attach) {
if *protocol > 7 || (*protocol > 5 && !c.allowProtocolV6Attach) {

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 Allow zero-value Go clients to attach to v7 servers

With the server now advertising protocol 7, callers using the normal zero-value Options path leave allowProtocolV6Attach false, so AttachSurface rejects every current server here before opening even PTY attach streams. The Go binding now parses the newer attach events, and the e2e only passes because it explicitly opts in; default Go clients should accept v7 or default this option to true.

Useful? React with 👍 / 👎.

protocol = self._protocol if self._protocol is not None else self.identify().protocol
if protocol > 7:
raise ProtocolError(f"unsupported protocol {protocol}; maximum supported is 7")
if protocol > 5 and not self.allow_protocol_v6_attach:

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 Allow default Python clients to attach to v7 servers

With protocol 7 now reported by current servers, CmuxClient() still defaults allow_protocol_v6_attach to false, so attach_surface() raises here for every default Python client unless callers know to pass a legacy-named opt-in. The parser now handles the newer resized replay/overflow fields and the Python e2e only succeeds because it opts in explicitly; default clients should accept v7 attach streams out of the box.

Useful? React with 👍 / 👎.

Comment thread cmux-tui/spec/events.md
| Attach stream v6 | `attach-surface` command | `vt-state`, `resized`, `output`, `colors-changed`, `scroll-changed`, `detached` |
| Subscribe stream | `subscribe` command | `tree-changed`, `layout-changed`, `surface-output`, `scroll-changed`, `surface-resized`, `surface-resize-failed`, `surface-exited`, `title-changed`, `bell`, `notification`, `config-reload-requested`, `window-title-requested`, `client-attached`, `client-changed`, `client-detached`, `empty`, `overflow` |
| Attach stream v5 | `attach-surface` command | `vt-state`, `output`, `detached`, `overflow` |
| Attach stream v6 | `attach-surface` command | `vt-state`, `resized`, `output`, `colors-changed`, `scroll-changed`, `detached`, `overflow` |

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 Document browser attach events in the v7 spec

This table is now the protocol-v7 event contract, but the attach stream row is still PTY-only. The server sends browser-state as the initial attach event and frame updates for browser surfaces, so bindings or conformance checks generated from this spec will continue to omit or drop the browser attach stream; add a v7 attach entry or include those event names here.

Useful? React with 👍 / 👎.

Comment thread cmux-tui/crates/cmux-tui/src/app.rs Outdated
Comment on lines +5186 to +5188
PtyMouseReleaseCapture::NotReported => {
self.pty_input.cancel_release_reservation(reservation_id);
}

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 Fallback Release Skipped

When a PTY app accepts a mouse press and then disables or changes mouse reporting before mouse-up, capture_pty_mouse_release can return NotReported. This branch then cancels the release reservation without sending the stored fallback bytes captured when the drag started. The inner app has already received the button-down, so it can stay in a pressed or dragging state after cmux clears local drag state. The cancellation path has the same NotReported behavior for focus loss, menus, and layout changes, so both paths need to enqueue the stored fallback release before cancelling the reservation.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@lawrencecchen
lawrencecchen merged commit a5304e5 into main Jul 15, 2026
6 checks passed
@lawrencecchen
lawrencecchen deleted the task-tui-mouse-passthrough branch July 15, 2026 17:10
@lawrencecchen
lawrencecchen restored the task-tui-mouse-passthrough branch July 18, 2026 10:19
@austinywang austinywang mentioned this pull request Jul 19, 2026
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