Skip to content

fix(tui): route IME-committed Chinese characters directly to composer instead of paste-burst buffer#2330

Open
donglovejava wants to merge 4 commits into
Hmbown:mainfrom
donglovejava:fix/ime-chinese-input-not-rendering-in-composer
Open

fix(tui): route IME-committed Chinese characters directly to composer instead of paste-burst buffer#2330
donglovejava wants to merge 4 commits into
Hmbown:mainfrom
donglovejava:fix/ime-chinese-input-not-rendering-in-composer

Conversation

@donglovejava
Copy link
Copy Markdown

@donglovejava donglovejava commented May 28, 2026

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:

  1. IME commits as individual KeyCode::Char(c) events, not as an Event::Paste. The bracketed_paste_seen flag stays false, so the paste-burst handler stays active for every character.

  2. Non-ASCII characters (!c.is_ascii()) fed into the same paste-burst pipeline as ASCIItry_append_char_if_active and on_plain_char_no_hold would 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.

  3. 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.rs

Non-ASCII characters now skip paste-burst buffering entirely:

  • note_plain_char(now) and extend_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() and on_plain_char_no_hold() are removed from the non-ASCII path. Characters are inserted directly via insert_char(c).

crates/tui/src/tui/paste_burst.rs

note_plain_char() visibility changed from fn (private) to pub(crate) so paste.rs can 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 via extend_window.
  • All 7 paste tests pass.

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_char and extend_window are 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 the ENABLE_WINDOW_INPUT console flag after crossterm's enable_raw_mode() clears it.

  • paste.rs: Non-ASCII KeyCode::Char events skip try_append_char_if_active / on_plain_char_no_hold and call insert_char directly, with flush_before_modified_input ensuring any in-flight ASCII burst is committed first.
  • paste_burst.rs: note_plain_char is widened to pub(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 after enable_raw_mode() in run_tui and at the top of recover_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

Filename Overview
crates/tui/src/tui/paste.rs Non-ASCII characters now bypass the paste-burst buffer entirely and are inserted directly; note_plain_char + extend_window are retained to keep timing state correct. A stale doc comment inside the test describes the old buffer-path behavior instead of the new direct-insert path.
crates/tui/src/tui/paste_burst.rs note_plain_char promoted to pub(crate) so paste.rs can call it directly. Both on_plain_char_no_hold and try_append_char_if_active lost their only call-sites in this PR and are now dead code, suppressed with #[allow(dead_code)].
crates/tui/src/tui/ui.rs Adds enable_windows_ime_console_mode() which re-sets ENABLE_WINDOW_INPUT after crossterm's enable_raw_mode() clears it. Correctly called after raw mode is enabled in run_tui and at the start of recover_terminal_modes, so focus-regain events also restore the flag.

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]
Loading

Comments Outside Diff (1)

  1. crates/tui/src/tui/paste_burst.rs, line 80-95 (link)

    P2 on_plain_char_no_hold is now dead code

    After this PR the only call-site for on_plain_char_no_hold was removed from the non-ASCII path in paste.rs. A codebase-wide search shows no remaining callers. Because the function is pub the 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 to on_plain_char is intended to be used.

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (4): Last reviewed commit: "fix: remove spurious #[cfg(target_os = w..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +68 to +69
app.paste_burst.note_plain_char(now);
app.paste_burst.extend_window(now);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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);
}

Comment on lines +68 to +69
app.paste_burst.note_plain_char(now);
app.paste_burst.extend_window(now);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/tui/paste.rs Outdated
@donglovejava
Copy link
Copy Markdown
Author

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`).

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 31, 2026

great point thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants