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
This commit is contained in:
xxiaoxiong 2026-05-28 21:15:09 +08:00 committed by mrcfps
parent e8c179d3a6
commit 191f04ac4a

View file

@ -1331,7 +1331,14 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
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<ChatComposerHandle, Props>(
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}`);