From 1c76dbefe40f320e9bb318be4b592711b5e2c161 Mon Sep 17 00:00:00 2001 From: Serhii Dziupin Date: Mon, 17 Aug 2026 14:24:39 +0300 Subject: [PATCH] fix(chat): defer composer value writeback during IME composition (Fixes #2527) (#2691) * fix(chat): defer composer value writeback during IME composition The controlled-writeback effect compared the value prop against the CodeMirror document and, on mismatch, dispatched a wholesale replacement with the caret forced to the end. While the browser composes (pinyin, kana, hangul) the uncommitted text lives in the DOM, not in the document, so the mismatch is expected and the dispatch interrupted the IME session and jumped the cursor. Skip the writeback while the view is composing, using CodeMirror's public compositionStarted getter; the composition commits through its own pipeline and reports via onChange. Fixes #2527 * fix(chat): preserve external composer writes during IME * fix(chat): restore composition-wide writeback guard --------- Co-authored-by: Bohdan Triapitsyn --- .../chat/composer/editor/ComposerEditor.tsx | 4 +++ .../writebackCompositionGuard.test.ts | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/ui/src/components/chat/composer/editor/__tests__/writebackCompositionGuard.test.ts diff --git a/packages/ui/src/components/chat/composer/editor/ComposerEditor.tsx b/packages/ui/src/components/chat/composer/editor/ComposerEditor.tsx index 41a2825f..c248a53f 100644 --- a/packages/ui/src/components/chat/composer/editor/ComposerEditor.tsx +++ b/packages/ui/src/components/chat/composer/editor/ComposerEditor.tsx @@ -344,6 +344,10 @@ export const ComposerEditor = React.forwardRef { + const start = composerEditorSource.indexOf('// Controlled value:'); + expect(start).toBeGreaterThan(-1); + const end = composerEditorSource.indexOf('}, [value]);', start); + expect(end).toBeGreaterThan(start); + return composerEditorSource.slice(start, end); +}; + +describe('composer value writeback composition guard (issue #2527)', () => { + test('checks equality, then composition, before dispatching', () => { + const effect = writebackEffect(); + const equalityCheck = effect.indexOf('if (current === value) return;'); + const compositionGuard = effect.indexOf('if (view.compositionStarted) return;'); + const dispatch = effect.indexOf('view.dispatch({'); + + expect(equalityCheck).toBeGreaterThan(-1); + expect(compositionGuard).toBeGreaterThan(equalityCheck); + expect(dispatch).toBeGreaterThan(compositionGuard); + }); +});