From 191f04ac4a7f05db383f43dd9f56bea1c0cc9382 Mon Sep 17 00:00:00 2001 From: xxiaoxiong <2482929840@qq.com> Date: Thu, 28 May 2026 21:15:09 +0800 Subject: [PATCH] fix: preserve cursor position when inserting async references Capture mention state snapshot before async operations (applyProjectSkill, applyById) to prevent cursor position issues. Previously, the mention state could be invalidated during async operations, causing the cursor to land in the wrong position after inserting skills or plugins. Changes: - Add optional mentionSnapshot parameter to replaceMentionWithText() - Capture mention state before async calls in insertSkillMention() - Capture mention state before async calls in insertPluginMention() - Use captured snapshot instead of potentially stale mention state This ensures the cursor position is calculated from the original mention context, not from state that may have changed during re-renders triggered by async operations. Fixes #3195 --- apps/web/src/components/ChatComposer.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/ChatComposer.tsx b/apps/web/src/components/ChatComposer.tsx index d63300c89..3fa33755e 100644 --- a/apps/web/src/components/ChatComposer.tsx +++ b/apps/web/src/components/ChatComposer.tsx @@ -1331,7 +1331,14 @@ export const ChatComposer = forwardRef( const prevEntries = pluginInsertedTokensRef.current; const prevActiveId = activePluginIdRef.current; - const result = replaceMentionWithText(`${inlineMentionToken(record.title)} `); + // Capture mention state before async operation to prevent cursor + // position issues if state changes during apply. + const mentionSnapshot = mention; + + const result = replaceMentionWithText( + `${inlineMentionToken(record.title)} `, + mentionSnapshot, + ); if (!result) return; // Capture the post-insert draft *snapshot* — the value the // composer is in immediately after our optimistic write. @@ -1468,10 +1475,12 @@ export const ChatComposer = forwardRef( function replaceMentionWithText( text: string, + mentionSnapshot?: typeof mention, ): { insertStart: number } | null { - if (!mention) return null; + const activeMention = mentionSnapshot ?? mention; + if (!activeMention) return null; const ta = textareaRef.current; - const cursor = mention.cursor; + const cursor = activeMention.cursor; const before = draft.slice(0, cursor); const after = draft.slice(cursor); const replaced = before.replace(/(^|\s)@([^\s@]*)$/, `$1${text}`);