agent-browser and Chrome for Testing (Chromium) are preinstalled in every orb. Run
agent-browser skills get core --full first; it is the version-matched reference for commands,
refs, sessions, restore, and recording. This skill adds only what the orb environment changes.
Environment
- The browser runs inside the orb. It can open
http://localhost:<port>dev servers directly. Only the user needs portal URLs, so share the portal URL with the user and never the localhost URL you browsed. - If Chrome is missing, run
agent-browser install. - For a self-signed HTTPS dev server, pass
--ignore-https-errorsand--args "--allow-running-insecure-content"on the firstopen. Prefer an HTTP port when the server offers one. - Launch flags such as
--args,--executable-path, and--ignore-https-errorstake effect when the session's browser starts. Pass them only on the firstopen, or identically on every command. Passing different flags, or passing--argsagain after commands without it, silently relaunches Chrome, which drops page state and the 2x viewport. To add a flag to a running session,closeit and reopen with the flag. --argssplits on every comma, so a comma-valued Chrome flag cannot go through it; use an--executable-pathwrapper instead (see Hover-gated UI).- Save every screenshot and recording the user should see under
.amp/in/artifacts/by absolute path. The thread renders media only from the workspace, so files under/tmpshow as broken placeholders. A path starting with.is read as a CSS selector.
Sessions
- Reuse one named
--sessionfor a whole workflow. Keep at most three live sessions in one namespace; reuse or close one instead of opening a fourth. Keep session names to 20 characters or fewer so the Unix socket path stays under its 103-byte limit. - Close sessions as soon as their part of the workflow ends, and before memory-heavy work such as test suites or builds.
- The browser daemon dies on orb pause and resume, on CLI restarts, and after an idle hour. Pass
bare
--restoreon every command for a session that must keep login state. Restore keeps cookies and storage only: after a relaunch,open <full URL>(not barereload) and reapplyset viewport,set device, andset media. [agent-browser] restore: missing; save: savedis startup metadata, not a failure.restore: load_failed; save: skipped_restore_failedmeans the browser started without the saved login and will not save a new one;session info --jsonshowsrestoreStatusDetail. Close the session, runstate clear <key>, reopen, and log in again.- Tab IDs start at
t1; runtab listfor current IDs.tab t0andtab 0are rejected. - Add
--auto-connectonly when the user asks to control an already-open browser. - A shell helper that creates sessions installs an
EXITtrap as soon as it knows the session names; a Node or TypeScript helper closes them infinally. Call cleanup explicitly on the success path too; a hard command timeout can skip the trap, so run cleanup in the next command after a timeout.
export AGENT_BROWSER_NAMESPACE=checkout-flow
SESSIONS=(buyer admin)
cleanup() {
for session in "${SESSIONS[@]}"; do
agent-browser --session "$session" --restore close >/dev/null 2>&1 || true
done
}
trap cleanup EXIT
agent-browser --session buyer --restore open "$BUYER_URL"
agent-browser --session admin --restore open "$ADMIN_URL"
for session in "${SESSIONS[@]}"; do
agent-browser --session "$session" --restore set viewport 1280 720 2
done
# workflow
cleanup
trap - EXIT
Screenshots
- Screenshots must be 2x (
devicePixelRatio2); 1x looks blurry on retina displays. Runset viewport <w> <h> 2after the firstopenof a new or restarted session and after anyset viewportthat omits the scale, which resets that tab to 1x. Check witheval devicePixelRatiobefore capturing. Image pixel coordinates are then 2x the CSS values; selector clips are unaffected. - Pass the output path as the last positional argument of
screenshot; there is no-sor--outputflag. Use unique filenames and confirm the fresh file exists withtest -f. For the whole page, the flag is--full, not--full-page. - Set screenshot scale with
set viewport, not Chromium's--force-device-scale-factorlaunch flag. Combining the launch flag with viewport DPR produces incorrectly sized full-page captures. set viewport,set device, and init scripts apply to the current tab only. A popup opened by page code (window.open,target=_blank) gets neither;tab new <url>andclick <selector> --new-tabreplay init scripts but not the viewport. Open new pages withtab new <url>instead of triggering a popup, and aftertab <id>onto any new tab checkeval devicePixelRatioand rerunset viewport <w> <h> 2if it is not 2.- Wait for the element, text, URL, or app-specific ready signal before every capture. After a
viewport change, wait for two animation frames:
eval "new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))". - Pass a selector or ref as the first
screenshotargument only when the element sits inside the initial unscrolled viewport. Selector captures clip against the unscrolled surface, so below-the- fold or scrolled elements come out blank white andscrollintoviewdoes not fix it. For those, scroll and capture the viewport without a selector, or capture--fulland crop. - Capture every representative affected state, not only the default one: open, selected, empty, loading, and error states when the change touches them.
- Inspect every capture with the media viewing tool before reporting it. It rejects files over
1 MiB; compress with
magick in.png -resize 1280x900 -quality 85 out.jpgand checkstat -c %s. For exact colors or measurements, sample pixels with Python and Pillow instead of judging by eye. - Use
set viewport <w> <h> 2at several widths for breakpoint checks. A 390-pixel Chromium capture proves the narrow layout only; it is not a phone capture (see Touch and mobile).
Hover-gated UI
Headless Chromium reports no input device: (hover: none) matches and (pointer: fine) does not.
Hover-gated UI such as tooltips never appears on hover or mouse move, and touch fallbacks render
in screenshots. To test hover, relaunch through a wrapper that adds the Blink settings:
cat > /tmp/chrome-desktop-pointer.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
chrome=$(ls -d "$HOME"/.agent-browser/browsers/chrome-*/chrome | sort -V | tail -n 1)
exec "$chrome" \
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4' \
"$@"
EOF
chmod +x /tmp/chrome-desktop-pointer.sh
agent-browser --session app close
agent-browser --session app --executable-path /tmp/chrome-desktop-pointer.sh open "<url>"
Verify eval "matchMedia('(hover: hover)').matches" is true before trusting hover results. A
relaunch without the wrapper reverts.
Interaction recordings
Record a clip when a change affects how UI responds to the pointer or keyboard: hover states, menus, popovers, inline forms, focus, or transitions. A clip shows what a person sees while moving through the UI; screenshots of the end states do not.
Record with scripts/action-video.mjs in this skill's base directory. It needs only Node and
ffmpeg, and it uses a project's own node_modules/.bin/agent-browser when the working directory
has one. Launch the session through the hover wrapper above first; start refuses a session that
reports (hover: none). Start from a settled state and route every browser command between
start and stop through the recorder:
REC="node <skill base directory>/scripts/action-video.mjs"
export ACTION_VIDEO_SESSION=app
$REC start "$PWD/.amp/in/flow-raw.mp4"
$REC mouse move 760 236
$REC mouse move 760 308
$REC click "[data-testid='add-comment']"
$REC wait "textarea"
$REC type "textarea" "Looks good."
$REC stop "$PWD/.amp/in/artifacts/flow.mp4"
- The recorder drives real input.
mouse move,hover,click,drag, and the pointer part offillandtypetravel along curved paths, so every element the pointer crosses gets real hover events.fillandtypesend real keystrokes, one character at a time. agent-browser's cursor overlay draws the pointer and click feedback; passstart RAW --no-cursorwhen the page draws its own pointer. - Each
mouse moveandhovertakes about 0.6 seconds and then holds for one second, so delayed hover effects such as tooltips appear. Do not add waits for pacing; wait only for required UI state. - Tour the area the way a person would: pass over neighboring rows and similar controls before the
target, click, wait for the result, then keep moving inside the new UI so its hover states show.
Get coordinates from
get box <selector>. - Keep
AGENT_BROWSER_NAMESPACEandACTION_VIDEO_SESSIONunchanged fromstarttostop. stopwrites the MP4 at the tab's pixel ratio with idle stretches between commands removed, and for clips up to 30 seconds a looping animated WebP next to it (.loop.webp). The transcript plays a video once, so embed the WebP in image format and link the MP4. Keep the raw file unless the user asks for the uncut video. Never type secrets into a recording.- Check the clip frame by frame before reporting. Video analysis samples about one frame per second
and misses short hover states, so view a contact sheet:
ffmpeg -i flow.mp4 -vf fps=5,scale=640:-1 /tmp/f%03d.pngandmagick montage /tmp/f*.png -tile 4x -geometry +4+4 sheet.png.
Touch and mobile
agent-browser is Chromium-only and never emulates touch. set viewport and set device change
size, DPR, and user agent only, so (pointer: coarse) stays unmatched and (hover: none) matches.
Touch-only styles change sizes, spacing, and layout, so never present a set device screenshot as
what the UI looks like on a phone.
For mobile screenshots, touch verification, and Safari-only rendering bugs, use Playwright device profiles from a scratch directory outside the workspace:
mkdir -p /tmp/wk && cd /tmp/wk && npm i playwright >/dev/null
npx playwright install webkit --with-deps # iOS Safari approximation
const { webkit, chromium, devices } = require('playwright')
const browser = await webkit.launch()
const context = await browser.newContext({ ...devices['iPhone 14'], ignoreHTTPSErrors: true })
const page = await context.newPage()
await page.goto(url)
if (!(await page.evaluate(() => matchMedia('(pointer: coarse)').matches))) throw new Error('no touch')
await page.screenshot({ path: '/absolute/workspace/.amp/in/artifacts/mobile.png' })
- The full device descriptor (
hasTouch) is what makes(pointer: coarse)match; use.tap(). - Android:
chromium.launch()with a Pixel descriptor covers Android Chrome touch behavior. Orbs have no KVM, so an emulator is not an option; never present this as a real device test. - Linux WebKit approximates macOS Safari; system colors and native selection painting differ. Confirm macOS-specific bugs on real Safari.
- Say which engine produced a mobile capture and that
(pointer: coarse)was verified. - For Playwright Chromium, pass
executablePathfrom the agent-browser Chrome (ls -1d "$HOME"/.agent-browser/browsers/chrome-*/chrome | tail -1) instead of runningnpx playwright install chromium, which is a large download.
Fake microphone
Chromium's fake media stack turns a WAV file into the microphone signal:
sudo apt-get install -y espeak-ng
espeak-ng -s 145 -v en-us -w /tmp/raw.wav "Your dictation text here."
ffmpeg -y -i /tmp/raw.wav -af "adelay=700:all=1,apad=pad_dur=1" -ar 48000 -ac 1 /tmp/speech.wav
agent-browser --session mic \
--args "--use-fake-device-for-media-stream,--use-fake-ui-for-media-stream,--use-file-for-fake-audio-capture=/tmp/speech.wav,--autoplay-policy=no-user-gesture-required" \
open "<url>"
The WAV must be mono 48 kHz pcm_s16le; it loops forever. Repeat the identical --args on every
command for that session. Verify with
eval "navigator.mediaDevices.getUserMedia({audio:true}).then(s=>s.getTracks()[0].label)", which
returns Fake Default Audio Input.
Troubleshooting
- There is no
mouse clicksubcommand. Useclick <selector-or-ref>; for coordinates, usemouse move <x> <y>,mouse down left,mouse up left. For a context menu, usemouse down rightandmouse up right.mouse movetakes integer coordinates. - After
Unknown commandorMissing arguments, runagent-browser --versionandagent-browser <subcommand> --helpbefore changing flags. - If
openfails withCould not configure browser: Failed to connect: No such file or directory, retry the sameopenonce, then checkget url. - Do not retry a hanging command in a loop. Run
agent-browser doctor --offline --quick, inspect the session, then close and reopen it. - If
openreportsCDP command timed out: Page.navigate, runget urlonce. If it shows the expected destination, continue. After aRuntime.evaluatetimeout, retry only a read-only or safely repeatable command; the evaluation may have run. - An unmet
wait --textorwait --urlreportsWait timed out; that is not a browser failure. Checkget urland asnapshotbefore restarting anything. Transport failures say so explicitly, for exampleCDP WebSocket connect failed. - If a click is rejected because another element covers the target, take a fresh snapshot, dismiss
or scroll past the obstruction, and target the visible control with
find label "X" clickorfind role button click --name "X". Use a DOM.click()inevalonly as a last resort; it can hide a real overlay bug. - Playwright pseudo-selectors such as
:has-text()do not work. Usefind text "…",find role <role> click --name "…", orevalwithquerySelectorAll. :hoverfollows only trusted input frommouse move; events dispatched inevaldo not trigger it. For hover performance work, send the moves in onebatchso per-command latency does not hide the problem.- If
typeshows nothing in a canvas-rendered terminal such as xterm.js, click it to focus and send batchedpresscalls per key, thenEnter. batchstep strings strip quotes. For quoted JavaScript or selectors, pass a JSON array of argument arrays on stdin:agent-browser batch <<'JSON'…[["open","https://example.com"],["eval","document.title"]].- Wrap multi-statement
evalin an IIFE or block. Top-levelconstandletpersist across calls in a session and causeIdentifier has already been declared. - The orb's agent-browser build has no runtime
addinitscriptcommand. Pass--init-script <path>when launching the browser instead. - If
openlands onabout:blankorsnapshotshows(no interactive elements)for a local HTTPS server, relaunch with the HTTPS flags from Environment on the firstopen.