@t4stack/solid-grab
v0.1.1
Published
Copy any UI element of a Solid.js 2 app for your agent: HTML + component stack + file:line
Downloads
45
Maintainers
Readme
solid-grab
Copy any UI element of a Solid.js 2 app for your agent.
Hold ⌘ (Ctrl on Windows/Linux), hover an element, press ⌘C, and you get agent-ready context on the clipboard: the element's HTML, its component stack, and file:line:col source locations.
[<li class="todo" data-index="0">Buy milk</li> in TodoItem (at src/components/TodoItem.tsx:2:10)
in TodoList (at src/components/TodoList.tsx:4:8)
in App (at src/App.tsx:4:16)]Requirements
solid-js≥ 2.0.0-rc.8@solidjs/vite-plugin≥ 3.0.0-next.43- Vite ≥ 6
Dev-only. Nothing from solid-grab ships in production builds.
Install
pnpm add -D @t4stack/solid-grab// vite.config.ts
import { defineConfig } from "vite";
import solid from "@solidjs/vite-plugin";
import solidGrab from "@t4stack/solid-grab/vite";
export default defineConfig({
plugins: [solid({ start: true }), solidGrab()],
});Plugin array order does not matter. That's it — under vite dev the plugin stamps a data-sg-loc="file:line:col" attribute on native JSX elements and auto-loads the runtime; under vite build it does nothing.
Usage
- Hold ⌘ (Ctrl on Windows/Linux) and hover an element to highlight it.
- ⌘C while hovering copies its context to the clipboard.
- Click to select an element (shift-click to add/remove from the selection); drag to marquee-select.
- Right-click, or ⌘. on a selection, opens the context menu: Copy, Open in editor, plus any plugin-registered actions.
- With a selection: arrow keys walk the tree (↑ parent, ↓ first child, ←/→ siblings), Enter copies, Esc backs out one step at a time (menu → selection → deactivate).
- A small draggable pill toolbar toggles solid-grab on/off and can be collapsed.
In activationMode: "toggle" the activation key behaves the same once grab mode is on: press it with something hovered or selected and it copies. It only turns grab mode back off when there is nothing to copy. Esc always exits.
How it works
Solid 2 attaches nothing to DOM nodes and the compiler emits no per-element source location, so solid-grab combines two pieces:
- Build-time attribute stamping — a Vite pre-transform (
enforce-independent,order: "pre", so it runs before the Solid compiler) insertsdata-sg-loc="file:line:col"onto native JSX elements. - A runtime bridge —
@t4stack/solid-grab/runtimeinstallsDEV.hooks.onOwner, tracks binding effects'_devElementback to the owner tree, and walks owners up to component frames (_component.name, and each component'slocationfrom the HMR refresh proxy).
Both only run in dev. See docs/superpowers/specs/2026-09-16-solid-grab-design.md for the full design.
Browser extension
The extension injects solid-grab into any page:
- With the Vite plugin present (the page announces
solid-grab:init/window.__SOLID_GRAB__), the extension defers to it — full HTML + component stack + source locations. - Without it, the extension injects its own bundled copy in HTML-and-selector-only mode (no component names or source). On non-localhost hosts, copied content is converted to Markdown instead of raw HTML.
Build and load unpacked:
pnpm --filter @solid-grab/extension build- Chrome:
chrome://extensions→ enable Developer mode → Load unpacked → selectpackages/extension/.output/chrome-mv3. - Firefox:
about:debugging#/runtime/this-firefox→ Load Temporary Add-on → selectpackages/extension/.output/firefox-mv3/manifest.json.
Firefox MV3 and site access. Firefox treats
<all_urls>host permissions as optional, so a normally installed (signed) build starts with no access to any page and the content script never runs. Grant it once inabout:addons→ Solid Grab → Permissions → allow access to all sites (or to the specific sites you want). Temporary add-ons loaded viaabout:debuggingare exempt and work straight away.
- Safari:
pnpm --filter @solid-grab/extension safari, then open the generated Xcode project inpackages/extension/.output/safari-xcodeand run it once to install the extension.
Plugin options (@t4stack/solid-grab/vite)
| Option | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true under vite (serve), false under vite build | Turn the transform on/off. |
| include | RegExp | /\.[jt]sx$/ | Files eligible for stamping. |
| exclude | RegExp | /node_modules/ | Files excluded from stamping. |
| attribute | string | "data-sg-loc" | Attribute name used for the stamped location. |
Runtime options (Options, passed to init() / window.__SOLID_GRAB__.setOptions())
| Option | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true | Whether solid-grab is enabled at all. |
| activationKey | string \| ActivationKey | Meta+C (mac) / Ctrl+C (elsewhere) | The hold/toggle key. |
| activationMode | "hold" \| "toggle" | "hold" | Hold the key vs. toggle it on/off. |
| keyHoldDuration | number | 100 | Milliseconds the key must be held before activating. |
| allowActivationInsideInput | boolean | false | Allow activation while focus is in an input/textarea/contenteditable. |
| maxContextLines | number | 3 | Max component-stack lines in the clipboard output. |
| getContent | (elements: Element[]) => string \| Promise<string> | — | Override how selected elements are serialized (used by the extension off-localhost). |
| bridge | SolidGrabDevBridge \| null | window.__SOLID_GRAB_DEV__ | The runtime bridge; null forces HTML-only mode. |
API (window.__SOLID_GRAB__)
activate()/deactivate()/toggle()/isActive()isEnabled()/setEnabled(enabled)getToolbarState()/setToolbarState(state)/onToolbarStateChange(cb)copyElement(elements)— copy one or more elements to the clipboardgetSource(element)— resolveSourceInfofor an elementgetContext(element)— the clipboard text for one elementsetOptions(options)registerPlugin(plugin)/unregisterPlugin(name)/getPlugins()dispose()
Events: solid-grab:init (fires once on boot, detail is the API), solid-grab:element-selected (fires on every copy, detail.elements is a summary of the copied elements).
Plugins
window.__SOLID_GRAB__.registerPlugin({
name: "my-plugin",
actions: [{ id: "log", label: "Log to console", onAction: (ctx) => console.log(ctx.context) }],
hooks: { onCopySuccess: (elements, content) => {/* ... */} },
options: { maxContextLines: 5 },
});Plugins can add context-menu actions and hook into the copy lifecycle (onBeforeCopy, transformCopyContent, onCopySuccess, onCopyError). This is the seam a future agent-relay phase would use.
A plugin's options are applied once, when it is registered. unregisterPlugin(name) removes its actions and hooks but does not revert those options — solid-grab does not track which value an option had before, and another plugin or the host may have changed it since. Call setOptions() yourself if you need the previous value back.
Limitations / roadmap
- Solid 2 only — the runtime depends on Solid 2 dev internals (
_component,_devElement,DEV.hooks.onOwner); no Solid 1.x support. - Vite only — the transform is bundler-agnostic in principle, but only Vite glue exists today.
- No agent relay yet — v1 is "core grab" (clipboard only); a relay/provider phase is out of scope for now.
- No freezing of reactive updates while the overlay is active.
Development
pnpm install
pnpm build
pnpm test
pnpm e2e
pnpm --filter playground dev