diff --git a/desktop/src/features/messages/lib/useRichTextEditor.ts b/desktop/src/features/messages/lib/useRichTextEditor.ts index e800787ca7a..aeb7198b4c5 100644 --- a/desktop/src/features/messages/lib/useRichTextEditor.ts +++ b/desktop/src/features/messages/lib/useRichTextEditor.ts @@ -742,17 +742,26 @@ export function useRichTextEditor({ [editor], ); - const setContentAndFocusEnd = React.useCallback( - (markdown: string) => { + /** + * Replace the editor document with literal plain text and focus its end. + * + * Unlike markdown `setContent`, this preserves trailing whitespace. The + * transaction is marked as programmatic so authored-update observers do not + * reconcile against the intermediate post-send restoration. + */ + const restorePlainTextAndFocusEnd = React.useCallback( + (text: string) => { if (!editor) return; - // The caller already synchronizes composer state. Keep this programmatic - // restoration out of user-edit observers (autocomplete/reconciliation), - // then move selection in the same command chain. - editor - .chain() - .setContent(markdown, { emitUpdate: false }) - .focus("end") - .run(); + const paragraph = editor.schema.nodes.paragraph.create( + null, + text ? editor.schema.text(text) : undefined, + ); + const tr = editor.state.tr + .replaceWith(0, editor.state.doc.content.size, paragraph) + .setMeta("preventUpdate", true); + tr.setSelection(TextSelection.atEnd(tr.doc)); + editor.view.dispatch(tr); + editor.view.focus(); }, [editor], ); @@ -950,7 +959,7 @@ export function useRichTextEditor({ isEmpty, clearContent, setContent, - setContentAndFocusEnd, + restorePlainTextAndFocusEnd, focus, focusEnd, focusPreserve, diff --git a/desktop/src/features/messages/ui/useMentionSendFlow.ts b/desktop/src/features/messages/ui/useMentionSendFlow.ts index cc51c733453..4a721dcfed9 100644 --- a/desktop/src/features/messages/ui/useMentionSendFlow.ts +++ b/desktop/src/features/messages/ui/useMentionSendFlow.ts @@ -73,7 +73,7 @@ type UseMentionSendFlowOptions = { >; richText: Pick< UseRichTextEditorResult, - "clearContent" | "setContent" | "setContentAndFocusEnd" + "clearContent" | "setContent" | "restorePlainTextAndFocusEnd" >; setContent: (content: string) => void; setIsEmojiPickerOpen: React.Dispatch>; @@ -334,7 +334,7 @@ export function useMentionSendFlow({ setContent(postSendContent); contentRef.current = postSendContent; if (postSendContent) { - richText.setContentAndFocusEnd(postSendContent); + richText.restorePlainTextAndFocusEnd(postSendContent); mentions.cancelMentionAutocomplete(); } else richText.clearContent(); setPendingImeta([]); @@ -352,7 +352,7 @@ export function useMentionSendFlow({ mentions.cancelMentionAutocomplete, mentions.clearMentions, richText.clearContent, - richText.setContentAndFocusEnd, + richText.restorePlainTextAndFocusEnd, setContent, setIsEmojiPickerOpen, setPendingImeta, diff --git a/desktop/tests/e2e/persistent-agent-audience.spec.ts b/desktop/tests/e2e/persistent-agent-audience.spec.ts index ae424b4e5c8..9636f2c7505 100644 --- a/desktop/tests/e2e/persistent-agent-audience.spec.ts +++ b/desktop/tests/e2e/persistent-agent-audience.spec.ts @@ -179,11 +179,24 @@ test("persistent agents transition atomically before Enter-send resolves", async // to ProseMirror coordinates proves selection.from/to === doc.content.size. return { empty: selection.isCollapsed, + text: element.textContent, + endsWithSpace: element.textContent?.endsWith(" ") ?? false, atDocumentEnd: position + 1 === viewDesc.size - 2, }; }), ) - .toEqual({ empty: true, atDocumentEnd: true }); + .toEqual({ + empty: true, + text: "@Morgarita ", + endsWithSpace: true, + atDocumentEnd: true, + }); + + // Exercise the reported contract, not just its selection prerequisites: + // immediate typing after restoration must remain outside the mention chip. + await input.pressSequentially("testing"); + await expect(input).toHaveText("@Morgarita testing"); + await expect(input.locator(".agent-mention-highlight")).toHaveCount(1); }); test("timeline agent send remains one-shot and returns to the placeholder", async ({