From f66215b36560b8db63cfe28aa9320541e05d4022 Mon Sep 17 00:00:00 2001 From: Haoran Xu <3230105281@zju.edu.cn> Date: Tue, 16 Jun 2026 13:45:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20prevent=20IME=20composition=20char?= =?UTF-8?q?acter=20loss=20at=20non-terminal=20cur=E2=80=A6=20(#8811)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/components/chat/ChatInput.vue | 33 ++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/dashboard/src/components/chat/ChatInput.vue b/dashboard/src/components/chat/ChatInput.vue index 2eb05373d..74109d532 100644 --- a/dashboard/src/components/chat/ChatInput.vue +++ b/dashboard/src/components/chat/ChatInput.vue @@ -523,7 +523,14 @@ const filteredCommands = computed(() => { const localPrompt = computed({ get: () => props.prompt, - set: (value) => emit("update:prompt", value), + set: (value) => { + // Suppress v-model sync during IME composition to avoid a reactive + // feedback loop. Vue's :value binding overwrites the native textarea + // DOM state mid-composition, which interferes with IME insertion at + // non-terminal cursor positions (alternating character loss). + // The final value is synced manually in handleCompositionEnd. + if (!isComposing.value) emit("update:prompt", value); + }, }); const sessionPlatformId = computed( @@ -768,6 +775,30 @@ function handleCompositionStart() { function handleCompositionEnd(e: CompositionEvent) { lastCompositionEndAt.value = e.timeStamp; clearCompositionState({ keepLastEndAt: true }); + + // Manually sync the final composited text to the parent component + // after the IME commits. The v-model setter is suppressed during + // composition (see localPrompt computed), so we must explicitly + // propagate the DOM value once composition ends. + // + // Capture the DOM value at compositionend to guard against a race + // where props.prompt is externally updated between now and nextTick. + const endValue = inputField.value?.value; + + nextTick(() => { + const el = inputField.value; + // Only sync if the DOM hasn't been changed externally in the meantime. + if (el && el.value === endValue && el.value !== props.prompt) { + emit("update:prompt", el.value); + // Re-evaluate command suggestions that were suppressed during IME + // composition (handleInput checks isComposing). Only needed when + // the value actually changed. Runs in a nested nextTick so + // props.prompt reflects the emit above. + nextTick(() => { + handleInput(); + }); + } + }); } function clearCompositionState({ keepLastEndAt = false } = {}) {