diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e474b7b..44fa75fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,6 +177,15 @@ Big thanks to every contributor below. typing / IME commits / autocomplete bursts. Terminals that never deliver bracketed paste (the original target audience) are unaffected; the heuristic still fires there. +- **Short CJK multi-line paste no longer auto-submits first line** + (#1302) — pasting `请联网搜索:\nSTM32 …` (short non-ASCII first line + followed by a newline) used to fail the paste-burst detection + heuristic because the first line had no whitespace and was under + the 16-char threshold; the trailing pasted newline then fell + through as a real Enter and submitted the first line on its own. + The heuristic now treats any non-ASCII run as paste-like, so the + Enter is absorbed into the burst buffer. Thanks **@reidliu41** + (PR #1342). - **HTTP 400 quota errors retried** (#1203) — some OpenAI-compatible gateways return quota/rate-limit errors as HTTP 400 instead of 429. These are now classified as retryable `RateLimited` errors. diff --git a/crates/tui/src/tui/paste.rs b/crates/tui/src/tui/paste.rs index 4e3dc78f..6cbbced4 100644 --- a/crates/tui/src/tui/paste.rs +++ b/crates/tui/src/tui/paste.rs @@ -165,6 +165,38 @@ mod tests { KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE) } + #[test] + fn raw_short_cjk_multiline_paste_buffers_enter_instead_of_submitting() { + // #1302: pasting short CJK content like "请联网搜索:\nSTM32 …" used + // to silently submit the first line because the heuristic decided + // it wasn't paste-like (no whitespace + under 16 chars). The + // non-ASCII bypass now classifies it as a paste so the Enter is + // absorbed into the burst buffer. + let mut app = test_app(); + let t0 = Instant::now(); + + let pasted = "请联网搜索:\nSTM32 商业应用案例"; + for (i, ch) in pasted.chars().enumerate() { + let key = if ch == '\n' { + KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE) + } else { + plain(ch) + }; + let handled = + handle_paste_burst_key(&mut app, &key, t0 + Duration::from_millis(i as u64)); + assert!( + handled, + "raw paste character {ch:?} must be handled by paste-burst detection" + ); + } + + assert!(app.flush_paste_burst_if_due( + t0 + Duration::from_millis(pasted.chars().count() as u64) + + crate::tui::paste_burst::PasteBurst::recommended_active_flush_delay() + )); + assert_eq!(app.input, pasted); + } + #[test] fn raw_multiline_paste_buffers_enter_instead_of_submitting() { let mut app = test_app(); diff --git a/crates/tui/src/tui/paste_burst.rs b/crates/tui/src/tui/paste_burst.rs index 35ee9ad3..62788451 100644 --- a/crates/tui/src/tui/paste_burst.rs +++ b/crates/tui/src/tui/paste_burst.rs @@ -193,8 +193,15 @@ impl PasteBurst { ) -> Option { let start_byte = retro_start_index(before, retro_chars); let grabbed = before[start_byte..].to_string(); - let looks_pastey = - grabbed.chars().any(char::is_whitespace) || grabbed.chars().count() >= 16; + // Short CJK first-line pastes (e.g. "请联网搜索:" copied from a web + // chat) used to fail the heuristic — no whitespace and under the + // 16-char threshold meant the trailing pasted newline fell through + // as a real Enter and submitted the first line on its own. + // Treating any non-ASCII run as paste-like fixes this without + // false-firing on ASCII typing (#1302, PR #1342 from @reidliu41). + let looks_pastey = grabbed.chars().any(char::is_whitespace) + || !grabbed.is_ascii() + || grabbed.chars().count() >= 16; if looks_pastey { self.begin_with_retro_grabbed(grabbed.clone(), now); Some(RetroGrab {