diff --git a/.changeset/config.json b/.changeset/config.json index 743a3125b..7cc7da135 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -3,13 +3,7 @@ "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], - "linked": [ - [ - "react-grab", - "grab", - "@react-grab/cli" - ] - ], + "linked": [["react-grab", "grab", "@react-grab/cli"]], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index 58e81fea0..8557a9bbe 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -4,10 +4,7 @@ import { detectNonInteractive } from "../utils/is-non-interactive.js"; import { detectProject } from "../utils/detect.js"; import { handleError } from "../utils/handle-error.js"; import { highlighter } from "../utils/highlighter.js"; -import { - installMcpServers, - promptMcpInstall, -} from "../utils/install-mcp.js"; +import { installMcpServers, promptMcpInstall } from "../utils/install-mcp.js"; import { logger } from "../utils/logger.js"; import { spinner } from "../utils/spinner.js"; diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index ba0d49a03..d5c2cd383 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -9,9 +9,7 @@ import { applyTransformWithFeedback, installPackagesWithFeedback, } from "../utils/cli-helpers.js"; -import { - promptMcpInstall, -} from "../utils/install-mcp.js"; +import { promptMcpInstall } from "../utils/install-mcp.js"; import { detectProject, findReactProjects, @@ -23,14 +21,10 @@ import { import { printDiff } from "../utils/diff.js"; import { handleError } from "../utils/handle-error.js"; import { highlighter } from "../utils/highlighter.js"; -import { - getPackagesToInstall, -} from "../utils/install.js"; +import { getPackagesToInstall } from "../utils/install.js"; import { logger } from "../utils/logger.js"; import { spinner } from "../utils/spinner.js"; -import { - type AgentIntegration, -} from "../utils/templates.js"; +import { type AgentIntegration } from "../utils/templates.js"; import { previewOptionsTransform, previewPackageJsonTransform, diff --git a/packages/cli/src/utils/detect.ts b/packages/cli/src/utils/detect.ts index d8b2fe1ba..1c2cdb633 100644 --- a/packages/cli/src/utils/detect.ts +++ b/packages/cli/src/utils/detect.ts @@ -417,9 +417,7 @@ export const detectReactGrab = (projectRoot: string): boolean => { return filesToCheck.some(hasReactGrabInFile); }; -const AGENT_PACKAGES = [ - "@react-grab/mcp", -]; +const AGENT_PACKAGES = ["@react-grab/mcp"]; export const detectUnsupportedFramework = ( projectRoot: string, diff --git a/packages/cli/src/utils/install.ts b/packages/cli/src/utils/install.ts index 39bbfd713..44bc90d3e 100644 --- a/packages/cli/src/utils/install.ts +++ b/packages/cli/src/utils/install.ts @@ -44,4 +44,3 @@ export const getPackagesToInstall = ( return packages; }; - diff --git a/packages/cli/src/utils/templates.ts b/packages/cli/src/utils/templates.ts index 8e7214354..d3c0ceef9 100644 --- a/packages/cli/src/utils/templates.ts +++ b/packages/cli/src/utils/templates.ts @@ -55,7 +55,9 @@ export const TANSTACK_EFFECT = `useEffect(() => { } }, []);`; -export const TANSTACK_EFFECT_WITH_AGENT = (_agent: AgentIntegration): string => { +export const TANSTACK_EFFECT_WITH_AGENT = ( + _agent: AgentIntegration, +): string => { return TANSTACK_EFFECT; }; diff --git a/packages/cli/test/detect.test.ts b/packages/cli/test/detect.test.ts index e8527da37..057b414f3 100644 --- a/packages/cli/test/detect.test.ts +++ b/packages/cli/test/detect.test.ts @@ -340,4 +340,3 @@ describe("detectUnsupportedFramework", () => { expect(detectUnsupportedFramework("/test")).toBe(null); }); }); - diff --git a/packages/cli/test/install.test.ts b/packages/cli/test/install.test.ts index 61950e3bc..38a21358d 100644 --- a/packages/cli/test/install.test.ts +++ b/packages/cli/test/install.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - getPackagesToInstall, -} from "../src/utils/install.js"; +import { getPackagesToInstall } from "../src/utils/install.js"; describe("getPackagesToInstall", () => { it("should return only react-grab when no agent and includeReactGrab is true", () => { diff --git a/packages/cli/test/transform.test.ts b/packages/cli/test/transform.test.ts index ef857c25b..9cd94fb4c 100644 --- a/packages/cli/test/transform.test.ts +++ b/packages/cli/test/transform.test.ts @@ -257,13 +257,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render( ); mockReadFileSync.mockReturnValue(entryContent); - const result = previewTransform( - "/test", - "vite", - "unknown", - "mcp", - false, - ); + const result = previewTransform("/test", "vite", "unknown", "mcp", false); expect(result.success).toBe(true); expect(result.newContent).toContain("react-grab"); diff --git a/packages/e2e-app/vite-plugin-git-blame.ts b/packages/e2e-app/vite-plugin-git-blame.ts deleted file mode 100644 index 1d7911cb8..000000000 --- a/packages/e2e-app/vite-plugin-git-blame.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { Plugin } from "vite"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import path from "node:path"; - -const execFileAsync = promisify(execFile); - -interface GitBlameResult { - author: string; - date: string; - commitHash: string; - commitMessage: string; -} - -const parseGitBlamePorcelain = (output: string): GitBlameResult | null => { - const lines = output.split("\n"); - if (lines.length === 0) return null; - - const commitHash = lines[0]?.split(" ")[0] ?? ""; - let author = ""; - let date = ""; - let commitMessage = ""; - - for (const line of lines) { - if (line.startsWith("author ")) { - author = line.slice("author ".length); - } else if (line.startsWith("author-time ")) { - const timestamp = Number(line.slice("author-time ".length)); - date = new Date(timestamp * 1000).toISOString(); - } else if (line.startsWith("summary ")) { - commitMessage = line.slice("summary ".length); - } - } - - if (!author) return null; - - return { author, date, commitHash: commitHash.slice(0, 8), commitMessage }; -}; - -export const reactGrabGitBlame = (): Plugin => ({ - name: "react-grab-git-blame", - configureServer(server) { - const root = server.config.root; - - server.middlewares.use(async (req, res, next) => { - const url = new URL(req.url ?? "", "http://localhost"); - if (url.pathname !== "/__react-grab/git-blame") return next(); - - const file = url.searchParams.get("file"); - const line = url.searchParams.get("line"); - - if (!file) { - res.writeHead(400, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Missing file parameter" })); - return; - } - - // Resolve root-relative paths (e.g. /src/App.tsx) to absolute - const absoluteFile = path.isAbsolute(file) - ? file.startsWith(root) - ? file - : path.join(root, file) - : path.resolve(root, file); - - const lineNum = line ? Number(line) : null; - - try { - const args = ["blame", "--porcelain"]; - if (lineNum && lineNum > 0) { - args.push("-L", `${lineNum},${lineNum}`); - } else { - args.push("-L", "1,1"); - } - args.push("--", absoluteFile); - - const { stdout } = await execFileAsync("git", args, { - timeout: 5000, - }); - - const result = parseGitBlamePorcelain(stdout); - if (!result) { - res.writeHead(404, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Could not parse blame output" })); - return; - } - - res.writeHead(200, { - "Content-Type": "application/json", - "Cache-Control": "no-store", - }); - res.end(JSON.stringify(result)); - } catch (error) { - const message = - error instanceof Error ? error.message : "Git blame failed"; - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: message })); - } - }); - }, -}); diff --git a/packages/gym/components/agent-playground.tsx b/packages/gym/components/agent-playground.tsx index 5759522c3..a2f25f4d6 100644 --- a/packages/gym/components/agent-playground.tsx +++ b/packages/gym/components/agent-playground.tsx @@ -112,7 +112,6 @@ export const AgentPlaygroundContent = ({ }); }, [loadedProviders, failedProviders, addLog]); - const handleAddProvider = (provider: string) => { const currentProviders = new URLSearchParams(window.location.search).get("provider") ?? ""; diff --git a/packages/react-grab/e2e/fixtures.ts b/packages/react-grab/e2e/fixtures.ts index 70a2b9159..412147aef 100644 --- a/packages/react-grab/e2e/fixtures.ts +++ b/packages/react-grab/e2e/fixtures.ts @@ -1008,9 +1008,7 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => { const items = dropdown.querySelectorAll( "[data-react-grab-menu-item]", ); - return Array.from(items).map( - (item) => item.textContent?.trim() ?? "", - ); + return Array.from(items).map((item) => item.textContent?.trim() ?? ""); } return []; }, ATTRIBUTE_NAME); @@ -1682,9 +1680,7 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => { id: "comment-action", label: "Edit", shortcut: "Enter", - onAction: (context: { - enterPromptMode?: () => void; - }) => { + onAction: (context: { enterPromptMode?: () => void }) => { context.enterPromptMode?.(); }, }, diff --git a/packages/react-grab/src/components/toolbar/index.tsx b/packages/react-grab/src/components/toolbar/index.tsx index f204bc2a5..bc544c960 100644 --- a/packages/react-grab/src/components/toolbar/index.tsx +++ b/packages/react-grab/src/components/toolbar/index.tsx @@ -1299,24 +1299,14 @@ export const Toolbar: Component = (props) => { - + to fine-tune target - + esc to cancel diff --git a/packages/react-grab/src/components/toolbar/toolbar-content.tsx b/packages/react-grab/src/components/toolbar/toolbar-content.tsx index c4ab12a4a..b4ee5d7ab 100644 --- a/packages/react-grab/src/components/toolbar/toolbar-content.tsx +++ b/packages/react-grab/src/components/toolbar/toolbar-content.tsx @@ -216,10 +216,12 @@ export const ToolbarContent: Component = (props) => { "grid relative overflow-visible", gridSizeTransitionClass(), props.isCollapsed - ? (isVertical() - ? "grid-rows-[0fr] pointer-events-none" - : "grid-cols-[0fr] pointer-events-none") - : (isVertical() ? "grid-rows-[1fr]" : "grid-cols-[1fr]"), + ? isVertical() + ? "grid-rows-[0fr] pointer-events-none" + : "grid-cols-[0fr] pointer-events-none" + : isVertical() + ? "grid-rows-[1fr]" + : "grid-cols-[1fr]", )} >
commentItems; -export const addCommentItem = ( - item: Omit, -): CommentItem[] => +export const addCommentItem = (item: Omit): CommentItem[] => persistCommentItems( [{ ...item, id: generateId("comment") }, ...commentItems].slice( 0, @@ -101,6 +99,9 @@ export const confirmClear = (): void => { sessionStorage.setItem(CLEAR_CONFIRMED_KEY, "1"); } catch (error) { // HACK: sessionStorage can throw in private browsing or when quota is exceeded - logRecoverableError("Failed to save clear preference to sessionStorage", error); + logRecoverableError( + "Failed to save clear preference to sessionStorage", + error, + ); } }; diff --git a/packages/react-grab/src/utils/fetch-git-blame.ts b/packages/react-grab/src/utils/fetch-git-blame.ts deleted file mode 100644 index afb94f633..000000000 --- a/packages/react-grab/src/utils/fetch-git-blame.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface GitBlameInfo { - author: string; - date: string; - commitHash: string; - commitMessage: string; -} - -let failCount = 0; -const MAX_CONSECUTIVE_FAILURES = 3; - -export const fetchGitBlame = async ( - filePath: string, - lineNumber: number | null, -): Promise => { - if (failCount >= MAX_CONSECUTIVE_FAILURES) return null; - - const params = new URLSearchParams({ file: filePath }); - if (lineNumber) params.set("line", String(lineNumber)); - - try { - const response = await fetch( - `/__react-grab/git-blame?${params}`, - ); - if (!response.ok) { - failCount++; - return null; - } - failCount = 0; - return (await response.json()) as GitBlameInfo; - } catch { - failCount++; - return null; - } -}; diff --git a/packages/react-grab/src/utils/measure-element.ts b/packages/react-grab/src/utils/measure-element.ts new file mode 100644 index 000000000..bc19d82dc --- /dev/null +++ b/packages/react-grab/src/utils/measure-element.ts @@ -0,0 +1,77 @@ +import type { ElementMeasurement } from "../types.js"; +import { + nativeRequestAnimationFrame, + nativeCancelAnimationFrame, +} from "./native-raf.js"; + +interface ElementMeasurerHandle { + observe: (element: Element) => void; + unobserve: (element: Element) => void; + disconnect: () => void; +} + +export const createElementMeasurer = ( + onMeasurement: (element: Element, measurement: ElementMeasurement) => void, +): ElementMeasurerHandle => { + const trackedElements = new Set(); + let pendingFrameId: number | undefined; + let didDisconnect = false; + + const cancelPendingFrame = () => { + if (pendingFrameId === undefined) return; + nativeCancelAnimationFrame(pendingFrameId); + pendingFrameId = undefined; + }; + + const scheduleNextFrame = () => { + if (didDisconnect || trackedElements.size === 0) return; + cancelPendingFrame(); + + pendingFrameId = nativeRequestAnimationFrame(() => { + pendingFrameId = undefined; + for (const element of trackedElements) { + intersectionObserver.observe(element); + } + }); + }; + + const intersectionObserver = new IntersectionObserver((entries) => { + for (const entry of entries) { + intersectionObserver.unobserve(entry.target); + if (!trackedElements.has(entry.target)) continue; + + const { boundingClientRect } = entry; + onMeasurement(entry.target, { + x: boundingClientRect.x, + y: boundingClientRect.y, + width: boundingClientRect.width, + height: boundingClientRect.height, + isIntersecting: entry.isIntersecting, + intersectionRatio: entry.intersectionRatio, + }); + } + + scheduleNextFrame(); + }); + + const observe = (element: Element) => { + if (didDisconnect) return; + trackedElements.add(element); + scheduleNextFrame(); + }; + + const unobserve = (element: Element) => { + trackedElements.delete(element); + intersectionObserver.unobserve(element); + if (trackedElements.size === 0) cancelPendingFrame(); + }; + + const disconnect = () => { + didDisconnect = true; + trackedElements.clear(); + intersectionObserver.disconnect(); + cancelPendingFrame(); + }; + + return { observe, unobserve, disconnect }; +}; diff --git a/packages/shadcn-registry/r/react-grab.json b/packages/shadcn-registry/r/react-grab.json index d1da7b2fa..e959f2950 100644 --- a/packages/shadcn-registry/r/react-grab.json +++ b/packages/shadcn-registry/r/react-grab.json @@ -4,9 +4,7 @@ "type": "registry:component", "title": "React Grab", "description": "Loads React Grab as early as possible — select context for coding agents directly from your website.", - "dependencies": [ - "react-grab" - ], + "dependencies": ["react-grab"], "files": [ { "path": "components/react-grab.tsx",