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:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user