diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 6f989ef9b2fc..9f956fe65a7b 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -1839,6 +1839,33 @@ describe("chat composer IME composition", () => { expect(onSend).not.toHaveBeenCalled(); expect(onHistoryKeydown).not.toHaveBeenCalled(); }); + + it("does not force textarea resize during IME composition", () => { + const container = renderChatView({}); + const textarea = requireElement( + container, + ".agent-chat__composer-combobox > textarea", + "composer textarea", + ) as HTMLTextAreaElement; + + // Set a sentinel height to detect unwanted overwrites + textarea.style.height = "42px"; + + textarea.dispatchEvent(new CompositionEvent("compositionstart", { bubbles: true })); + textarea.value = "shi"; + textarea.dispatchEvent(new InputEvent("input", { bubbles: true, isComposing: true })); + textarea.value = "shichang"; + textarea.dispatchEvent(new InputEvent("input", { bubbles: true, isComposing: true })); + + // Height must stay untouched — no forced reflow during composition + expect(textarea.style.height).toBe("42px"); + + textarea.value = "市场"; + textarea.dispatchEvent(new CompositionEvent("compositionend", { bubbles: true })); + + // After composition ends, adjustTextareaHeight runs via syncComposerValue + expect(textarea.style.height).not.toBe("42px"); + }); }); describe("chat slash menu accessibility", () => { diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index ae20677a0321..812a2b969d59 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -2422,7 +2422,10 @@ export function renderChat(props: ChatProps) { const handleInput = (e: InputEvent) => { const target = e.target as HTMLTextAreaElement; if (vs.composerComposing || e.isComposing) { - adjustTextareaHeight(target); + // Skip adjustTextareaHeight during IME composition — each pinyin + // keystroke fires `input` and the height read/write forces a + // synchronous reflow that blocks the composition thread. + // Resize runs once in handleCompositionEnd → syncComposerValue. draftMirror.value = target.value; return; }