Skip to content

Browser API

tab = await browser.get("*example.com*") # URL glob
tab = await browser.get("9222:a1b2c3") # target ID
tab = await browser.get("*app*", fresh=True) # only newly-appearing tabs
tab = await browser.get("*app*", timeout=10) # wait up to 10s
tab = await browser.get("*app*", ready="#root") # wait for element after attach
tab = await browser.open("https://...") # open new tab
await browser.watch("*pattern*") # auto-attach current + future
browser.tabs # list[Tab] attached
await browser.pages() # all Chrome targets
browser.patterns # active watch patterns (property)
await browser.detach("*pattern*") # detach by pattern
await browser.detach() # detach everything
browser.clear(target=) # clear captured data
await browser.connect(9223) # add another Chrome instance
await browser.connect(profile="/path/to/profile") # port from DevToolsActivePort
await browser.disconnect() # unpin tabs, close all WebSockets
await browser.disconnect(port=9222) # unpin + close one Chrome instance

Stores a selector or a JS expression on the Tab. Used by get(), open(), navigate(), reload(), and session recovery after HMR. It’s the one parameter that accepts either, so the shape decides:

  • Selector → resolved and polled every 100ms. Anything starting with ., #, [, data-, text=, role= or label=, anything containing :has-text(, and any bare name (main, my-app) — the same set click() and wait_for() accept.
  • JS expressionRuntime.evaluate, must return truthy. Anything else, which in practice means it has a dot or a call in it (window.ready, app.isLoaded()).
  • Default (no ready=): waits for document.readyState === 'complete'.

The bare-name case is the one worth knowing: ready="main" and ready="my-app" are ordinary CSS, and are treated as such.

await tab.js(expr, *, await_promise=True, user_gesture=True) → Any

Evaluate JavaScript with REPL semantics. Top-level await works. Promise results are awaited by default. let/const can be redeclared across calls. Raises BrowserJSError on exceptions.

await tab.click(selector, *, button='left', click_count=1) → None

Mouse click via Input.dispatchMouseEvent. Auto-waits up to 2s. Produces isTrusted=true events.

await tab.type_text(selector, text, *, delay_ms=0, press_enter=False) → None

Focus element, select-all, type character-by-character. Auto-waits up to 2s.

await tab.tap(selector_or_x, y=None) → None
await tab.swipe(x1, y1, x2, y2, *, steps=10, duration_ms=300) → None
await tab.scroll(selector, dy=0, dx=0, *, steps=10, duration_ms=300) → None

Touch events for mobile Chrome via ADB. scroll() is sugar over swipe() — resolves selector to its center and swipes the opposite direction (scrollBy semantics: positive dy scrolls down, positive dx scrolls right).

await tab.key(key) → None

Dispatch a keyDown+keyUp pair for a named key (e.g. "Enter", "Escape", "Space"). "Enter" and "Space" carry the produced character so Chromium’s native button/form activation fires — without it, the DOM keydown/keyup still dispatch but a focused button silently doesn’t click.

await tab.fetch(url, *, method='GET', body=None, headers=None) → dict

In-page fetch() — inherits cookies, session, CORS origin. Returns {"status": int, "ok": bool, "body": Any}. Body is auto-parsed as JSON when content-type includes json.

await tab.navigate(url) → None
await tab.reload() → None

Both wait for the ready signal after page load.

await tab.close() → None

Closes this tab (Target.closeTarget). Session cleanup follows from the resulting Target.targetDestroyed event, same as a user closing it.

await tab.tree() → list[str]

Compact accessibility tree as text lines. Crosses iframes.

await tab.screenshot(*, full_page=False, path=None) → dict

Always writes a PNG — to path if given, otherwise a 0600 file under $XDG_RUNTIME_DIR/repld/. Returns {path, source: {width, height}, model: {width, height}, scale, bytes}. The image is resized to the vision API’s token grid; when scale < 1, multiply coordinates by 1/scale to map back to page pixels.

await tab.wait_for(selector, *, timeout=5.0) → None
await tab.wait_for_idle(*, timeout=5.0, quiet=0.5) → int # settle ms
await tab.pin(reason='', guard_unload=True) → None # guard_unload=False for live-reload dev servers
await tab.unpin() → None
await tab.confirm(prompt) → bool
await tab.choose(prompt, options) → str
await tab.ask(prompt) → str
await tab.cdp(method, **params) → dict

Raw CDP passthrough.

await tab.cookies() → list[dict]

All cookies for this tab via Network.getCookies.

await tab.controls() → dict | None
await tab.invoke(control, action, args=None) → dict

controls() calls the page’s window.controls.describeAll(), returning the schema for every registered control — or None if the page exposes no window.controls. invoke() runs one action and returns {returned, stateBefore, stateAfter, duration}. See the controls guide for the protocol a page implements.

All four take since=, and on all four it is epoch seconds — pass time.time(). The three underlying CDP clocks (wall-time seconds, Runtime.Timestamp milliseconds, Network.MonotonicTime from an arbitrary origin) are converted for you.

tab.network(url=, method=, status=, type=, since=, include_assets=False) → Rows

Query captured requests. url uses LIKE matching (*%). Assets excluded by default. Max 500 rows, newest-first.

tab.console(level=, source=, since=) → Rows

Query console messages. Max 200 rows.

tab.sse(url=, event_name=, since=) → Rows

Query SSE (EventSource) messages. Each row: request_id, event_name, event_id, data, timestamp.

tab.lifecycle(name=, since=) → Rows

Query Page.lifecycleEvent entries: DOMContentLoaded, load, networkIdle, etc.

tab.request(request_id) → dict # full HAR entry (headers, timing, postData)
tab.body(request_id) → dict # response body {"body": str, "base64Encoded": bool}
row.body() → dict # shortcut on any network Row
tab.clear() → None

browser.connect(port) adds a Chrome instance to the pool — call it multiple times for multi-browser setups. Target IDs include the port prefix (42829:abc123 vs 43213:def456), so tab-scoped tools route to the right Chrome automatically.

await browser.connect(42829)
await browser.connect(43213)
await browser.watch("*localhost:5200*") # watches across both
browser.tabs # tabs from all instances

Connected ports and watch patterns persist across kernel restarts. On boot, repld prompts on the terminal ([Y/n], default yes) before reconnecting and re-watching — headless boot (--no-display) or non-tty stdin skips the restore entirely.

The dashboard’s Connections tab gives you the same connect/watch/disconnect controls from a browser instead of exec.

Console errors and uncaught exceptions from watched tabs push as [console:error] channel messages the moment they happen — no polling:

[console:error] 9222:af5ae1: TypeError: Cannot read property 'x' of null

Cross-tab duplicates within 2 seconds are collapsed into one follow-up message (... (×14 tabs)). Mute noisy patterns:

browser.suppress("[vite] failed to connect") # mute matching errors
browser.unsuppress("[vite] failed to connect") # un-mute
browser.suppressed # list active patterns

Suppress patterns persist across kernel restarts.

PropertyTypeDescription
tab.urlstrCurrent URL (cached — use tab.js("location.href") for live)
tab.titlestrPage title (cached)
tab.typestr"page", "iframe", "service_worker", etc.
tab.target_idstrShort ID in {port}:{6-hex} format
tab.capture_bodiesboolToggle Fetch body capture (True on get/open, False on watch)
tab.labelstrHuman-readable identifier
PatternTypeFocus-safe
.class, #id, [attr]CSSYes
[data-testid='name']CSSYes
text=SubmitText matchNo
role=button[name="Save"]ARIANo
label=UsernameLabelNo
tag:has-text('OK')CSS + textNo