diff --git a/frontend-web/src/components/workspace/messages/message-list-item.tsx b/frontend-web/src/components/workspace/messages/message-list-item.tsx index a1242072..37b94528 100644 --- a/frontend-web/src/components/workspace/messages/message-list-item.tsx +++ b/frontend-web/src/components/workspace/messages/message-list-item.tsx @@ -358,6 +358,20 @@ function MessageImage({ ); } +// While a notebook citation marker is still streaming in (e.g. "[[cite:64" or +// "[[cite:101]"), Streamdown's parseIncompleteMarkdown treats the trailing +// unclosed "[" as an incomplete link and rewrites it to +// "[[cite:64](streamdown:incomplete-link)", which the rehypeCitations regex can +// no longer match — so the citation flickers as a broken link mid-stream. We +// strip the half-arrived marker so it stays hidden until the closing "]]" +// arrives, at which point rehypeCitations renders it. A complete +// "[[cite:101]]" has no trailing incomplete marker, so this is a no-op. +const INCOMPLETE_CITE_TAIL = /\[\[(?:c(?:i(?:t(?:e(?::\d*\]?)?)?)?)?)?$/; + +function stripIncompleteCitationMarker(content: string): string { + return content.replace(INCOMPLETE_CITE_TAIL, ''); +} + function MessageContent_({ className, message, @@ -460,7 +474,8 @@ function MessageContent_({ ? stripArtifactTag(stripUploadedFilesTag(rawContent)) : ''; } - return applyInlineCitations(rawContent ?? '', displaySources); + const withCitations = applyInlineCitations(rawContent ?? '', displaySources); + return stripIncompleteCitationMarker(withCitations); }, [rawContent, isHuman, displaySources]); const parsedHumanContent = useMemo( () =>