Skip to content

fix(react-grab): keep third-party component names in stack context#323

Open
aidenybai wants to merge 7 commits into
mainfrom
cursor/include-library-components-in-stack-2482
Open

fix(react-grab): keep third-party component names in stack context#323
aidenybai wants to merge 7 commits into
mainfrom
cursor/include-library-components-in-stack-2482

Conversation

@aidenybai
Copy link
Copy Markdown
Owner

@aidenybai aidenybai commented May 3, 2026

Problem

When the user selects an element rendered by a third-party component (e.g. SquareIcon from lucide-react), the selection label correctly shows the component name (SquareIcon), but the stack context appended to the copied payload drops it. The pasted output looks like:

<svg xmlns="http://www.w3.o..." width="24" ...>
  (3 elements)
</svg>
  in TooltipTrigger (at /src/components/ui/tooltip.tsx)
  in PreviewTabStrip (at /src/components/preview/PreviewTabStrip.tsx)
  in PreviewView (at /src/components/preview/PreviewView.tsx)

The component the user actually selected (SquareIcon) is missing entirely. The agent receiving this payload has no way to know what was selected — it only sees the raw <svg> and a stack that starts at the nearest user-source ancestor.

Root cause

formatStackContext in packages/react-grab/src/core/context.ts filters owner-stack frames through isSourceFile() and only keeps frames whose fileName looks like user-authored source. Library components in node_modules (or vendor bundles) fail that check, so frames like SquareIcon (at .../lucide-react/.../square.js) get dropped silently. The label UI doesn't have this problem because it walks the fiber tree directly via getComponentDisplayName.

Fix

In formatStackContext, when a frame has a real-looking component functionName but no user-source fileName, emit a name-only line (\n in SquareIcon) instead of skipping it. This mirrors React's own component-stack format and preserves the most informative bit (the component identity) without leaking misleading library file paths into the payload.

hasFormattableFrames is updated correspondingly so the fallback fiber-name path isn't used when the owner stack already carries the component identity.

Testing

  • New Playwright case in element-context.spec.ts selects a lucide-react <SquareIcon> rendered inside the e2e app and asserts the clipboard contains in SquareIcon plus the user-source ancestor (LibraryIconSection).
  • A LibraryIconSection was added to apps/e2e-app/src/App.tsx and lucide-react was added as a dependency to surface the third-party scenario in tests.
  • pnpm typecheck, pnpm lint, and pnpm format are clean.
Open in Web Open in Cursor 

Summary by cubic

Fixes react-grab so the copied stack context keeps third‑party component names and tags them with the originating package (and server when applicable). Collapses consecutive library frames to preserve user frames; simplifies package parsing with a single version regex and better CDN detection.

  • Bug Fixes

    • In formatStackContext, emit a name‑only line when a frame has a valid component name but no user‑source file; update hasFormattableFrames to treat these as valid.
    • Add e2e that selects the lucide-react icon and asserts the clipboard includes "in Square (lucide-react)" and the user‑source ancestor.
  • New Features

    • Tag library frames with their npm package and collapse consecutive frames from the same package to keep user frames within maxLines; include server frames as "(pkg at Server)".
    • Add parse-package-name to recover package names across node_modules, Vite optimized deps (incl. deps_temp* and flattened scopes), Webpack/Turbopack/Next URL shapes, Windows paths, and CDN URLs; switch to a version‑prefix check (no host allow‑list), treat file:// as filesystem paths, and collapse name/version detection into one regex with a cleaner last‑match search. Add a scoped @radix-ui/react-dialog fixture and e2e to verify scope round‑trips.
    • Refactor stack formatting for clearer flow (small emit helper and extracted resolved‑source builder); add unit tests for parse-package-name and wire vp test to run before Playwright (test, test:unit); add Vitest config.

Written for commit 54dabef. Summary will update on new commits.

When a user selects an element rendered by a third-party component
(e.g. SquareIcon from lucide-react), the selection label correctly
displays the component name via the React fiber walk, but the stack
context appended to the copied payload filtered out frames whose
fileName lives in node_modules or vendor bundles. The result was a
payload that jumped straight to the nearest user-source ancestor
(e.g. TooltipTrigger), leaving the agent with no idea what component
was actually selected.

formatStackContext now emits a name-only line ("in SquareIcon") for
frames that have a real-looking component name but no user-source
file. This mirrors React's own component-stack format and preserves
the most informative signal without leaking misleading library paths.

hasFormattableFrames is updated to recognize these frames as
formattable so the fallback fiber-name path is no longer used when
the owner stack carries the component identity.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 3, 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 5, 2026 10:34am
react-grab-website Ready Ready Preview, Comment May 5, 2026 10:34am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 3, 2026

