mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(desktop): preserve agent mention separator after send (#5623)
**Category:** fix **User Impact:** Typing immediately after sending to a persistently addressed agent now continues after the agent mention instead of corrupting it. **Problem:** Post-send restoration passed the persistent `@Agent ` prefix through the Markdown parser, which discarded its trailing separator and left WebKit rendering the caret at the mention boundary. **Solution:** Restore the prefix as literal ProseMirror text, preserve the separator, and focus a selection placed at the restored document end. This does not expand or otherwise change the setting’s existing scope: persistent addressed agents remain thread-only. <details> <summary>File changes</summary> **desktop/src/features/messages/lib/useRichTextEditor.ts** Adds a focused plain-text restoration helper that preserves trailing whitespace while suppressing authored-update reconciliation. **desktop/src/features/messages/ui/useMentionSendFlow.ts** Routes non-empty post-send persistent audience restoration through the literal-text helper instead of Markdown content loading. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Extends the real Enter-send flow to assert the preserved separator, document-end selection, and immediate typing outside the agent mention. </details> ## Reproduction steps 1. Open a thread with a persistently addressed agent. 2. Send a message with Enter. 3. Confirm the composer restores the addressed agent and a trailing space. 4. Type immediately without clicking the composer. 5. Confirm the new text appears after the agent mention and the mention remains highlighted. https://github.com/user-attachments/assets/92f088aa-a516-48d1-acde-35e29f558f14 --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
Reference in New Issue
Block a user