installAgentVisuals
Installs the visual layer: a ghost cursor, a focus ring, an optional spotlight and a yield-to-user watcher, all driven by WebMCP lifecycle events.
Installs the visual layer: a ghost cursor, a focus ring, an optional spotlight and a yield-to-user watcher, all driven by WebMCP lifecycle events. Call it once, before tools register, so the page shows what the agent does without the tools knowing. createWebAgents and <WebAgentsProvider> call it for you; use it directly when you register tools yourself.
Import
import { installAgentVisuals } from "@webmcp-agents/core";
import type {
AgentVisuals,
AgentVisualsOptions,
SpotlightOptions,
YieldOptions,
} from "@webmcp-agents/core";
Load the stylesheet once beside it: @webmcp-agents/core/cursor.css.
Example
Install the layer with a tag, a spotlight and tuned yielding, log events, and change a setting later.
import { installAgentVisuals, onAgentEvent } from "@webmcp-agents/core";
// Before the first tool registers (before your app renders).
const visuals = installAgentVisuals({
name: "Assistant",
linger: 1500,
spotlight: { blur: 4, dim: 0.08, padding: 12 },
yieldToUser: { resumeAfter: 800 },
});
// The same events `log` would get, for your own use.
const stop = onAgentEvent((event) => console.debug(event));
// Later, for example from a settings switch.
visuals.setSpotlight(false);
visuals.setYieldToUser(true);
// When the page is done with it (the last handle removes the layer).
stop();
visuals.destroy();
Options
installAgentVisuals(options?) takes an AgentVisualsOptions.
| Name | Type | Default | Description |
|---|---|---|---|
log |
(entry: AgentEvent) => void |
not said | Receives one entry per lifecycle step: the same events onAgentEvent delivers. For a console trace while you build. |
linger |
number |
1200 |
How long the cursor stays after a tool finishes, in ms, so the person can see where it ended. |
name |
string |
"" |
Name shown in a tag beside the cursor (an assistant’s name). Empty shows no tag. |
sync |
boolean |
true |
Hold the agent’s field values and submit until the cursor reaches them, so the page reacts at the cursor’s click. false animates after the fact without touching the form. |
spotlight |
boolean | SpotlightOptions |
false |
Blur and dim the page except the tool the cursor is working in. true uses the defaults below. |
yieldToUser |
boolean | YieldOptions |
true |
Hide the cursor when a person clicks, scrolls, touches or types; the running tool then finishes without waiting for animations. false turns it off. |
SpotlightOptions
| Name | Type | Default | Description |
|---|---|---|---|
blur |
number |
4 |
Blur radius for the rest of the page, px. |
dim |
number |
0.08 |
Darkening over the rest of the page, 0 to 1. |
padding |
number |
12 |
Clear space around the focused element, px. |
YieldOptions
| Name | Type | Default | Description |
|---|---|---|---|
moveThreshold |
number |
none | Pointer travel (px, within 150 ms) that counts as the person interacting. By default moving the pointer never yields: only a click, a scroll, a touch or a key does. Set a number (6 ignores sensor jitter) to hide the cursor the moment the pointer moves. |
resumeAfter |
number |
800 |
Quiet time (ms) after the person’s last input before the cursor may appear again. |
ignore |
string |
"[data-web-agents-ui]" |
Input inside an element matching this selector never yields: a panel the person drives the agent from. See UI_HOST_ATTR. |
Returns
An AgentVisuals handle:
| Name | Type | Description |
|---|---|---|
setSpotlight(spotlight) |
(spotlight: boolean | SpotlightOptions) => void |
Turn the spotlight on, off, or retune it at runtime. |
setYieldToUser(value) |
(value: boolean | YieldOptions) => void |
Change or turn off yielding at runtime. |
destroy() |
() => void |
Release this handle. Idempotent. The last release removes every node, listener and timer, and restores document.modelContext.registerTool. |
Notes
- Installs are shared and ref-counted. A second call while the layer is installed reuses it and applies only its
spotlightandyieldToUseroptions. - On the server (
typeof document === "undefined") it returns a no-op handle. Importing the package never touches the DOM. - Tools do not know about the cursor, and nothing is added to the markup for it. A
WebToolis found on the page by role and name (itstitle, or itstarget), and its parameters are typed into the fields named or labelled like them; see Where the cursor goes. A declarative form is found by itstoolnameattribute, and an element markedtoolname="<tool name>"is still honoured as a tool’s control. - In
syncmode,registerToolis wrapped at install so a tool’sexecuteruns when the cursor presses its control (after typing its fields and running itsfill). A tool registered before install is held too, but a tool with no control cannot be: it runs at once. A gatedexecuteis never held for more than 2.5 s per step (2.5 s × (1 + the number of typed fields)), and cancelling releases it. At the timeout the tool writes what the cursor has not and runs. - Tools with no control on screen run at once but still emit
startandresult/error. Invalid input answers without the cursor moving. - Only trusted input yields the cursor. The agent’s own synthetic
input/changeevents never do. While the browser waits for a confirmation click, the ring on that button stays. - Lifecycle events are read from
document.modelContextwhen it hasontoolactivated/ontoolcancel(Chrome 156 and later), otherwise fromwindow. - After teardown, in-flight tools finish without animation: held submits and gated tools run at once.
- Under
prefers-reduced-motionthe cursor snaps and fades.
See also
- The agent cursor, how it behaves and why
onAgentEventUI_HOST_ATTR@webmcp-agents/core/cursor.cssdefineWebToolcreateWebAgents, which installs it for you