annotai

Select page elements in the browser, collect precise source context with questions, and copy AI-ready Markdown; Alt+Shift jumps straight into Neovim
1,071
Install
npm install annotai
Latest Version:0.6.0
Vue:3
License:MIT
Last Updated:Oct 13, 2026
Links: npm
Maintainer: _corazon

annotai

annotai = annotate + AI — annotate page elements into source context your AI can use

English | 简体中文

npm version npm downloads MIT license
TypeScript Vite Playwright
React Vue Solid

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.

demo

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

spotnote

Click a UI element in your Vite dev server, write a note, and copy an AI-agent-r...

0 25
element-source-dev

Get the source file location of any DOM element in the browser

590
vite-plugin-markdown

Import markdown files in vite

1,962,521 294
vite-plugin-markdoc

Markdoc plugin for Vite

15,122
@ashwindojo/codeatlas-vite-plugin

Vite plugin for CodeAtlas — real-time annotation parsing and live MCP HTTP serve...

1,356

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