Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions src/tui/src/ui/harness_pane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,10 @@ impl LocalSessions {
/// the notch is forwarded and *its* scrollback moves — which is the one
/// the operator means, because it holds the whole conversation rather than
/// the last screenful the emulator happened to retain;
/// - Codex does not negotiate mouse reports in current releases. Once its
/// input layer is up (bracketed paste — the readiness signal the pane
/// reads from the emulator) its TUI
/// consumes cursor keys to move through the transcript, so a notch becomes
/// cursor-key input even when it does not advertise alternate scrolling;
/// - Codex accepts location-aware crossterm mouse events but does not
/// negotiate mouse reports in current releases. Once its input layer is
/// up, the notch is sent in SGR form so Codex receives the pane-relative
/// pointer location;
/// - another harness enables alternate scrolling without mouse reporting,
/// so the notch becomes cursor-key input as xterm's alternate-scroll mode
/// specifies;
Expand Down Expand Up @@ -144,15 +143,23 @@ impl LocalSessions {
return;
}
}
// Codex gets cursor keys only once its input layer is up: a codex that
// is still painting (or a shell standing in for one) has nothing to
// consume them, and sending them would only garble its first paint.
// Codex handles crossterm mouse events but currently does not emit the
// DECSET sequence that would make `mouse_protocol` select the branch
// above. Bracketed paste is its input-readiness signal: before then a
// report could only garble the first paint. SGR is deliberate because
// it carries coordinates; the old arrow-key fallback moved composer
// history and discarded where the wheel event happened.
let codex = self
.sessions
.row(session_id)
.is_some_and(|row| row.provider == medulla::protocol::HarnessProvider::Codex)
&& self.sessions.bracketed_paste(session_id) == Some(true);
if codex || self.sessions.alternate_scroll(session_id) == Some(true) {
if codex {
let bytes = mouse::sgr_wheel(col, row, up);
let _ = self.sessions.write(session_id, &bytes);
return;
}
if self.sessions.alternate_scroll(session_id) == Some(true) {
let arrow = if up { b"\x1b[A" } else { b"\x1b[B" };
let mut bytes = Vec::with_capacity(arrow.len() * rows);
for _ in 0..rows {
Expand Down
17 changes: 16 additions & 1 deletion src/tui/src/ui/harness_pane/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ fn sgr(button: u8, col: u16, row: u16, press: bool) -> Vec<u8> {
.into_bytes()
}

/// Encode a location-aware SGR wheel report without relying on negotiated mode.
///
/// Codex accepts crossterm mouse events but does not currently ask its terminal
/// to enable mouse reporting. The provider-specific fallback uses this after
/// its input layer is known to be ready, preserving the pointer coordinates
/// that cursor-key emulation necessarily loses.
pub(super) fn sgr_wheel(col: u16, row: u16, up: bool) -> Vec<u8> {
let button = if up {
BUTTON_WHEEL_UP
} else {
BUTTON_WHEEL_DOWN
};
sgr(button, col, row, true)
}

/// A normal (X10) report: `ESC [ M Cb Cx Cy`, every field a byte offset by 32.
fn normal(button: u8, col: u16, row: u16) -> Vec<u8> {
// Widened before the `+ 1`, not after. The clamp below bounds the *result*,
Expand Down Expand Up @@ -194,7 +209,7 @@ pub fn wheel(
Some(match encoding {
// A wheel notch has no release, so there is no matching `m` report to
// send after it — it is always encoded as a press.
MouseProtocolEncoding::Sgr => sgr(button, col, row, true),
MouseProtocolEncoding::Sgr => sgr_wheel(col, row, up),
// UTF-8 encoding (DECSET 1005) is a dead end almost nothing negotiates,
// and its multi-byte coordinates decode ambiguously. Treating it as the
// normal encoding is what xterm-compatible terminals do for the low
Expand Down
18 changes: 12 additions & 6 deletions src/tui/src/ui/harness_pane/tests/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,13 @@ fn alternate_scroll_without_mouse_reporting_gets_arrow_scroll_events() {
}

#[test]
fn codex_without_mouse_or_alternate_scroll_gets_arrow_scroll_events() {
fn codex_without_negotiated_mouse_mode_gets_location_aware_wheel_events() {
let sessions = PtyManager::new();
let harnesses = harnesses(sessions.clone());
// Current Codex releases enable bracketed paste and enhanced keyboard input,
// but no longer advertise DECSET 1007. The provider still expects wheel
// scrolling to reach its transcript as cursor keys.
// Current Codex releases enable bracketed paste and handle crossterm mouse
// events, but do not ask their terminal to enable mouse reporting. Preserve
// the pointer location by sending the SGR event Codex can already decode;
// cursor-key fallback would operate prompt history and discard (3,4).
let id = sessions
.open(sh(
"printf '\\033[?2004hready'; sleep 0.3; cat -v; sleep 30",
Expand All @@ -320,9 +321,14 @@ fn codex_without_mouse_or_alternate_scroll_gets_arrow_scroll_events() {

harnesses.scroll(&id, 3, 4, true, 3);

wait_for("Codex to receive translated wheel input", || {
text(&harnesses, &id).contains("^[[A^[[A^[[A")
wait_for("Codex to receive a location-aware wheel event", || {
text(&harnesses, &id).contains("[<64;4;5M")
});
let out = text(&harnesses, &id);
assert!(
!out.contains("^[[A"),
"wheel must not navigate history: {out}"
);
sessions.close(&id);
}

Expand Down
Loading