diff --git a/.changeset/grab-source-fetch-lag-fixes.md b/.changeset/grab-source-fetch-lag-fixes.md deleted file mode 100644 index 273a4f04c..000000000 --- a/.changeset/grab-source-fetch-lag-fixes.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"react-grab": patch ---- - -Fix the grab hanging on "Grabbing…" when the app saturates the dev server's connection pool. Source resolution (bundle and source-map fetches via bippy, plus Next.js server-frame symbolication) now runs through a concurrency-capped, abortable queue with a timeout, so it no longer queues indefinitely behind the app's own requests. Requires bippy ≥0.5.42 so an aborted source-map fetch no longer poisons bippy's cache and later grabs recover. - -Also fixes: - -- A click immediately after keyboard navigation selecting a stale element instead of the one under the pointer. -- The page jumping when focus is restored after a grab (focus now restores with `preventScroll`). -- Being unable to select page content while a modal sets `body { pointer-events: none }` (e.g. Radix), via a hit-test override. - -Plus activation and drag performance: animations freeze via the Web Animations API instead of a universal-selector recalc, activation batches its layout reads before writes, the React-update freeze walks fibers iteratively, and drag de-duplication is O(n·d) instead of O(n²). diff --git a/.changeset/style-design-tokens.md b/.changeset/style-design-tokens.md deleted file mode 100644 index 2f5e6c212..000000000 --- a/.changeset/style-design-tokens.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"react-grab": patch ---- - -Style mode now resolves committed values to the project's design tokens when copying. Tokens are derived from the CSS custom properties already defined in the page's cascade, so this works for any library that exposes design tokens as CSS variables (shadcn/ui, Radix, Chakra, MUI, Tailwind v4 `@theme`, Panda, vanilla-extract, …) rather than a single hard-coded framework. When a tweaked color matches a token, or a length matches a token whose name shares the property's family (spacing/size/radius/font-size/…), the copied CSS annotates the declaration with a `/* var(--token) */` hint and the prompt nudges the agent to prefer the token over the raw value. - -Arrow-key stepping in the Style panel now snaps a px property through that token scale (e.g. `←`/`→` walk the spacing tokens) instead of always nudging ±1px. `Shift` keeps the coarse raw step (×10) and `Alt`/`Option` does a fine raw ±1px step — both opt out of snapping so values can land between tokens. A value sitting outside the scale falls back to a raw step so stepping never dead-ends. When a framework exposes spacing as a single base unit instead of discrete tokens (Tailwind v4's `--spacing`), stepping walks that base-unit grid. - -Color tokens are resolved through the browser's own rasterizer, so modern wide-gamut values that `getComputedStyle` returns — `lab()`, `lch()`, `oklab()`, `oklch()`, `color()` — are matched too (these are what Tailwind v4 / shadcn themes compile to), not just hex/rgb/hsl. diff --git a/.changeset/theme-detect-transparent-page.md b/.changeset/theme-detect-transparent-page.md deleted file mode 100644 index c9370c26a..000000000 --- a/.changeset/theme-detect-transparent-page.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-grab": patch ---- - -Fix theme detection mis-classifying an undeclared light page as dark for visitors on a dark OS. When a page has no theme marker, no `color-scheme`, and no painted background, detection now derives the real backdrop from the CSS `Canvas` system color instead of guessing from `prefers-color-scheme`. `Canvas` honors the root element's used `color-scheme`, so it stays light under the default `normal` (regardless of the OS) and only tracks the OS preference when the page opts into a dark-capable scheme such as `light dark` - matching exactly what the browser paints behind the page. diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index e6f3d92d1..730b6a5d4 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,7 @@ # @react-grab/cli +## 0.1.48 + ## 0.1.47 ## 0.1.46 diff --git a/packages/cli/package.json b/packages/cli/package.json index 2de006c2f..c18531b61 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@react-grab/cli", - "version": "0.1.47", + "version": "0.1.48", "repository": { "type": "git", "url": "git+https://github.com/aidenybai/react-grab.git" diff --git a/packages/grab/CHANGELOG.md b/packages/grab/CHANGELOG.md index b038fdb6e..fbce007de 100644 --- a/packages/grab/CHANGELOG.md +++ b/packages/grab/CHANGELOG.md @@ -1,5 +1,11 @@ # grab +## 0.1.48 + +### Patch Changes + +- @react-grab/cli@0.1.48 + ## 0.1.47 ### Patch Changes diff --git a/packages/grab/package.json b/packages/grab/package.json index ca89a9f13..4faa3764b 100644 --- a/packages/grab/package.json +++ b/packages/grab/package.json @@ -1,6 +1,6 @@ { "name": "grab", - "version": "0.1.47", + "version": "0.1.48", "description": "Select context for coding agents directly from your website", "keywords": [ "agent", diff --git a/packages/react-grab/CHANGELOG.md b/packages/react-grab/CHANGELOG.md index 0411274cd..5a92fd26e 100644 --- a/packages/react-grab/CHANGELOG.md +++ b/packages/react-grab/CHANGELOG.md @@ -1,5 +1,28 @@ # react-grab +## 0.1.48 + +### Patch Changes + +- bc3a591: Fix the grab hanging on "Grabbing…" when the app saturates the dev server's connection pool. Source resolution (bundle and source-map fetches via bippy, plus Next.js server-frame symbolication) now runs through a concurrency-capped, abortable queue with a timeout, so it no longer queues indefinitely behind the app's own requests. Requires bippy ≥0.5.42 so an aborted source-map fetch no longer poisons bippy's cache and later grabs recover. + + Also fixes: + + - A click immediately after keyboard navigation selecting a stale element instead of the one under the pointer. + - The page jumping when focus is restored after a grab (focus now restores with `preventScroll`). + - Being unable to select page content while a modal sets `body { pointer-events: none }` (e.g. Radix), via a hit-test override. + + Plus activation and drag performance: animations freeze via the Web Animations API instead of a universal-selector recalc, activation batches its layout reads before writes, the React-update freeze walks fibers iteratively, and drag de-duplication is O(n·d) instead of O(n²). + +- e56fcc1: Style mode now resolves committed values to the project's design tokens when copying. Tokens are derived from the CSS custom properties already defined in the page's cascade, so this works for any library that exposes design tokens as CSS variables (shadcn/ui, Radix, Chakra, MUI, Tailwind v4 `@theme`, Panda, vanilla-extract, …) rather than a single hard-coded framework. When a tweaked color matches a token, or a length matches a token whose name shares the property's family (spacing/size/radius/font-size/…), the copied CSS annotates the declaration with a `/* var(--token) */` hint and the prompt nudges the agent to prefer the token over the raw value. + + Arrow-key stepping in the Style panel now snaps a px property through that token scale (e.g. `←`/`→` walk the spacing tokens) instead of always nudging ±1px. `Shift` keeps the coarse raw step (×10) and `Alt`/`Option` does a fine raw ±1px step — both opt out of snapping so values can land between tokens. A value sitting outside the scale falls back to a raw step so stepping never dead-ends. When a framework exposes spacing as a single base unit instead of discrete tokens (Tailwind v4's `--spacing`), stepping walks that base-unit grid. + + Color tokens are resolved through the browser's own rasterizer, so modern wide-gamut values that `getComputedStyle` returns — `lab()`, `lch()`, `oklab()`, `oklch()`, `color()` — are matched too (these are what Tailwind v4 / shadcn themes compile to), not just hex/rgb/hsl. + +- 853ec52: Fix theme detection mis-classifying an undeclared light page as dark for visitors on a dark OS. When a page has no theme marker, no `color-scheme`, and no painted background, detection now derives the real backdrop from the CSS `Canvas` system color instead of guessing from `prefers-color-scheme`. `Canvas` honors the root element's used `color-scheme`, so it stays light under the default `normal` (regardless of the OS) and only tracks the OS preference when the page opts into a dark-capable scheme such as `light dark` - matching exactly what the browser paints behind the page. + - @react-grab/cli@0.1.48 + ## 0.1.47 ### Patch Changes @@ -7,6 +30,7 @@ - 5407d4e: Surface deeper copy context for wrapper-heavy elements. App-owned shared-UI / design-system frames (files under `components/ui/`, `packages/ui/`, `design-system(s)/`, or `primitives/`, e.g. shadcn's `components/ui` or a monorepo `packages/ui`) are now treated like `node_modules` frames: still shown, but exempt from the compact line budget, so a grabbed wrapper digs through its UI primitives to the meaningful feature source by default. Adds a `maxContextLines` option (also settable via the script `data-options` attribute) to raise the budget further for large apps and agent/edit prompts — restoring the option the CLI already writes. Also hardens the trace: a non-finite/negative `maxContextLines` no longer disables the hard line cap (it falls back to the default), and consecutive duplicate trace lines from shared-UI frames are collapsed so the output stays readable. + - @react-grab/cli@0.1.47 ## 0.1.46 diff --git a/packages/react-grab/package.json b/packages/react-grab/package.json index 1f5e55ad7..1281fe8cb 100644 --- a/packages/react-grab/package.json +++ b/packages/react-grab/package.json @@ -1,6 +1,6 @@ { "name": "react-grab", - "version": "0.1.47", + "version": "0.1.48", "description": "Select context for coding agents directly from your website", "keywords": [ "agent", diff --git a/packages/react-grab/src/components/overlay-canvas.tsx b/packages/react-grab/src/components/overlay-canvas.tsx index 623fa5877..c55ff1816 100644 --- a/packages/react-grab/src/components/overlay-canvas.tsx +++ b/packages/react-grab/src/components/overlay-canvas.tsx @@ -290,6 +290,7 @@ export const OverlayCanvas: Component = (props) => { const compositeAllLayers = () => { if (!mainContext || !canvasRef) return; + if (canvasWidth <= 0 || canvasHeight <= 0) return; mainContext.setTransform(1, 0, 0, 1, 0, 0); mainContext.clearRect(0, 0, canvasRef.width, canvasRef.height); diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index b8b71dc6b..8e5703b81 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -17,6 +17,7 @@ import { import { createElementSelector } from "../utils/create-element-selector.js"; import { isSharedUiSourcePath } from "../utils/is-shared-ui-source-path.js"; import { isNextProjectRuntime } from "../utils/is-next-project-runtime.js"; +import { formatComponentNameLines } from "../utils/format-component-name-lines.js"; import { enrichServerFrameLocations, symbolicateServerFrames } from "./next-server-frames.js"; import { runQueuedSourceFetch } from "../utils/source-fetch-queue.js"; import { getHTMLPreview, getInlineHTMLPreview } from "./html-preview.js"; @@ -233,9 +234,15 @@ export interface StackContextOptions { interface TraceContextResult { text: string; shouldAppendSelectorHint: boolean; + hasBudgetedStackFrame: boolean; + renderedComponentNames: Set; } -const getComponentNamesFromFiber = (element: Element, maxCount: number): string[] => { +const getComponentNamesFromFiber = ( + element: Element, + maxCount: number, + shouldIncludeName: (componentName: string) => boolean = () => true, +): string[] => { if (!isInstrumentationActive()) return []; const fiber = getFiberFromHostInstance(element); if (!fiber) return []; @@ -247,7 +254,7 @@ const getComponentNamesFromFiber = (element: Element, maxCount: number): string[ if (componentNames.length >= maxCount) return true; if (isCompositeFiber(currentFiber)) { const displayName = getDisplayName(currentFiber.type); - if (displayName && isUsefulComponentName(displayName)) { + if (displayName && isUsefulComponentName(displayName) && shouldIncludeName(displayName)) { componentNames.push(displayName); } } @@ -367,16 +374,23 @@ export const formatStackContext = ( const hardMaxLines = Math.max(maxLines, MAX_TRACE_CONTEXT_LINES); const isNextProject = isNextProjectRuntime(); const lines: string[] = []; + const renderedComponentNames = new Set(); let previousLibraryFrameKey: string | null = null; let didDedupeLeadingComponent = false; let hasTrustedSource = false; + let hasBudgetedStackFrame = false; let budgetedLineCount = 0; + const addComponentName = (componentName: string | null | undefined) => { + if (componentName) renderedComponentNames.add(componentName); + }; + if (leadingSource) { hasTrustedSource = leadingSource.origin === "app"; // A shared-UI leading source means the user grabbed a primitive directly; // keep its budget free so the feature ancestors that consume it surface. if (!isSharedUiSourcePath(leadingSource.filePath)) budgetedLineCount += 1; + addComponentName(leadingSource.componentName); lines.push(formatSourceContextLine(leadingSource, isNextProject)); } @@ -424,7 +438,11 @@ export const formatStackContext = ( if (frameLine.text === lines[lines.length - 1]) continue; if (frameLine.isAppSource) hasTrustedSource = true; - if (frameLine.consumesBudget) budgetedLineCount += 1; + if (frameLine.consumesBudget) { + budgetedLineCount += 1; + hasBudgetedStackFrame = true; + } + addComponentName(componentName); lines.push(frameLine.text); previousLibraryFrameKey = libraryFrameKey; } @@ -432,6 +450,8 @@ export const formatStackContext = ( return { text: lines.join(""), shouldAppendSelectorHint: !hasTrustedSource, + hasBudgetedStackFrame, + renderedComponentNames, }; }; @@ -442,6 +462,28 @@ const resolveLeadingSource = async (element: Element): Promise { + const missingAncestorNames = getComponentNamesFromFiber( + findNearestFiberElement(element), + maxAncestorCount, + (ancestorName) => + isSourceComponentName(ancestorName) && !stackContext.renderedComponentNames.has(ancestorName), + ); + if (missingAncestorNames.length === 0) return stackContext; + + return { + ...stackContext, + text: `${stackContext.text}${formatComponentNameLines(missingAncestorNames)}`, + }; +}; + const getTraceContext = async ( element: Element, options: StackContextOptions = {}, @@ -449,21 +491,20 @@ const getTraceContext = async ( const leadingSource = await resolveLeadingSource(element); const stack = await getStack(element); + const maxLines = resolveMaxContextLines(options.maxLines); const stackContext = formatStackContext(stack ?? [], options, leadingSource); - if (stackContext.text) return stackContext; - - const componentNames = getComponentNamesFromFiber( - findNearestFiberElement(element), - resolveMaxContextLines(options.maxLines), - ); - if (componentNames.length > 0) { - return { - text: componentNames.map((componentName) => `\n in ${componentName}`).join(""), - shouldAppendSelectorHint: true, - }; + if (stackContext.text) { + if (stackContext.hasBudgetedStackFrame) return stackContext; + return appendFiberAncestorNames(element, stackContext, maxLines); } - return { text: "", shouldAppendSelectorHint: true }; + const componentNames = getComponentNamesFromFiber(findNearestFiberElement(element), maxLines); + return { + text: formatComponentNameLines(componentNames), + shouldAppendSelectorHint: true, + hasBudgetedStackFrame: false, + renderedComponentNames: new Set(componentNames), + }; }; export const getStackContext = async ( diff --git a/packages/react-grab/src/utils/format-component-name-lines.ts b/packages/react-grab/src/utils/format-component-name-lines.ts new file mode 100644 index 000000000..390279d9f --- /dev/null +++ b/packages/react-grab/src/utils/format-component-name-lines.ts @@ -0,0 +1,2 @@ +export const formatComponentNameLines = (componentNames: string[]): string => + componentNames.map((componentName) => `\n in ${componentName}`).join(""); diff --git a/packages/react-grab/tests/context.test.ts b/packages/react-grab/tests/context.test.ts index 329223cf3..e71f217a0 100644 --- a/packages/react-grab/tests/context.test.ts +++ b/packages/react-grab/tests/context.test.ts @@ -232,4 +232,20 @@ describe("formatStackContext", () => { expect(result.shouldAppendSelectorHint).toBe(false); }); + + it("reports when no high-signal stack frame was rendered", () => { + const libraryOnly = formatStackContext([packageFrame], {}, fiberSource); + expect(libraryOnly.hasBudgetedStackFrame).toBe(false); + + const withAppFrame = formatStackContext([appFrame], {}, fiberSource); + expect(withAppFrame.hasBudgetedStackFrame).toBe(true); + }); + + it("collects rendered component names for downstream deduplication", () => { + const result = formatStackContext([packageFrame, appFrame], {}, fiberSource); + + expect(result.renderedComponentNames.has("Page")).toBe(true); + expect(result.renderedComponentNames.has("Tabs")).toBe(true); + expect(result.renderedComponentNames.has("Widget")).toBe(true); + }); });