Skip to content
Source
Sign inSign up

fix(web): copying an alert callout keeps its alert Markdown #35932

Mergedjuliusmarmingemerged 3 commits intomain fromrefs/pull/35932/headmerged by juliusmarminge View on GitHub

juliusmarmingecommented

Both 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.

BeforeAfter
ed2a3198147a73f2aef0
Before: rendered label copied as plain paragraphsAfter: alert marker and quote prefixes preserved
Before recordingAfter 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

    2. github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:trusted

    3. 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:506

      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.

            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.
    4. 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
      Adjust the Minimum Blocking Severity for this repo — including turning it Off — in [Settings](https://app.macroscope.com/pingdotgg/settings#Repos). You can add or adjust custom eligibility rules. [Learn more](https://docs.macroscope.com/approvability#example-custom-eligibility-rules).
    5. Explain Change →

      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.tsx
      Rendered 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.tsx
      The 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 to 7a73f

      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: Passed 4

      Check nameStatusExplanation
      Title checkPassedThe 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 checkPassedIssue #35894 requires 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 checkPassedAll 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 checkPassedThe 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 #35894 requires that copying a complete GitHub warning callout produce plain-text Markdown with a warning marker and quote prefixes, such as &gt; [!WARNING]\n&gt; Back up first.. At the reviewed head, chatMarkdownClipboardPayload in 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. serializeNode routes elements with data-markdown-alert to serializeBlockquote with the marker [!${kind.toUpperCase()}]. serializeQuoteBody skips the data-markdown-alert-label element, so the label is not duplicated. In apps/web/src/components/ChatMarkdown.tsx, the callout div role="note" now carries data-markdown-alert, and its label paragraph carries data-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 same data-markdown-alert branch 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

      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.tsx
      • apps/web/src/components/ChatMarkdown.tsx
      • apps/web/src/markdown-clipboard.test.ts
      • apps/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.

    6. juliusmarminge merged thisb62e80e

    7. juliusmarminge closed this

Sign in to join this conversation.