annotai
annotai
annotai = annotate + AI — annotate page elements into source context your AI can use
English | 简体中文
Show AI exactly which element you're talking about: select elements in the browser, collect precise source context with your questions, and copy AI-ready Markdown. Hold Alt+Shift and click to open the source in your editor or IDE. When a click lands on DOM rendered by a prebuilt component library (no definition marks), the selection falls back to the component's call-site JSX — as long as the call-site path has propagated to the root element.

Open the panel → select elements → type your question → pick the next spot (finished groups are kept automatically) → copy. Hover any element for a live source preview with the element's line highlighted and syntax coloring.
How it compares
| Capability | annotai | code-inspector-plugin | agentation |
|---|---|---|---|
| Source locating | sourcemap-exact to line & column | exact | inaccurate, no file path |
| Neovim jump | ✔ socket discovery, cwd matching, UTF-16 column conversion | ✘ | ✘ |
| Other editors | open hook for anything; falls back to launch-ide |
VSCode / Cursor / WebStorm | ✘ |
| Plain-text DOM location | data-annotai-path (definition) + data-annotai-use-path (call site) on one element, readable in DevTools |
data-insp-path, call site overwrites the definition site |
✘ |
| Hover source preview | ✔ highlighted element line + syntax coloring + auto scroll into view | ✘ | ✘ |
| Output for AI | Markdown with paths, line/column and snippets — paste and go | none (human-oriented jumping) | CSS selectors, AI has to grep again |
| Question list | select + annotate + field toggles, kept automatically per group | ✘ | ✘ |
| Frameworks | React / Vue / Solid | React / Vue etc. | React |
| Shape | build-time plugin, dev only, zero residue in production | same | React runtime component, mounted in production too |
Getting started
npm install -D annotai
yarn add -D annotai
pnpm add -D annotai
bun add -d annotai
import react from '@vitejs/plugin-react'
import { annotate } from 'annotai/vite'
import { defineConfig } from 'vite'
export default defineConfig({
// put annotai before framework plugins
plugins: [annotate({ editor: { name: 'nvim' } }), react()],
})
The toolbar injects itself — no components to mount. Production builds skip injection and never start the source service.
Common options (all optional):
annotate({
editor: { name: 'nvim' }, // or an open hook for any editor
hoverPreview: true, // hover source preview; { width, maxLines } to size the card
locale: 'en', // UI & Markdown export language: 'en' | 'zh'
hotKeys: ['altKey', 'shiftKey'], // [] disables the jump hotkey
surroundingLines: 4, // context lines around exported snippets
context: { domPath: false }, // Markdown field toggles
animation: true, // respects prefers-reduced-motion
theme: {
dimOpacity: 0.4, // panel fade while selecting on the page (1 disables it)
vars: { '--sn-primary': '#6366f1' }, // override any --sn-* CSS variable: panel palette, selection highlight, preview, token colors
},
})
Everything the toolbar paints is driven by --sn-* CSS variables on its shadow host — theme.vars overrides any of them (panel palette --sn-panel/--sn-text/--sn-line/..., selection highlight --sn-highlight*/--sn-drag-*, hover preview --sn-preview-*, syntax tokens --sn-tok-*). Once set, the toolbar stops following the system light/dark scheme.
All options
| Option | Type | Default | Description |
|---|---|---|---|
editor |
{ name?: 'nvim'; server?: string } | { open } |
Neovim auto-discovery | Jump target editor: discovers Neovim sockets by default and falls back to launch-ide for running IDEs (pin one with CODE_EDITOR); server targets a specific socket; the open hook takes full control |
hotKeys |
('altKey' | 'shiftKey' | 'ctrlKey' | 'metaKey')[] |
['altKey', 'shiftKey'] |
Modifier combo that opens the editor; [] disables the hotkey |
hoverPreview |
boolean | { width?; maxLines? } |
true (width 560, maxLines 20) |
Hover source preview |
locale |
'zh' | 'en' |
'en' |
UI & Markdown export language |
surroundingLines |
number |
4 |
Context lines added above/below exported snippets (0–100) |
context |
Partial<ContextOptions> |
location / snippet / class / text on, DOM path off | Markdown field toggles |
animation |
boolean |
true |
Panel & highlight animations; prefers-reduced-motion wins |
theme |
{ dimOpacity?; vars? } |
built-in theme (dimOpacity 0.3) |
Panel fade opacity and any --sn-* CSS variable override |
formatCopy |
(context) => string |
built-in Markdown | Browser-side clipboard formatter; inlined as source, must be self-contained |
onCopy |
(payload) => void | Promise<void> |
— | Node-side copy hook: receives the final Markdown plus all annotations; runs async without blocking the browser, failures print to the terminal |
maxBodyBytes |
number |
2097152 |
Per-request body cap of the source service, in bytes |
projectRoot |
string |
Vite workspace root | Base for relative Markdown paths; does not widen readable dirs |
allowedRoots |
string[] |
[vite.root] |
Dirs whose sources may be processed, relative to the Vite root; symlinks validated by real path |
transforms |
SourceTransform[] |
[reactTransform] |
Syntax adapters, first supporting one wins; Vue / Solid pass their built-in transform |
client |
boolean |
true |
Whether to inject the browser toolbar |
Customize what lands on the clipboard — annotai hands you the context:
annotate({
// runs in the browser; must be self-contained (no closures / Node APIs)
formatCopy: ({ annotations, fields }) => annotations.map((a) => a.question).join('\n'),
})
Forward every copy to your own Node-side hook — you get the final Markdown plus the structured annotations, and decide what happens next (hand it to an agent, POST it somewhere, write a file…):
annotate({
// runs in the Vite dev server process (Node); fire-and-forget, errors surface in the terminal
onCopy: ({ markdown }) => {
spawn('pi', ['-p', markdown], { stdio: 'inherit' })
},
})
The payload rides the same dev-server endpoint as element resolution and is capped at 2 MB by default — raise it with maxBodyBytes if your copies grow huge.
Alt+Shift click opens the source in the editor or IDE you're using: running IDEs such as VS Code, Cursor and WebStorm, as well as Neovim, are detected automatically. Pin one with the CODE_EDITOR env var (or in .env.local), or take full control with the open hook:
annotate({
editor: {
open: ({ file, line, column }) => execFile('code', ['--goto', `${file}:${line}:${column}`]),
},
})
Vue / Solid ship their own built-in transforms:
import { annotate } from 'annotai/vite'
import { vueTransform } from 'annotai/vue'
// Solid: import { solidTransform } from 'annotai/solid'
export default defineConfig({
plugins: [annotate({ transforms: [vueTransform] }), vue()],
})
You declare the framework because .tsx/.jsx can't tell React from Solid compile targets.
License
MIT
Related Packages
Vite plugin for CodeAtlas — real-time annotation parsing and live MCP HTTP serve...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.6.0 | ^3.5.42 | MIT | |
| 0.5.0 | ^3.5.42 | MIT | |
| 0.4.0 | ^3.5.42 | MIT | |
| 0.3.1 | ^3.5.42 | MIT | |
| 0.3.0 | ^3.5.42 | MIT | |
| 0.2.0 | ^3.5.42 | MIT | |
| 0.1.0 | ^3.5.42 | MIT |