fix(web): copying an alert callout keeps its alert Markdown #35932
main fromrefs/pull/35932/head·merged by juliusmarminge View on GitHubBoth review items were correct. When a selection starts and ends inside a callout, its common ancestor is the callout div, and Range.cloneContents() returns the paragraphs without the wrapper or its data-markdown-alert attribute. The alert branch never ran, and because the label was copy-empty the clipboard got only 'Back up first.', which was worse than main. Fix: (1) the label paragraph loses data-markdown-copy="" and gets data-markdown-alert-label instead, so it copies normally as text. A new serializeQuoteBody, used by serializeBlockquote and serializeInsight, skips that label only when the wrapper itself is serialized and the [!KIND] marker or ★ Insight rule replaces it. (2) chatMarkdownClipboardPayload finds the enclosing [data-markdown-alert] callout from the range's common ancestor. When the selected text (trimmed) equals the callout's full text, it wraps the cloned contents in a shallow clone of the callout, so the alert serializer runs and the copy is '> [!WARNING]\n> Back up first.'. A partial selection inside the callout copies as text, like main ('Warning\n\nBack up'). Selections that extend outside the callout already clone the wrapper and are unchanged. No new regex or parser. The added comparison is linear in the callout's text length.
Fixes pingdotgg/t3code#35894
Reproduction on main#
Commit ed2a319814 (origin/main). In a fresh worktree: vp i, then (scratch only, not committed) added > [!WARNING]\n> Back up first. to the final agent message of apps/server/scripts/dev-replay/showcase_transcript.ndjson. Ran T3CODE_DEV_SEED=0 vp run dev, added an empty project with t3 project add, ran a free Replay (dev) turn with t3 thread start --provider devReplay --model showcase --wait, minted a pairing token with t3 pair, and opened the thread in headless Chromium (Playwright, because the T3 preview browser is blocked by AppArmor here). Selected the whole rendered div[role=note] callout, pressed Ctrl+C, and read navigator.clipboard. The DOM was <div role="note"><p data-thread-find-ignore>…svg…Warning</p><p>Back up first.</p></div> and the plain-text clipboard held "Warning\n\nBack up first.": no [!WARNING] marker and no > prefixes. Added a unit test to apps/web/src/markdown-clipboard.test.ts with the same DOM shape. It fails with exactly that string, and the other 11 tests pass.
Evidence#
Select the whole rendered warning callout, press Ctrl+C, and read the browser clipboard. Both sides use fresh synthetic Replay state, Chromium at 1280×800, and dark theme. The labeled panel displays actual clipboard text.
| Before | After |
|---|---|
| ed2a319814 | 7a73f2aef0 |
![]() | ![]() |
| Before recording | After recording |
Clipboard: Warning\n\nBack up first. | Clipboard: > [!WARNING]\n> Back up first. |
Driver and replay seeding, server launcher, launcher helper.
Base clipboard and DOM, head clipboard and DOM.
Not shown:
- Other alert types and partial callout selections were not exercised; this proof covers the reported whole-warning selection in the web client.
Tests#
vp test run src/components/ChatMarkdown.browser.test.tsx -t "alert copy" (new browser test: real Range from label start to body end expects alert Markdown; a partial range expects 'Warning\n\nBack up'): passes with the change, fails with 540e0ed's markdown-clipboard.ts and ChatMarkdown.tsx (missing '> [!WARNING]'). vp run check:changed: format, lint, knip, typecheck (2 packages), tests (3 files incl. markdown-clipboard.test.ts), react compiler all pass.
Not in this change#
- The HTML clipboard flavor (sanitizedHtmlFrom) still includes the rendered alert label paragraph; only the plain-text Markdown was in scope.
- The HTML clipboard flavor (sanitizedHtmlFrom) still includes the rendered alert label paragraph; only the plain-text Markdown is in scope.
Who did the work#
- Written by Claude Opus 5.5, in Claude Code
- Reviewed by GPT-6.1 Sol, in Codex, and by Claude Opus 5.5, each on its own
- Proof captured by GPT-6.1 Sol and checked by Claude Opus 5.5
Run by the t3code factory in T3 Code.
- 1
tanstack-source added labelsbug
github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:trusted
github-actions[bot](GitHub profile, opens in a new tab) added labelssize:M
- apps/web/src/markdown-clipboard.ts lines 506 to 508@@ -478,3 +500,9 @@478500 }479501 continue;480502 }503+ // A range spanning exactly one callout's text clones its contents but not the callout.504+ const callout = ancestorElement?.closest(`[${ALERT_ATTRIBUTE}]`);505+ if (callout && range.toString().trim() === (callout.textContent ?? "").trim()) {506+ const wrapper = callout.cloneNode(false) as Element;507+ wrapper.append(...container.childNodes);508+ container.replaceChildren(wrapper);
🟡 Medium
src/markdown-clipboard.ts:506Selecting the rendered label of a marker-only alert serializes an extra
> Warningbody line instead of an empty alert.range.cloneContents()omits thedata-markdown-alert-labelparagraph when the range selects only its text, and the shallow callout clone then makes that text look like body content; preserve the label wrapper when reconstructing the callout.container.replaceChildren(callout.cloneNode(true));🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/markdown-clipboard.ts around lines 506-508: Selecting the rendered label of a marker-only alert serializes an extra `> Warning` body line instead of an empty alert. `range.cloneContents()` omits the `data-markdown-alert-label` paragraph when the range selects only its text, and the shallow callout clone then makes that text look like body content; preserve the label wrapper when reconstructing the callout. Approvability#
Verdict: Would Approve
Macroscope's review found this PR approvable — This change preserves alert Markdown when copying rendered chat callouts. Its exposure is limited to clipboard formatting; the reported marker-only selection issue remains subject to the separate correctness gate.
Needs human approval because:
- 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity
No actionable comments were generated in the recent review.
Walkthrough
-
Rendered alert callouts now carry metadata for clipboard serialization. Copying a complete callout preserves its alert format, while copying part of its text produces plain selected text.
Changes: Callout Clipboard Serialization
Layer / File(s) Summary Mark rendered alert elements
apps/web/src/components/ChatMarkdown.tsxRendered alert containers now expose the alert kind, and their heading paragraphs have an alert-label marker. Serialize callout selections
apps/web/src/markdown-clipboard.ts,apps/web/src/markdown-clipboard.test.ts,apps/web/src/components/ChatMarkdown.browser.test.tsxThe serializer emits Markdown alert syntax for warning callouts and a labeled, divided format for Insight callouts. It omits rendered alert labels from the body and preserves callout metadata when the selection covers the complete callout. Tests cover both serialized formats and full versus partial warning selections. Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix · Severity of issue fixed: Medium
Suggested reviewers:
maria-rcks,yash-singh1,nikitamgrimm
Merge Risk:
Minimal · up to7a73f-
Copying a complete alert or insight callout in web chat now preserves its Markdown structure, and partial selections copy as plain text. No merge-blocking risk was found in the supplied changes.
Pre-merge checks:
4-
Check name Status Explanation Title check The title names the main change: copying an alert callout now keeps its alert Markdown. It follows conventional commit style and matches the raw summary, which adds alert serialization in markdown-cli… Linked Issues check Issue #35894requires that copying a complete GitHub warning callout produce plain-text Markdown with a warning marker and quote prefixes, such as> [!WARNING]\\n> Back up first.. At the reviewed hea…Out of Scope Changes check All changes support the linked issue's copy path. The changes are: the alert attribute and label attribute in ChatMarkdown.tsx; the alert branch, label skipping, and full-callout wrapper in markdown-c… Description check The description follows the template well. It covers the problem (selected GitHub alert and Insight callouts copy as plain text without their Markdown marker), the change (label made copyable as text,… Full details: Title check
-
Explanation
The title names the main change: copying an alert callout now keeps its alert Markdown. It follows conventional commit style and matches the raw summary, which adds alert serialization in markdown-clipboard.ts and a full-callout wrapper restore. The title is specific and short, so it passes.
Full details: Linked Issues check
-
Explanation
Issue
#35894requires that copying a complete GitHub warning callout produce plain-text Markdown with a warning marker and quote prefixes, such as> [!WARNING]\n> Back up first.. At the reviewed head,chatMarkdownClipboardPayloadin apps/web/src/markdown-clipboard.ts detects a range whose trimmed text equals the enclosing[data-markdown-alert]callout's trimmed textContent. It then wraps the cloned contents in a shallow clone of that callout.serializeNoderoutes elements withdata-markdown-alerttoserializeBlockquotewith the marker[!${kind.toUpperCase()}].serializeQuoteBodyskips thedata-markdown-alert-labelelement, so the label is not duplicated. In apps/web/src/components/ChatMarkdown.tsx, the calloutdiv role="note"now carriesdata-markdown-alert, and its label paragraph carriesdata-markdown-alert-label. The described full-selection result matches the issue's expected output. The PR also reports new tests: a unit test in markdown-clipboard.test.ts and a browser test in ChatMarkdown.browser.test.tsx for full and partial selections. I did not run these tests. The result is based on source inspection and the PR's reported test output. Other alert kinds share this branch, which matches the issue's stated scope.
Full details: Out of Scope Changes check
-
Explanation
All changes support the linked issue's copy path. The changes are: the alert attribute and label attribute in ChatMarkdown.tsx; the alert branch, label skipping, and full-callout wrapper in markdown-clipboard.ts; and the associated unit and browser tests. The Insight serializer (
serializeInsight) is new in this diff. It extends the samedata-markdown-alertbranch to Claude Insight callouts. The issue names Insight callouts as sharing this renderer path, so the extension is related work and not an unrelated change. The PR does not change the HTML clipboard flavor, and it states this as a deliberate exclusion. The partial-selection behavior is a stated PR change, not an unrelated change. No unrelated file changes were identified in the diff.
Full details: Description check
-
Explanation
The description follows the template well. It covers the problem (selected GitHub alert and Insight callouts copy as plain text without their Markdown marker), the change (label made copyable as text, serializeQuoteBody skips the label when the wrapper is serialized, and a shallow callout wrapper is restored when the selection covers all callout text), and the scope (a bug fix tied to issue
#35894). Verification is detailed: a before/after reproduction on main, unit and browser test results, check:changed results, and before/after evidence with screenshots and recordings. The author also names the models that did the work, as the template asks. Remaining gaps are minor. The Scope section does not explicitly say why the change needs no issue, but it links one. The HTML clipboard flavor is listed as out of scope, and the same bullet appears twice in the Not in this change section. The Other alert types and partial callout selections were not exercised note is a stated limitation.
-
Finishing Touches
-
Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info-
Run configuration-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: factory/issue-35894
- File: .coderabbit.config.ts
- Commit: 7a73f2a
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
2c4840a9-eb2d-4d1f-b671-af1bef36775c
Commits-
Reviewing files that changed from the base of the PR and between 274474dd19b459b339628b5c427b12c6664d2cb3 and 7a73f2aef0bb1289c5a7889db50f0e7900af34f4.
Files selected for processing: 4-
apps/web/src/components/ChatMarkdown.browser.test.tsxapps/web/src/components/ChatMarkdown.tsxapps/web/src/markdown-clipboard.test.tsapps/web/src/markdown-clipboard.ts
Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.
-
-
juliusmarminge merged this
b62e80ejuliusmarminge closed this
Sign in to join this conversation.


macroscopeapp[bot](GitHub profile, opens in a new tab) reviewed