file:line source pointer works on React and Svelte, not on Vue or Angular. Nuxt, React Router framework mode and Create React App have their install gated the same way. TanStack Start, Preact and plain HTML are wired by init and documented but not gated at all, so treat them as plausible rather than proven. The exact wiring for each is below.
Reticle is a verification layer that embeds a dev-only SDK in your running web app so an AI coding agent can prove a change works. Its core is framework-neutral: DOM, network, console, routing, animations and source mapping work anywhere JavaScript runs.
What varies is two things: how connect() gets into your app, and whether you get component identity on top.
Source pointer is the file:line on a verdict, the thing an agent follows straight to the code after a red. It comes from the build plugin stamping your JSX, not from the SDK, so it is a property of how the framework is wired rather than of the framework. Remix and Astro render their own HTML, so the plugin’s connect-injection cannot fire there, and both examples used to drop the plugin entirely, which quietly cost them the stamping too. Both now wire it with inject: false, which keeps the half that applies. A row marked unproven has no example app, so nobody has checked.
Where each framework stands
We separate “supported” from “what CI proves”. Four frameworks have an example app and a CI gate. Vue, SvelteKit and Angular are supported and install gated. The rest are wired and documented, and nothing in CI proves they still work. Two columns say different things and it is worth keeping them apart. CI gateyes means an example app runs in CI: React + Vite and Next.js are driven to a verdict, Remix and Astro are booted and must register a session. install only means the install gate scaffolds that stack from scratch, runs init, boots it and waits for a session, so the setup is proven and the drive is not. Vue is there because a release once nearly shipped an installer that left a Vue app connecting 0% of the time, and the gate that exists for exactly that had only React scaffolds.
Source pointer is the file:line on a verdict. It needs the build plugin to stamp the source, which it does for JSX and, separately, for Svelte components. Vue single-file components are neither, so a Vue app gets every other signal and no source field. Measured, not assumed: a Svelte counter returns src/lib/Counter.svelte:5 and the same drive on Vue returns none.
init detects the UI library separately from the framework. That distinction exists because detection used to stop at “vite is in package.json”, which installed the React adapter into Vue and Preact apps and reported all green with nothing attached.
React on Vite
The shortest path, and the one with the most in it.connect() and stamps source locations, which is what turns a DOM node into src/components/Login.tsx:81.
Next.js
Keeps SWC. No Babel migration.@reticlehq/next is CommonJS on purpose. next.config.js is loaded by Node before any ESM
transform, so an ESM-only helper fails at the least helpful moment.Remix and Astro
Remix v2 on Vite is its own framework toinit, not plain Vite. Remix SSRs its own HTML, so the plugin’s index.html injection never reaches the page and the connect comes from app/entry.client.tsx instead, the same way as React Router framework mode. init adds the dev-only connect line to that entry, or writes the file when the app has none: app/entry.client.tsx only exists after remix reveal, so the written file is Remix’s own default entry (RemixBrowser, hydrateRoot) with the one line added. The Vite plugin, reticle() in the vite.config that carries vitePlugin, stamps file:line and serves the connect module the entry imports. The classic Remix compiler (no vite.config) gets a manual step rather than a green plan, because there is no plugin to serve that module.
Astro uses the Vite plugin (wire it with inject: false if you want source pointers on JSX islands) and uses a different channel for the pairing token. vite.define does not reach Astro’s client pipeline on 7.2+, so the identifier stays literal and the bridge refuses the dial. init reads the token in frontmatter and puts it on a <meta name="reticle-pairing-token">. Reading it per request also means a token written after the dev server started is picked up on the next load, with no restart.
Use a processed page <script> with a static SDK import. Vite owns that module graph from the first transform:
<script> tag must not sit inside a conditional. Astro hoists them statically, so {isDev && (<script />)} is never hoisted. init writes the same <meta> tags and a local src/components/ReticleDev.ts that the script statically imports. It also raises vite.build.target to es2022 (Astro’s default down-levels the SDK), declares the SDK in optimizeDeps.include, and keeps .reticle/ out of the watcher. Static .astro markup still gets DOM, network and routing without the React integration.
SvelteKit
SvelteKit renders throughapp.html and never triggers Vite’s index.html injection, so the plugin cannot auto-connect. A client hook is the reliable path, and it is what init writes.
data-reticle-source into .svelte components, so verdicts carry file:line.
For state, use the svelteStore adapter. See State management.
TanStack Start
TanStack Start SSRs<html> from src/routes/__root.tsx and never sends Vite’s index.html, so the plugin cannot auto-connect. Keep the plugin with inject: false: only the injection half is inapplicable, and the stamping half is what puts file:line on a verdict.
Connect from a client-only effect. A static import of the SDK on __root.tsx SSRs and 500s.
init writes this for you: a src/reticle-connect.tsx client effect that connects, declares __RETICLE_TOKEN__ so tsc passes, and loads src/reticle-dev.ts, plus <ReticleConnect /> rendered before <Scripts /> in src/routes/__root.tsx. When the root route is not a shape it can patch, it prints a recipe naming the real parts (shellComponent, <Scripts />) instead. There is no example app and no CI gate, so treat a connected session as the proof, not the plan.
Nuxt
Nuxt owns its own Vite instance and renders its own HTML. There is novite.config to patch and no index.html to inject into, so reticle init writes a dev-only client plugin and adds a vite.define block to nuxt.config. That block is the only place a Nuxt app can inline the pairing token, which the bridge requires even on localhost. What it writes is this, and the notes below are why each line is the way it is.
The .client suffix is load-bearing
The .client suffix is load-bearing
It is what keeps the plugin out of the server bundle. Without it you are running the SDK during
SSR, where
window does not exist.Guard on import.meta.dev, never on hostname
Guard on import.meta.dev, never on hostname
window.location.hostname === 'localhost' fails twice over here. window does not exist in
SSR, and the check is false on any hosts-file alias or LAN address. import.meta.dev resolves
at build time and does not care what host you develop on.Restart the dev server
Restart the dev server
A running dev server does not pick up a new plugin. It will not appear in
.nuxt/plugins/client.mjs, and the app comes up with no SDK at all and no error saying so.@reticlehq/browser, the framework-neutral sensor. You get DOM, network, console and routing. You do not get React component identity (the React adapter adds that) or a source field: Vue single-file components are not stamped.
Vue
Vue apps outside Nuxt are Vite apps, soinit finds them. It installs the sensor rather than the React kit, plus the Vite plugin:
reticle() to the plugins of your Vite config, as in the React example above. The plugin injects connect() with the pairing token, so there is no connect line to write.
Pinia is a first-class store. See State management.
Preact
Detected as its own UI library, because a Preact app usingpreact/compat aliases React and would otherwise be handed the React kit with nothing to attach to.
The sensor works. If you use preact/compat, the React adapter can attach through the alias, but that path is not gated.
The source-pointer transform works on Preact code, and that is measured rather than assumed. The stamper is JSX-syntactic: it tags a host element by its tag name and reads nothing about React, so a Preact .jsx or .tsx file is stamped exactly as a React one is. class instead of className, a @jsxImportSource preact pragma and a @preact/signals value in the tree all leave it unmoved. There are tests for each of those in adapters/build/babel-plugin. They prove the Babel transform, not the @preact/preset-vite pipeline end to end: nobody has yet driven a Preact app and seen a verdict name a file:line, so the drive stays unproven until someone does.
What is still missing on Preact is the enrichment above the DOM: reticle_inspect returns no component/componentStack, @preact/signals has no reticle_state adapter, and the render meter counts React commits only. Tracked in #129.
Create React App and plain bundlers
No config file to patch, soconnect() goes in your entry module.
reticle init writes this machine’s pairing token to reticle.local.js at the project root and adds that file to .gitignore. Point the import at that file from your entry module. Do not commit it: each teammate runs reticle init for their own. The same shape works for webpack, Parcel, and the Vue and Svelte CLIs. On Create React App, init writes the connect for you: a dev-only connect module that reads the token from REACT_APP_RETICLE_TOKEN, which it puts in the git-ignored .env.development.local.
Plain HTML
A directory with anindex.html and no package.json is wired by init itself: it writes the connect snippet into index.html, before </body>, writes .reticle.json, and hands over to setup. The block is marked, so a re-run finds it and never writes a second one, and a re-run with a new --port rewrites the port inside it. It loads the SDK from a CDN and runs only when the page is opened on localhost, 127.0.0.1 or [::1]: a static file has no development build to strip it, so the host is the only line between the snippet and a published page. The page carries no pairing token: init writes it into reticle.local.js beside the page, which the snippet imports, and adds that file to .gitignore. Keep it out of anything you publish, and let each teammate run reticle init for their own. Several pages and no index.html is a choice init does not make for you: it prints the snippet, filled in with your project id, for you to paste. The snippet does not contain the token. It imports reticle.local.js, which init writes at the project root and adds to .gitignore. If your page is not served from the project root, change ./reticle.local.js so that it points to that file:
index.html is the exception. Its packaged renderer can also run on localhost, so init writes neither the snippet nor reticle.local.js, and gives a manual step instead. Connect only in development, for example only when !app.isPackaged, and see Desktop apps.
A bare /node_modules/... import does not work here: the SDK’s own dist imports bare specifiers, which a browser cannot resolve without a bundler. A page with a package.json and a bundler takes the entry-module path above instead.
Angular
Angular 17 and later, on the Vite-based@angular/build dev server. init installs only @reticlehq/browser, the framework-neutral sensor, and makes three edits:
- The browser entry (
src/main.ts, or whateverangular.jsonnames) gets a connect guarded on Angular’sngDevModeglobal (typeof ngDevMode === 'undefined' || ngDevMode), through a dynamicimport('@reticlehq/browser')appended after the bootstrap. A productionng builddefinesngDevModeasfalse, so the build drops the block and the chunk with it. The output carries no Reticle code at all, and the app never waits on Reticle.isDevMode()is not used because the build cannot fold a function call; an entry wired that way by an olderinitis rewritten in place on the next run. reticle.proxy.mjs, besideangular.json, answers one same-origin route,/__reticle/pairing-token, with this machine’s pairing token, read from~/.reticle/pairing-tokenat request time. It refuses a request that did not arrive over loopback or came from another origin.angular.jsongets the serve target’sproxyConfigpointed at that file.ng serveloads it;ng buildnever reads a proxy config, so a production bundle has no route to the token and the token is never written into the project.
angular.json commits a per-machine secret, and a gitignored module imported from main.ts is bundled into ng build whether or not anything can reach it. An existing proxyConfig is yours and is never replaced: the step goes manual and names the entry to add to it. A workspace with several served applications is also manual, because which one is the app is not init’s to guess.
You get DOM, network, console, routing and state tools. You do not get component identity (that is the React adapter) or a source field (the stamper covers JSX and Svelte). The install gate scaffolds an Angular app (no SSR) whenever the install path changes and requires a session; SSR was proven by hand, and nothing drives an Angular app to a verdict.
Server-rendered apps (Django, Flask, FastAPI, Rails)
There is no JS build to import the SDK from, and no single template to paste a script tag into: an app has many templates and may not have a base one. The SDK loads from a URL instead, so a page with no build step is not a blocker.reticle init detects these and prints the right thing for the stack it found, writing .reticle.json as it goes. What follows is the same wiring, written down so you can read it before running anything, or wire it by hand.
Django
Middleware is the Django-shaped answer: one place, runs for every page, no template to find.reticle init prints this with your real version and project id. Copy it from there, not from this page. The middleware reads this machine’s pairing token from ~/.reticle/pairing-token (or RETICLE_PAIRING_TOKEN_DIR) each time it serves a page, so the token is never in your code.
Four details in that middleware are load-bearing, and each is there because the obvious version is wrong:
settings.DEBUGis the guard. Reticle is dev-only, and this is the lock that cannot be forgotten at deploy time.- the
text/htmlcheck keeps it from corrupting a JSON API response or a file download. - the
streamingcheck exists because aStreamingHttpResponsehas no.contentto rewrite, and touching it raises. </body>, not<head>so the SDK sees a parsed document.
Content-Length matters for the same reason: the body grew, and a stale length truncates the page in the browser rather than failing loudly.
Flask and FastAPI with Jinja
Same shape, one hook instead of a middleware class: an@app.after_request in Flask, or a middleware in FastAPI, with the same four guards: a debug flag, text/html only, not streaming, and injected before </body>.
Streamlit
Different again: Streamlit owns the top-level document, so there is no served template, and scripts passed tost.markdown do not run. Use st.html on Streamlit 1.63 or newer. reticle init prints the helper, which also guards against connecting twice across reruns.
What you get, and what you do not
Everything that comes from reading the live page works: queries, snapshots, actions, network and console channels, verdicts. What is missing is the source pointer.file:line on a verdict comes from the build plugin stamping data-reticle-source, and there is no build step here to stamp it. You get every other signal and no source field, the same way a Vue single-file component does.
Desktop is a different shape
Electron and Tauri invert the direction: your app dials the daemon, and there is no URL for an agent to open. Reticle also reaches the main-process and Rust IPC boundary, which the renderer cannot see.Desktop apps
Electron and Tauri, IPC observation, and the CSP trap whose failure is completely silent.
Not supported
Solid appears in dependency detection but has no wiring, no adapter and no gate. The sensor may work if you callconnect() yourself. Nothing proves it, so we are not going to list them as supported.
Frequently asked questions
Does Reticle work with Next.js?
Does Reticle work with Next.js?
Yes, and it keeps SWC. Install
@reticlehq/react and @reticlehq/next, then wrap your config
with withReticle. There is no Babel migration. Next.js has an example app and a CI gate, so it
is supported rather than merely wired, and you get component identity and file:line source
pointers.Does Reticle work with Vue?
Does Reticle work with Vue?
Yes. Install
@reticlehq/browser rather than the React kit, and you get DOM, network, console,
routing, actions, verdicts and Pinia state. The one thing you do not get is a source field:
source stamping covers JSX and Svelte components, and a Vue single-file component is neither. The
install path is gated in CI; no example app is driven to a verdict there yet.Which framework should I pick if I want the strongest verdicts?
Which framework should I pick if I want the strongest verdicts?
React on Vite. It is the shortest install, and it is the only combination that gives you all three
of component identity,
file:line source pointers and an example app driven to a verdict in CI.
Next.js gives you the same. On Remix or Astro you get the same identity and source pointers, but
their example apps are only checked for a session in CI, not driven to a verdict.My framework is not listed at all. Will it work?
My framework is not listed at all. Will it work?
Probably, with one line.
@reticlehq/browser is the framework-neutral sensor: call
reticle.connect() behind a dev guard anywhere your app boots, and you get DOM, network, console
and routing. What you lose is component identity and, unless a build plugin stamps your source,
the file:line pointer. If it does not register a session, that is a bug worth reporting.What does 'wired, unverified' actually mean for me?
What does 'wired, unverified' actually mean for me?
The recipe on this page was written from a real integration and is expected to work, but no app
in CI exercises it, so nothing will tell us when it breaks. If you depend on one of these, the
useful thing you can do is report it when it stops working, since that is the only signal there
is.
Something missing?
A framework you need is a
gap report. It is the signal that decides what gets built.