Open in StackBlitz

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

commit: 54dabef

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Recovers the npm package each `node_modules`-resolved frame came from and
appends it to the stack context (`in Square (lucide-react)` and, for RSC
trees, `in Image (next at Server)`) so the agent sees what library the
grabbed component actually belongs to instead of a bare name.

The new `parse-package-name` util handles direct paths, pnpm/Bun/Yarn-PnP
nested layouts, Vite optimized deps (incl. `deps_temp` and scope-flattened
`@scope_name.js`), Webpack/Turbopack/Next.js layer prefixes (`(rsc)`,
`(app-pages-browser)`, etc.), Windows backslashes, multi-slash paths, and
common CDN URL shapes (esm.sh / unpkg / jsdelivr / skypack), with an
allowlist + path-boundary anchoring to avoid false positives on user paths
that happen to contain `@`.

Also coalesces consecutive frames from the same package into a single line
so a deeply nested Radix/MUI/Chakra tree no longer evicts the user's own
component frames from the tight `maxLines=3` budget.

E2E fixture adds `@radix-ui/react-dialog` to cover scoped-package
round-tripping through Vite's flat `@radix-ui_react-dialog.js?v=...` form.

Co-authored-by: Cursor <cursoragent@cursor.com>
aidenybai and others added 2 commits May 5, 2026 02:40
Centralize the line-push + previousLibraryPackage bookkeeping in a small
emit() helper so individual branches can't forget to update tracking
state, compute componentName once per iteration instead of three times,
and extract the resolved-source line builder into formatResolvedSourceLine
so the main loop body reads top-to-bottom (compute > coalesce > server >
library > source) without inline string concatenation noise.

Also polish parse-package-name: use splitPathSegments consistently in
findVersionedPackageInPath, and document why un-versioned scoped CDN
paths like /npm/@types/foo intentionally return null.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wire vite-plus test runner into packages/react-grab so its pure utils
can be unit-tested without spinning up Playwright, and codify the
bundler matrix as 40 cases in test/parse-package-name.test.ts:

- plain node_modules (scoped + unscoped)
- pnpm hoist (incl. peer-dep variant), Yarn PnP zips, Bun cache
- Vite optimized deps: deps/, deps_temp/, deps_temp_<hash>/, scope
  flattening (@radix-ui_react-dialog -> @radix-ui/react-dialog),
  internal chunk-<hash>.js rejection
- Webpack/webpack-internal/Turbopack URL shapes
- Windows backslash paths, multi-slash defensive
- hoist meta-dir rejection (.bin, .cache)
- CDN URLs: esm.sh (incl. v135/, stable/), unpkg, jsdelivr (incl.
  /npm/ + scoped), skypack /pin/
- ambiguous (un-versioned scoped) and untrusted-host fallbacks

Chain `pnpm test` to run the unit suite before Playwright; expose
`pnpm test:unit` for the unit-only path.

Co-authored-by: Cursor <cursoragent@cursor.com>
…heck

The hardcoded `KNOWN_PACKAGE_CDN_HOSTS` set existed only to prevent the
`<name>@<version>` CDN heuristic from misfiring on user paths that
incidentally contain `@` (`me@work`, `foo@bar.com`, twitter `@handle`).
The discriminator is actually staring at us: real npm versions in CDN
URLs always start with a digit (or `v<digit>` for skypack's pinned
URLs), while the false-positive cases never do.

Replacing the host allow-list with a `^v?\d/` version-prefix check
removes a magic list that had to be maintained as new CDNs appeared,
and as a bonus correctly recognizes self-hosted internal CDN URLs
that the allow-list previously rejected. file:// URLs are still
routed to the node_modules matcher rather than treated as CDN URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Three small elegance wins in the package-name parser:

1. Fold `PACKAGE_VERSION_PREFIX_REGEX` and the three-step
   `splitNameAtVersion` helper into a single `NAME_AT_VERSION_REGEX`
   (`^(.+)@v?\d/`). The new `matchNameAtVersion` is a one-liner that
   captures the name only when an npm-shaped version follows, so
   `me@work` and `foo@bar.com` are still rejected by the same
   discriminator without the intermediate slice/test plumbing.

2. Replace the imperative `let lastMatchEnd; for (...)` accumulator
   in `matchAfterLastPattern` with `[...matchAll(pattern)].at(-1)`
   for declarative "last match if any" semantics.

3. Inline the one-call `stripFileExtension` helper into
   `readViteOptimizedDepPackage` and split the destructure for
   readability; replace the index-based loop in
   `findVersionedPackageInPath` with `segments.entries()` so the
   look-ahead reads as `segments[index + 1]` instead of two `let`s.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

No issues found across 9 files

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.

2 participants