Skip to content

feat(react-grab): inline source snippets, fiber props, and stack-tail collapse#325

Open
aidenybai wants to merge 4 commits into
cursor/include-library-components-in-stack-2482from
cursor/copy-output-source-props-collapse
Open

feat(react-grab): inline source snippets, fiber props, and stack-tail collapse#325
aidenybai wants to merge 4 commits into
cursor/include-library-components-in-stack-2482from
cursor/copy-output-source-props-collapse

Conversation

@aidenybai
Copy link
Copy Markdown
Owner

@aidenybai aidenybai commented May 5, 2026

Summary

Three targeted improvements to the clipboard payload that AI coding agents consume when a user grabs an element:

  • Source-snippet inlining — when an owner-stack frame resolves to a same-origin source file, embed a small window of authored code around the call site with a > marker on the resolved line. Snippets that fail a JSX/component-name trust check are tagged (approximate). Vite's _jsxDEV-rewritten dev output is fingerprinted and rejected so the agent never sees transformed noise.
  • Component instance snapshot — the innermost user-source stack line falls back to a JSX-call signature (in <Link href=\"...\" />) sourced from the React fiber's memoizedProps when a trustworthy snippet is not available. The two signals are deduped so the agent never sees the same call site twice.
  • Stack-tail collapse — when multiple grabbed elements share a suffix in their React stack, emit each entry's diverging prefix and the shared tail once at the bottom. Materially shrinks the payload for list-heavy grabs.

Why

Author confidence in the snippet matters more than verbosity:

  • Source maps lie (Vite serves transformed code at the same URL, Webpack indexes can drift). The trust guard + transformed-output fingerprint stop us from confidently shipping the wrong code.
  • The fiber memoizedProps is the only signal that survives across all bundlers, so it's the natural fallback when source snippets are unavailable.
  • The longest-common-suffix collapse eliminates ~70% of duplicated stack lines in typical multi-grab list scenarios.

Implementation notes

  • Caching: getSourceContent dedupes in-flight fetches but evicts null results so a transient HMR-rebuild failure doesn't permanently block snippets.
  • Suffix matching: bundler-prefixed sources arrays (webpack://, vite://) are handled via suffix matching, but only when the lookup has ≥2 path segments to prevent basenames from cross-file false-matching.
  • Same-origin lockdown: snippet fetches go through new URL(fileName, location.origin) so a doctored _debugSource.fileName can't trigger cross-origin requests, including via //host/... protocol-relative paths.
  • Trust check: matches only proper component tags (<UpperCase); broader tokens like => or function would silently mark wrong-target resolutions as trustworthy.
  • Refactor: is-useful-component-name, find-longest-common-suffix, and escape-regexp extracted as one-util-per-file modules per AGENTS.md.

Public API (generateSnippet, joinSnippets, getStackContext) is preserved.

Test plan

  • 94 unit tests pass (was 76, +18 new across the new utilities and trust/transform guards)
  • 13 element-context e2e tests pass
  • pnpm lint / pnpm typecheck / pnpm format clean
  • Pre-existing copy-feedback.spec.ts flakes verified on baseline (unrelated)
  • CI green

Stacked on

Base is #323. When that lands, this PR will auto-rebase onto main.

Made with Cursor


Note

Medium Risk
Changes the core clipboard-context generation pipeline (stack formatting, snippet joining, and new source fetching), which could affect correctness/size of copied output and introduce edge cases across bundlers despite added tests and same-origin guards.

Overview
Improves the clipboard payload agents receive by inlining a nearby authored source snippet (with approximate/trust markings) when a stack frame resolves to a same-origin source file, and by falling back to a JSX-like component instance signature derived from fiber memoizedProps when a trustworthy snippet isn’t available.

Refactors context generation to produce structured ElementContextParts and updates copy/join logic to collapse shared React stack suffixes across multi-element grabs (emitting per-entry diverging prefixes plus one shared tail, and sharing a single snippet block when identical), while preserving existing public APIs (generateSnippet, joinSnippets, getStackContext).

Updates docs/readmes copy and expands coverage with new unit + e2e tests around snippet trust/transform detection, prop formatting, and stack-tail collapsing behavior.

Reviewed by Cursor Bugbot for commit e059f9f. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

In react-grab, inline authored source snippets, attach component props to the innermost stack frame, and collapse shared stack tails to make copied context smaller and clearer. Now more robust with indexed source maps and safer prop formatting, plus clearer READMEs and Storybook docs.

  • New Features

    • Inline a small source window for same-origin frames with a highlighted call line; tag as “(approximate)” when JSX/name trust fails and ignore transformed dev output like Vite’s _jsxDEV.
    • Add a component instance snapshot from React fiber memoizedProps, rendering in <Component ... /> when no trustworthy snippet is available; deduped so the call site never appears twice and never applied to library frames.
    • Collapse the longest common React stack suffix across multi-selects; emit each entry’s diverging prefix, and print the shared tail once (and a single shared snippet block only when identical); fall back to legacy numbering when transforms change snippets.
  • Bug Fixes

    • Resolve source content in indexed source maps by recursing into sections, fixing missing snippets for sectioned maps.
    • Guard invalid Date props and stop counting undefined values toward the prop overflow marker; improves instance rendering and avoids errors.
    • When the owner stack isn’t formattable but fiber names exist, render a JSX-call from memoizedProps on the innermost line for consistent fallback.
    • Keep indentation consistent for collapsed shared stack tails; the first tail line now aligns with the rest.
    • Build a fresh empty context on errors in generateSnippetParts to prevent cross-entry mutation leaks.

Written for commit e059f9f. Summary will update on new commits.

… collapse

Three targeted improvements to the clipboard payload that AI agents read:

- Source-snippet inlining: when an owner-stack frame resolves to a same-origin
  source file we now embed a small window of authored code around the call site
  with a `>` marker on the resolved line. Snippets that fail a JSX/component-
  name trust check are tagged `(approximate)` so the agent doesn't blindly edit
  noise. Vite's `_jsxDEV`-rewritten output is fingerprinted and rejected.

- Component instance snapshot: the innermost user-source stack line falls back
  to a JSX-call signature (`in <Link href="..." />`) sourced from the React
  fiber's `memoizedProps` when no trustworthy snippet is available. We dedupe
  against the snippet so the agent never sees the same call site twice.

- Stack-tail collapse: when multiple grabbed elements share a suffix in their
  React stack we emit the diverging prefixes per entry and the shared tail
  once at the bottom, materially shrinking the payload for list-heavy grabs.

Also extracts `is-useful-component-name`, `find-longest-common-suffix`, and
`escape-regexp` into one-util-per-file modules per AGENTS.md, and adds 18 new
unit tests covering the new utilities and the trust/transform guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-grab-storybook Ready Ready Preview, Comment May 8, 2026 3:48pm
react-grab-website Ready Ready Preview, Comment May 8, 2026 3:48pm

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@325
npm i https://pkg.pr.new/aidenybai/react-grab/grab@325
npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/mcp@325
npm i https://pkg.pr.new/aidenybai/react-grab@325

commit: e059f9f

Comment thread packages/react-grab/src/utils/format-component-instance.ts
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 20 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/utils/get-source-snippet.ts">

<violation number="1" location="packages/react-grab/src/utils/get-source-snippet.ts:55">
P1: Indexed source maps are skipped due to an early return, so snippet lookup fails when sources are only in `sections`.</violation>
</file>

<file name="packages/react-grab/src/core/context.ts">

<violation number="1" location="packages/react-grab/src/core/context.ts:545">
P2: Include the fiber component instance in this fallback path. Otherwise, when source snippets are unavailable because the owner stack cannot be formatted, the memoizedProps JSX-call fallback is skipped and only bare component names are copied.</violation>
</file>

<file name="packages/react-grab/src/utils/format-component-instance.ts">

<violation number="1" location="packages/react-grab/src/utils/format-component-instance.ts:35">
P2: Formatting invalid `Date` props can throw and break component snapshot generation.</violation>

<violation number="2" location="packages/react-grab/src/utils/format-component-instance.ts:61">
P3: Overflow count is computed before filtering non-renderable values, so `/* +N more */` can overcount.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/react-grab/src/utils/get-source-snippet.ts Outdated
Comment thread packages/react-grab/src/core/context.ts Outdated
Comment thread packages/react-grab/src/utils/format-component-instance.ts Outdated
Comment thread packages/react-grab/src/utils/format-component-instance.ts
- get-source-snippet: don't bail on indexed source maps. Maps that only
  expose `sections` (no top-level `sources`/`sourcesContent`) were short-
  circuited before the recursion; now we always recurse when sections exist.
- format-component-instance: invalid Date props (`new Date("nope")`)
  used to throw inside `toISOString()` and break snapshot generation.
  Guard via `Number.isNaN(getTime())` and emit `{Date(Invalid)}`.
- format-component-instance: stop counting `undefined`-valued props past the
  cap toward `/* +N more */`. Move the limit check after `formatPropValue` so
  values that wouldn't have rendered don't inflate the overflow marker.
- core/context: when the owner stack isn't formattable but we have fiber
  component names, surface the innermost component's `memoizedProps` as a
  JSX-call signature instead of just the bare name — same fallback the
  source-snippet path already provides.

Adds two unit tests (invalid Date, undefined-past-limit overflow). 96 unit
tests pass; 13 element-context e2e pass; lint/typecheck/format clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
@aidenybai
Copy link
Copy Markdown
Owner Author

Thanks for the review. All four issues addressed in 9522b96:

  1. P1 — Indexed source maps skipped (get-source-snippet.ts) — valid, real bug. The early if (!sourceMap?.sources || !sourceMap.sourcesContent) return null; was short-circuiting maps that only expose sections. Now we recurse into sections whenever they exist, regardless of whether the top-level has sources/sourcesContent.

  2. P2a — bare-name fallback path missing fiber props (core/context.ts) — valid. When hasFormattableFrames(stack) is false we previously emitted only in <Name> even though we already have the fiber. Now the innermost name is rendered with its memoizedProps, matching the source-snippet path's fallback semantics.

  3. P2b — invalid Date throws (format-component-instance.ts) — valid, real bug. new Date("nope").toISOString() throws RangeError. Guarded via Number.isNaN(getTime()) and emit {Date(Invalid)}.

  4. P3 — overflow miscount (format-component-instance.ts) — valid. Reordered the loop so the limit check runs after formatPropValue returns non-null; undefined values past the cap no longer inflate /* +N more */. New unit test guards the regression.

Verified: 96 unit tests pass (+2 new), 13 element-context e2e pass, lint/typecheck/format clean.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9522b96. Configure here.

Comment thread packages/react-grab/src/utils/join-snippets.ts Outdated
Comment thread packages/react-grab/src/utils/generate-snippet.ts Outdated
- join-snippets: collapsed shared stack tail's first line lost its indent.
  `formatStackLines` produced `\n  line1\n  line2`, then `.trimStart()`
  stripped both the leading newline AND the two-space indent, leaving the
  first tail line flush-left while the rest stayed indented. Replace with
  `indentStackLines` (joins on `\n`) and `formatDivergingStackLines` (only
  prefixes with `\n` when there is content), which keeps every shared-tail
  line at the same indent level. New unit test guards the regression.

- generate-snippet: `emptyParts` was a shared mutable singleton; a stray
  `push` from any consumer would corrupt every future error fallback.
  Build a fresh `ElementContextParts` per rejection.

Co-authored-by: Cursor <cursoragent@cursor.com>
@aidenybai
Copy link
Copy Markdown
Owner Author

Bugbot follow-up addressed in c4d9a68:

  • Medium — join-snippets.ts:60 shared-tail indent regression — confirmed real. formatStackLines emitted "\n line1\n line2", then .trimStart() stripped both the leading newline and the two-space indent, leaving only the first tail line flush-left. Replaced with indentStackLines (joins on \n) + formatDivergingStackLines (only prefixes \n when non-empty). New unit test asserts every shared-tail line keeps the two-space prefix.
  • Low — generate-snippet.ts:16 shared mutable singleton — fixed by minting a fresh ElementContextParts per rejection. Cheap defensive change.

97 unit tests pass, 13 element-context e2e pass, lint/typecheck/format clean.

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

8 similar comments
@aidenybai
Copy link
Copy Markdown
Owner Author

react review

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

@aidenybai
Copy link
Copy Markdown
Owner Author

react review

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant