Skip to content

Rework the browser tools after computer-use style browser tools - #217

Merged
stippi merged 13 commits into
mainfrom
feature/browser-cc-tools
Oct 3, 2026
Merged

stippi merged 13 commits into
mainfrom
feature/browser-cc-tools

Conversation

@stippi

@stippi stippi commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Why

The browser tools returned a screenshot, up to 4,000 characters of text and up to 200 CSS-selector elements after every step. That cost 3–7k tokens per call, and the :nth-of-type selectors broke on re-renders. They also had no console, no network view, no tabs and no form helpers. This PR moves them to the shape of the computer-use style browser tools models are trained on, such as the built-in browser in Claude Code. Old sessions are allowed to break.

The tools

Tool Does
browser_navigate load a URL, or back/forward; returns URL and title
browser_read_page / browser_find accessibility tree, - role "name" [ref_N] state, whole, interactive-only, as a subtree or by query
browser_computer click (by ref or coordinates, with modifiers, double/triple), type, key chords with repeat, hold_key, key_down/key_up, left_mouse_down/up, scroll, scroll_to, hover, drag, wait, screenshot/zoom with scale
browser_form_input set a select, checkbox or field by ref
browser_get_page_text, browser_javascript main text; JS with REPL semantics
browser_read_console_messages, browser_read_network_requests console, exceptions, failed loads; requests and response bodies
browser_tabs_context/create/select/close, browser_resize_window tabs; viewport presets, phone emulation, dark mode
browser_batch several steps in one call, stopping at the first error
browser_close, browser_login, browser_profiles unchanged in behavior, now on tabs

Actions return one line, plus notes for dialogs answered, tabs the page opened, or a navigation the action caused. The model asks for a screenshot or the tree when it needs one. Coordinates refer to the frame of the latest screenshot, which reports its size. The tool definitions grow from about 7.2k to 12.8k characters (~3.3k tokens), while the output of each call shrinks.

Engine (web)

  • BrowserSession holds the browser and its tabs. Tab holds one page and its state: refs, the screenshot frame, dialogs, and the console/network log.
  • The accessibility tree is read through a raw CDP command with lenient structs, because chromiumoxide's typed bindings fail on newer property names. Rendering is pure and unit-tested.
  • Raw mouse and wheel events go to the point. Key chords accept aliases.
  • Screenshots are clipped at the scroll offset and fitted to the 1568 px image limit.
  • The headless viewport is 1280×800 at device scale 1.
  • Popups are adopted only when they have an opener, so Chrome's initial blank tab stays out.
  • The selector-based verbs and the element-discovery script are removed.

The page keeps running between calls, also while the model thinks. The descriptions say so, and point timing-sensitive input (games, animations) to browser_batch, where steps run back to back and wait/hold_key durations are exact.

UI

Each browser call renders as one line, described the same way in both frontends through the shared browser::describe module: "Click ref_4", "Find "Sign in"", "Browser steps (3 steps)".

  • GPUI: one inline renderer for all browser tools, replacing the screenshot cards. The header tag shows a named profile and the tab. Expanding a call shows its result: trees and logs in monospace, capped at 40 lines, and screenshots at up to 380 px high.
  • Terminal: browser tools had no renderer before, so the generic fallback dumped every parameter and the full output (a page tree, the page text) into the scrollback. Each call now shows its description and the first line of its result; a batch lists its steps.
  • ACP: unchanged. Browser tools map to ToolKind::Other, with titles from the tools' title_template.

