From f267d4b874862f0e389fc1613cb58ac9af22976d Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 10 May 2026 10:30:57 -0500 Subject: [PATCH] fix(tui): paste-burst detects short CJK first line as paste (#1302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked from @reidliu41's PR #1342. Pasting `请联网搜索:\n…` (short non-ASCII first line + newline) used to fail the `decide_begin_buffer` heuristic — `grabbed.chars().any(is_whitespace)` is false on a 6-codepoint Chinese run, and `chars().count() >= 16` is false at 6 chars — so the trailing pasted newline fell through as a real Enter and submitted the first line on its own. The heuristic now also treats `!grabbed.is_ascii()` as paste-like, which captures the CJK case without false-firing on ASCII typing (plain ASCII typists still need either whitespace or 16+ chars to look like a paste). Includes the regression test from PR #1342, slightly reworded. Closes #1302. Thanks @reidliu41. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 9 +++++++++ crates/tui/src/tui/paste.rs | 32 +++++++++++++++++++++++++++++++ crates/tui/src/tui/paste_burst.rs | 11 +++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) 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 {