Skip to content

[Bug]: Selecting a warning callout copies a label instead of alert Markdown #35894

Closed

Description

@coygeek

Summary

Selecting and copying a GitHub warning callout in web chat turns it into ordinary paragraphs: the clipboard contains the renderer's “Warning” label and the body, but loses the warning marker and quote structure. Text selected from a message is documented to copy as readable Markdown, so the pasted content should preserve the callout's meaning. A focused serializer run reproduced the output, and a parser run confirmed the warning input reaches the callout renderer.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

  1. In web chat, render a message containing > [!WARNING] on one line and > Back up first. on the next.
  2. Select the complete warning callout and copy the selection; inspect the clipboard's plain text. For a checkout-only check, save the script below as repro.sh and run it with the checkout path.

The serializer fixture failed once in one run. The ordinary blockquote control passed. Separately, parsing the warning input with the repository's remarkGithubAlerts plugin produced {"type":"blockquote","alert":"warning","text":"Back up first."}.

The script requires the checkout's installed dependencies, Node, and vp. It creates one temporary test file named markdown-clipboard.y06-02.test.ts, runs only that test, and removes the file and its scratch directory on exit. Status 1 with RESULT: defect reproduced means the control passed and the alert copy differs from its source; status 0 means the expected alert Markdown was copied; status 2 means the setup or control was inconclusive. Run sh repro.sh /path/to/t3code-checkout.

Reproduction script: repro.sh
#!/bin/sh
set -eu

checkout=${1:?usage: repro.sh CHECKOUT}
scratch=$(mktemp -d)
test_file="$checkout/apps/web/src/markdown-clipboard.y06-02.test.ts"
cleanup() {
  rm -f "$test_file"
  rm -rf "$scratch"
}
trap cleanup EXIT HUP INT TERM

printf 'node: '
node --version
printf 'vp: '
vp --version

cat > "$test_file" <<'TEST'
import { expect, it, vi } from "vite-plus/test";
import { serializeRenderedMarkdownFragment } from "./markdown-clipboard";

class TextNode {
  readonly nodeType = 3;
  readonly childNodes: never[] = [];
  constructor(readonly textContent: string) {}
}
class ElementNode {
  readonly nodeType = 1;
  readonly childNodes: Array<TextNode | ElementNode> = [];
  readonly classList = { contains: () => false };
  constructor(readonly tagName: string) {}
  get localName() { return this.tagName.toLowerCase(); }
  get textContent() { return this.childNodes.map(child => child.textContent).join(""); }
  getAttribute(_name: string) { return null; }
  hasAttribute(_name: string) { return false; }
  append(...children: Array<TextNode | ElementNode>) { this.childNodes.push(...children); return this; }
}

it("copies ordinary quotes but loses a rendered warning's alert marker", () => {
  vi.stubGlobal("Node", { TEXT_NODE: 3, ELEMENT_NODE: 1 });
  try {
    const control = new ElementNode("DIV").append(
      new ElementNode("BLOCKQUOTE").append(new ElementNode("P").append(new TextNode("Back up first."))),
    );
    const alert = new ElementNode("DIV").append(
      new ElementNode("DIV").append(
        new ElementNode("P").append(new TextNode("Warning")),
        new ElementNode("P").append(new TextNode("Back up first.")),
      ),
    );
    const plain = serializeRenderedMarkdownFragment(control as unknown as Node);
    const copied = serializeRenderedMarkdownFragment(alert as unknown as Node);
    console.log(`CONTROL=${JSON.stringify(plain)}`);
    console.log(`ALERT_COPY=${JSON.stringify(copied)}`);
    expect(plain).toBe("> Back up first.");
    expect(copied).toBe("> [!WARNING]\n> Back up first.");
  } finally {
    vi.unstubAllGlobals();
  }
});
TEST