Notes for reviewers

  • Key events no longer set nativeVirtualKeyCode. The key table holds Windows virtual key codes, which name other keys on macOS (W's 87 is Keypad 5). A held W made Chrome auto-repeat hundreds of Numpad5 keydowns.

  • Persisted outputs of the old tools still deserialize. Records of the removed tool names (browser_read, browser_act) are skipped when a session is restored, the same path already used for removed MCP tools.

  • browser_computer and browser_form_input are write tools. An embedder that gates outward actions can tag them (see docs/browser-agency-plan.md).

  • test_web_search needs network access to DuckDuckGo. It fails locally only because DuckDuckGo is unreachable from this machine.

  • docs/browser-tools.md describes the tools and concepts.

Testing

  • web: unit tests for the AX renderer. Browser tests cover tabs and popups, refs, mouse and keyboard, the screenshot frame (also on a scrolled page), console and network, REPL eval, form input, emulation, dialogs, timeouts and cookies.
  • code_assistant_core: tool tests per group:
    • navigate/read/find/fill/inspect
    • click by ref and by scaled coordinates
    • dialogs through the tool
    • a hung page failing fast
    • console and network
    • tabs and resize
    • batch with an error in the middle
    • login handoff
  • ui_gpui: descriptions of the cards.
  • cargo test --no-fail-fast passes apart from test_web_search. cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --check are clean.

stippi added 13 commits October 2, 2026 11:52
The page verbs move from BrowserSession to a new Tab, which also owns
its answered dialogs. A BrowserSession now holds the launched browser
and its tabs: it creates, selects and closes them, and sync_tabs adopts
tabs the page opened itself (target=_blank, window.open) without making
them active. The session's timeouts are shared with every tab.

Session-level verbs that act on the active tab stay for now, for the
tools that predate tabs.

Headless browsers get a 1280×800 viewport at device scale 1 instead of
chromiumoxide's 800×600, so pages lay out for a desktop and screenshot
pixels are CSS pixels. A headful login window fills its real window.
Tab::read_page renders CDP's full accessibility tree as YAML-style
lines, `- role "name" [ref_N] attrs`, with state such as value,
checked, expanded, disabled and focused, and the href of links.
Nameless wrappers and text that repeats its parent's label are left
out. It can be limited to actionable elements (a flat list), to the
subtree of one ref, or to a depth. Tab::find returns the lines that
match a query.

Every printed node with a DOM node gets a ref_N mapped to its backend
node id, stable for as long as the document lives. ref_point scrolls
an element into view and returns the center of its box, where a click
on it lands.

The tree is read through a raw command with lenient structs, since
chromiumoxide's typed bindings fail on property names newer than its
bundled protocol.
Tab gets the input verbs of a computer-use style tool:
- click_point with button, click count (double/triple) and modifiers,
  hover_point, drag, and wheel, which scrolls whatever container is
  under the pointer;
- press_keys for space-separated keys and chords with a repeat count,
  accepting common aliases and any case (Return, Esc, pagedown, f5);
- type_into_focused for the focused element.

screenshot_frame captures the viewport at a scale (default 1), fitted
so neither edge exceeds the model API's image limit, and records that
frame: frame_point maps a point read off the latest screenshot back to
CSS pixels. zoom enlarges a region of that frame for a closer look.
Captures clip at the scroll offset, so a scrolled page shows its
viewport.
Each tab collects in the background what it logged:
- console messages, uncaught exceptions and browser log entries (such
  as failed resource loads), with level and source location;
- network requests with method, status, type and MIME type, or why
  they failed. A response body can be fetched by request id.

More Tab verbs:
- javascript runs code with REPL semantics (top-level await, the last
  expression's value) and returns an exception as an error;
- form_input sets a control by ref: a select by option value or text,
  a checkbox or ARIA switch by true/false, a field through the value
  setter frameworks such as React track. It fires input/change events;
- set_viewport and set_color_scheme emulate a size, a phone (touch,
  mobile layout, Android user agent) and prefers-color-scheme;
- page_text returns the main/article text, or the body's;
- history goes back or forward.
The browser tools now follow the shape of the browser tools models are
trained on. browser_read and browser_act are replaced by small tools
that each do one thing, and a tool returns what was asked for instead
of a screenshot, page text and element list after every step:

- browser_navigate: load a URL, or go back/forward; returns URL+title.
- browser_read_page / browser_find: the accessibility tree with ref_N
  handles and element state, whole, interactive-only, as a subtree or
  filtered by a query.
- browser_computer: click (by ref or by coordinates in the latest
  screenshot's frame, with modifiers and click counts), type, key
  chords with repeat, scroll, scroll_to, hover, drag, wait, and
  screenshot/zoom with a scale. Each screenshot reports its frame.
- browser_form_input: set a select, checkbox or field by ref.
- browser_get_page_text, browser_javascript (REPL semantics).
- browser_read_console_messages, browser_read_network_requests
  (including a response body by request id).
- browser_tabs_context/create/select/close, browser_resize_window
  (presets, phone emulation, color scheme).
- browser_batch: several of these in one call, stopping at the first
  error, screenshots returned in order.

Each action reports what changed around it: dialogs answered, tabs the
page opened, a navigation it caused. Every tool takes an optional
profile and tab_id. browser_close, browser_login and browser_profiles
stay, moved onto tabs.

A session adopts only pages that have an opener, so Chrome's initial
blank tab no longer shows up as a tab.

Outputs stored by the old tools still load; their tool names are gone.
The GPUI cards show screenshots for navigate, computer, batch and
login; the other tools render inline.
The tools now act by accessibility refs and screenshot coordinates, so
Tab drops what only the old browser_act/browser_read used: the
element-discovery script and observe, CSS and text=/role=/aria=
selectors with click, type_text, fill, clear, press_key and wait_for,
selector-based scroll, eval and the full-page screenshot. So do the
session-level verbs that forwarded to the active tab.

The web tests move to the tab API. Tests that only covered the removed
verbs are gone; their behavior is covered by the AX, mouse, keyboard
and form tests.
docs/browser-tools.md lists the tools and explains refs, the screenshot coordinate frame, dialogs, timeouts and tabs. The agency plan points to it where it describes the replaced tools.
a_hung_page_fails_fast_instead_of_hanging clicked a button whose handler started an endless loop 50 ms later. Under a full parallel test run the loop could start before the click's own CDP acknowledgment arrived, so the setup click timed out under the test's 1 s limit. The loop now starts through a script with a 100 ms margin.
On Linux CI the drag logged its mousedown but no mouseup. Pressing on
selected text starts a native drag-and-drop, which ends without a
mouseup, and what the preceding double click leaves selected differs
between platforms. The test now clears the selection before dragging,
and the page also logs dragstart, so a drag-and-drop shows up in the
failure message if it happens anyway.
Browser work is many small calls, so each now reads like the other
inline tools: "Click ref_4", "Find "Sign in"", "Browser steps (3
steps)". code_assistant_core::tools::impls::browser::describe holds the
wording, so GPUI and the terminal describe calls the same way.

- GPUI: one inline renderer for all browser tools. The header tag shows
  a named profile and the tab. Expanding a call shows its result:
  trees and logs in monospace, capped at 40 lines, and screenshots at
  up to 380 px high. The browser cards and their starts_collapsed
  override are gone.
- Terminal: browser tools had no renderer, so the fallback dumped
  every parameter and the whole output (a page tree, the page text)
  into the scrollback. Each call now shows its description and the
  first line of its result; a batch lists its steps.
A game reads "keep walking" as a key held down, and drag targets want
a button held through a move. Tab gets key_down / key_up (keys stay
down across calls, released last first), hold_keys (down, wait, up, as
trusted CDP events), and mouse_down / mouse_up at a point or where the
mouse is. The tab tracks the mouse position and held buttons, so moves
while a button is down carry it.

Key events no longer set nativeVirtualKeyCode. The key table's codes
are Windows virtual key codes; on macOS they name other keys (W's 87
is Keypad 5), and a held W made Chrome auto-repeat hundreds of Numpad5
keydowns.
browser_computer gets hold_key (`text` held for `duration` seconds),
key_down / key_up (held across steps, e.g. walk while jumping) and
left_mouse_down / left_mouse_up. Without them a model drove a game by
dispatching synthetic KeyboardEvents through browser_javascript, which
only works on pages that accept untrusted events.

The descriptions of browser_computer and browser_batch now say that the
page keeps running between calls, also while the model thinks, and that
timing-sensitive input belongs in one batch. In a game session the
model held a key across a separate wait call and its thinking, and
misjudged how far a "0.5 s" push had carried it.
A key pressed with key_down stays down until key_up, also across turns.
A model that forgets to release it leaves the page receiving a held
key. Every browser action's result now notes what is still held down
("Still held down: w, left mouse button"), until it is released.

Held keys are tracked by key code, so a release matches whatever name
or case the press used.
@stippi
stippi merged commit af610e3 into main Oct 3, 2026
5 checks passed
@stippi
stippi deleted the feature/browser-cc-tools branch October 3, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant