feat(desktop): the agent's browser draws on the Code screen after every action - #503
Merged
Merged
Conversation
…ry action Item 2 (the cockpit). Measured first: a viewport JPEG at quality 55 is 13-96 KB and 24-106 ms, so one frame per browser action, never on a timer. The tool announces to a sink the assembly binds only when a screen exists; the frame rides the turn's stream and never the run log; the panel shows where the browser is, with the address as text beside the picture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
|
Full gate on a clean copy in WSL (uv venv, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The agent's browser draws on the Code screen: after every browser action, the viewport appears under the turn, with the page's address as text beside it. Item 2 of the list audited on 2026-09-16 (the "cockpit").
Why, and what was measured first
The model reads a page as text — the tag tree
render_elementsprints — and the person read the same text, or watched a second Chromium window beside the app withCHIMERA_BROWSER_HEADLESSoff. Neither was watching the agent open the site and type the dates.Before wiring anything, the cost of a frame (viewport 1280×720, three pages, this machine):
So: JPEG at quality 55, one frame per browser action, never on a timer. A turn that browsed twenty times costs about a megabyte over its stream; an idle browser costs nothing; a run with no screen captures nothing.
How
BrowserDriver.frame() -> BrowserFrame | None— the Playwright driver captures the viewport (type="jpeg", quality=55, full_page=False) with the page's URL, title and size.BrowserTool.on_frame— set byassemble_registryonly when the request has a screen (frame_sink, a late-boundFrameAnnouncerlike the approval one). The tool announces after every action, success or failure (a failed click still shows the page it failed on), and checks the sink before asking the driver: a headless run never captures — pinned by a test that counts the driver'sframe()calls. A capture that fails is logged and skipped, never a word in the observation the model reads.event: browser—action,url,title,width,height, base64jpeg,n(this turn's frame count). Never written to the run log: replay exists for the words a dropped connection lost, not to redraw a page that has moved on; a reopened conversation shows no frame, honestly.CodeBrowserFrame,onBrowser, the dispatcher'sbrowserbranch;BrowserViewunder the exchange, replaced on every frame (where the browser is), the URL printed as text — a picture of a page is a claim about where the browser is and the address is the part a person can check. Nothing in the panel is interactive: the person is watching, not driving. Ten dictionaries (code.browser.title/frame/alt).Tests
tests/test_the_agents_browser_draws_on_the_code_screen.py, 6): a frame follows every action, success or failure, with the URL of that action; without a sink the driver is never asked; a failing capture never reaches the observation; the announcer drops frames until a screen binds; throughassemble_registrythe browser gets its sink only when there is one; through the real app the turn streams the frame (base64 round-trips,n == 1,seqset) and the run log holdsdoneand nojpeg.Code.browser.test.tsx(3) — the picture with the address beside it, the last frame wins and there is one panel, nothing for a turn that never browsed;lib/browser-frame.test.ts(1) — the frame read off the wire reachesonBrowserwith every field and no other handler. Sabotage: the dispatcher'sbrowserbranch removed → the wire test fails; restored. 154 files, 1102 tests green, tsc clean. OpenAPI byte-identical (SSE frames are not in it).Full gate on a clean copy in WSL — see the last comment.
Not in this PR
A frame on a timer while the page loads or scrolls (per-action was the measured trade); the browser's frames on the autonomous run's stream (
POST /api/runshas no screen sink today); a clickable panel.🤖 Generated with Claude Code