View as markdown

Page awareness

The headline reason to use the NPM package over the script tag: the agent can see your page and fill your forms. Two primitives make this work — extractPageContext (which walks the DOM automatically) and LiveLayerRegion (for curated content sections). Forms need NO special markup — every <form> is auto-discovered.

How it works (30 seconds)

  1. The agent emits request_page_context over its data channel when it needs to "look around."
  2. The widget walks the DOM, extracts headings, links, visible text, form fields, and any explicitly marked regions.
  3. The walked snapshot is sent back to the agent as JSON.
  4. When the agent emits a fill_field or submit_form command, the widget targets the right element by ID.

The DOM walk respects two opt-out signals: data-ll-private="true" excludes a subtree, and password fields are always excluded.

extractPageContext

The default DOM walker. You can call it directly or override it via the getPageContext prop.

tsx
import { extractPageContext } from "@livelayer/react";

const context = extractPageContext({
  // Optional extras passed straight through
  currentUser: "user_abc",
  cartTotal: 12.99,
});
// → { headings, links, visibleText, formFields, regions, extras }

For most apps, the default extraction is enough. Override it when:

  • Your app's "real" state lives in a store, not the DOM (e.g., virtualized lists where most items aren't rendered).
  • You want to feed the agent richer context (current route's title, user role, etc.).
  • The default walk is too slow on a complex page.
tsx
import { AvatarWidget } from "@livelayer/react";

<AvatarWidget
  agentId="agt_abc123"
  getPageContext={async () => ({
    headings: [{ level: 1, text: "Dashboard" }],
    visibleText: useDashboardStore.getState().summarizeForAgent(),
    formFields: [],
    links: [],
    regions: [],
  })}
  pageContextExtras={{
    currentUser: useUserStore.getState().email,
    plan: useUserStore.getState().plan,
  }}
/>

<LiveLayerRegion> — mark important regions

Wrap a region of your UI in <LiveLayerRegion> to give it a stable name the agent can address.

tsx
import { LiveLayerRegion } from "@livelayer/react";

export function PricingPage() {
  return (
    <main>
      <LiveLayerRegion name="hero" description="Top hero with primary CTA">
        <h1>Pricing</h1>
        <p>Simple, transparent.</p>
      </LiveLayerRegion>

      <LiveLayerRegion name="plans" description="Pricing tiers">
        <PlanCard tier="starter" />
        <PlanCard tier="pro" />
        <PlanCard tier="enterprise" />
      </LiveLayerRegion>

      <LiveLayerRegion name="faq" description="Frequently asked questions">
        <FaqList />
      </LiveLayerRegion>
    </main>
  );
}

Now the agent's view of the page includes named regions:

json
{
  "regions": [
    { "name": "hero",  "description": "Top hero with primary CTA",  "text": "Pricing. Simple, transparent." },
    { "name": "plans", "description": "Pricing tiers",              "text": "Starter $9/mo... Pro $29/mo... Enterprise..." },
    { "name": "faq",   "description": "Frequently asked questions", "text": "..." }
  ]
}

The agent can then say "scroll to plans" and the widget knows where to scroll.

Agent-fillable forms

Just write regular HTML. Every <form> on the page is auto-discovered and surfaced to the agent. The agent can fill, validate, and submit it — or run a guided voice sub-conversation through it via collect_from_page.

tsx
export function ContactForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [message, setMessage] = useState("");

  return (
    <form onSubmit={() => sendMessage({ name, email, message })}>
      <label>Your name
        <input name="name" value={name}
               onChange={(e) => setName(e.target.value)} required />
      </label>
      <label>Email address
        <input name="email" type="email" value={email}
               onChange={(e) => setEmail(e.target.value)} required />
      </label>
      <label>Message
        <textarea name="message" value={message}
                  onChange={(e) => setMessage(e.target.value)} />
      </label>
      <button type="submit">Send</button>
    </form>
  );
}

What this enables:

  • Visitor: "Help me fill this out — my name is Jordan, email jordan@acme.com, message: When are you launching?"
  • Agent: calls fill_form with all three values at once, then submit_form when the user confirms.
  • OR — for a voice-first walk-through — the agent calls collect_from_page and asks each question one at a time with per-kind normalization.

The agent infers each field's label from the wrapping <label> / aria-label / placeholder, and infers the kind from the type= attribute. Pass <AvatarWidget onCollect={...} /> to receive the typed payload when the agent finishes a guided collection.

Opt-out — keep forms or fields invisible

tsx
<form data-ll-skip>...</form>           // exclude an entire form
<input data-ll-private />               // exclude a single input

And, always-on (browser-native) privacy guards: type="password", autocomplete="cc-*", and autocomplete="off" are ALWAYS excluded — you don't need to mark them.

Disambiguation hint

When a page has multiple forms and the LLM can't tell them apart, add data-ll-intent:

tsx
<form data-ll-intent="request a demo">...</form>
<form data-ll-intent="newsletter signup">...</form>

data-ll-private="true" — opt-out subtree

Mark any subtree as off-limits to context extraction:

tsx
<section data-ll-private="true">
  {/* This entire section is invisible to the agent's page context */}
  <CreditCardField />
  <SocialSecurityField />
</section>

Use this for:

  • Sensitive PII (medical records, financial details, government IDs)
  • Internal admin UI you don't want the agent surfacing
  • Diagnostic / debug panels

Password fields (<input type="password">) are excluded automatically — you don't need to mark them.

Capability gating

Page awareness is gated behind capabilities. To allow form filling, include fill_forms and submit_forms:

tsx
<AvatarWidget
  agentId="agt_abc123"
  capabilities={["read_page", "fill_forms", "submit_forms", "navigate"]}
/>

Without the right capabilities, the agent can request page context but won't be able to act on what it sees. See AvatarWidget capabilities.

Performance notes

  • The default DOM walk is cached. Successive request_page_context calls within the same route hit the cache.
  • The cache is invalidated when pathname changes (which is why passing it for SPAs matters).
  • For pages over ~5000 elements, write a custom getPageContext that returns a structured summary instead of walking the whole DOM.
  • Use clearPageContextCache() (exported) if you mutate the page meaningfully without a route change.
  • Routingpathname, showOn, hideOn for SPAs
  • AvatarWidget props — full reference for getPageContext, pageContextExtras, getRoutes, capabilities