Site adapters are the #1 most-wanted contribution (see CONTRIBUTIONS.md). They inject short, hand-curated guidance into the agent's first message when it operates on a known site. The goal is NOT to encode every selector (those rot fast), but to capture the non-obvious quirks that cost an LLM several dead-end tool calls to discover on its own.
src/chrome/src/agent/adapters.js (and src/firefox/src/agent/adapters.js — both builds share the same file content, mirror changes to both).
getActiveAdapter(url) iterates the ADAPTERS array and returns the first adapter whose match(url) returns true:
export function getActiveAdapter(url) {
if (!url) return null;
for (const a of ADAPTERS) {
try {
if (a.match(url)) return a;
} catch (e) { /* skip malformed matchers */ }
}
return null;
}Only ONE adapter fires at a time, so prompt cost is fixed regardless of total adapter count.
- First turn: the adapter's
notesare appended to the first user message in_enrichUserMessageWithCurrentPage(). - Mid-conversation navigation: if the user navigates to a URL matching a different adapter, the agent injects a
[Site context changed → now on <name>]message. Controlled by_maybeReinjectAdapter().
UNIVERSAL_PREAMBLE is injected alongside every system prompt when useSiteAdapters is enabled. It covers cookie/consent banners and paywalls — two patterns that appear across the public web and cause LLMs to make bad assumptions.
{
name: 'my-site', // unique short identifier
category: 'general', // 'general' | 'finance'
match: (url) => /^https?:\/\/(www\.)?example\.com\//.test(url),
notes: `
- Bullet 1: the actionable tip.
- Bullet 2: another tip.
- Keep these SHORT (4–8 bullets max). Every adapter costs tokens on every first turn.
`,
}| Field | Type | Description |
|---|---|---|
name |
string | Unique identifier for the adapter. Used in system-prompt headings. |
category |
'general' or 'finance' |
'finance' adds a [FINANCE / HIGH-STAKES] banner to the heading and triggers extra safety guidance in the system prompt. |
match |
(url) => boolean |
Returns true when the adapter should fire for this URL. Regex is preferred — keep it specific enough to avoid false matches. |
notes |
string | Bulleted guidance injected into the first user message. Keep 4–8 lines max. See style guidance below. |
Adapters are ordered by category/site in the ADAPTERS array. Finance adapters must come BEFORE finance-generic, since finance-generic uses a broad regex that would shadow specific adapters. Currently: Stripe → Coinbase → Robinhood → TradingView → finance-generic.
- Describe the SHAPE of the page rather than literal selectors. Selectors rot; page layout patterns are stable longer.
// Good notes: `- The composer is a contenteditable div, not a textarea.` // Bad notes: `- Click div[contenteditable="true"] to compose.`
- Name the tool to prefer: guide toward AX tools (
click_ax,set_field) over legacy tools (click({text}),type_text). - Flag destructive subtleties: "The 'Cancel' button on the billing page immediately stops service — read the confirmation modal."
- Flag SPA navigation traps: "Settings changes autosave; navigating via browser back discards unsaved edits."
- Flag sticky overlays: "The cookie banner reappears every 24h. Don't describe its text as page content."
- Flag virtualized containers: "The timeline is virtualized — scroll to load more items."
- Keep each bullet to a single actionable tip. The model has limited context and will skim.
- Don't encode CSS selectors — they change with every site redesign.
- Don't write more than 8 bullets — the token cost compounds on every conversation.
- Don't include obvious advice the model would figure out by reading the page (e.g., "the submit button submits the form").
- Don't duplicate the universal preamble (cookie/paywall guidance).
- Don't add alphabetical or reference adapters — each adapter must provide real guidance that saves the model from at least 2–3 trial-and-error tool calls.
{
name: 'twitter',
category: 'general',
match: (url) => /^https?:\/\/(www\.)?(twitter\.com|x\.com)\//.test(url),
notes: `
- The composer is a contenteditable, not a textarea. Character count is enforced client-side.
- The timeline is virtualized — tweets scroll out of the DOM. Use search, not scroll, to find a tweet.
- "Reply", "Retweet", "Like" icons are below each tweet.
- Quote tweets vs reposts: the retweet icon opens a menu with both options.
`,
}{
name: 'stripe',
category: 'finance',
match: (url) => /^https?:\/\/(dashboard\.)?stripe\.com\//.test(url),
notes: `
- LIVE vs TEST mode toggle in the top-right. Always confirm which mode.
- Refunds are partial-by-default — check the amount carefully.
- Customer deletion is irreversible.
- SUBSCRIPTIONS: proration prompts ("Charge prorated amount immediately" vs "On next invoice").
`,
}- Add the adapter to both
src/chrome/src/agent/adapters.jsandsrc/firefox/src/agent/adapters.js. - Verify matching: navigate to the target URL in a browser with the extension loaded. Open the DevTools console on the service worker / background page and run:
import { getActiveAdapter, listAdapters } from './agent/adapters.js'; console.log(getActiveAdapter('https://example.com/some-page'));
- Verify the notes appear: in Act mode, type a simple instruction (e.g., "what's on this page?"). Open the side panel's verbose mode and confirm the first user message contains
[Site guidance for <name>]with your notes. - Verify only ONE adapter fires: navigate to a URL that could match multiple matchers. Check that the first match wins and no others leak through.
- Test navigation re-injection: start a conversation on a non-adapted site, then navigate to your adapted site. Confirm a
[Site context changed]message appears.
Open each adapted site and verify:
- The adapter loads on page 1 (not on a SPA route change)
- The notes are useful (don't mislead the model)
- The model doesn't follow outdated instructions
- Add the adapter object to the
ADAPTERSarray insrc/chrome/src/agent/adapters.js - Mirror the exact same change to
src/firefox/src/agent/adapters.js - Ensure the
match()regex is specific and doesn't shadow neighboring adapters - If
category: 'finance', place it BEFOREfinance-genericin the array - Verify the notes are 4–8 concise bullets
- Test matching with
getActiveAdapter(url) - Test end-to-end with the extension loaded
- If the adapter targets a non-English market, add localized label hints (see the WordPress adapter for an example of how to annotate non-English UI labels)