From 2af4da45ec24d849c1c707eacbc3bd61891c469c Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Thu, 25 Jun 2026 03:47:28 +0800 Subject: [PATCH] fix(dashboard): prevent React from dropping first keystroke during IME composition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React 18's root-level event delegation intercepts keydown events with keyCode 229 (the "composition in progress" signal sent by the browser during non-Latin IME input) and synthesises an onCompositionStart event. That synthetic path sets internal composing state that interferes with xterm.js's own IME handling on its hidden textarea, causing the first keystroke of each composition chunk to be silently dropped. The fix adds a capture-phase keydown listener on the terminal host div that stops propagation of keyCode-229 events before they reach React's delegation layer. xterm.js relies on native compositionstart/ compositionend on its internal textarea — not on keydown — so blocking the propagation is safe. Fixes #52111 --- web/src/pages/ChatPage.tsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index ea6d9f36e5..c97d247cc2 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -748,6 +748,30 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { let mobileInputCleanup: (() => void) | null = null; term.open(host); + // IME composition guard (fixes #52111). + // + // React 18's root-level event delegation intercepts keydown events with + // keyCode 229 (the "composition in progress" signal sent by the browser + // during non-Latin IME input) and synthesises an onCompositionStart + // event. That synthetic path sets internal composing state that + // interferes with xterm.js's own IME handling on its hidden textarea, + // causing the first keystroke of each composition chunk to be silently + // dropped — most visible with Cyrillic (Ukrainian/Russian) on + // Firefox-based browsers, but affects any locale that uses composition + // events (CJK, Arabic, Hebrew). + // + // xterm.js relies on native compositionstart/compositionend on its + // internal textarea, not on keydown, so blocking the keyCode-229 + // keydown from reaching React's delegation layer is safe. The listener + // sits in the *capture* phase on the terminal host so it fires before + // the event bubbles up to the React root. + const _imeCompositionGuard = (e: KeyboardEvent) => { + if (e.keyCode === 229 || e.key === "Process") { + e.stopPropagation(); + } + }; + host.addEventListener("keydown", _imeCompositionGuard, true); + const textarea = term.textarea; if (textarea) { textarea.setAttribute("autocomplete", "off"); @@ -1331,6 +1355,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { // the ticket fetch resolves and ``wsRef.current`` was never assigned. wsRef.current?.close(); wsRef.current = null; + host.removeEventListener("keydown", _imeCompositionGuard, true); term.dispose(); termRef.current = null; fitRef.current = null;