Skip to content
WebMCP Agents
Esc
↑↓navigate↵open⌘Jpreview
On this page

The agent cursor

How the ghost cursor moves, waits for a confirmation and gets out of the person's way, plus its options, the spotlight and how to restyle it.

The cursor is the visible half of WebMCP Agents. It shows where the agent is working and what it is typing, parks where the person has to decide, and disappears the moment they move.

The provider installs it before its first tool registers (the installation pages show the line), so the rest of this page is what the person sees and what you can tune.

Yielding to the person

The person scrolls while the agent is adding to the cart: the cursor vanishes and the tool still completes.

A real person always wins. When they click, scroll, touch or press a key, the cursor, its tag and the spotlight fade out in 80 ms, and every pending step fast-forwards: moves arrive at once, typing writes the final value, and a held press or execute() runs immediately. The tool still completes and answers the agent; it just stops waiting for the animation.

  • Only trusted input counts. The agent’s own fills never make the cursor yield.
  • Moving the pointer is not using the page. Someone who starts a call and moves the mouse away still sees it run. Pass yieldToUser: { moveThreshold: 6 } for a cursor that hides the moment the pointer moves.
  • A confirmation keeps its ring. While the browser waits for a confirming click, the ring on that button stays, so the person knows where to confirm. Once they go quiet the cursor returns to it.
  • A press still leaves a mark. A control pressed while the cursor was out of the way gets the ring for a moment, so a call made while the person was busy is not a silent one.
  • A background tab counts as away. No frame is drawn there, so calls run at once instead of waiting for a cursor nobody sees.
  • It comes back on the next call. After resumeAfter ms (800 by default) without input, the next tool call shows the cursor again, entering from where the person’s pointer was.
  • Your own agent UI never counts. Input inside an element marked with data-web-agents-ui (UI_HOST_ATTR) is the person driving the agent from a chat or the devtools. The chat and the devtools set it; mark your own panel the same way.

Confirmation

The agent fills checkout and the cursor parks on Place order until the person clicks it.

A tool with requireConfirmation: true makes the agent wait for the person: the cursor types what it can, then parks on the tool’s control with a Click "…" to confirm prompt, and the call waits for a trusted click on it. That click is the only thing that completes the call. It waits 10 seconds: after that the agent is told the person did not confirm (needs_user), and the prompt goes. A click elsewhere or a scroll ends it at once (cancelled). A declarative form without toolautosubmit gets the same pause from the browser.

The control that is parked on carries data-agent-active, and the cursor has the is-waiting class (what the example apps’ tests look for before they click). If the name of the control matches two buttons, or none, the call fails with unavailable instead of parking somewhere wrong. Confirm before it runs is the page for writing one.

A waiting call always shows where to confirm:

  • Out of view. The control is scrolled to once. If the person then scrolls away, the page is left alone and the cursor waits at the edge of the window nearest the control, prompt beside it.
  • Busy when the call started. Nothing scrolls under a person who is using the page. When the person goes quiet, the page scrolls to the control and the cursor parks on it.
  • The control changes. If the tool’s target now finds another control (a header link that folds into a menu on scroll, leaving the footer’s), the cursor and data-agent-active move to it, and a click there confirms.
  • The agent calls something else meanwhile. The cursor does that call’s work without the prompt, then returns to the waiting control. An agent cannot dismiss a confirmation it asked for.
  • The person runs something else. A call the person starts themself (a button or chip that runs a tool, the devtools) cancels the waiting one: the agent reads cancelled, and the prompt goes.
  • The control is hidden. With no control left to click (removed, disabled, or hidden by CSS), the call fails with unavailable instead of waiting for ever.

How it moves

Minimum-jerk travel, a spring on arrival, the values typed in place.

The cursor goes to the tool’s control, then to each field it types into, then back to press the control. One requestAnimationFrame loop drives everything, and every animation uses transforms and opacity only.

Part What it does
Curved paths The target point travels an arc with a minimum-jerk profile, the curve a human reach follows. The arrow chases it on a near-critically-damped spring, so a new move mid-flight bends the path instead of restarting it.
Fitts’s-law timing Travel takes 180 to 650 ms depending on distance and target size, and each step waits for arrival, not a fixed delay.
Anchored until the click Positions are re-read every frame, so the cursor rides along with scrolling and layout shifts.
A press in sight A control outside the window is scrolled to the middle first, and the cursor waits inside the window, at the edge nearest it, until it is there: the press never happens off screen. A control in a sticky or fixed box (a header, a drawer) that is already in view is left where it is.
Gone when the control is After the click the cursor rests on the control for linger ms. If the control is no longer under it (a drawer opened over it, the list shifted, it was removed), the cursor fades out at once instead of pointing at the wrong thing.
At rest It sways gently around its tip, a little more while waiting for the person.
Reduced motion Under prefers-reduced-motion, everything snaps and fades.

Tool status is also announced in a polite live region for screen readers.

Spotlight

spotlight: true blurs and dims the page except the control being worked in (for a declarative form, the whole [toolname] form), and the clear window glides from tool to tool. Tune it with spotlight: { blur: 4, dim: 0.08, padding: 12 }. It never takes pointer events, so the person can still click to confirm.

Options

You pass these as the provider’s visuals prop, or to installAgentVisuals() yourself.

Option Default What it does
sync true Hold the agent’s call until the cursor has typed and reached the control, so the page reacts at the click (how).
spotlight false Blur and dim everything except the tool being worked in. true, or { blur, dim, padding }.
yieldToUser true Hide the cursor when the person interacts. false, or { moveThreshold, resumeAfter, ignore }.
linger 1200 Milliseconds the cursor stays after a tool finishes.
name "" A tag shown beside the cursor.
log none Receives every agent event.

installAgentVisuals() returns { setSpotlight, setYieldToUser, destroy }, so both switches can change at runtime (the devtools’ Settings tab is wired to them). Every option and the return value: installAgentVisuals.

Styling

The cursor is a black multiplayer-style arrow with a white edge, readable on light and dark surfaces. Its colours are custom properties at the top of @webmcp-agents/core/cursor.css. To brand it, override them in a stylesheet that loads after it:

:root {
  --agent: #4f46e5;        /* fill for the arrow and the tag */
  --agent-strong: #4338ca; /* outlines, rings and borders */
  --agent-ink: #ffffff;    /* text on the tag */
  --agent-wait: #4f46e5;   /* while waiting for the person to confirm */
}

cursor.css lists every custom property, class and state.

Was this page helpful?