From 3143335fc3a568cbbb5174764961889272135cb9 Mon Sep 17 00:00:00 2001 From: Afonso Ferreira <99990310+Aforno@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:58:53 +0100 Subject: [PATCH] fix(web): cancelling a new citation no longer leaves a stray space (#16828) --- .../components/ComposerPromptEditorTiptap.tsx | 30 +++++++++++-------- apps/web/src/components/chat/ChatComposer.tsx | 15 ++++++++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index 6e830991ab7eb454b8d6da7c4e5c12973c93c2ff..ae2c6e460f263390a98a6b3dafc7be5a5ed7c56f 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -183,12 +183,16 @@ export type ComposerCitationCommentRequest = { value: string; citationStart: number; sourceAnchor: AssistantCitationSourceAnchor; + insertedSpaces: CitationInsertedSpaces; }; +// Spaces added around a freshly inserted citation, removed with it if its comment is cancelled. +type CitationInsertedSpaces = { before: boolean; after: boolean }; + type OpenCitationComment = { key: string; sourceAnchor?: AssistantCitationSourceAnchor; - removeOnCancel?: boolean; + removeOnCancel?: CitationInsertedSpaces; }; const ComposerCitationCommentContext = createContext<{ @@ -392,18 +396,20 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { [editor, nodePos], ); + // Undo a fresh insertion: drop the chip and the spaces inserted with it. + const removeOnCancel = commentTarget?.removeOnCancel; const onRemove = useCallback(() => { - if (!editor.isEditable) return; + if (!editor.isEditable || !removeOnCancel) return; const pos = nodePos(); if (pos === null) return; - const current = editor.state.doc.nodeAt(pos); + const { doc } = editor.state; + const current = doc.nodeAt(pos); if (!current) return; - editor - .chain() - .focus() - .deleteRange({ from: pos, to: pos + current.nodeSize }) - .run(); - }, [editor, nodePos]); + const end = pos + current.nodeSize; + const from = removeOnCancel.before && doc.textBetween(pos - 1, pos) === " " ? pos - 1 : pos; + const to = removeOnCancel.after && doc.textBetween(end, end + 1) === " " ? end + 1 : end; + editor.chain().focus().deleteRange({ from, to }).run(); + }, [editor, nodePos, removeOnCancel]); // Put the caret right after the chip so Enter sends and typing continues the prompt. const onRestoreFocus = useCallback(() => { @@ -450,7 +456,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { if (open && !editor.isEditable) return; commentContext.onOpenChange(citeKey, open); }, - ...(commentTarget?.removeOnCancel ? { onCancel: onRemove } : {}), + ...(removeOnCancel ? { onCancel: onRemove } : {}), onSave: onSaveComment, onSaveAndSend: (comment) => { if (!onSaveComment(comment)) return false; @@ -1531,7 +1537,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { setOpenCitation({ key: citeKey, sourceAnchor: pendingCitation.sourceAnchor, - removeOnCancel: true, + removeOnCancel: pendingCitation.insertedSpaces, }); } } @@ -1618,7 +1624,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { setOpenCitation({ key: citeKey, sourceAnchor: request.sourceAnchor, - removeOnCancel: true, + removeOnCancel: request.insertedSpaces, }); } }, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 4923e1fc47e7f61c16a6f3d64cd72aacd33de785..c7eb2628e1c9a66844ce39be8f93e29a50b66948 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -200,7 +200,11 @@ import { shouldUseRestingComposerLayout, } from "../composerFooterLayout"; import { measureRestingComposerControls } from "./restingComposerControlsMeasurement"; -import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor"; +import { + type ComposerCitationCommentRequest, + type ComposerPromptEditorHandle, + ComposerPromptEditor, +} from "../ComposerPromptEditor"; import { ComposerContextActionsContext, composerContextRecordsFromDraft, @@ -3832,7 +3836,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) expectedText?: string; expandedCursorAfterReplace?: number; focusEditorAfterReplace?: boolean; - citationComment?: { start: number; sourceAnchor: AssistantCitationSourceAnchor }; + citationComment?: { + start: number; + sourceAnchor: AssistantCitationSourceAnchor; + insertedSpaces: ComposerCitationCommentRequest["insertedSpaces"]; + }; }, ): boolean => { if ( @@ -3862,6 +3870,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) value: next.text, citationStart: options.citationComment.start, sourceAnchor: options.citationComment.sourceAnchor, + insertedSpaces: options.citationComment.insertedSpaces, }); } promptRef.current = next.text; @@ -6122,6 +6131,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) citationComment: { start: cursor + (needsLeadingSpace ? 1 : 0), sourceAnchor: options.citationCommentAnchor, + // An existing space after the cursor is consumed and re-emitted, so it stays. + insertedSpaces: { before: needsLeadingSpace, after: rangeEnd === cursor }, }, focusEditorAfterReplace: false, } -- 2.54.0