Click a UI element in your dev server, leave a comment, and your coding agent picks it up β with file:line and a screenshot β over MCP.
Pinagent is a localhost-only Vite or Next.js plugin. It tags every JSX element with its source location, drops a small π¬ widget into the page, and writes each captured comment to .pinagent/feedback/. An MCP server surfaces the queue inside your existing Claude Code session, so the next thing you say can be "fix the pending feedback."
The fastest way to see the loop end-to-end is the React + Vite example in this repo:
pnpm install # from the repo root
pnpm --filter react-vite-example devThe example's predev hook builds @pinagent/vite-plugin (and its upstream packages via turbo) before starting Vite, so the dev server boots with a fresh plugin bundle that has the widget, the migrations, and the agent runtime baked in.
- Open http://localhost:5173. You should see a "Pinagent demo" header with three counters.
- Click the Pinagent logo in the bottom-right corner. A picker activates and your cursor highlights elements as you hover.
- Click a counter (e.g. "Apples"), type a comment ("rename to Potato"), submit.
- Watch the widget pane that opens next to the element β the Claude Agent SDK runs against
examples/react-vite/, streaming text, tool calls, and the resulting edit back into the page. - Verify in your editor that
examples/react-vite/src/App.tsxwas changed β Pinagent callsmcp__pinagent__resolve_feedbackwhen it's done.
The full feedback record lives at .pinagent/feedback/<id>.json and the captured screenshot at .pinagent/screenshots/<id>.png (both under the example's project root).
If the dev server returns 500 on POST /__pinagent/feedback, the plugin dist is stale β pnpm build from the repo root forces a clean rebuild. See examples/react-vite/README.md for more.
The Next.js example works the same way:
pnpm --filter next-app-example dev # :3000 browser dev server agent
ββββββββββββββββββ ββββββββββββββββββββββββββββ ββββββββββββββββββββ
β click element β β /__pinagent middleware β β Claude Code β
β leave comment ββββΆ β writes ββββΆ β + @pinagent/mcp β
β widget snaps β β .pinagent/feedback/<id> β β reads, edits, β
β a screenshot β β + screenshots/<id>.png β β resolves β
ββββββββββββββββββ ββββββββββββββββββββββββββββ ββββββββββββββββββββ
β² β
βββββββ data-pa-loc="src/Foo.tsx:42:7" ββββ resolves ββββ
JSX is tagged at dev-build time by @pinagent/babel-plugin (Vite) or a webpack/Turbopack loader (Next.js). The widget walks up from the clicked node, finds the nearest data-pa-loc, and POSTs { comment, file, line, col, selector, url, viewport, screenshot } to /__pinagent/feedback.
pnpm add -D @pinagent/vite-plugin # or @pinagent/next-pluginpnpm 10+ blocks postinstall build scripts by default. Pinagent's agent runner uses better-sqlite3 server-side, which needs its native .node binding compiled β otherwise comment submission returns a 500 with:
Could not locate the bindings file. Tried: .../better-sqlite3/build/Release/better_sqlite3.node ...
Pick one:
pnpm approve-builds # interactive; select better-sqlite3 in the picker
pnpm install # re-run so the postinstall actually firesOr β non-interactive, useful for team setup scripts β add to your project's package.json:
{
"pnpm": {
"onlyBuiltDependencies": ["better-sqlite3"]
}
}β¦then pnpm install. Same outcome, no prompt.
(npm and yarn build native binaries by default, so this step only matters on pnpm.)
Vite (vite.config.ts)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import pinagent from '@pinagent/vite-plugin';
export default defineConfig({
plugins: [pinagent(), react()],
});Next.js (next.config.ts)
import pinagent from '@pinagent/next-plugin/config';
export default pinagent(
{ /* your existing nextConfig */ },
{ spawnAgent: 'off' }, // see "Hands-off mode" to flip this on
);Then add two short files:
// app/layout.tsx β somewhere inside <body>
import { Pinagent } from '@pinagent/next-plugin';
<Pinagent />// app/pinagent/[[...slug]]/route.ts
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';
export { GET, POST, PATCH } from '@pinagent/next-plugin/route';Register the MCP server with Claude Code:
claude mcp add pinagent pinagent-mcpThat's it. In your next Claude Code session, ask it to "address pending Pinagent feedback" and it will call the tools below.
| Tool | What it does |
|---|---|
list_pending_feedback |
Lists open items. Optional since (ISO-8601) and file (substring) filters. |
get_feedback |
Returns one item, with the screenshot as an inline image. |
resolve_feedback |
Marks fixed / wontfix / deferred; optional note + commit sha. |
get_source_context |
Reads a window of source around a given file:line. |
Each feedback item carries:
commentβ free text from the composerfile,line,colβ project-relative, from thedata-pa-locattributeselectorβ short CSS path; fallback when source mapping is missingscreenshotβ PNG, downscaled to ~1280px max widthviewport,url,userAgent
The plugin writes JSON to .pinagent/feedback/<id>.json and the screenshot to .pinagent/screenshots/<id>.png. Add .pinagent/ to your .gitignore β the Vite plugin will warn if you forget.
If you'd rather not bounce into Claude Code for every comment, the per-submit spawn flow is on by default in both plugins:
// Vite β defaults to spawnAgent: 'inline'
pinagent({ spawnAgent: 'inline' })
// Next.js β same option, same default
pinagent(nextConfig, { spawnAgent: 'inline' })inline runs the Claude Agent SDK against your project root for each comment, streaming events back to the widget over WebSocket. Switch to worktree to give each comment its own git worktree at .pinagent/worktrees/<id> on branch pinagent/<id> β true parallel agents, review each like a PR. Pass 'off' (or false) to disable per-submit spawning entirely. See packages/agent-runner for the full surface.
@pinagent/vite-pluginβ Vite 5/6/7 plugin: JSX tagging, widget injection,/__pinagentmiddleware.@pinagent/next-pluginβ Next.js 14+ adapter: webpack and Turbopack loaders, route handler,<Pinagent />client component.@pinagent/mcpβ stdio MCP server. Ships thepinagent-mcpbin.@pinagent/widgetβ the browser UI (shadow-root button β pick β composer). Embedded by the plugins at build time.@pinagent/babel-pluginβ the JSX βdata-pa-loctransform used by both plugins.@pinagent/agent-runnerβ SDK-driven local runtime that backsspawnAgentin both plugins. WebSocket server, storage, worktree management,ask_user.@pinagent/cliβpinagentCLI. Currently exposespinagent mcp(stdio MCP server).@pinagent/browser-runtime,@pinagent/db,@pinagent/shared,@pinagent/uiβ internal.
- Localhost only. Middleware and WebSocket bind to
127.0.0.1. - No auth. The trust boundary is your own machine.
- File system is the message bus between the plugin and the MCP server.
- Dev-only. The loader, widget, and middleware are gated on
NODE_ENV !== 'production'. Production builds are untouched.
- Apache-2.0 β
packages/,apps/cli/,examples/. Free for any use. See LICENSE. - Elastic-2.0 β
ee/andapps/cloud/. Source-available; may not be offered as a hosted service to third parties. See ee/LICENSE.
Rule of thumb: if it runs on the developer's own machine, it's Apache-2.0. If it runs as a hosted multi-tenant service, it's Elastic-2.0.
See CONTRIBUTING.md. External PRs are welcome against packages/* and apps/cli/; we don't accept external PRs against ee/* or apps/cloud/.