fix(tui): route IME-committed Chinese characters directly to composer instead of paste-burst buffer#2330
Conversation
… instead of paste-burst buffer
There was a problem hiding this comment.
Code Review
This pull request updates the TUI paste-burst handling to insert non-ASCII characters (such as IME-committed CJK characters) directly into the composer instead of buffering them, preventing character loss from slow IME commits. Feedback on the changes highlights an issue where unconditionally calling extend_window on every non-ASCII character can cause manual typing of CJK characters to trigger the Enter-suppression window, resulting in unexpected newlines. A suggestion was provided to conditionally extend the window only when a paste burst is active.
| app.paste_burst.note_plain_char(now); | ||
| app.paste_burst.extend_window(now); |
There was a problem hiding this comment.
Calling extend_window(now) unconditionally for every non-ASCII character means that during manual typing of Chinese/Japanese/Korean characters, the Enter-suppression window will be opened and extended on every keystroke. If a user types a CJK character and presses Enter within 120ms (which is very common for fast typists), the Enter key will insert a newline instead of submitting the message.
To fix this, we should only call extend_window(now) if a paste burst is actually active or detected. We can achieve this by calling on_plain_char_no_hold(now) (which internally calls note_plain_char(now)) and checking if it returns Some(_), indicating a burst is active or has just been triggered.
| app.paste_burst.note_plain_char(now); | |
| app.paste_burst.extend_window(now); | |
| if app.paste_burst.on_plain_char_no_hold(now).is_some() { | |
| app.paste_burst.extend_window(now); | |
| } |
| app.paste_burst.note_plain_char(now); | ||
| app.paste_burst.extend_window(now); |
There was a problem hiding this comment.
Unconditional
extend_window regresses Enter-submit for single-char IME input
extend_window is now called for every non-ASCII character, including slow IME commits. This sets burst_window_until = now + 120ms unconditionally, so any Enter keypress within 120ms of a non-ASCII char on a non-bracketed-paste terminal will hit the newline_should_insert_instead_of_submit guard and insert \n instead of submitting.
In the old code, on_plain_char_no_hold only set burst_window_until when consecutive_plain_char_burst >= PASTE_BURST_MIN_CHARS (i.e., a real burst was detected). For slow IME typing (gaps > 8ms), the counter resets each time, on_plain_char_no_hold returned None, and burst_window_until was never updated. A quick Enter after an IME commit correctly submitted.
After this change, a user who types a CJK character via IME and immediately presses Enter (common for users who confirm the IME candidate with Enter and then submit with another quick Enter within ~100ms) will see a newline inserted instead of submission on SSH/tmux sessions. Consider guarding extend_window to only fire when consecutive_plain_char_burst exceeds the burst threshold, matching the conditions under which the old code would have set the window.
|
Added a second fix: on Windows, crossterm's `enable_raw_mode()` clears the `ENABLE_WINDOW_INPUT` console mode flag. Without this flag, the IME compositor cannot commit characters on some Windows configurations. The new `enable_windows_ime_console_mode()` function re-adds `ENABLE_WINDOW_INPUT` after `enable_raw_mode()` and on every `FocusGained` recovery (via `recover_terminal_modes`). |
…ort on non-Windows
|
great point thank you! |
Summary
On terminals without bracketed paste (e.g., Windows Terminal on first session, SSH, tmux), Chinese input method (IME) committed characters were silently consumed by the paste-burst heuristic and landed in the burst buffer instead of the composer. The user saw nothing on screen despite typing.
Three factors converged:
IME commits as individual
KeyCode::Char(c)events, not as anEvent::Paste. Thebracketed_paste_seenflag staysfalse, so the paste-burst handler stays active for every character.Non-ASCII characters (
!c.is_ascii()) fed into the same paste-burst pipeline as ASCII —try_append_char_if_activeandon_plain_char_no_holdwould buffer them when the burst heuristic decided typing was "paste-like", but IME-committed characters arrive at human speed (tens of milliseconds between each character), too slow for the burst flush timer to commit them promptly.The burst buffer never flushed because IME typing progresses in bursts far exceeding
PASTE_BURST_ACTIVE_IDLE_TIMEOUT, so buffered characters lingered indefinitely until the next real keystroke.Changes
crates/tui/src/tui/paste.rsNon-ASCII characters now skip paste-burst buffering entirely:
note_plain_char(now)andextend_window(now)are still called so the burst timing counter advances and the Enter-suppression window stays open during a rapid non-ASCII sequence — this preserves correct behavior for non-bracketed-paste CJK content.try_append_char_if_active()andon_plain_char_no_hold()are removed from the non-ASCII path. Characters are inserted directly viainsert_char(c).crates/tui/src/tui/paste_burst.rsnote_plain_char()visibility changed fromfn(private) topub(crate)sopaste.rscan call it directly.Verification
ime_chinese_chars_route_through_to_composer— IME (slow) Chinese input now lands in composer directly.raw_short_cjk_multiline_paste_buffers_enter_instead_of_submitting— fast CJK paste (0ms intervals) still enters the Enter-suppression path viaextend_window.Greptile Summary
This PR fixes a silent input-loss bug for Chinese/Japanese/Korean IME on terminals without bracketed paste: IME-committed characters now bypass the paste-burst buffer entirely and are inserted directly into the composer, while
note_plain_charandextend_windoware still called to keep timing state and Enter-suppression working for fast non-ASCII pastes. A Windows-only helper (enable_windows_ime_console_mode) also restores theENABLE_WINDOW_INPUTconsole flag aftercrossterm'senable_raw_mode()clears it.paste.rs: Non-ASCIIKeyCode::Charevents skiptry_append_char_if_active/on_plain_char_no_holdand callinsert_chardirectly, withflush_before_modified_inputensuring any in-flight ASCII burst is committed first.paste_burst.rs:note_plain_charis widened topub(crate); two now-unused helpers (on_plain_char_no_hold,try_append_char_if_active) are silenced with#[allow(dead_code)].ui.rs(Windows-only):enable_windows_ime_console_mode()is called afterenable_raw_mode()inrun_tuiand at the top ofrecover_terminal_modes, so the IME console flag is restored on startup and on every focus-regain event.Confidence Score: 5/5
Safe to merge. The non-ASCII bypass is well-scoped and the timing state is preserved so fast CJK pastes still get Enter-suppression. The Windows IME console flag is restored at the right points in startup and recovery.
The core change is a narrowly targeted bypass for non-ASCII characters that avoids the burst buffer while keeping timing counters correct. The Windows console flag helper is idempotent and best-effort. No existing test contracts are broken, and the two newly dead helper functions are correctly silenced.
No files require special attention beyond the stale doc comment in the test body and the two now-dead helpers in paste_burst.rs.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["KeyCode::Char(c) received"] --> B{is ASCII?} B -->|No - CJK/IME| C[flush pending ASCII burst] C --> D[note_plain_char + extend_window] D --> F[insert_char directly into composer] B -->|Yes - ASCII| G[on_plain_char - CharDecision] G --> H{Decision} H -->|RetainFirstChar| I[hold in pending_first_char] H -->|BeginBuffer / BufferAppend| J[append to paste burst buffer] J --> K[flush on idle timeout or next non-ASCII char] K --> L[insert_str into composer]Comments Outside Diff (1)
crates/tui/src/tui/paste_burst.rs, line 80-95 (link)on_plain_char_no_holdis now dead codeAfter this PR the only call-site for
on_plain_char_no_holdwas removed from the non-ASCII path inpaste.rs. A codebase-wide search shows no remaining callers. Because the function ispubthe compiler won't warn, but it can safely be removed to reduce surface area and avoid confusing future readers who might try to understand when this alternative toon_plain_charis intended to be used.Reviews (4): Last reviewed commit: "fix: remove spurious #[cfg(target_os = w..." | Re-trigger Greptile