Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions desktop/src/features/messages/lib/useRichTextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
);
Expand Down Expand Up @@ -950,7 +959,7 @@ export function useRichTextEditor({
isEmpty,
clearContent,
setContent,
setContentAndFocusEnd,
restorePlainTextAndFocusEnd,
focus,
focusEnd,
focusPreserve,
Expand Down
6 changes: 3 additions & 3 deletions desktop/src/features/messages/ui/useMentionSendFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ type UseMentionSendFlowOptions = {
>;
richText: Pick<
UseRichTextEditorResult,
"clearContent" | "setContent" | "setContentAndFocusEnd"
"clearContent" | "setContent" | "restorePlainTextAndFocusEnd"
>;
setContent: (content: string) => void;
setIsEmojiPickerOpen: React.Dispatch<React.SetStateAction<boolean>>;
Expand Down Expand Up @@ -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([]);
Expand All @@ -352,7 +352,7 @@ export function useMentionSendFlow({
mentions.cancelMentionAutocomplete,
mentions.clearMentions,
richText.clearContent,
richText.setContentAndFocusEnd,
richText.restorePlainTextAndFocusEnd,
setContent,
setIsEmojiPickerOpen,
setPendingImeta,
Expand Down
15 changes: 14 additions & 1 deletion desktop/tests/e2e/persistent-agent-audience.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand Down
Loading