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
resumeAfterms (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
targetnow finds another control (a header link that folds into a menu on scroll, leaving the footer’s), the cursor anddata-agent-activemove 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
unavailableinstead 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.