fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(web): key ChatMarkdown renderers on thread ids, not object identity - #9310

Closed
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref
Closed

fix(web): key ChatMarkdown renderers on thread ids, not object identity#9310
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/stable-markdown-thread-ref

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

Follow-up to #9306. ChatMarkdown's markdownComponents memo depends on threadRef, and react-markdown uses those renderer functions as React element types. Any caller that passes a fresh-but-equal threadRef object therefore remounts every rendered <p>/<pre>/<code> in the message — which is what was destroying native text selections and dropping the cite toolbar on working threads. #9306 fixed the one caller doing that, but the component stayed one careless parseScopedThreadKey(...) away from regressing.

Fix

Memoize threadRef on environmentId + threadId at the top of ChatMarkdown, before anything downstream depends on it. Every existing use (markdownComponents, the PR-link callbacks, the media effect, the child memo comparators) now sees a stable object as long as the ids are unchanged, regardless of what the caller passes.

Verified with vp lint, apps/web typecheck, and the ChatMarkdown tests (69 passing).


Claude Fable 5 via Claude Code

🤖 Generated with Claude Code


Note

Low Risk
Small, localized React memoization in chat markdown rendering with no auth, data, or API changes.

Overview
Stabilizes threadRef inside ChatMarkdown so react-markdown does not treat every new object reference as a renderer change and remount the whole message tree (which was clearing native text selection and breaking the cite toolbar).

The prop is renamed to threadRefProp and wrapped in useMemo keyed on environmentId and threadId. Downstream logic (markdownComponents, PR-link handlers, media preview, effects) keeps the same API but sees a stable reference while the scoped thread is unchanged, even if parents pass a freshly allocated ScopedThreadRef each render.

Reviewed by Cursor Bugbot for commit 834f807. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix ChatMarkdown to key renderers on thread IDs, not object identity

Memoizes the thread reference inside ChatMarkdown using environment and thread IDs so an equivalent-but-new thread object does not create a new renderer identity. This preserves native text selections inside rendered markdown nodes across re-renders when the IDs are unchanged.

Macroscope summarized 834f807.

ChatMarkdown's react-markdown component map depends on threadRef, and
react-markdown uses those renderers as element types, so any caller that
hands over a fresh-but-equal threadRef object remounts every rendered
markdown node. #9306 fixed the one caller that did this; this makes the
component itself immune by memoizing threadRef on environmentId and
threadId before anything downstream depends on it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.2 KiB−219 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.3 KiB−224 B (−3.3%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB+20 B (+0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+5 B (+0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB+15 B (+0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 355fbd9 · PR result: 834f807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 834f807

Macroscope's review found this PR approvable — This is a narrowly scoped one-file fix that stabilizes existing ChatMarkdown renderer identity without introducing new capabilities, schema changes, defaults, or sensitive behavior. Renderer updates still occur when the environment or thread ID changes.

You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge