Spotnote

Concepts

How Spotnote stamps source locations at dev time and reads them back in the browser.

Stamps

Line 12 of components/ProductCard.jsx goes through vite dev, where Spotnote stamps it before your framework compiles it, and reaches the browser with the stamp components/ProductCard.jsx:12:5.

A stamp is a data-spotnote attribute holding the element's source location, relative to your repository's root:

<button data-spotnote="components/ProductCard.jsx:12:5" data-spotnote-name="ProductCard">

The location is path:line:column:

  • path is relative to the root of the Git repository around your Vite root, with / separators: where your agent works from. In a monorepo, it starts with your app's folder (apps/web/src/App.jsx). Outside a Git repository, it's relative to the Vite root.
  • line and column both count from 1, as in your editor.
  • The position is the start of the opening tag (<).

The plugin adds it to every host element (a real DOM tag such as <div> or <button>) written in your own source. An element that already has a data-spotnote attribute is left as is.

Components (<ProductCard>) aren't stamped, because a component doesn't always pass attributes down to a DOM node. Their elements are stamped inside their own file instead.

How the stamp is added depends on the file type:

  • .jsx / .tsx: a Babel pass over the JSX. It doesn't depend on React, so Solid and other JSX frameworks get the same stamps.
  • .vue: the template is parsed with your app's own vue/compiler-sfc.
  • .svelte: the markup is parsed with your app's own svelte/compiler.
  • .astro: the markup is parsed with your app's own Astro compiler.

The stamping runs before your framework's compiler, wherever spotnote() sits in plugins. Files in node_modules are never stamped.

Alongside the location, data-spotnote-name records the owning component. The client uses it to build the source path.

FileValue
.jsx / .tsxThe nearest enclosing function whose name starts with a capital letter (function TodoList, const TodoList = () =>, also through memo() / forwardRef()). Omitted when there is none.
.vue / .svelte / .astroThe file name without its extension.

The client

Select, click, send, paste: pick an element, write the note, and paste the copied prompt into your agent.

The plugin also adds a script to the page Vite serves, so the client starts without an import in your app.

It draws everything (the launcher, outlines, panel and pins) inside the open shadow root of a single <spotnote-root> element appended to <html>. Your app's CSS doesn't affect it, and its CSS doesn't leak into your app. The element carries data-spotnote-ui, so Spotnote ignores its own UI when it looks for elements under the cursor.

If your end-to-end tests run against your dev server, the stamps and <spotnote-root> are in the DOM there too.

When you click in selection mode, the client:

  1. finds the element under the cursor that carries a stamp (the nearest stamped ancestor if the exact element has none),
  2. opens the note panel,
  3. on send, builds the prompt from the stamp, the element itself, its computed styles, and the owning components of its stamped ancestors.

How a note finds its element

A note is saved with its element's stamp and its position among elements with the same stamp (for repeated elements). To show a pin, the client looks for the element with that stamp at that position.

  • Why notes survive reloads and hot updates: the stamp comes back with the element, with no IDs in your code.
  • Why a note loses its element when an edit moves the opening tag: the stamp changes, so nothing matches. See Why did a pin disappear after an edit?.

Next

Reference: every option, field and key, for looking things up.

On this page