| `list()` | every tab: id, title, url, kind (`web`/`settings`/`addon`/`home`), active, pending; then the left sidebar's web app (Telegram, WhatsApp, X, the AI chat) as tab `0` with `sidebar: <app name>`, while it holds a web page. It is one view, not a tab, and runs the web tabs' preload, so it hands out nothing a tab doesn't; it comes last so it is never taken for the active tab. `activate` reopens the panel, `close` closes it, `navigate` loads in it |
| `webContents(id)` | the tab's webContents, after loading a dormant tab and thawing a frozen one; throws for non-web tabs |
| `open(url, {background})` | new tab through main's own `createTab` (so BCNR names resolve as typed) |
| `navigate(id, url)` | main's `navigateTab` |
| `close(id)`, `activate(id)` | as the tab strip does |
| `setStatus({connected, clients, busy, tabIds})` | drives the toolbar indicator and the per-tab marker |
| `onStop(cb)` | the indicator's stop button |
### Indicator
- A chip in the toolbar, next to Downloads: **Claude** with a dot that pulses
while a tool call runs. Visible whenever a client is connected (an SSE
stream is open or a request arrived in the last 2 minutes). Clicking it
opens the connector panel; its ✕ stops the connector at once (closes the
port, ends sessions, detaches every debugger).
- Each tab the client is controlling gets an outline marker in the tab strip.
### Tools
| tool | how |
| --- | --- |
| `list_tabs` | `tabControl.list()`; tabs on sites not yet approved show only their host |
| `open_tab(url, background?)` | approval for the target origin first |
| `navigate(tab_id, url \| action)` | url, or `back`/`forward`/`reload` |
| `close_tab(tab_id)`, `activate_tab(tab_id)` | |
| `screenshot(tab_id, full_page?)` | `Page.captureScreenshot`, scaled to CSS pixels so coordinates match `click(x, y)`; a hidden tab is brought to the front first (hidden views render no frames) |
| `read_page_text(tab_id, max_chars?)` | `innerText` read in an isolated world |
| `snapshot(tab_id, interactive_only?)` | `Accessibility.getFullAXTree`, compacted to `role "name" [ref=eN]` lines; refs map to `backendDOMNodeId` |
| `type(tab_id, text, ref?)` | `DOM.focus`, then `Input.insertText` per line and a real Enter (`Input.dispatchKeyEvent`) between lines; `mode: "keys"` sends every character as keyDown/keyUp |
| `paste_html(tab_id, html, ref?)`, `paste_text(tab_id, text, ref?)` | write the real clipboard (`clipboard.write({html, text})`), `webContents.paste()`, then put the user's clipboard back |
| `upload_file(tab_id, ref, path)` | `DOM.setFileInputFiles` on an `<input type=file>` |
| `wait_for(tab_id, selector \| text, timeout_ms?)` | polls in an isolated world |
| `evaluate(tab_id, expression)` | `Runtime.evaluate` in the page; listed only while **Allow evaluate** is on (off by default) |
Page reads run in an isolated world (`Page.createIsolatedWorld`), so page
scripts can neither see them nor tamper with the helpers.
### Safety
- **Per-site allowlist.** The first time a client acts on an origin (opens it,
navigates to it, reads or drives a tab on it), Theseus asks: *Allow Claude
to control medium.com?* — **Allow** (remembered), **This session only**, or
**Don't allow**. A refusal is remembered for 10 minutes, so a client cannot
spam the prompt. The panel lists approved sites with a remove button.
- **Never types into password fields.** Before `type`, `press_key`,
`paste_*` and `evaluate`-free input, the focused element (followed into
shadow roots and same-origin frames) is checked in an isolated world: