mentionable.dev

Widget Embed Guide

Single-line summary: widget.js is a zero-dependency, Shadow DOM-isolated chat widget that embeds with a single <script> tag — no attributes required when serving from the same origin. It auto-discovers available agents (Schema.org JSON-LD → data-agents attr → /.well-known/mentionable-agents.json), renders a FAB + panel, and communicates via A2A transport when available (auto-detected from the agent card) with REST as the fallback. Agent replies are rendered as rich HTML — markdown is parsed inline (headings, bold, italic, code blocks, links) and tool calls are shown as collapsible cards with in-flight / success / error states.

See also: transport-module-guide#rest, webfinger-agent-card-guide, deployment-patterns, glossary

Implementation: examples/llm-agent-vercel/public/widget.js. Runtime spec: docs/spec/transport-rest-v0.1.md.


Installation

Zero-config (same-origin)

When the widget is served from the same domain as the agent host, no attributes are needed:

<script src="/widget.js"></script>

The widget uses window.location.hostname as the domain, fetches /.well-known/mentionable-agents.json to discover all agents, and shows a <select> dropdown automatically when two or more agents are available.

Cross-origin embed

When embedding on an external site, specify the agent address:

<script src="https://your-agent-domain/widget.js" data-agent="@lean@your-agent-domain"></script>

No build step, no npm install. The script mounts a floating action button (FAB) and a chat panel into a Shadow DOM container on document.body. It is safe to include the same script multiple times — duplicate mounts on the same domain are silently skipped.


Attribute Reference

AttributeRequiredDefaultDescription
data-agentNoauto-discovered from current domain@handle@domain address of the primary agent. Required only for cross-origin embeds. Must match WebFinger grammar ([a-z0-9_.-]{1,64}@[a-z0-9.-]{1,253}).
data-agentsNoauto-fetched from /.well-known/…Comma-separated list of @handle@domain addresses. When 2+ are provided, a <select> dropdown appears at the top of the panel. Overrides auto-fetch when present.
data-themeNo"auto""light" | "dark" | "auto". "auto" follows prefers-color-scheme.
data-positionNo"bottom-right""bottom-right" | "bottom-left" — corner where the FAB and panel are anchored.
data-titleNoagent’s displayNamePanel header title shown for the primary agent.
data-endpointNohttps://<domain>/~<handle>/Full REST endpoint URL override. Useful when embedding behind a CORS proxy or when the endpoint path differs from the default.
data-base-urlNowindow.location.originBase URL prefix. The widget derives the domain from this when data-agent is absent. Ignored when data-endpoint is set.

CSS Customisation

The widget uses Shadow DOM, so host-page CSS cannot bleed in. All visual tokens are exposed as CSS custom properties (--mw-*) on the :host element. Override them by targeting the widget’s host element ID from the outside:

#mentionable-widget-your-domain {
  --mw-accent: #6366f1; /* brand colour (FAB, focus rings, pay links) */
  --mw-radius: 16px; /* panel + FAB border-radius */
  --mw-fab-bg: #6366f1; /* FAB background */
  --mw-fab-icon: #ffffff; /* FAB icon colour */
  --mw-bg: #ffffff; /* panel transcript background */
  --mw-surface: #f8f8f8; /* panel header + input row background */
  --mw-surface2: #efefef; /* avatar + close button hover background */
  --mw-text: #0f0f0f; /* primary text */
  --mw-text-muted: #555555; /* secondary text */
  --mw-text-dim: #aaaaaa; /* placeholder, domain labels */
  --mw-border: #e0e0e0; /* panel + bubble border */
  --mw-border-soft: #eeeeee; /* input row top border */
  --mw-user-bubble-bg: #0f0f0f; /* user message background */
  --mw-user-bubble-text: #fff; /* user message text */
  --mw-err-bg: #fef2f2; /* error bubble background */
  --mw-err-border: #fca5a5; /* error bubble border */
  --mw-err-text: #dc2626; /* error bubble text */
  --mw-font: system-ui, sans-serif; /* font stack */
}

The host element ID follows the pattern mentionable-widget-<domain>, e.g. mentionable-widget-example.com.

Dark-mode defaults are already built in via @media (prefers-color-scheme: dark). Override only the tokens you want to change — unset tokens fall back to the built-in defaults.


Multi-Agent Support

The widget supports multiple agents on the same domain and shows a <select> dropdown when two or more agents are available. Switching agents clears the current chat history and reconnects to the new agent’s endpoint.

Agent discovery priority order

