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 = {...@@ -183,12 +183,16 @@ export type ComposerCitationCommentRequest = {
183 value: string;183 value: string;
184 citationStart: number;184 citationStart: number;
185 sourceAnchor: AssistantCitationSourceAnchor;185 sourceAnchor: AssistantCitationSourceAnchor;
186 insertedSpaces: CitationInsertedSpaces;
186};187};
187188
189// Spaces added around a freshly inserted citation, removed with it if its comment is cancelled.
190type CitationInsertedSpaces = { before: boolean; after: boolean };
191
188type OpenCitationComment = {192type OpenCitationComment = {
189 key: string;193 key: string;
190 sourceAnchor?: AssistantCitationSourceAnchor;194 sourceAnchor?: AssistantCitationSourceAnchor;
191 removeOnCancel?: boolean;195 removeOnCancel?: CitationInsertedSpaces;
192};196};
193197
194const ComposerCitationCommentContext = createContext<{198const ComposerCitationCommentContext = createContext<{
...@@ -392,18 +396,20 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {...@@ -392,18 +396,20 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {
392 [editor, nodePos],396 [editor, nodePos],
393 );397 );
394398
399 // Undo a fresh insertion: drop the chip and the spaces inserted with it.
400 const removeOnCancel = commentTarget?.removeOnCancel;
395 const onRemove = useCallback(() => {401 const onRemove = useCallback(() => {
396 if (!editor.isEditable) return;402 if (!editor.isEditable || !removeOnCancel) return;
397 const pos = nodePos();403 const pos = nodePos();
398 if (pos === null) return;404 if (pos === null) return;
399 const current = editor.state.doc.nodeAt(pos);405 const { doc } = editor.state;
406 const current = doc.nodeAt(pos);
400 if (!current) return;407 if (!current) return;
401 editor408 const end = pos + current.nodeSize;
402 .chain()409 const from = removeOnCancel.before && doc.textBetween(pos - 1, pos) === " " ? pos - 1 : pos;
403 .focus()410 const to = removeOnCancel.after && doc.textBetween(end, end + 1) === " " ? end + 1 : end;
404 .deleteRange({ from: pos, to: pos + current.nodeSize })411 editor.chain().focus().deleteRange({ from, to }).run();
405 .run();412 }, [editor, nodePos, removeOnCancel]);
406 }, [editor, nodePos]);
407413
408 // Put the caret right after the chip so Enter sends and typing continues the prompt.414 // Put the caret right after the chip so Enter sends and typing continues the prompt.
409 const onRestoreFocus = useCallback(() => {415 const onRestoreFocus = useCallback(() => {
...@@ -450,7 +456,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {...@@ -450,7 +456,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {
450 if (open && !editor.isEditable) return;456 if (open && !editor.isEditable) return;
451 commentContext.onOpenChange(citeKey, open);457 commentContext.onOpenChange(citeKey, open);
452 },458 },
453 ...(commentTarget?.removeOnCancel ? { onCancel: onRemove } : {}),459 ...(removeOnCancel ? { onCancel: onRemove } : {}),
454 onSave: onSaveComment,460 onSave: onSaveComment,
455 onSaveAndSend: (comment) => {461 onSaveAndSend: (comment) => {
456 if (!onSaveComment(comment)) return false;462 if (!onSaveComment(comment)) return false;
...@@ -1531,7 +1537,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {...@@ -1531,7 +1537,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {
1531 setOpenCitation({1537 setOpenCitation({
1532 key: citeKey,1538 key: citeKey,
1533 sourceAnchor: pendingCitation.sourceAnchor,1539 sourceAnchor: pendingCitation.sourceAnchor,
1534 removeOnCancel: true,1540 removeOnCancel: pendingCitation.insertedSpaces,
1535 });1541 });
1536 }1542 }
1537 }1543 }
...@@ -1618,7 +1624,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {...@@ -1618,7 +1624,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {
1618 setOpenCitation({1624 setOpenCitation({
1619 key: citeKey,1625 key: citeKey,
1620 sourceAnchor: request.sourceAnchor,1626 sourceAnchor: request.sourceAnchor,
1621 removeOnCancel: true,1627 removeOnCancel: request.insertedSpaces,
1622 });1628 });
1623 }1629 }
1624 },1630 },
apps/web/src/components/chat/ChatComposer.tsx+13-2
...@@ -200,7 +200,11 @@ import {...@@ -200,7 +200,11 @@ import {
200 shouldUseRestingComposerLayout,200 shouldUseRestingComposerLayout,
201} from "../composerFooterLayout";201} from "../composerFooterLayout";
202import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";202import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";
203import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor";203import {
204 type ComposerCitationCommentRequest,
205 type ComposerPromptEditorHandle,
206 ComposerPromptEditor,
207} from "../ComposerPromptEditor";
204import {208import {
205 ComposerContextActionsContext,209 ComposerContextActionsContext,
206 composerContextRecordsFromDraft,210 composerContextRecordsFromDraft,
...@@ -3832,7 +3836,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -3832,7 +3836,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
3832 expectedText?: string;3836 expectedText?: string;
3833 expandedCursorAfterReplace?: number;3837 expandedCursorAfterReplace?: number;
3834 focusEditorAfterReplace?: boolean;3838 focusEditorAfterReplace?: boolean;
3835 citationComment?: { start: number; sourceAnchor: AssistantCitationSourceAnchor };3839 citationComment?: {
3840 start: number;
3841 sourceAnchor: AssistantCitationSourceAnchor;
3842 insertedSpaces: ComposerCitationCommentRequest["insertedSpaces"];
3843 };
3836 },3844 },
3837 ): boolean => {3845 ): boolean => {
3838 if (3846 if (
...@@ -3862,6 +3870,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -3862,6 +3870,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
3862 value: next.text,3870 value: next.text,
3863 citationStart: options.citationComment.start,3871 citationStart: options.citationComment.start,
3864 sourceAnchor: options.citationComment.sourceAnchor,3872 sourceAnchor: options.citationComment.sourceAnchor,
3873 insertedSpaces: options.citationComment.insertedSpaces,
3865 });3874 });
3866 }3875 }
3867 promptRef.current = next.text;3876 promptRef.current = next.text;
...@@ -6122,6 +6131,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -6122,6 +6131,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
6122 citationComment: {6131 citationComment: {
6123 start: cursor + (needsLeadingSpace ? 1 : 0),6132 start: cursor + (needsLeadingSpace ? 1 : 0),
6124 sourceAnchor: options.citationCommentAnchor,6133 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 },
6125 },6136 },
6126 focusEditorAfterReplace: false,6137 focusEditorAfterReplace: false,
6127 }6138 }