Skip to content

Commit dd36581

Browse files
masonwyatt23claude
andcommitted
fix: exclude review summary card from copy/export output
Polish review caught a cross-feature bug: the agent review-doc summary card (#24) renders as an <aside class="review-card"> inside .markdown-body, which both the rich-text copy and the HTML/PDF/DOCX export capture wholesale — so a copied/exported review doc embedded the synthesized card (badges, chips) that was never in the source. Add cloneWithoutInjectedChrome() to strip injected UI chrome from both capture paths; +2 tests. (A second reported issue — duplicate React keys in the card — was a false positive: the parser emits at most one finding per line, so f.line is unique.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 22cfd40 commit dd36581

4 files changed

Lines changed: 42 additions & 3 deletions

File tree

src/components/viewer/ReviewSummaryCard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ interface ReviewSummaryCardProps {
3939
function useFindingAnchors(summary: ReviewSummary): Map<number, string> {
4040
return useMemo(() => {
4141
const slugger = new GithubSlugger();
42+
// Keyed by source line, which is unique per finding (the parser emits at
43+
// most one finding per line).
4244
const map = new Map<number, string>();
4345
for (const f of summary.findings) {
4446
if (!f.title) continue;

src/lib/copyRichText.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
import { describe, expect, it } from "vitest";
22
import { wrapRichTextBody } from "./copyRichText";
3+
import { cloneWithoutInjectedChrome } from "./export";
4+
5+
describe("cloneWithoutInjectedChrome", () => {
6+
it("strips the injected review card but keeps document content", () => {
7+
const body = document.createElement("div");
8+
body.className = "markdown-body";
9+
body.innerHTML =
10+
'<aside class="review-card">SUMMARY</aside><h1>Real Title</h1><p>Body text</p>';
11+
const cleaned = cloneWithoutInjectedChrome(body);
12+
expect(cleaned.querySelector(".review-card")).toBeNull();
13+
expect(cleaned.innerHTML).toContain("<h1>Real Title</h1>");
14+
expect(cleaned.innerHTML).toContain("Body text");
15+
// The original element is untouched (we clone, not mutate in place).
16+
expect(body.querySelector(".review-card")).not.toBeNull();
17+
});
18+
19+
it("is a no-op for a body with no injected chrome", () => {
20+
const body = document.createElement("div");
21+
body.innerHTML = "<h1>Doc</h1><p>x</p>";
22+
expect(cloneWithoutInjectedChrome(body).innerHTML).toBe("<h1>Doc</h1><p>x</p>");
23+
});
24+
});
325

426
describe("wrapRichTextBody", () => {
527
it("wraps body markup in an inline-styled div", () => {

src/lib/copyRichText.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import DOMPurify from "dompurify";
2323
import { useDocumentStore } from "../store/documentStore";
2424
import { toast } from "../store/toastStore";
25-
import { buildStandaloneHtml } from "./export";
25+
import { buildStandaloneHtml, cloneWithoutInjectedChrome } from "./export";
2626
import { waitForElement } from "./waitForElement";
2727

2828
/**
@@ -73,7 +73,10 @@ export function buildRichTextHtml(bodyInnerHtml: string): string {
7373
*/
7474
function captureRenderedBodyHtml(): string {
7575
const el = document.querySelector(".markdown-body");
76-
if (el) return el.innerHTML;
76+
// Strip injected chrome (the review card) so the clipboard gets the document,
77+
// not Ashlr's rendering overlay. The fallback below routes through
78+
// buildStandaloneHtml → captureMarkdownBody, which already strips it.
79+
if (el) return cloneWithoutInjectedChrome(el).innerHTML;
7780

7881
// Fallback: reuse the export pipeline, then extract just the rendered body.
7982
try {

src/lib/export.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ function currentLayoutVars(): string {
3939
return `--content-width:${width};--content-font-size:${fontSize};`;
4040
}
4141

42+
/**
43+
* Clone an element and strip Ashlr-injected UI chrome that lives inside
44+
* `.markdown-body` but is NOT part of the document — currently the review
45+
* summary card. Exports and rich-text copies should contain the document, not
46+
* the app's rendering overlay.
47+
*/
48+
export function cloneWithoutInjectedChrome(el: Element): Element {
49+
const clone = el.cloneNode(true) as Element;
50+
for (const node of clone.querySelectorAll(".review-card")) node.remove();
51+
return clone;
52+
}
53+
4254
/**
4355
* Reads the live `.markdown-body` element and returns its `outerHTML`.
4456
* Throws a descriptive string (shown in the dialog) when the element is
@@ -49,7 +61,7 @@ function captureMarkdownBody(): string {
4961
if (!el) {
5062
throw "Switch to Read view before exporting.";
5163
}
52-
return el.outerHTML;
64+
return cloneWithoutInjectedChrome(el).outerHTML;
5365
}
5466

5567
/**

0 commit comments

Comments
 (0)