
# Chrome extension

The Agentak repository includes a Chrome MV3 side panel built with the same `ChatPanel`
and Pi session as the embeddable widget. The panel adds browser capabilities that a chat
inside a normal page does not need.

::get-extension
::

## Install the unpacked extension

1. Download and unzip the archive.
2. Open `chrome://extensions`.
3. Enable **Developer mode**.
4. Select **Load unpacked** and choose the unzipped folder.
5. Click the Agentak toolbar button to open the side panel.

The extension is not in the Chrome Web Store yet.

## What the panel adds

### Read the active page

The built-in `read_active_tab` tool returns the active tab's title, URL, and rendered text.
It is read-only, so it runs without a confirmation. Page text is marked untrusted: the model
and the transcript both warn that it is data, not instructions.

The panel follows tab changes and navigation, so the next call reads the page currently in
front of the user rather than the tab where the panel first opened.

An empty panel offers one starter at the foot of its greeting, **Summarize this page**,
which is that tool in one click. It is the panel's `prompts`, and it goes with the greeting
once something has been said.

The panel drops this tool when the active page publishes a reader of its own — a WebMCP
tool named `read_page`, `read_current_page`, `read_page_content`, `read_this_page`,
`get_page_content`, or `get_page_text`. A page that reads itself knows its own structure,
and two readers under one list only make the model choose between them.

### Discover WebMCP tools

The panel also lists tools published by the active page on `document.modelContext`. Calls
run in the tab's main world because a live WebMCP tool cannot be serialized into the
extension. Only metadata, JSON arguments, and result text cross the bridge.

The toolbar badge shows how many tools the page publishes. It does not count
`read_active_tab`, which exists on nearly every reachable page.

### Resolve links against the tab

A relative link such as `/settings` in an answer belongs to the page being discussed. The
panel passes the active tab URL as `linkBase`, so the link resolves against that site
instead of `chrome-extension:`. A browser-internal page with no safe HTTP base leaves an
unsafe link as text.

### Keep history per site

Provider keys and model choices belong to the whole extension. Conversations do not: the
panel keeps a separate history shelf per site. Moving to another origin moves the chat to
that site's shelf. A conversation being answered during navigation remains owned by the
site where it started.

Tabs without an HTTP origin share a shelf named for the current tab.

### Bundle provider catalogs

Normal Agentak pages load keyed-provider catalogs from esm.sh. MV3 blocks remote modules,
so the extension bundles one lazy catalog chunk per keyed provider and supplies it through
`useCatalogSource()`.

The extension can list Kilo Gateway and OpenCode Zen because its host permissions are not
gated by page CORS. Chrome Built-in AI is listed when the Prompt API is available.

### Bundle wllama

On Device (wllama) is listed in the panel as well, for the same reason and with one extra
step. `extension/wllama/` supplies the module and the wasm through `useWllamaSource()`,
which is what puts the row back in the picker.

The extra step is the Worker. wllama joins the emscripten glue and its own worker code
into one string per run and starts it as a `blob:` URL. MV3 matches a worker script
against `worker-src`, which falls back to `script-src 'self'`, and `blob:` is not
`'self'` — nor can the policy be widened to allow it. So the build writes that worker out
as a file next to the bundle and rewrites wllama's `createWorker()` to load it, passing
the per-run options on the worker's URL. The package is not patched; the rewrite is a Vite
transform, and the version is pinned exactly so an update is read rather than assumed.

The panel runs single-threaded. Threads mean pthreads, which emscripten also starts from a
blob, and wllama asks for `SharedArrayBuffer` first — the manifest declares no
cross-origin isolation, so it never gets one.

Model weights still come from Hugging Face on first use and are cached in OPFS, so the
first answer on a model waits for the download and later ones do not.

## Storage

The panel uses `chrome.storage.local` for keys, choices, and conversation shelves. It reads
and writes that area directly, because `PiStorage` is asynchronous, and mounts on
`session.ready` so the first render never shows forgotten choices. A write the area refuses
for want of room rejects, which is how built-in history learns to give up an older
conversation.

Closing Chrome's side panel destroys its document. Stored state restores on the next open,
and the composer requests focus because the chat is the whole document.

## Permissions

The manifest requests:

- `sidePanel` to provide the browser panel
- `storage` for choices, keys, and conversations
- `scripting` to read the active tab and bridge WebMCP
- host access to HTTP and HTTPS pages and to follow their selected URLs

Broad host access is intentional. `activeTab` would cover only the tab where the toolbar
button was clicked, while a side panel remains open as the user changes tabs. The cost is
Chrome's all-sites install warning.

The panel cannot read browser-internal screens such as `chrome://` pages. That failure is
returned as a tool error instead of breaking the whole turn.

## Build it from source

From the repository root:

```sh
pnpm build:extension
```

Load `extension/dist` unpacked. The same command creates the zip served by this
documentation site.

The MV3 Content Security Policy permits WebAssembly because Agentak's Markdown parser uses
wasm. It still disallows remote executable code and Workers built at run time, which is
why catalogs are bundled and why wllama's module, wasm, and Worker are all written into
the build. The panel is about 8 MB larger than it would otherwise be, most of it the
wllama wasm.
