# v0.9.0 — Tabs you can say out loud, sized the way you asked

**2026-08-08**

## TL;DR

Tabs answer to a word now. `a5126214` is `copper`; `ed10e6e6` is `maple`. The word shows
in every response envelope, in `list_pages`, on the VNC badge, and it works anywhere a tab
id was accepted before — including as the `tab` argument.

And a viewport is a property of your tab, not of the browser. `set_viewport_size` used to
resize the operating system window, which meant one agent laying out a phone screen
reshaped every other agent's page and the window a person was watching over VNC. It now
names its own tab, takes presets instead of numbers, can pretend the screen is denser than
it is, and can hand the tab back.

## Why

Both halves of this release are the same complaint from two directions: **a shared browser
is only workable if a tab is a thing you can point at.**

An eight-character hex prefix is not a thing you can point at. It cannot be held in your
head, said out loud to another agent, or matched against a window on a VNC screen without
reading it character by character. In practice that meant the person running the fleet
could see *that* a tab was claimed but not *which* claimed tab it was — the ownership
markers shipped in v0.7.1 were legible and still not useful.

The viewport had the opposite problem: it was too easy to point at. Every caller pointed
at the same thing, because there was only one thing — the window. An agent testing a
responsive layout at 390px wide was silently doing that to everyone.

## Highlights

| | |
|---|---|
| **Word names** | Every tab gets one word from a 208-word vocabulary — `copper`, `maple`, `saffron`. Assigned from a hash of the window handle, so a tab keeps its name across a server restart, then walked forward on collision so two live tabs never share one. |
| **Names are selectors** | `tab: "copper"` works everywhere `tab: "a5126214"` did, case-insensitively. Hex prefixes still resolve, so an id copied out of an older transcript does not break. |
| **Per-tab viewport** | `set_viewport_size` resizes your tab through `browsingContext.setViewport`. Other tabs, and the VNC view, are untouched. Survives navigation. |
| **Presets** | `preset: "phone"` instead of remembering that a phone is 390x844. Also `phone-small`, `phone-large`, `tablet`, `laptop`, `desktop`. |
| **Pixel density** | `devicePixelRatio: 3` makes a page choose retina assets, as a real device would. |
| **Reset** | `reset: true` gives the tab back to the window size and clears the density. |
| **Geometry in the envelope** | A tab under an override reports it on every response: `tab copper 390x844@3x "Example Domain"`. Inheriting a phone-shaped tab is no longer a mystery. |
| **`scope: "window"`** | The old fleet-wide resize, still available when the window itself is the point. Now something you ask for rather than something you get. |

## How it works

### Names

A word is minted the first time a tab is seen and released when it closes. The starting
word comes from a hash of the window handle, so the same tab tends to get the same name
after a restart; if that word is already spoken for, the registry walks forward through the
vocabulary until it finds a free one. That walk is what makes the name safe to use as a
selector — determinism alone would eventually hand two live tabs the same word.

The vocabulary is concrete nouns chosen to look unlike each other when skimmed, and kept
clear of anything that reads as a status word so a name is never mistaken for a message.

### Viewports

`browsingContext.setViewport` names the browsing context it acts on, which is exactly the
per-tab addressing the background screenshot and navigation paths already use. The classic
WebDriver `setRect` remains behind `scope: "window"`, and is also the automatic fallback if
the browser has no BiDi session — an older browser gets a working tool with a wider blast
radius rather than an error.

Density is always sent, never omitted. A call that says nothing about `devicePixelRatio`
means native density, not "whatever the last call happened to set" — otherwise `reset`
would leave the tab at 1920x994 still pretending to be a retina screen, which is a reset
that does not reset.

## Limits worth knowing

**Only the viewport is emulated, not the device.** In a tab set to 390x844:

- `navigator.userAgent` still reports desktop Firefox on X11
- `matchMedia('(pointer:coarse)')` is `false` — the page believes it has a mouse

Sites that choose their layout with CSS media queries render correctly, which is nearly all
of them. Sites that sniff the user agent, or gate behaviour on touch support, will still
serve the desktop experience inside a phone-shaped window. Firefox's user-agent override is
a browser-wide preference, so honouring it per tab would reintroduce exactly the fleet-wide
contamination this release removes.

**An override outlives the claim.** Releasing a tab does not reset its viewport, by design —
a tab handed from one agent to another keeps the geometry it was set up with. The envelope
reports it so the next holder is told rather than surprised.

## Config

Nothing new. Presets and density are per-call arguments; names need no configuration.

## Files changed

| File | Change |
|---|---|
| `src/tenancy.ts` | 208-word vocabulary, name registry with collision walk and release-on-close, viewport registry, name-first tab resolution with hex-prefix fallback, geometry in the envelope |
| `src/tools/utilities.ts` | `set_viewport_size` rewritten: presets, `devicePixelRatio`, `reset`, tab-scoped by default |
| `src/firefox/bidi-ops.ts` | `setViewportInContext` — the per-tab primitive |
| `src/firefox/index.ts` | `setTabViewport` alongside the window-level `setViewportSize` |
| `src/tools/pages.ts` | `list_pages` shows names, and geometry for any tab under an override |
| `src/firefox/tab-marker.ts` | VNC badge label carries the word |
| `src/index.ts`, `src/tools/tenancy-tools.ts` | `shortTabId` renamed to `tabName` at every call site |
