From 8e629b9f386d12b726bccb32e9d7b48402ea73ea Mon Sep 17 00:00:00 2001 From: xxxigm Date: Fri, 5 Jun 2026 14:31:23 +0700 Subject: [PATCH] fix(desktop): flush committed IME text on compositionend so the send button appears MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing committed multi-character IME text (e.g. Chinese "你好", and equally Japanese/Korean or any IME-composed script) left the send button hidden until an unrelated edit. Input events during composition carry uncommitted preedit text and are intentionally skipped; the code assumed a trailing input event after compositionend would deliver the finalized text, but Chromium does not reliably emit one on Windows IMEs. The committed text therefore never reached composer state, so `hasComposerPayload` stayed false and the send button stayed hidden (deleting a char fired a non-composition input that finally synced it). Flush the live editor text into composer state in onCompositionEnd. Extract the shared sync into flushEditorToDraft so input and compositionend both update state. Fixes #39614 --- apps/desktop/src/app/chat/composer/index.tsx | 36 ++++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 2288a7b7f82..51ed8cae8ad 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -549,16 +549,10 @@ export function ChatBar({ } }, [trigger]) - const handleEditorInput = (event: FormEvent) => { - // During IME composition the DOM contains uncommitted preedit text - // mixed with real content. Skip state writes — compositionend will - // deliver the finalized text via a clean input event. - if (composingRef.current) { - return - } - - const editor = event.currentTarget - + // Pull the live contentEditable text into draftRef + the AUI composer state + // (which drives `hasComposerPayload` → the send button). Shared by the input + // and compositionend paths so committed IME text reaches state through either. + const flushEditorToDraft = (editor: HTMLDivElement) => { if (editor.childNodes.length === 1 && editor.firstChild?.nodeName === 'BR') { editor.replaceChildren() } @@ -573,6 +567,17 @@ export function ChatBar({ window.setTimeout(refreshTrigger, 0) } + const handleEditorInput = (event: FormEvent) => { + // During IME composition the DOM contains uncommitted preedit text + // mixed with real content. Skip state writes — compositionend flushes + // the finalized text (see onCompositionEnd). + if (composingRef.current) { + return + } + + flushEditorToDraft(event.currentTarget) + } + const triggerAdapter: Unstable_TriggerAdapter | null = trigger?.kind === '@' ? at.adapter : trigger?.kind === '/' ? slash.adapter : null @@ -1208,8 +1213,17 @@ export function ChatBar({ data-placeholder={placeholder} data-slot={RICH_INPUT_SLOT} onBlur={() => window.setTimeout(closeTrigger, 80)} - onCompositionEnd={() => { + onCompositionEnd={event => { composingRef.current = false + + // The input events fired *during* composition were skipped (they + // carried uncommitted preedit text), and Chromium does NOT reliably + // emit a trailing input event after compositionend on Windows IMEs. + // Without flushing here, committed multi-character IME input (e.g. + // Chinese "你好", Japanese, Korean) never reaches composer state, so + // `hasComposerPayload` stays false and the send button stays hidden + // until an unrelated edit forces a sync (#39614). + flushEditorToDraft(event.currentTarget) }} onCompositionStart={() => { composingRef.current = true