fix(tui): paste-burst detects short CJK first line as paste (#1302)

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) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-10 10:30:57 -05:00
parent 8bf2557692
commit f267d4b874
3 changed files with 50 additions and 2 deletions
+9
View File
@@ -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.
+32
View File
@@ -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();
+9 -2
View File
@@ -193,8 +193,15 @@ impl PasteBurst {
) -> Option<RetroGrab> {
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 {