Suppress stale terminal mouse reports after reconnect
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-07-04 22:56:32 -04:00
parent b1e8326b0a
commit 252f081a61
+71 -2
View File
@@ -63,6 +63,8 @@ const STREAM_INITIAL_WINDOW: usize = 1024 * 1024;
const MAX_PENDING_USER_INPUT_BYTES: usize = 1024 * 1024;
const STARTUP_INPUT_HOLD: Duration = Duration::from_millis(750);
const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2);
const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(2);
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
/// represents an SSH-agent connection (rather than a TCP target). The client
@@ -4376,6 +4378,7 @@ async fn run_terminal(
let mut pending_user_input_bytes = 0usize;
let mut startup_input_hold_until: Option<Instant> = None;
let mut startup_gate_mode = StartupGateMode::HoldControl;
let mut stale_terminal_input_suppress_until: Option<Instant> = None;
if let Some(frame) = first_frame {
if !forward_only {
render_frame(&frame)?;
@@ -4433,10 +4436,19 @@ async fn run_terminal(
biased;
stdin_msg = stdin_rx.recv() => {
match stdin_msg {
Some(bytes) => {
Some(mut bytes) => {
if input_matches_escape(&bytes, escape_key.as_deref()) {
break;
}
if should_strip_stale_terminal_reports(
last_packet_at.elapsed(),
stale_terminal_input_suppress_until,
) {
bytes = strip_stale_mouse_reports(&bytes);
if bytes.is_empty() {
continue;
}
}
if cred.mode != "view-only" && cred.mode != "forward-only" {
if let Some(deadline) = startup_input_hold_until {
if !predictor.alternate_screen
@@ -4535,6 +4547,9 @@ async fn run_terminal(
.await?
{
refresh_live_addr(&mut addr, &cred)?;
arm_stale_terminal_input_suppression(
&mut stale_terminal_input_suppress_until,
);
if !forward_only {
render_frame(&frame)?;
predictor.observe_output(&frame.bytes);
@@ -4594,6 +4609,9 @@ async fn run_terminal(
.await?
{
refresh_live_addr(&mut addr, &cred)?;
arm_stale_terminal_input_suppression(
&mut stale_terminal_input_suppress_until,
);
if !forward_only {
render_frame(&frame)?;
predictor.observe_output(&frame.bytes);
@@ -4644,6 +4662,9 @@ async fn run_terminal(
.await?
{
refresh_live_addr(&mut addr, &cred)?;
arm_stale_terminal_input_suppression(
&mut stale_terminal_input_suppress_until,
);
if !forward_only {
render_frame(&frame)?;
predictor.observe_output(&frame.bytes);
@@ -4765,6 +4786,9 @@ async fn run_terminal(
.await?
{
refresh_live_addr(&mut addr, &cred)?;
arm_stale_terminal_input_suppression(
&mut stale_terminal_input_suppress_until,
);
if !forward_only {
render_frame(&frame)?;
predictor.observe_output(&frame.bytes);
@@ -5157,6 +5181,9 @@ async fn run_terminal(
.await?
{
refresh_live_addr(&mut addr, &cred)?;
arm_stale_terminal_input_suppression(
&mut stale_terminal_input_suppress_until,
);
if !forward_only {
render_frame(&frame)?;
predictor.observe_output(&frame.bytes);
@@ -5562,6 +5589,18 @@ fn should_flush_terminal_input_after_contact(
!forward_only && !pending_empty && mode != "view-only" && mode != "forward-only"
}
fn should_strip_stale_terminal_reports(
silent_for: Duration,
suppress_until: Option<Instant>,
) -> bool {
silent_for >= STALE_TERMINAL_INPUT_AFTER
|| suppress_until.is_some_and(|deadline| Instant::now() < deadline)
}
fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) {
*suppress_until = Some(Instant::now() + POST_RECONNECT_STALE_INPUT_GRACE);
}
async fn flush_pending_user_input(
socket: &UdpSocket,
addr: SocketAddr,
@@ -5623,6 +5662,7 @@ fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option<usize> {
match bytes.get(offset).copied()? {
0x1b if bytes.get(offset + 1) == Some(&b'[') => match bytes.get(offset + 2).copied() {
Some(b'M') if offset + 6 <= bytes.len() => Some(6),
Some(b'I' | b'O') => Some(3),
Some(b'<') => mouse_report_params_len(bytes, offset + 3, 2).map(|len| len + 3),
Some(byte) if byte.is_ascii_digit() => {
mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2)
@@ -5631,13 +5671,15 @@ fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option<usize> {
},
0x9b => match bytes.get(offset + 1).copied() {
Some(b'M') if offset + 5 <= bytes.len() => Some(5),
Some(b'I' | b'O') => Some(2),
Some(b'<') => mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2),
Some(byte) if byte.is_ascii_digit() => {
mouse_report_params_len(bytes, offset + 1, 2).map(|len| len + 1)
}
_ => None,
},
byte if byte.is_ascii_digit() => mouse_report_params_len(bytes, offset, 1),
byte if byte.is_ascii_digit() => mouse_report_params_len(bytes, offset, 1)
.or_else(|| orphan_mouse_suffix_len(bytes, offset)),
_ => None,
}
}
@@ -5659,6 +5701,21 @@ fn mouse_report_params_len(bytes: &[u8], offset: usize, min_semicolons: usize) -
None
}
fn orphan_mouse_suffix_len(bytes: &[u8], offset: usize) -> Option<usize> {
if offset != 0 {
return None;
}
let mut cursor = offset;
while let Some(byte) = bytes.get(cursor).copied() {
match byte {
b'0'..=b'9' => cursor += 1,
b'M' | b'm' if cursor > offset => return Some(cursor + 1 - offset),
_ => return None,
}
}
None
}
fn stale_mouse_report_maybe_incomplete(bytes: &[u8], offset: usize) -> bool {
let Some(rest) = bytes.get(offset..) else {
return false;
@@ -8495,6 +8552,18 @@ mod tests {
assert_eq!(strip_stale_mouse_reports(b"152;1Mls\r"), b"ls\r");
}
#[test]
fn split_stale_mouse_suffixes_are_stripped_from_pending_input() {
assert_eq!(strip_stale_mouse_reports(b"1M35;149;1Mls\r"), b"ls\r");
assert_eq!(strip_stale_mouse_reports(b"10m"), b"");
}
#[test]
fn stale_focus_reports_are_stripped_from_pending_input() {
assert_eq!(strip_stale_mouse_reports(b"\x1b[Ihello\x1b[O"), b"hello");
assert_eq!(strip_stale_mouse_reports(b"\x9bIhello\x9bO"), b"hello");
}
#[test]
fn non_mouse_escape_input_survives_stale_filter() {
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");