authorgravatar for 99990310+Aforno@users.noreply.github.comAfonso Ferreira <99990310+Aforno@users.noreply.github.com> 2026-10-07 23:58:53+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 17:58:53-05:00
log3143335fc3a568cbbb5174764961889272135cb9
tree112c81e1487de6d945d57da3495f99a71b6bfbf5
parent46dd0949053ae387e785c5e9801725a9ccc28de1
signature Signed by PGP key B5690EEEBB952194

fix(web): cancelling a new citation no longer leaves a stray space (#16828)


2 files changed, 31 insertions(+), 14 deletions(-)

apps/web/src/components/ComposerPromptEditorTiptap.tsx+18-12
......@@ -183,12 +183,16 @@ export type ComposerCitationCommentRequest = {
183183 value: string;
184184 citationStart: number;
185185 sourceAnchor: AssistantCitationSourceAnchor;
186 insertedSpaces: CitationInsertedSpaces;
186187};
187188
189// Spaces added around a freshly inserted citation, removed with it if its comment is cancelled.
190type CitationInsertedSpaces = { before: boolean; after: boolean };
191
188192type OpenCitationComment = {
189193 key: string;
190194 sourceAnchor?: AssistantCitationSourceAnchor;
191 removeOnCancel?: boolean;
195 removeOnCancel?: CitationInsertedSpaces;
192196};
193197
194198const ComposerCitationCommentContext = createContext<{
......@@ -392,18 +396,20 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {
392396 [editor, nodePos],
393397 );
394398
399 // Undo a fresh insertion: drop the chip and the spaces inserted with it.
400 const removeOnCancel = commentTarget?.removeOnCancel;
395401 const onRemove = useCallback(() => {
396 if (!editor.isEditable) return;
402 if (!editor.isEditable || !removeOnCancel) return;
397403 const pos = nodePos();
398404 if (pos === null) return;
399 const current = editor.state.doc.nodeAt(pos);
405 const { doc } = editor.state;
406 const current = doc.nodeAt(pos);
400407 if (!current) return;
401 editor
402 .chain()
403 .focus()
404 .deleteRange({ from: pos, to: pos + current.nodeSize })
405 .run();
406 }, [editor, nodePos]);
408 const end = pos + current.nodeSize;
409 const from = removeOnCancel.before && doc.textBetween(pos - 1, pos) === " " ? pos - 1 : pos;
410 const to = removeOnCancel.after && doc.textBetween(end, end + 1) === " " ? end + 1 : end;
411 editor.chain().focus().deleteRange({ from, to }).run();
412 }, [editor, nodePos, removeOnCancel]);
407413
408414 // Put the caret right after the chip so Enter sends and typing continues the prompt.
409415 const onRestoreFocus = useCallback(() => {
......@@ -450,7 +456,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {
450456 if (open && !editor.isEditable) return;
451457 commentContext.onOpenChange(citeKey, open);
452458 },
453 ...(commentTarget?.removeOnCancel ? { onCancel: onRemove } : {}),
459 ...(removeOnCancel ? { onCancel: onRemove } : {}),
454460 onSave: onSaveComment,
455461 onSaveAndSend: (comment) => {
456462 if (!onSaveComment(comment)) return false;
......@@ -1531,7 +1537,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {
15311537 setOpenCitation({
15321538 key: citeKey,
15331539 sourceAnchor: pendingCitation.sourceAnchor,
1534 removeOnCancel: true,
1540 removeOnCancel: pendingCitation.insertedSpaces,
15351541 });
15361542 }
15371543 }
......@@ -1618,7 +1624,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {
16181624 setOpenCitation({
16191625 key: citeKey,
16201626 sourceAnchor: request.sourceAnchor,
1621 removeOnCancel: true,
1627 removeOnCancel: request.insertedSpaces,
16221628 });
16231629 }
16241630 },
apps/web/src/components/chat/ChatComposer.tsx+13-2
......@@ -200,7 +200,11 @@ import {
200200 shouldUseRestingComposerLayout,
201201} from "../composerFooterLayout";
202202import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";
203import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor";
203import {
204 type ComposerCitationCommentRequest,
205 type ComposerPromptEditorHandle,
206 ComposerPromptEditor,
207} from "../ComposerPromptEditor";
204208import {
205209 ComposerContextActionsContext,
206210 composerContextRecordsFromDraft,
......@@ -3832,7 +3836,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
38323836 expectedText?: string;
38333837 expandedCursorAfterReplace?: number;
38343838 focusEditorAfterReplace?: boolean;
3835 citationComment?: { start: number; sourceAnchor: AssistantCitationSourceAnchor };
3839 citationComment?: {
3840 start: number;
3841 sourceAnchor: AssistantCitationSourceAnchor;
3842 insertedSpaces: ComposerCitationCommentRequest["insertedSpaces"];
3843 };
38363844 },
38373845 ): boolean => {
38383846 if (
......@@ -3862,6 +3870,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
38623870 value: next.text,
38633871 citationStart: options.citationComment.start,
38643872 sourceAnchor: options.citationComment.sourceAnchor,
3873 insertedSpaces: options.citationComment.insertedSpaces,
38653874 });
38663875 }
38673876 promptRef.current = next.text;
......@@ -6122,6 +6131,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
61226131 citationComment: {
61236132 start: cursor + (needsLeadingSpace ? 1 : 0),
61246133 sourceAnchor: options.citationCommentAnchor,
6134 // An existing space after the cursor is consumed and re-emitted, so it stays.
6135 insertedSpaces: { before: needsLeadingSpace, after: rangeEnd === cursor },
61256136 },
61266137 focusEditorAfterReplace: false,
61276138 }