How it worksDocsAboutLive demoStar on GitHub
React · Next.js · MIT

The guide that
walks one step
ahead of you.

Virgil reads the screen your user is actually looking at, decides what they should do next, and points at it. No scripted paths, no selectors to keep in sync, nothing to rewrite when a button moves.

0
selectors to maintain
1
component to mount
any
AI SDK provider
Try
Invoices
ProtocolSupplierStatus
PR-2049Kessler & BruhnApproved
PR-2048Nordgren LogistikIn review
PR-2047Ferrante ImpiantiApproved
PR-2046Okonkwo StudioDraft
4 of 128 documents · updated 2 minutes ago

Live · the real package, on a fictional back office

The premise

A written tour is a promise about an interface that no longer exists.

Every other onboarding library

You write the path by hand. Step three is #supplier-select. A designer renames it on a Tuesday. The tour keeps running and points at nothing, and nobody notices until a customer does.

driver.jsshepherdintro.jsreact-joyride
Virgil

Nothing is written down. The question asked is given this screen and this goal, what should the user do next? — and it is answered against the screen that actually rendered.

Rename the button and the guide still finds it, because it was never looking for a name.

The mechanism

Three moves, repeated until the goal is reached.

01

Read

A deterministic pass over the DOM collects what can be acted on right now: role, accessible name, section, destination, whether a field is already filled. No model touches this path — it is free, and it is where privacy is decided.

02

Decide

The model gets the goal, the steps already taken, and that list. It answers with an id from the list — never a selector, never a label it made up. It cannot point at something that is not on screen, because there is nothing else it can name.

03

Point

The cursor travels to the element, the reticle locks on, and one sentence says what to do and why. The step completes on the click, on the route change, or on the user confirming they looked. Then the next one is decided.

The other way in

Or point at the thing you don’t understand.

Not everyone can name what they want. Switch the mode on — from your own toolbar, or with Mod+I — and the pointer outlines whatever it is over. Click it and you are told what it is and what happens if you use it, with no question typed.

It is a mode, and it says so: while it is on, it takes the app’s clicks. Escape, the same button, or a click anywhere outside the answer all give them back.

Invoices
ProtocolSupplierStatus
PR-2049Kessler & BruhnApproved
PR-2048Nordgren LogistikIn review
PR-2047Ferrante ImpiantiApproved
PR-2046Okonkwo StudioDraft
4 of 128 documents · updated 2 minutes ago

Live · sweep the pointer, then click something

What you get

What your app calls things, not what it shows.

Roles, authored labels and structure are forwarded. A field is described as [filled] or [empty] — its value is never read. Inside a row, a cell, or anything you mark data-virgil-data, the text stays behind too: those become “a cell in Supplier”.

Sent
role, authored label, section, state
Withheld
every value, every cell

Bring your own model

Any Vercel AI SDK provider. Your key, your account, your choice of model for chat and for step decisions.

Failure mode

If the guide breaks, it removes itself and your app carries on.

An assistant is never worth a white page.

Per-page knowledge

Drop a companion.md next to a route and the guide learns that screen. Build-time generated, checkable in CI.

Speaks your language

Prompts and copy are configurable, not translated. Ship the guide in the language your product is in.

Off by default

Gated by a build flag and a user switch. Until both are on it renders nothing and registers no listeners.

Installation

One component, one route handler.

Mount it once in your dashboard layout and hand the handler a model. There is no step three.

app/api/virgil/[...route]/route.ts
import { createVirgilHandler } from "virgil-ui/next"
import { openai } from "@ai-sdk/openai"

export const { POST } = createVirgilHandler({
    model: openai("gpt-5.6-terra"),
    auth: async () => Boolean(await getSession()),
})
app/dashboard/layout.tsx
import { Virgil } from "virgil-ui/react"

<Virgil />
// that is the whole integration

See it find its own way.

The demo is a working back office nobody has ever used before. Give it a goal in plain words and watch it work out the path.