set +e
(cd "$checkout" && HOME="$scratch" TMPDIR="$scratch" vp test run apps/web/src/markdown-clipboard.y06-02.test.ts) > "$scratch/test.out" 2>&1
status=$?
set -e
grep -E 'CONTROL=|ALERT_COPY=|AssertionError|expected|Test Files|Tests ' "$scratch/test.out" || cat "$scratch/test.out"
if [ "$status" -eq 0 ]; then
  printf 'RESULT: expected behavior\n'
  exit 0
fi
if grep -q 'CONTROL="> Back up first."' "$scratch/test.out" && grep -q 'ALERT_COPY="Warning\\n\\nBack up first."' "$scratch/test.out"; then
  printf 'RESULT: defect reproduced\n'
  exit 1
fi
printf 'RESULT: inconclusive\n'
exit 2

Expected behavior

Text copied from a message should remain readable Markdown, as the docs/user/composer.md#L373-L375 says. For a warning callout, the copied plain text should retain a warning marker and its association with the body, such as > [!WARNING]\n> Back up first.; the rendered title alone does not encode that structure. A normal blockquote already copies with quote prefixes in apps/web/src/markdown-clipboard.ts#L163-L173.

Actual behavior

For > [!WARNING] followed by > Back up first., selecting the whole rendered callout and copying it reaches apps/web/src/components/ChatMarkdown.tsx#L675-L681 and apps/web/src/markdown-clipboard.ts#L442-L487. The focused run of the same rendered element shape returned Warning\n\nBack up first.. The warning marker and blockquote prefix were absent; “Warning” is a label inserted by the renderer rather than message text. The observed bytes are from the serializer fixture; the full browser clipboard action was traced in source, not exercised in a running client.

Impact and scope

Impact: P2 - a bounded clipboard content defect changes the meaning and structure of a copied warning, but the body remains available and the user can repair the pasted Markdown manually.

The confirmed path is web chat selection copy for a full GitHub warning callout. The same renderer handles the other GitHub alert types and Claude Insight callouts, so their selected-copy behavior follows the same branches; those variants were not separately run. This does not assess the message copy button or mobile copy behavior. Repeating the same selection follows the same serializer path.

Earliest failure stage

Output or presentation.

Analysis or suspected cause

Source trace: The parser recognizes the input as a warning and removes the authored marker before rendering: packages/shared/src/markdownGithubAlerts.ts#L10-L42, registered in packages/shared/src/markdownPipeline.ts#L246-L258. The renderer creates a div role="note" and a label paragraph, not a blockquote: apps/web/src/components/ChatMarkdown.tsx#L1057-L1079. The serializer has a BLOCKQUOTE branch but sends that div and its label through generic paragraph and DIV branches: apps/web/src/markdown-clipboard.ts#L212-L285. The copy handler writes this result to text/plain: apps/web/src/components/ChatMarkdown.tsx#L675-L681.

Additional context

  • Upstream main still has no alert-specific serializer branch, and its callout renderer still emits the same labeled div.

Evidence provenance: the serializer output and parser result were observed in focused runs; the browser clipboard path was traced in source. Local verification: reproduced in the serializer fixture. Reproduction completeness: the fixture is complete for the serializer output, while the interactive browser copy action was not exercised. The restoration check is the script's warning assertion passing alongside its ordinary blockquote control.

Impact

Minor bug or occasional failure

Version or commit

main @ c00a373

Environment

Darwin arm64; Node v26.12.1; vp v1.5.1 with local vite-plus v1.4.0. The serializer check ran in the repository's unit test environment.

Logs or stack traces

CONTROL="> Back up first."
ALERT_COPY="Warning\n\nBack up first."
AssertionError: expected 'Warning\n\nBack up first.' to be '> [!WARNING]\n> Back up first.'
Test Files  1 failed (1)
Tests  1 failed (1)
RESULT: defect reproduced

Screenshots, recordings, or supporting files

No response

Workaround

Repair the pasted Markdown manually by restoring the warning marker and quote prefixes.

References

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions