# Browser

With `browser` in `dozin.capabilities`, a Connector or task gets `ctx.browser` to read and act on web pages in Cowork tabs. Only tabs the person hands over can be used, and when `dozin.origins` is given, only pages on those sites.

## ctx.browser.adoptTab(tabId)

Takes control of a tab. Cowork hands the `tabId` to the Extension when the person asks it to take over a page; an Extension cannot pick a tab by itself.

| Name | Type | Required | Description |
|---|---|---|---|
| `tabId` | `string` | Yes | The tab Cowork handed over |

Returns `Promise<{ tabId, url, documentId }>`. `documentId` changes each time the page loads a new document. At most 8 tabs at a time.

```js
const tab = await ctx.browser.adoptTab(tabId);
ctx.log(`watching ${tab.url}`);
```

## ctx.browser.releaseTab(tabId)

Gives the tab back. Its injected scripts and audio stop.

| Name | Type | Required | Description |
|---|---|---|---|
| `tabId` | `string` | Yes | A tab this Extension adopted |

Returns `Promise<void>`.

```js
await ctx.browser.releaseTab(tabId);
```

## ctx.browser.evaluate(tabId, script)

Runs a script once in the page and returns its result.

| Name | Type | Required | Description |
|---|---|---|---|
| `tabId` | `string` | Yes | An adopted tab |
| `script.source` | `string` | Yes | A function source, at most 64 KB; it receives `args` |
| `script.args` | `unknown` | No | Passed as JSON, not pasted into the source |

Returns `Promise<unknown>`: the result, as JSON, at most 64 KB.

```js
const title = await ctx.browser.evaluate(tabId, { source: '(args) => document.title' });
```

## ctx.browser.inject(tabId, script)

Installs a script that stays in the page and can send messages back.

| Name | Type | Required | Description |
|---|---|---|---|
| `tabId` | `string` | Yes | An adopted tab |
| `script.id` | `string` | Yes | 1–80 letters, digits, `_` or `-`; unique in this tab |
| `script.source` | `string` | Yes | A function `(args, { emit, onDispose }) => cleanup?`, at most 64 KB; `emit(data)` arrives as a `message` event |
| `script.args` | `unknown` | No | Passed as JSON |

Returns `Promise<void>`. At most 16 scripts per tab.

```js
await ctx.browser.inject(tabId, {
  id: 'chat-watch',
  source: `(args, { emit }) => {
    const observer = new MutationObserver(() => emit({ count: document.querySelectorAll(args.selector).length }));
    observer.observe(document.body, { childList: true, subtree: true });
    return () => observer.disconnect();
  }`,
  args: { selector: '.message' }
});
```

## ctx.browser.uninject(tabId, injectionId)

Removes an injected script and runs its cleanup.

| Name | Type | Required | Description |
|---|---|---|---|
| `tabId` | `string` | Yes | An adopted tab |
| `injectionId` | `string` | Yes | The `id` given to `inject` |

Returns `Promise<void>`.

```js
await ctx.browser.uninject(tabId, 'chat-watch');
```

## ctx.browser.captureAudio(tabId)

Needs `audio` too. Starts capturing the tab's sound.

| Name | Type | Required | Description |
|---|---|---|---|
| `tabId` | `string` | Yes | An adopted tab that has finished loading |

Returns `Promise<AudioStream>`:

| Field | Type | Description |
|---|---|---|
| `tabId` · `url` · `documentId` | | The tab, as in `adoptTab` |
| `streamId` | `string` | This capture |
| `frames` | `AsyncIterable<{ streamId, sequence, timestamp, sampleRate, channels, pcm: Float32Array }>` | Sound, chunk by chunk |
| `ended` | `Promise` | Settles when the capture ends |
| `stop()` | `() => Promise<void>` | Stop it |

One capture per tab at a time.

```js
const stream = await ctx.browser.captureAudio(tabId);
for await (const chunk of stream.frames) handle(chunk.pcm, chunk.sampleRate);
```

## ctx.browser.stopAudio(tabId)

Stops the tab's capture.

| Name | Type | Required | Description |
|---|---|---|---|
| `tabId` | `string` | Yes | A tab with a running capture |

Returns `Promise<void>`.

```js
await ctx.browser.stopAudio(tabId);
```

## ctx.browser.onEvent(listener)

| Name | Type | Required | Description |
|---|---|---|---|
| `listener` | `(event) => void` | Yes | Called for every event of the Extension's tabs |

Returns `() => void` to stop listening. Every event carries `tabId`, `url` and `documentId`, and a `type`:

| `type` | Extra fields | When |
|---|---|---|
| `url-changed` · `navigation-start` · `ready` | — | The page moves or finishes loading |
| `closed` · `crashed` | — | The tab is gone |
| `injected` | `injectionId` | A script was installed |
| `message` | `injectionId`, `data` | An injected script called `emit(data)` |
| `error` | `code` | Something failed in the tab |
| `audio-ended` | `streamId`, `lastSequence` | A capture ended |

```js
const off = ctx.browser.onEvent((event) => {
  if (event.type === 'message') ctx.emit({ text: String(event.data.count) });
  if (event.type === 'closed') off();
});
```

## Errors

The functions reject with one of these codes:

| Code | Meaning |
|---|---|
| `capability_denied` | `browser` (or `audio`) not declared |
| `invalid_tab` · `tab_not_granted` · `tab_not_owned` | Bad tab id, Cowork did not hand this tab over, or it was released |
| `origin_not_allowed` | The page's site is not in `dozin.origins` |
| `tab_limit` · `injection_limit` | More than 8 tabs or 16 scripts |
| `tab_unavailable` · `document_not_ready` · `stale_document` | The tab closed, is still loading, or loaded another page |
| `invalid_script` · `invalid_injection_id` · `injection_exists` · `invalid_json` · `result_too_large` | Bad input or output |
| `audio_already_started` · `audio_transport_unavailable` · `invalid_audio_request` | Audio capture could not start |
| `session_closed` · `browser_operation_failed` | The Extension is stopping, or Cowork could not complete the call |

## Related

- [Injection](https://docs.micromeet.ai/extend/injections.md): Scripts in web pages
- [Connector](https://docs.micromeet.ai/extension-sdk/connector.md): Where `ctx.browser` is used most
