From 56e508016f7a63077562dfc26ebe3457ce561e80 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 06:12:24 +0000 Subject: [PATCH 01/15] Improve HTML preview text extraction Co-authored-by: Aiden Bai --- .../react-grab/e2e/element-context.spec.ts | 42 +++++++++++++ packages/react-grab/src/core/context.ts | 59 +++++++++++++++---- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index a955c9427..b1e2955d7 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -139,5 +139,47 @@ test.describe("Element Context Fallback", () => { expect(clipboard).toContain("long-dom-element"); expect(clipboard.length).toBeLessThanOrEqual(510); }); + + test("should include descendant text for syntax highlighted code blocks", async ({ + reactGrab, + }) => { + await reactGrab.page.evaluate(() => { + const wrapper = document.createElement("div"); + Object.assign(wrapper.style, { + position: "fixed", + top: "200px", + left: "200px", + width: "600px", + height: "160px", + zIndex: "999", + }); + + const codeBlock = document.createElement("pre"); + codeBlock.className = "shiki shiki-themes github-light github-dark"; + codeBlock.tabIndex = 0; + codeBlock.innerHTML = ` + + git add .github/workflows/react-doctor.yml + git commit -m "Add React Doctor to CI" + git push + + `; + + wrapper.appendChild(codeBlock); + document.body.appendChild(wrapper); + }); + + await reactGrab.activate(); + + await reactGrab.hoverElement("pre.shiki"); + await reactGrab.waitForSelectionBox(); + await reactGrab.clickElement("pre.shiki"); + + const clipboard = await reactGrab.getClipboardContent(); + expect(clipboard).toContain(""); + }); }); }); diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index 850a446c4..5f1cb9451 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -552,8 +552,8 @@ const getFallbackContext = (element: Element): string => { const tagName = getTagName(element); const attrsText = formatAttrsForPreview(element); - const directText = getDirectTextContent(element); - const truncatedText = truncateString(directText, PREVIEW_TEXT_MAX_LENGTH); + const previewText = getPreviewTextContent(element, tagName); + const truncatedText = truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH); if (truncatedText.length > 0) { return `<${tagName}${attrsText}>\n ${truncatedText}\n`; @@ -591,6 +591,37 @@ const formatPriorityAttrs = ( const isClassOrStyleAttr = (name: string): boolean => name === "class" || name === "className" || name === "style"; +const TEXT_CONTENT_PREVIEW_TAGS = new Set([ + "a", + "abbr", + "b", + "button", + "caption", + "cite", + "code", + "dd", + "dt", + "em", + "figcaption", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "label", + "legend", + "li", + "p", + "pre", + "small", + "span", + "strong", + "summary", + "td", + "th", +]); + const formatAttrsForPreview = (element: Element): string => { const identifyingParts: string[] = []; const remainingParts: string[] = []; @@ -614,11 +645,13 @@ const formatAttrsForPreview = (element: Element): string => { return identifyingParts.join("") + remainingParts.join("") + classAttr; }; +const collapseTextContent = (text: string): string => text.replace(/\s+/g, " ").trim(); + const getDirectTextContent = (element: Element): string => { let directText = ""; for (const node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { - const trimmed = node.textContent?.trim() ?? ""; + const trimmed = collapseTextContent(node.textContent ?? ""); if (trimmed) { directText += (directText ? " " : "") + trimmed; } @@ -627,6 +660,12 @@ const getDirectTextContent = (element: Element): string => { return directText; }; +const getPreviewTextContent = (element: Element, tagName: string): string => { + const directText = getDirectTextContent(element); + if (directText || !TEXT_CONTENT_PREVIEW_TAGS.has(tagName)) return directText; + return collapseTextContent(element.textContent ?? ""); +}; + const formatChildElements = (elements: Array): string => { if (elements.length === 0) return ""; if (elements.length <= 2) { @@ -647,8 +686,8 @@ export const getInlineHTMLPreview = (element: Element): string => { } const attrsText = formatAttrsForPreview(element); - const directText = getDirectTextContent(element); - const truncatedText = truncateString(directText, PREVIEW_TEXT_MAX_LENGTH); + const previewText = getPreviewTextContent(element, tagName); + const truncatedText = truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH); if (truncatedText) { return `<${tagName}${attrsText}>${truncatedText}`; @@ -659,7 +698,7 @@ export const getInlineHTMLPreview = (element: Element): string => { export const getHTMLPreview = (element: Element): string => { const tagName = getTagName(element); const attrsText = formatAttrsForPreview(element); - const directText = getDirectTextContent(element); + const previewText = getPreviewTextContent(element, tagName); const topElements: Array = []; const bottomElements: Array = []; @@ -682,12 +721,12 @@ export const getHTMLPreview = (element: Element): string => { let content = ""; const topElementsStr = formatChildElements(topElements); - if (topElementsStr) content += `\n ${topElementsStr}`; - if (directText.length > 0) { - content += `\n ${truncateString(directText, PREVIEW_TEXT_MAX_LENGTH)}`; + if (topElementsStr && !previewText) content += `\n ${topElementsStr}`; + if (previewText.length > 0) { + content += `\n ${truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH)}`; } const bottomElementsStr = formatChildElements(bottomElements); - if (bottomElementsStr) content += `\n ${bottomElementsStr}`; + if (bottomElementsStr && !previewText) content += `\n ${bottomElementsStr}`; if (content.length > 0) { return `<${tagName}${attrsText}>${content}\n`; From 3ee3f97064e48dcd01404ff84b941d26afba8556 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 06:25:22 +0000 Subject: [PATCH 02/15] Stabilize code block preview regression Co-authored-by: Aiden Bai --- packages/react-grab/e2e/element-context.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index b1e2955d7..f51a92781 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -169,11 +169,8 @@ test.describe("Element Context Fallback", () => { document.body.appendChild(wrapper); }); - await reactGrab.activate(); - - await reactGrab.hoverElement("pre.shiki"); - await reactGrab.waitForSelectionBox(); - await reactGrab.clickElement("pre.shiki"); + const didCopy = await reactGrab.copyElementViaApi("pre.shiki"); + expect(didCopy).toBe(true); const clipboard = await reactGrab.getClipboardContent(); expect(clipboard).toContain(" Date: Sun, 7 Jun 2026 07:27:59 +0000 Subject: [PATCH 03/15] Cover nested link HTML previews Co-authored-by: Aiden Bai --- .../react-grab/e2e/element-context.spec.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index f51a92781..06f0e4b73 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -178,5 +178,38 @@ test.describe("Element Context Fallback", () => { expect(clipboard).toContain('git commit -m "Add React Doctor to CI"'); expect(clipboard).toContain(""); }); + + test("should include descendant text for nested link labels", async ({ reactGrab }) => { + await reactGrab.page.evaluate(() => { + const wrapper = document.createElement("div"); + Object.assign(wrapper.style, { + position: "fixed", + top: "200px", + left: "200px", + width: "320px", + height: "80px", + zIndex: "999", + }); + + const link = document.createElement("a"); + link.href = "/docs/ci-and-prs/github-actions-setup"; + link.className = "flex h-8 w-full items-center gap-2 rounded-md px-2"; + link.innerHTML = ` + + GitHub Actions setup + `; + + wrapper.appendChild(link); + document.body.appendChild(wrapper); + }); + + const didCopy = await reactGrab.copyElementViaApi("a[href='/docs/ci-and-prs/github-actions-setup']"); + expect(didCopy).toBe(true); + + const clipboard = await reactGrab.getClipboardContent(); + expect(clipboard).toContain('"); + }); }); }); From a039a9653b1c715832b74c9c857d6e8924dc9c9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 07:29:36 +0000 Subject: [PATCH 04/15] Format link preview regression Co-authored-by: Aiden Bai --- packages/react-grab/e2e/element-context.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index 06f0e4b73..ab66518a8 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -203,7 +203,9 @@ test.describe("Element Context Fallback", () => { document.body.appendChild(wrapper); }); - const didCopy = await reactGrab.copyElementViaApi("a[href='/docs/ci-and-prs/github-actions-setup']"); + const didCopy = await reactGrab.copyElementViaApi( + "a[href='/docs/ci-and-prs/github-actions-setup']", + ); expect(didCopy).toBe(true); const clipboard = await reactGrab.getClipboardContent(); From 3d929e77cf1a8bc4286a78075c806e27f18cfabc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 07:42:18 +0000 Subject: [PATCH 05/15] Add selector hints for low-signal traces Co-authored-by: Aiden Bai --- .../react-grab/e2e/element-context.spec.ts | 1 + packages/react-grab/src/constants.ts | 1 + packages/react-grab/src/core/context.ts | 57 ++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index ab66518a8..974233fcc 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -211,6 +211,7 @@ test.describe("Element Context Fallback", () => { const clipboard = await reactGrab.getClipboardContent(); expect(clipboard).toContain('"); }); }); diff --git a/packages/react-grab/src/constants.ts b/packages/react-grab/src/constants.ts index efaf6384d..5494bdfe3 100644 --- a/packages/react-grab/src/constants.ts +++ b/packages/react-grab/src/constants.ts @@ -72,6 +72,7 @@ export const LABEL_GAP_PX = 4; export const PREVIEW_TEXT_MAX_LENGTH = 100; export const PREVIEW_ATTR_VALUE_MAX_LENGTH = 15; export const PREVIEW_MAX_ATTRS = 3; +export const CONTEXT_SELECTOR_MAX_LENGTH_CHARS = 160; export const PREVIEW_PRIORITY_ATTRS: readonly string[] = [ "id", "class", diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index 5f1cb9451..5829fc8bb 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -23,6 +23,7 @@ import { PREVIEW_IDENTIFYING_ATTRS, SYMBOLICATION_TIMEOUT_MS, DEFAULT_MAX_CONTEXT_LINES, + CONTEXT_SELECTOR_MAX_LENGTH_CHARS, } from "../constants.js"; import { getTagName } from "../utils/get-tag-name.js"; import { truncateString } from "../utils/truncate-string.js"; @@ -31,6 +32,7 @@ import { normalizeFilePath } from "../utils/normalize-file-path.js"; import { parsePackageName } from "../utils/parse-package-name.js"; import { safeDecodeURIComponent } from "../utils/safe-decode-uri-component.js"; import { isInternalAttribute } from "../utils/strip-internal-attributes.js"; +import { createElementSelector } from "../utils/create-element-selector.js"; import { isInternalComponentName, isUsefulComponentName, @@ -378,6 +380,52 @@ const hasFormattableFrames = (stack: StackFrame[] | null): boolean => { }); }; +const isResolvedSourceFrame = (frame: StackFrame): boolean => + Boolean(frame.fileName && isSourceFile(frame.fileName)); + +const isTrustedSourcePath = (filePath: string | null | undefined): boolean => + Boolean(filePath && isSourceFile(filePath) && !parsePackageName(filePath)); + +const getSourceComponentNameFromFrame = (frame: StackFrame): string | null => + frame.functionName && isSourceComponentName(frame.functionName) ? frame.functionName : null; + +const getFirstContextFrame = (stack: StackFrame[] | null): StackFrame | null => { + if (!stack) return null; + for (const frame of stack) { + if (parsePackageName(frame.fileName)) return frame; + if (isResolvedSourceFrame(frame)) return frame; + if (frame.isServer && (!frame.functionName || getSourceComponentNameFromFrame(frame))) { + return frame; + } + if (getSourceComponentNameFromFrame(frame)) return frame; + } + return null; +}; + +const hasTrustedSourceTrace = ( + leadingSource: ResolvedSource | null, + stack: StackFrame[] | null, +): boolean => + isTrustedSourcePath(leadingSource?.filePath) || + Boolean(stack?.some((frame) => isTrustedSourcePath(frame.fileName))); + +const shouldAppendSelectorHint = ( + leadingSource: ResolvedSource | null, + stack: StackFrame[] | null, +): boolean => { + if (isTrustedSourcePath(leadingSource?.filePath)) return false; + + const firstContextFrame = getFirstContextFrame(stack); + if (!firstContextFrame) return true; + if (parsePackageName(firstContextFrame.fileName)) return true; + return !hasTrustedSourceTrace(leadingSource, stack); +}; + +const formatSelectorContextLine = (element: Element): string => { + const selector = createElementSelector(element); + return `\n selector: ${truncateString(selector, CONTEXT_SELECTOR_MAX_LENGTH_CHARS)}`; +}; + const getComponentNamesFromFiber = (element: Element, maxCount: number): string[] => { if (!isInstrumentationActive()) return []; const fiber = getFiberFromHostInstance(element); @@ -513,9 +561,12 @@ export const getStackContext = async ( const maxLines = options.maxLines ?? DEFAULT_MAX_CONTEXT_LINES; const leadingSource = await getFiberSource(element); const stack = await getStack(element); + const selectorContext = shouldAppendSelectorHint(leadingSource, stack) + ? formatSelectorContextLine(element) + : ""; if (stack && hasFormattableFrames(stack)) { - return formatStackContext(stack, options, leadingSource); + return `${formatStackContext(stack, options, leadingSource)}${selectorContext}`; } if (leadingSource) { @@ -524,10 +575,10 @@ export const getStackContext = async ( const componentNames = getComponentNamesFromFiber(findNearestFiberElement(element), maxLines); if (componentNames.length > 0) { - return componentNames.map((name) => `\n in ${name}`).join(""); + return `${componentNames.map((name) => `\n in ${name}`).join("")}${selectorContext}`; } - return ""; + return selectorContext; }; export const getElementContext = async ( From 92afd4dca00ce950e0ee00e02baa63f780a88428 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 07:43:26 +0000 Subject: [PATCH 06/15] Prefer link attributes for selector hints Co-authored-by: Aiden Bai --- packages/react-grab/src/utils/create-element-selector.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-grab/src/utils/create-element-selector.ts b/packages/react-grab/src/utils/create-element-selector.ts index 0352286d8..4bc0766b5 100644 --- a/packages/react-grab/src/utils/create-element-selector.ts +++ b/packages/react-grab/src/utils/create-element-selector.ts @@ -18,6 +18,8 @@ const PREFERRED_SELECTOR_ATTRIBUTE_NAMES = new Set([ "data-cy", "data-qa", "aria-label", + "href", + "src", "role", "name", "title", From 4f309bb7a67b80b089d2a7cd7a3016a48a6caff9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 07:44:14 +0000 Subject: [PATCH 07/15] Expect compact href selector hint Co-authored-by: Aiden Bai --- packages/react-grab/e2e/element-context.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index 974233fcc..a8f89495c 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -211,7 +211,7 @@ test.describe("Element Context Fallback", () => { const clipboard = await reactGrab.getClipboardContent(); expect(clipboard).toContain('"); }); }); From e1b635d846f78bbbdd88e1048c9931d53afaa7df Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 07:59:39 +0000 Subject: [PATCH 08/15] Clean up preview context formatting boundaries Co-authored-by: Aiden Bai --- .../react-grab/e2e/element-context.spec.ts | 1 + packages/react-grab/src/constants.ts | 30 +++ packages/react-grab/src/core/context.ts | 197 ++++++++---------- packages/react-grab/src/core/copy.ts | 9 +- .../src/utils/get-preview-text-content.ts | 50 +++++ 5 files changed, 176 insertions(+), 111 deletions(-) create mode 100644 packages/react-grab/src/utils/get-preview-text-content.ts diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index a8f89495c..30b4d5529 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -211,6 +211,7 @@ test.describe("Element Context Fallback", () => { const clipboard = await reactGrab.getClipboardContent(); expect(clipboard).toContain('"); }); diff --git a/packages/react-grab/src/constants.ts b/packages/react-grab/src/constants.ts index 5494bdfe3..2df35df02 100644 --- a/packages/react-grab/src/constants.ts +++ b/packages/react-grab/src/constants.ts @@ -73,6 +73,36 @@ export const PREVIEW_TEXT_MAX_LENGTH = 100; export const PREVIEW_ATTR_VALUE_MAX_LENGTH = 15; export const PREVIEW_MAX_ATTRS = 3; export const CONTEXT_SELECTOR_MAX_LENGTH_CHARS = 160; +export const PREVIEW_TEXT_TAGS = new Set([ + "a", + "abbr", + "b", + "button", + "caption", + "cite", + "code", + "dd", + "dt", + "em", + "figcaption", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "label", + "legend", + "li", + "p", + "pre", + "small", + "span", + "strong", + "summary", + "td", + "th", +]); export const PREVIEW_PRIORITY_ATTRS: readonly string[] = [ "id", "class", diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index 5829fc8bb..2f7a425ef 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -33,6 +33,7 @@ import { parsePackageName } from "../utils/parse-package-name.js"; import { safeDecodeURIComponent } from "../utils/safe-decode-uri-component.js"; import { isInternalAttribute } from "../utils/strip-internal-attributes.js"; import { createElementSelector } from "../utils/create-element-selector.js"; +import { getPreviewTextContent } from "../utils/get-preview-text-content.js"; import { isInternalComponentName, isUsefulComponentName, @@ -368,6 +369,12 @@ interface StackContextOptions { maxLines?: number; } +interface TraceContextResult { + text: string; + firstSignalKind: "trusted-source" | "untrusted-source" | "package" | "server" | "component" | null; + hasTrustedSource: boolean; +} + const hasFormattableFrames = (stack: StackFrame[] | null): boolean => { if (!stack) return false; return stack.some((frame) => { @@ -380,45 +387,18 @@ const hasFormattableFrames = (stack: StackFrame[] | null): boolean => { }); }; -const isResolvedSourceFrame = (frame: StackFrame): boolean => - Boolean(frame.fileName && isSourceFile(frame.fileName)); - const isTrustedSourcePath = (filePath: string | null | undefined): boolean => Boolean(filePath && isSourceFile(filePath) && !parsePackageName(filePath)); -const getSourceComponentNameFromFrame = (frame: StackFrame): string | null => - frame.functionName && isSourceComponentName(frame.functionName) ? frame.functionName : null; +const getSourceSignalKind = (filePath: string): TraceContextResult["firstSignalKind"] => + isTrustedSourcePath(filePath) ? "trusted-source" : "untrusted-source"; -const getFirstContextFrame = (stack: StackFrame[] | null): StackFrame | null => { - if (!stack) return null; - for (const frame of stack) { - if (parsePackageName(frame.fileName)) return frame; - if (isResolvedSourceFrame(frame)) return frame; - if (frame.isServer && (!frame.functionName || getSourceComponentNameFromFrame(frame))) { - return frame; - } - if (getSourceComponentNameFromFrame(frame)) return frame; +const shouldAppendSelectorHint = (traceContext: TraceContextResult): boolean => { + if (!traceContext.text) return true; + if (traceContext.firstSignalKind === "package" || traceContext.firstSignalKind === "server") { + return true; } - return null; -}; - -const hasTrustedSourceTrace = ( - leadingSource: ResolvedSource | null, - stack: StackFrame[] | null, -): boolean => - isTrustedSourcePath(leadingSource?.filePath) || - Boolean(stack?.some((frame) => isTrustedSourcePath(frame.fileName))); - -const shouldAppendSelectorHint = ( - leadingSource: ResolvedSource | null, - stack: StackFrame[] | null, -): boolean => { - if (isTrustedSourcePath(leadingSource?.filePath)) return false; - - const firstContextFrame = getFirstContextFrame(stack); - if (!firstContextFrame) return true; - if (parsePackageName(firstContextFrame.fileName)) return true; - return !hasTrustedSourceTrace(leadingSource, stack); + return !traceContext.hasTrustedSource; }; const formatSelectorContextLine = (element: Element): string => { @@ -483,22 +463,38 @@ const formatStackContext = ( stack: StackFrame[], options: StackContextOptions = {}, leadingSource: ResolvedSource | null = null, -): string => { +): TraceContextResult => { const { maxLines = DEFAULT_MAX_CONTEXT_LINES } = options; const isNextProject = isNextProjectRuntime(); const lines: string[] = []; let previousLibraryPackage: string | null = null; let didDedupeLeadingComponent = false; + let firstSignalKind: TraceContextResult["firstSignalKind"] = null; + let hasTrustedSource = false; - if (leadingSource) { - lines.push(formatSourceContextLine(leadingSource, isNextProject)); - } + const recordSignal = (signalKind: TraceContextResult["firstSignalKind"]) => { + firstSignalKind ??= signalKind; + if (signalKind === "trusted-source") hasTrustedSource = true; + }; - const emit = (line: string, libraryPackage: string | null) => { + const emit = ( + line: string, + libraryPackage: string | null, + signalKind: TraceContextResult["firstSignalKind"], + ) => { lines.push(line); previousLibraryPackage = libraryPackage; + recordSignal(signalKind); }; + if (leadingSource) { + emit( + formatSourceContextLine(leadingSource, isNextProject), + null, + getSourceSignalKind(leadingSource.filePath), + ); + } + for (const frame of stack) { if (lines.length >= maxLines) break; @@ -523,7 +519,11 @@ const formatStackContext = ( if (frame.isServer && !resolvedSource && (componentName || !frame.functionName)) { const tag = libraryPackage ? `${libraryPackage} at Server` : "at Server"; - emit(`\n in ${componentName ?? ""} (${tag})`, libraryPackage); + emit( + `\n in ${componentName ?? ""} (${tag})`, + libraryPackage, + libraryPackage ? "package" : "server", + ); continue; } @@ -531,6 +531,7 @@ const formatStackContext = ( emit( libraryPackage ? `\n in ${componentName} (${libraryPackage})` : `\n in ${componentName}`, libraryPackage, + libraryPackage ? "package" : "component", ); continue; } @@ -547,38 +548,75 @@ const formatStackContext = ( isNextProject, ), null, + getSourceSignalKind(resolvedSource), ); } } - return lines.join(""); + return { + text: lines.join(""), + firstSignalKind, + hasTrustedSource, + }; }; -export const getStackContext = async ( +const getTraceContext = async ( element: Element, options: StackContextOptions = {}, -): Promise => { +): Promise => { const maxLines = options.maxLines ?? DEFAULT_MAX_CONTEXT_LINES; const leadingSource = await getFiberSource(element); const stack = await getStack(element); - const selectorContext = shouldAppendSelectorHint(leadingSource, stack) - ? formatSelectorContextLine(element) - : ""; if (stack && hasFormattableFrames(stack)) { - return `${formatStackContext(stack, options, leadingSource)}${selectorContext}`; + return formatStackContext(stack, options, leadingSource); } if (leadingSource) { - return formatSourceContextLine(leadingSource, isNextProjectRuntime()); + const signalKind = getSourceSignalKind(leadingSource.filePath); + return { + text: formatSourceContextLine(leadingSource, isNextProjectRuntime()), + firstSignalKind: signalKind, + hasTrustedSource: signalKind === "trusted-source", + }; } const componentNames = getComponentNamesFromFiber(findNearestFiberElement(element), maxLines); if (componentNames.length > 0) { - return `${componentNames.map((name) => `\n in ${name}`).join("")}${selectorContext}`; + return { + text: componentNames.map((name) => `\n in ${name}`).join(""), + firstSignalKind: "component", + hasTrustedSource: false, + }; } - return selectorContext; + return { + text: "", + firstSignalKind: null, + hasTrustedSource: false, + }; +}; + +export const getStackContext = async ( + element: Element, + options: StackContextOptions = {}, +): Promise => { + const traceContext = await getTraceContext(element, options); + return traceContext.text; +}; + +const getSelectorContext = (element: Element, traceContext: TraceContextResult): string => + shouldAppendSelectorHint(traceContext) ? formatSelectorContextLine(element) : ""; + +export const getElementReferenceContext = async ( + element: Element, + options: StackContextOptions = {}, +): Promise => { + const traceContext = await getTraceContext(element, options); + return `${getInlineHTMLPreview(element)}${traceContext.text}${getSelectorContext( + element, + traceContext, + )}`; }; export const getElementContext = async ( @@ -587,10 +625,11 @@ export const getElementContext = async ( ): Promise => { const resolvedElement = findNearestFiberElement(element); const html = getHTMLPreview(resolvedElement); - const stackContext = await getStackContext(resolvedElement, options); + const traceContext = await getTraceContext(resolvedElement, options); + const selectorContext = getSelectorContext(resolvedElement, traceContext); - if (stackContext) { - return `${html}${stackContext}`; + if (traceContext.text || selectorContext) { + return `${html}${traceContext.text}${selectorContext}`; } return getFallbackContext(resolvedElement); @@ -642,37 +681,6 @@ const formatPriorityAttrs = ( const isClassOrStyleAttr = (name: string): boolean => name === "class" || name === "className" || name === "style"; -const TEXT_CONTENT_PREVIEW_TAGS = new Set([ - "a", - "abbr", - "b", - "button", - "caption", - "cite", - "code", - "dd", - "dt", - "em", - "figcaption", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "label", - "legend", - "li", - "p", - "pre", - "small", - "span", - "strong", - "summary", - "td", - "th", -]); - const formatAttrsForPreview = (element: Element): string => { const identifyingParts: string[] = []; const remainingParts: string[] = []; @@ -696,27 +704,6 @@ const formatAttrsForPreview = (element: Element): string => { return identifyingParts.join("") + remainingParts.join("") + classAttr; }; -const collapseTextContent = (text: string): string => text.replace(/\s+/g, " ").trim(); - -const getDirectTextContent = (element: Element): string => { - let directText = ""; - for (const node of element.childNodes) { - if (node.nodeType === Node.TEXT_NODE) { - const trimmed = collapseTextContent(node.textContent ?? ""); - if (trimmed) { - directText += (directText ? " " : "") + trimmed; - } - } - } - return directText; -}; - -const getPreviewTextContent = (element: Element, tagName: string): string => { - const directText = getDirectTextContent(element); - if (directText || !TEXT_CONTENT_PREVIEW_TAGS.has(tagName)) return directText; - return collapseTextContent(element.textContent ?? ""); -}; - const formatChildElements = (elements: Array): string => { if (elements.length === 0) return ""; if (elements.length <= 2) { diff --git a/packages/react-grab/src/core/copy.ts b/packages/react-grab/src/core/copy.ts index e63542667..d2d69b82d 100644 --- a/packages/react-grab/src/core/copy.ts +++ b/packages/react-grab/src/core/copy.ts @@ -1,4 +1,4 @@ -import { getInlineHTMLPreview, getStackContext } from "./context.js"; +import { getElementReferenceContext } from "./context.js"; import { copyContent } from "../utils/copy-content.js"; import { normalizeError } from "../utils/normalize-error.js"; @@ -15,11 +15,8 @@ interface CopyFlowHooks { onCopyError: (error: Error) => void; } -const formatElementReference = async (element: Element): Promise => { - const inlinePreview = getInlineHTMLPreview(element); - const inlineStack = (await getStackContext(element)).replace(/\n\s+/g, " "); - return `[${inlinePreview}${inlineStack}]`; -}; +const formatElementReference = async (element: Element): Promise => + `[${(await getElementReferenceContext(element)).replace(/\n\s+/g, " ")}]`; const buildClipboardPayload = async (elements: Element[]): Promise => { const references = await Promise.all(elements.map(formatElementReference)); diff --git a/packages/react-grab/src/utils/get-preview-text-content.ts b/packages/react-grab/src/utils/get-preview-text-content.ts new file mode 100644 index 000000000..51b2f8995 --- /dev/null +++ b/packages/react-grab/src/utils/get-preview-text-content.ts @@ -0,0 +1,50 @@ +import { PREVIEW_TEXT_TAGS } from "../constants.js"; + +const SKIPPED_TEXT_TAGS = new Set(["script", "style", "template", "noscript"]); + +const collapseTextContent = (text: string): string => text.replace(/\s+/g, " ").trim(); + +const getDirectTextContent = (element: Element): string => { + let directText = ""; + for (const node of element.childNodes) { + if (node.nodeType !== Node.TEXT_NODE) continue; + + const trimmed = collapseTextContent(node.textContent ?? ""); + if (trimmed) { + directText += (directText ? " " : "") + trimmed; + } + } + return directText; +}; + +const shouldSkipElementText = (element: Element): boolean => { + if (element.getAttribute("aria-hidden") === "true") return true; + if (element.hasAttribute("hidden")) return true; + return SKIPPED_TEXT_TAGS.has(element.tagName.toLowerCase()); +}; + +const collectDescendantText = (node: Node, parts: string[]): void => { + if (node.nodeType === Node.TEXT_NODE) { + const trimmed = collapseTextContent(node.textContent ?? ""); + if (trimmed) parts.push(trimmed); + return; + } + + if (!(node instanceof Element)) return; + if (shouldSkipElementText(node)) return; + + for (const childNode of node.childNodes) { + collectDescendantText(childNode, parts); + } +}; + +export const getPreviewTextContent = (element: Element, tagName: string): string => { + const directText = getDirectTextContent(element); + if (directText || !PREVIEW_TEXT_TAGS.has(tagName)) return directText; + + const parts: string[] = []; + for (const childNode of element.childNodes) { + collectDescendantText(childNode, parts); + } + return collapseTextContent(parts.join(" ")); +}; From 80a3a082ccd3c44a6178d807137ce49c50bd1725 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 08:00:55 +0000 Subject: [PATCH 09/15] Format trace context result type Co-authored-by: Aiden Bai --- packages/react-grab/src/core/context.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index 2f7a425ef..cf6600428 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -371,7 +371,13 @@ interface StackContextOptions { interface TraceContextResult { text: string; - firstSignalKind: "trusted-source" | "untrusted-source" | "package" | "server" | "component" | null; + firstSignalKind: + | "trusted-source" + | "untrusted-source" + | "package" + | "server" + | "component" + | null; hasTrustedSource: boolean; } From 015d4e65f35b1d375b72a7cde8e223f28c5cbe4d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 08:10:52 +0000 Subject: [PATCH 10/15] Respect hidden roots in preview text Co-authored-by: Aiden Bai --- .../react-grab/e2e/element-context.spec.ts | 29 +++++++++++++++++++ packages/react-grab/src/core/context.ts | 17 +++++++---- .../src/utils/get-preview-text-content.ts | 24 +++++++++++++-- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index 30b4d5529..14a9654e0 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -215,5 +215,34 @@ test.describe("Element Context Fallback", () => { expect(clipboard).toContain('selector: [href="/docs/ci-and-prs/github-actions-setup"]'); expect(clipboard).toContain(""); }); + + test("should skip preview text for hidden selected roots", async ({ reactGrab }) => { + await reactGrab.page.evaluate(() => { + const wrapper = document.createElement("div"); + Object.assign(wrapper.style, { + position: "fixed", + top: "200px", + left: "200px", + width: "200px", + height: "80px", + zIndex: "999", + }); + + const hiddenLabel = document.createElement("span"); + hiddenLabel.setAttribute("aria-hidden", "true"); + hiddenLabel.setAttribute("data-testid", "decorative-hidden-label"); + hiddenLabel.textContent = "Decorative Hidden Label"; + + wrapper.appendChild(hiddenLabel); + document.body.appendChild(wrapper); + }); + + const didCopy = await reactGrab.copyElementViaApi("[data-testid='decorative-hidden-label']"); + expect(didCopy).toBe(true); + + const clipboard = await reactGrab.getClipboardContent(); + expect(clipboard).toContain('aria-hidden="true"'); + expect(clipboard).not.toContain("Decorative Hidden Label"); + }); }); }); diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index cf6600428..f95621650 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -33,7 +33,10 @@ import { parsePackageName } from "../utils/parse-package-name.js"; import { safeDecodeURIComponent } from "../utils/safe-decode-uri-component.js"; import { isInternalAttribute } from "../utils/strip-internal-attributes.js"; import { createElementSelector } from "../utils/create-element-selector.js"; -import { getPreviewTextContent } from "../utils/get-preview-text-content.js"; +import { + getPreviewTextContent, + getPreviewTextContentResult, +} from "../utils/get-preview-text-content.js"; import { isInternalComponentName, isUsefulComponentName, @@ -742,7 +745,7 @@ export const getInlineHTMLPreview = (element: Element): string => { export const getHTMLPreview = (element: Element): string => { const tagName = getTagName(element); const attrsText = formatAttrsForPreview(element); - const previewText = getPreviewTextContent(element, tagName); + const previewText = getPreviewTextContentResult(element, tagName); const topElements: Array = []; const bottomElements: Array = []; @@ -765,12 +768,14 @@ export const getHTMLPreview = (element: Element): string => { let content = ""; const topElementsStr = formatChildElements(topElements); - if (topElementsStr && !previewText) content += `\n ${topElementsStr}`; - if (previewText.length > 0) { - content += `\n ${truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH)}`; + if (topElementsStr && previewText.source !== "descendant") content += `\n ${topElementsStr}`; + if (previewText.text.length > 0) { + content += `\n ${truncateString(previewText.text, PREVIEW_TEXT_MAX_LENGTH)}`; } const bottomElementsStr = formatChildElements(bottomElements); - if (bottomElementsStr && !previewText) content += `\n ${bottomElementsStr}`; + if (bottomElementsStr && previewText.source !== "descendant") { + content += `\n ${bottomElementsStr}`; + } if (content.length > 0) { return `<${tagName}${attrsText}>${content}\n`; diff --git a/packages/react-grab/src/utils/get-preview-text-content.ts b/packages/react-grab/src/utils/get-preview-text-content.ts index 51b2f8995..58663d582 100644 --- a/packages/react-grab/src/utils/get-preview-text-content.ts +++ b/packages/react-grab/src/utils/get-preview-text-content.ts @@ -1,5 +1,10 @@ import { PREVIEW_TEXT_TAGS } from "../constants.js"; +export interface PreviewTextContent { + text: string; + source: "direct" | "descendant" | null; +} + const SKIPPED_TEXT_TAGS = new Set(["script", "style", "template", "noscript"]); const collapseTextContent = (text: string): string => text.replace(/\s+/g, " ").trim(); @@ -38,13 +43,26 @@ const collectDescendantText = (node: Node, parts: string[]): void => { } }; -export const getPreviewTextContent = (element: Element, tagName: string): string => { +export const getPreviewTextContentResult = ( + element: Element, + tagName: string, +): PreviewTextContent => { + if (shouldSkipElementText(element)) return { text: "", source: null }; + const directText = getDirectTextContent(element); - if (directText || !PREVIEW_TEXT_TAGS.has(tagName)) return directText; + if (directText) return { text: directText, source: "direct" }; + if (!PREVIEW_TEXT_TAGS.has(tagName)) return { text: "", source: null }; const parts: string[] = []; for (const childNode of element.childNodes) { collectDescendantText(childNode, parts); } - return collapseTextContent(parts.join(" ")); + const descendantText = collapseTextContent(parts.join(" ")); + return { + text: descendantText, + source: descendantText ? "descendant" : null, + }; }; + +export const getPreviewTextContent = (element: Element, tagName: string): string => + getPreviewTextContentResult(element, tagName).text; From e648c5535ab1f11941ae4f93713abfc1e5e8ef5a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 08:22:03 +0000 Subject: [PATCH 11/15] Keep preview text formatting compact Co-authored-by: Aiden Bai --- packages/react-grab/src/core/context.ts | 17 +++++-------- .../src/utils/get-preview-text-content.ts | 24 ++++--------------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index f95621650..cf6600428 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -33,10 +33,7 @@ import { parsePackageName } from "../utils/parse-package-name.js"; import { safeDecodeURIComponent } from "../utils/safe-decode-uri-component.js"; import { isInternalAttribute } from "../utils/strip-internal-attributes.js"; import { createElementSelector } from "../utils/create-element-selector.js"; -import { - getPreviewTextContent, - getPreviewTextContentResult, -} from "../utils/get-preview-text-content.js"; +import { getPreviewTextContent } from "../utils/get-preview-text-content.js"; import { isInternalComponentName, isUsefulComponentName, @@ -745,7 +742,7 @@ export const getInlineHTMLPreview = (element: Element): string => { export const getHTMLPreview = (element: Element): string => { const tagName = getTagName(element); const attrsText = formatAttrsForPreview(element); - const previewText = getPreviewTextContentResult(element, tagName); + const previewText = getPreviewTextContent(element, tagName); const topElements: Array = []; const bottomElements: Array = []; @@ -768,14 +765,12 @@ export const getHTMLPreview = (element: Element): string => { let content = ""; const topElementsStr = formatChildElements(topElements); - if (topElementsStr && previewText.source !== "descendant") content += `\n ${topElementsStr}`; - if (previewText.text.length > 0) { - content += `\n ${truncateString(previewText.text, PREVIEW_TEXT_MAX_LENGTH)}`; + if (topElementsStr && !previewText) content += `\n ${topElementsStr}`; + if (previewText.length > 0) { + content += `\n ${truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH)}`; } const bottomElementsStr = formatChildElements(bottomElements); - if (bottomElementsStr && previewText.source !== "descendant") { - content += `\n ${bottomElementsStr}`; - } + if (bottomElementsStr && !previewText) content += `\n ${bottomElementsStr}`; if (content.length > 0) { return `<${tagName}${attrsText}>${content}\n`; diff --git a/packages/react-grab/src/utils/get-preview-text-content.ts b/packages/react-grab/src/utils/get-preview-text-content.ts index 58663d582..25596ee5d 100644 --- a/packages/react-grab/src/utils/get-preview-text-content.ts +++ b/packages/react-grab/src/utils/get-preview-text-content.ts @@ -1,10 +1,5 @@ import { PREVIEW_TEXT_TAGS } from "../constants.js"; -export interface PreviewTextContent { - text: string; - source: "direct" | "descendant" | null; -} - const SKIPPED_TEXT_TAGS = new Set(["script", "style", "template", "noscript"]); const collapseTextContent = (text: string): string => text.replace(/\s+/g, " ").trim(); @@ -43,26 +38,15 @@ const collectDescendantText = (node: Node, parts: string[]): void => { } }; -export const getPreviewTextContentResult = ( - element: Element, - tagName: string, -): PreviewTextContent => { - if (shouldSkipElementText(element)) return { text: "", source: null }; +export const getPreviewTextContent = (element: Element, tagName: string): string => { + if (shouldSkipElementText(element)) return ""; const directText = getDirectTextContent(element); - if (directText) return { text: directText, source: "direct" }; - if (!PREVIEW_TEXT_TAGS.has(tagName)) return { text: "", source: null }; + if (directText || !PREVIEW_TEXT_TAGS.has(tagName)) return directText; const parts: string[] = []; for (const childNode of element.childNodes) { collectDescendantText(childNode, parts); } - const descendantText = collapseTextContent(parts.join(" ")); - return { - text: descendantText, - source: descendantText ? "descendant" : null, - }; + return collapseTextContent(parts.join(" ")); }; - -export const getPreviewTextContent = (element: Element, tagName: string): string => - getPreviewTextContentResult(element, tagName).text; From 73fea8596f875c401a9a0b2443da38aed071771f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 08:31:21 +0000 Subject: [PATCH 12/15] Include mixed inline preview text Co-authored-by: Aiden Bai --- .../react-grab/e2e/element-context.spec.ts | 31 +++++++++++++++++++ .../src/utils/get-preview-text-content.ts | 3 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/react-grab/e2e/element-context.spec.ts b/packages/react-grab/e2e/element-context.spec.ts index 14a9654e0..3a77e223a 100644 --- a/packages/react-grab/e2e/element-context.spec.ts +++ b/packages/react-grab/e2e/element-context.spec.ts @@ -244,5 +244,36 @@ test.describe("Element Context Fallback", () => { expect(clipboard).toContain('aria-hidden="true"'); expect(clipboard).not.toContain("Decorative Hidden Label"); }); + + test("should include nested text for mixed inline content", async ({ reactGrab }) => { + await reactGrab.page.evaluate(() => { + const wrapper = document.createElement("div"); + Object.assign(wrapper.style, { + position: "fixed", + top: "200px", + left: "200px", + width: "260px", + height: "80px", + zIndex: "999", + }); + + const link = document.createElement("a"); + link.href = "/docs/mixed-content"; + link.textContent = "Read "; + const emphasizedText = document.createElement("em"); + emphasizedText.textContent = "the docs"; + link.appendChild(emphasizedText); + + wrapper.appendChild(link); + document.body.appendChild(wrapper); + }); + + const didCopy = await reactGrab.copyElementViaApi("a[href='/docs/mixed-content']"); + expect(didCopy).toBe(true); + + const clipboard = await reactGrab.getClipboardContent(); + expect(clipboard).toContain("Read the docs"); + expect(clipboard).not.toContain(""); + }); }); }); diff --git a/packages/react-grab/src/utils/get-preview-text-content.ts b/packages/react-grab/src/utils/get-preview-text-content.ts index 25596ee5d..ef492ef09 100644 --- a/packages/react-grab/src/utils/get-preview-text-content.ts +++ b/packages/react-grab/src/utils/get-preview-text-content.ts @@ -42,7 +42,8 @@ export const getPreviewTextContent = (element: Element, tagName: string): string if (shouldSkipElementText(element)) return ""; const directText = getDirectTextContent(element); - if (directText || !PREVIEW_TEXT_TAGS.has(tagName)) return directText; + if (!PREVIEW_TEXT_TAGS.has(tagName)) return directText; + if (directText && element.children.length === 0) return directText; const parts: string[] = []; for (const childNode of element.childNodes) { From 17c3cff4d9d8a391658680fb26d84b7d507c01ee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 09:14:46 +0000 Subject: [PATCH 13/15] Simplify selector hint trace heuristic Co-authored-by: Aiden Bai --- packages/react-grab/src/core/context.ts | 68 ++++++++++--------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index cf6600428..bf6e16119 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -371,14 +371,7 @@ interface StackContextOptions { interface TraceContextResult { text: string; - firstSignalKind: - | "trusted-source" - | "untrusted-source" - | "package" - | "server" - | "component" - | null; - hasTrustedSource: boolean; + shouldAppendSelectorHint: boolean; } const hasFormattableFrames = (stack: StackFrame[] | null): boolean => { @@ -396,17 +389,6 @@ const hasFormattableFrames = (stack: StackFrame[] | null): boolean => { const isTrustedSourcePath = (filePath: string | null | undefined): boolean => Boolean(filePath && isSourceFile(filePath) && !parsePackageName(filePath)); -const getSourceSignalKind = (filePath: string): TraceContextResult["firstSignalKind"] => - isTrustedSourcePath(filePath) ? "trusted-source" : "untrusted-source"; - -const shouldAppendSelectorHint = (traceContext: TraceContextResult): boolean => { - if (!traceContext.text) return true; - if (traceContext.firstSignalKind === "package" || traceContext.firstSignalKind === "server") { - return true; - } - return !traceContext.hasTrustedSource; -}; - const formatSelectorContextLine = (element: Element): string => { const selector = createElementSelector(element); return `\n selector: ${truncateString(selector, CONTEXT_SELECTOR_MAX_LENGTH_CHARS)}`; @@ -475,29 +457,33 @@ const formatStackContext = ( const lines: string[] = []; let previousLibraryPackage: string | null = null; let didDedupeLeadingComponent = false; - let firstSignalKind: TraceContextResult["firstSignalKind"] = null; let hasTrustedSource = false; - - const recordSignal = (signalKind: TraceContextResult["firstSignalKind"]) => { - firstSignalKind ??= signalKind; - if (signalKind === "trusted-source") hasTrustedSource = true; - }; + let startsWithLowSignalContext = false; const emit = ( line: string, libraryPackage: string | null, - signalKind: TraceContextResult["firstSignalKind"], + options: { isTrustedSource?: boolean; isLowSignal?: boolean } = {}, ) => { + if (lines.length === 0 && options.isLowSignal) { + startsWithLowSignalContext = true; + } + if (options.isTrustedSource) { + hasTrustedSource = true; + } lines.push(line); previousLibraryPackage = libraryPackage; - recordSignal(signalKind); }; if (leadingSource) { + const isTrustedSource = isTrustedSourcePath(leadingSource.filePath); emit( formatSourceContextLine(leadingSource, isNextProject), null, - getSourceSignalKind(leadingSource.filePath), + { + isTrustedSource, + isLowSignal: !isTrustedSource, + }, ); } @@ -528,7 +514,7 @@ const formatStackContext = ( emit( `\n in ${componentName ?? ""} (${tag})`, libraryPackage, - libraryPackage ? "package" : "server", + { isLowSignal: true }, ); continue; } @@ -537,12 +523,13 @@ const formatStackContext = ( emit( libraryPackage ? `\n in ${componentName} (${libraryPackage})` : `\n in ${componentName}`, libraryPackage, - libraryPackage ? "package" : "component", + { isLowSignal: Boolean(libraryPackage) }, ); continue; } if (resolvedSource) { + const isTrustedSource = isTrustedSourcePath(resolvedSource); emit( formatSourceContextLine( { @@ -554,15 +541,17 @@ const formatStackContext = ( isNextProject, ), null, - getSourceSignalKind(resolvedSource), + { + isTrustedSource, + isLowSignal: !isTrustedSource, + }, ); } } return { text: lines.join(""), - firstSignalKind, - hasTrustedSource, + shouldAppendSelectorHint: startsWithLowSignalContext || !hasTrustedSource, }; }; @@ -579,11 +568,10 @@ const getTraceContext = async ( } if (leadingSource) { - const signalKind = getSourceSignalKind(leadingSource.filePath); + const isTrustedSource = isTrustedSourcePath(leadingSource.filePath); return { text: formatSourceContextLine(leadingSource, isNextProjectRuntime()), - firstSignalKind: signalKind, - hasTrustedSource: signalKind === "trusted-source", + shouldAppendSelectorHint: !isTrustedSource, }; } @@ -591,15 +579,13 @@ const getTraceContext = async ( if (componentNames.length > 0) { return { text: componentNames.map((name) => `\n in ${name}`).join(""), - firstSignalKind: "component", - hasTrustedSource: false, + shouldAppendSelectorHint: true, }; } return { text: "", - firstSignalKind: null, - hasTrustedSource: false, + shouldAppendSelectorHint: true, }; }; @@ -612,7 +598,7 @@ export const getStackContext = async ( }; const getSelectorContext = (element: Element, traceContext: TraceContextResult): string => - shouldAppendSelectorHint(traceContext) ? formatSelectorContextLine(element) : ""; + traceContext.shouldAppendSelectorHint ? formatSelectorContextLine(element) : ""; export const getElementReferenceContext = async ( element: Element, From b2addcfdd46d6a444fab07948dbae197a45c952c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 09:15:51 +0000 Subject: [PATCH 14/15] Format simplified trace heuristic Co-authored-by: Aiden Bai --- packages/react-grab/src/core/context.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index bf6e16119..2feab3267 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -477,14 +477,10 @@ const formatStackContext = ( if (leadingSource) { const isTrustedSource = isTrustedSourcePath(leadingSource.filePath); - emit( - formatSourceContextLine(leadingSource, isNextProject), - null, - { - isTrustedSource, - isLowSignal: !isTrustedSource, - }, - ); + emit(formatSourceContextLine(leadingSource, isNextProject), null, { + isTrustedSource, + isLowSignal: !isTrustedSource, + }); } for (const frame of stack) { @@ -511,11 +507,9 @@ const formatStackContext = ( if (frame.isServer && !resolvedSource && (componentName || !frame.functionName)) { const tag = libraryPackage ? `${libraryPackage} at Server` : "at Server"; - emit( - `\n in ${componentName ?? ""} (${tag})`, - libraryPackage, - { isLowSignal: true }, - ); + emit(`\n in ${componentName ?? ""} (${tag})`, libraryPackage, { + isLowSignal: true, + }); continue; } From 39e0b55d054b491f6b3a0daa65c34b0e28e7d2be Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 09:29:21 +0000 Subject: [PATCH 15/15] Minimize preview text policy changes Co-authored-by: Aiden Bai --- packages/react-grab/src/constants.ts | 31 ------------------- packages/react-grab/src/core/context.ts | 3 +- .../src/utils/get-preview-text-content.ts | 5 ++- 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/packages/react-grab/src/constants.ts b/packages/react-grab/src/constants.ts index 2df35df02..efaf6384d 100644 --- a/packages/react-grab/src/constants.ts +++ b/packages/react-grab/src/constants.ts @@ -72,37 +72,6 @@ export const LABEL_GAP_PX = 4; export const PREVIEW_TEXT_MAX_LENGTH = 100; export const PREVIEW_ATTR_VALUE_MAX_LENGTH = 15; export const PREVIEW_MAX_ATTRS = 3; -export const CONTEXT_SELECTOR_MAX_LENGTH_CHARS = 160; -export const PREVIEW_TEXT_TAGS = new Set([ - "a", - "abbr", - "b", - "button", - "caption", - "cite", - "code", - "dd", - "dt", - "em", - "figcaption", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "label", - "legend", - "li", - "p", - "pre", - "small", - "span", - "strong", - "summary", - "td", - "th", -]); export const PREVIEW_PRIORITY_ATTRS: readonly string[] = [ "id", "class", diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index 2feab3267..015d86fbc 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -23,7 +23,6 @@ import { PREVIEW_IDENTIFYING_ATTRS, SYMBOLICATION_TIMEOUT_MS, DEFAULT_MAX_CONTEXT_LINES, - CONTEXT_SELECTOR_MAX_LENGTH_CHARS, } from "../constants.js"; import { getTagName } from "../utils/get-tag-name.js"; import { truncateString } from "../utils/truncate-string.js"; @@ -391,7 +390,7 @@ const isTrustedSourcePath = (filePath: string | null | undefined): boolean => const formatSelectorContextLine = (element: Element): string => { const selector = createElementSelector(element); - return `\n selector: ${truncateString(selector, CONTEXT_SELECTOR_MAX_LENGTH_CHARS)}`; + return `\n selector: ${selector}`; }; const getComponentNamesFromFiber = (element: Element, maxCount: number): string[] => { diff --git a/packages/react-grab/src/utils/get-preview-text-content.ts b/packages/react-grab/src/utils/get-preview-text-content.ts index ef492ef09..74c0c89f3 100644 --- a/packages/react-grab/src/utils/get-preview-text-content.ts +++ b/packages/react-grab/src/utils/get-preview-text-content.ts @@ -1,5 +1,4 @@ -import { PREVIEW_TEXT_TAGS } from "../constants.js"; - +const DESCENDANT_TEXT_TAGS = new Set(["a", "code", "pre"]); const SKIPPED_TEXT_TAGS = new Set(["script", "style", "template", "noscript"]); const collapseTextContent = (text: string): string => text.replace(/\s+/g, " ").trim(); @@ -42,7 +41,7 @@ export const getPreviewTextContent = (element: Element, tagName: string): string if (shouldSkipElementText(element)) return ""; const directText = getDirectTextContent(element); - if (!PREVIEW_TEXT_TAGS.has(tagName)) return directText; + if (!DESCENDANT_TEXT_TAGS.has(tagName)) return directText; if (directText && element.children.length === 0) return directText; const parts: string[] = [];