From e1a5f5c464e30240564cab5ddbbec286ee803c6e Mon Sep 17 00:00:00 2001 From: Hunter B Date: Thu, 11 Jun 2026 19:41:31 -0700 Subject: [PATCH] fix(tui): defensive mouse-report sanitizer and move provider-wait logging off render path Strip SGR mouse coordinate tails even when mouse capture is disabled, covering orphaned terminal reporting state after crashes or focus races (#3063/#3067). Move provider-wait incident logging from footer render to the main tick loop so stall diagnostics do not fire on every redraw (#3095 harvest note). Co-authored-by: Hunter Bown <101357273+Hmbown@users.noreply.github.com> Co-authored-by: Cursor --- crates/tui/src/tui/app.rs | 29 ++++++++++++++++++++++++----- crates/tui/src/tui/footer_ui.rs | 1 - crates/tui/src/tui/ui.rs | 1 + 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index a09b8157..5e3f2085 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -3821,9 +3821,6 @@ impl App { } fn strip_raw_mouse_reports_from_input(&mut self) { - if !self.use_mouse_capture { - return; - } if let Some((input, cursor_position)) = strip_raw_mouse_report_runs(&self.input, self.cursor_position) { @@ -5653,12 +5650,34 @@ mod tests { } #[test] - fn composer_keeps_mouse_like_text_when_mouse_capture_is_disabled() { + fn composer_strips_raw_sgr_mouse_report_when_mouse_capture_is_disabled() { let mut app = App::new(test_options(false), &Config::default()); app.insert_str("[<35;44;18M"); - assert_eq!(app.input, "[<35;44;18M"); + assert_eq!(app.input, ""); + assert_eq!(app.cursor_position, 0); + } + + #[test] + fn composer_strips_tail_only_mouse_report_burst_when_mouse_capture_is_disabled() { + let mut app = App::new(test_options(false), &Config::default()); + app.insert_str("draft "); + + app.insert_str(";76;20M35;74;22M35;73;23M"); + + assert_eq!(app.input, "draft "); + assert_eq!(app.cursor_position, "draft ".chars().count()); + } + + #[test] + fn composer_keeps_coordinate_like_text_when_mouse_capture_is_disabled() { + let mut app = App::new(test_options(false), &Config::default()); + + app.insert_str("Size 12;34M"); + + assert_eq!(app.input, "Size 12;34M"); + assert_eq!(app.cursor_position, "Size 12;34M".chars().count()); } #[test] diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index 9f55c6f7..37a996b4 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -93,7 +93,6 @@ pub(crate) fn render_footer(f: &mut Frame, area: Rect, app: &mut App) { if let Some(reason) = stall_reason(app) { label = format!("{label} ({reason})"); } - maybe_log_provider_wait_incident(app); props.state_label = label; props.state_color = palette::DEEPSEEK_SKY; diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 426a88a5..d0bc756c 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -2759,6 +2759,7 @@ async fn run_event_loop( // window. Triggers a redraw if the prompt was visible. app.tick_quit_armed(); app.tick_receipt(); + crate::tui::footer_ui::maybe_log_provider_wait_incident(app); // While the user is drag-selecting past the transcript edge, advance // the viewport on a fixed cadence and extend the selection head so a // long passage can be selected in one drag (#1163).