On every mount the widget resolves the agent list using the following priority — stopping at the first successful source:

  1. Schema.org JSON-LD on the page — parses <script type="application/ld+json"> looking for contactPoint[].email fields in handle@domain form. Zero network cost; works on same-origin pages that already embed the JSON-LD block (e.g. examples/llm-agent-vercel landing page). Takes precedence over all other sources.

  2. data-agents attribute — comma-separated list of @handle@domain addresses evaluated synchronously. Used when JSON-LD yields no agents.

    <script
      src="https://your-domain/widget.js"
      data-agents="@lean@your-domain,@ops@your-domain,@docs@your-domain"
    ></script>
  3. /.well-known/mentionable-agents.json — falls back to a single GET request if neither JSON-LD nor data-agents produces a list.

    Expected response shape:

    {
      "agents": [
        { "handle": "lean", "displayName": "Lean", "description": "LLM assistant" },
        { "handle": "ops", "displayName": "Ops", "description": "DevOps agent" }
      ]
    }

    If the request fails for any reason (CORS error, 404, parse error, network timeout) the widget silently stays with the initial single-agent state. The fetch runs after mount and does not block the initial render.


Transport Selection

The widget auto-detects which transport to use per agent. No configuration is needed.

Priority order

  1. A2A — if the agent card at /.well-known/agent-card/<handle> exposes an a2a.endpoint, the widget uses A2A JSON-RPC.
    • a2a.capabilities.streaming: truemessage/stream (SSE, token-by-token)
    • a2a.capabilities.streaming absent/false → message/send + tasks/get polling (2 s interval)
  2. REST — fallback when no agent card is found or the card has no a2a.endpoint.
    • Server responds Content-Type: text/event-stream → SSE streaming
    • Server responds Content-Type: application/json → full JSON response
    • Any other Content-Type → treated as plain text

The agent card fetch result is cached for the page lifetime. Switching agents (multi-agent dropdown) clears the A2A contextId and starts a fresh conversation.

Zero-config transport

When the widget runs in zero-config mode (no data-agent, HANDLE is unknown), agent card lookup is skipped and REST is used directly. Once the agent list is resolved (via JSON-LD or well-known fetch) and the user switches to a named agent, A2A detection runs on the next send.


CORS Considerations

The widget POSTs multipart/form-data to the REST endpoint from the embedding page’s origin. This triggers a CORS preflight.

The agent host must serve the following headers on the REST endpoint:

Access-Control-Allow-Origin: *          (or the specific embedding origin)
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Accept, Content-Type

examples/llm-agent-vercel serves these headers automatically on /~<handle>/ routes.

When you cannot configure CORS on the agent host (e.g. third-party deployment), set data-endpoint to a same-origin proxy:

<script
  src="https://your-site.com/widget.js"
  data-agent="@lean@agent-host.example"
  data-endpoint="https://your-site.com/api/agent-proxy/"
></script>

The /.well-known/mentionable-agents.json discovery fetch is a simple GET and also requires CORS headers if the widget is embedded cross-origin.


Message Rendering

Agent replies are rendered as structured HTML rather than plain text. The widget includes a zero-dependency inline parser — no external libraries, no innerHTML assignment.

Markdown

renderMarkdown(text) converts the text part of each reply into DOM nodes:

SyntaxRendered as
# H1 / ## H2 / ### H3<h1><h3> with .wgt-md-h1/h2/h3
**bold**<strong>
*italic*<em>
`inline code`<code> with .wgt-md-code
``` fenced ```<pre><code> with .wgt-md-pre
https://… URLs<a> with .wgt-md-a (HTTPS-only; bare http:// is left as text)
everything else<p> with .wgt-md-p

Tool calls

renderToolCall(part) renders each tool_call part as a card (.wgt-tool-card) with three states:

CSS tokens for rendering

The rendering helpers use scoped class names prefixed with .wgt-. You can target them from outside the Shadow DOM using ::part() if you expose the elements, or by overriding the --mw-* tokens that the classes reference internally.


unmount() API

To programmatically tear down a widget, call unmount() on its registry entry:

const id = 'mentionable-widget-your-domain'
window.MentionableWidgets?.[id]?.unmount()

unmount() removes the host element from document.body, removes the keydown listener used for Escape-to-close, and removes the prefers-color-scheme media listener (when data-theme="auto"). The registry entry is deleted so the same address can be re-mounted later.

window.MentionableWidgets is a plain object keyed by widget ID. It is created on first mount and persists for the page lifetime.