Compare commits

...

1 Commits

Author SHA1 Message Date
Naiyuan Qing
05e3552c6b fix(editor): keep code-block selection stable during background re-renders (MUL-3621)
Selecting text in a readonly code block (comment/issue markdown) lost the
selection within seconds, making copy impossible, whenever the surrounding
view re-rendered — most reliably while a sibling agent task streamed over
WebSocket (a re-render roughly every ~100ms).

Root cause: the `code` renderer emits highlighted HTML via
`dangerouslySetInnerHTML={{ __html }}`, a fresh prop object every render. Each
unrelated parent re-render re-ran react-markdown, and React rewrote the
`<code>` innerHTML even though the HTML string was byte-identical, tearing down
and rebuilding all 161 hljs `<span>` nodes. The native selection is anchored to
those nodes, so it collapsed.

Fix: memoize the entire `<ReactMarkdown>` subtree on its only real inputs
(`processed` + `components`). A stable element reference lets React bail out of
the subtree on unrelated re-renders, so the code-block DOM is never rebuilt
while content is unchanged. Confirmed via an instrumentation probe: zero
`<code>` DOM mutations during streaming after the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:42:17 +08:00

View File

@@ -425,21 +425,36 @@ export const ReadonlyContent = memo(function ReadonlyContent({
// <Attachment>, which reads the surrounding AttachmentDownloadProvider.
const components = useMemo(() => buildComponents(), []);
// Memoize the whole react-markdown subtree on its only real inputs
// (`processed` + `components`). Unrelated parent re-renders (e.g. a sibling
// agent task streaming over WebSocket fires one every ~100ms) would otherwise
// re-run react-markdown, which hands `<code>` a fresh `dangerouslySetInnerHTML`
// object each time; React then rewrites the highlighted innerHTML even though
// the HTML string is byte-identical, tearing down and rebuilding every hljs
// <span> — which collapses any active text selection inside a code block
// (MUL-3621). A stable element reference lets React bail out of the subtree.
const markdown = useMemo(
() => (
<ReactMarkdown
remarkPlugins={[
[remarkMath, { singleDollarTextMath: false }],
remarkBreaks,
[remarkGfm, { singleTilde: false }],
]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema], rehypeKatex]}
urlTransform={urlTransform}
components={components}
>
{processed}
</ReactMarkdown>
),
[processed, components],
);
return (
<AttachmentDownloadProvider attachments={attachments}>
<div ref={wrapperRef} className={cn("rich-text-editor readonly text-sm", className)}>
<ReactMarkdown
remarkPlugins={[
[remarkMath, { singleDollarTextMath: false }],
remarkBreaks,
[remarkGfm, { singleTilde: false }],
]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema], rehypeKatex]}
urlTransform={urlTransform}
components={components}
>
{processed}
</ReactMarkdown>
{markdown}
<LinkHoverCard {...hover} />
</div>
</AttachmentDownloadProvider>