The centered-wheel default no-op'd on some pages (headless e2e_hover_scroll_press
regressed). Restore window.scrollBy for plain page scroll; the coordinate wheel
stays opt-in via --at/--frame for cross-origin iframe content.
Follow-up to #36 — make the whole interaction surface reach cross-origin OOPIFs
and stop coordinate events drifting onto the user's foreground tab over the relay.
- hover/dblclick/drag now DOM-dispatch over the relay or into an iframe (like
click already did): a coordinate Input event isn't confined to the target tab
on a busy real Chrome and can't map an OOPIF element's box to a top-viewport
point. drag does an HTML5 DnD in the element's frame; cross-frame drag errors
loudly instead of drifting.
- snapshot recurses iframes to MAX_IFRAME_DEPTH (3) instead of one level, so refs
inside nested payment/checkout widgets get a frame_id and resolve into the
right frame.
- relay tab adoption merges several Target.getTargets snapshots — a single flaky
relay snapshot was dropping live tabs (a driven tab vanished after restart).
- `type --key-events` (alias --keys) sends real per-character keyDown/keyUp
instead of Input.insertText, so autocomplete/combobox widgets that ignore the
insertText input event fire (Google address postal lookup; commits Angular
reactive forms so Save enables).
- SKILL: hard "snapshot-first, never default to screenshot+coordinates" rule;
snapshot -i pierces cross-origin iframes since v1.5.12; cross-origin iframe
driving guidance (#37).
#35: `open` auto-reattaches when the bound relay tab is gone — drops the dead
page, opens a fresh tab in the session's group, and navigates it, instead of
only `tab new` recovering.
#36: scroll and click now reach content inside cross-origin OOPIFs:
- scroll dispatches a real wheel at a viewport point (default center, --at x,y,
or --frame n) so it scrolls the iframe under the pointer, which
window.scrollBy on the top document silently no-ops on.
- over the extension relay, clicks always use DOM-dispatch instead of
coordinate Input events — a coordinate event isn't confined to the target tab
on a busy real Chrome (it drifted onto the foreground tab) and an OOPIF
element's box can't be mapped to a top-viewport point.
#33: some sites prepend runs of ZWJ/word-joiner/invisible-times/BOM to
document.title (badging/anti-scrape); left in, they polluted 'tab list', broke
text matching, and wrecked column alignment. sanitize_title() now strips
zero-width/bidi-format chars at every title ingestion point + get_title().
#34: 'screenshot <selector>' (element capture) already worked but was
undocumented; added 'screenshot --clip x,y,w,h' for an explicit pixel region
(CDP captureScreenshot clip), documented both in --help. Verified live.
Passing a Cloudflare challenge mints an HttpOnly cf_clearance cookie bound to
IP+UA. 'chrome-use cf-status' (aliases cf/cloudflare-status/clearance) reports
whether the active page is currently a CF challenge and whether a still-valid
cf_clearance exists (read via CDP — HttpOnly is invisible to document.cookie),
plus CF_VERIFIED_DEVICE trust, and a recommendation: proceed (already cleared,
don't re-solve) / solve (challenge up, no clearance) / reissue (clearance present
but page still blocks → IP/UA drifted). Lets an agent avoid re-solving what it
already cleared — the persistence optimization. Pure helpers unit-tested; live
-verified on a real cf_clearance.
Some injected UI (browser-extension debug panels, web components) renders into a
CLOSED shadow root that eval/innerText cannot read. --pierce walks the CDP DOM
tree (DOM.getDocument depth:-1 pierce:true), which includes closed shadow roots
and child documents, and collects text nodes (skipping script/style/etc).
Review-safe: rides the per-tab debugger session already attached, no new Chrome
permission and no ab-connect/extension change — so it works in extension-relay
mode without touching the published extension. Verified live: a closed-shadow
panel that main-world eval reports HIDDEN is read in full via --pierce.
First slice of #30 (read extension/injected-panel content). Deeper extension
introspection (background SW / chrome.storage) stays a launch-mode / raw-CDP
concern, deliberately NOT done by expanding ab-connect's debugger powers.
- chrome-use sessions: top-level alias for the daemon inventory (the skill
advertises sessions, so it's a natural guess that used to error).
- Unknown commands now suggest the nearest valid one (Levenshtein + prefix
match), staying silent when nothing is close (e.g. 'clik' -> click,
'sesions' -> sessions, 'xyzzy' -> no suggestion).
- tab <id>: probe the switched session and show a warning indicator instead of
a green check when it isn't responding yet, so a switch onto a re-attaching
(churned-tabId) session no longer reports false success. The #24 targetId
recovery self-heals within ~6s, hence a warning rather than a hard error.
On listing/marketplace pages (Yahoo Auctions, Rakuten, Mercari shops) the
seller's description lives in a child frame or under a related-items sidebar,
so 'get text body' returned only header/nav boilerplate.
- get text --all-frames: aggregate visible text across every reachable frame.
Same-process child frames are read via Page.createIsolatedWorld; OOPIFs via
their auto-attached debugger session (iframe_sessions). Each non-top frame is
labelled with a '----- frame [kind] url -----' separator.
- get text --main: readability-lite — prefer the densest <main>/<article>
region over the whole body, dropping global header/nav/footer chrome.
- frames: enumerate frames (kind + url + per-frame text length) so an agent can
see where a page's text actually lives and pick the right read.
Verified live: inline srcdoc frame text aggregated through --all-frames; Yahoo
Auctions <main> (2881 chars) extracted via --main, stripping the Yahoo header.
#25 — fill() didn't fire the events framework inputs / site autocomplete need:
it set value directly (bypassing React's value-tracker) and typed via
Input.insertText, so controlled components and input/change/blur listeners (e.g.
Mercari's postal-code → 都道府県 lookup) never ran though the value showed. fill
now emulates a real edit: focus, set through the element's prototype value setter
(React _valueTracker registers), then dispatch input → input → change → blur/
focusout. SELECT and contenteditable handled too. type <sel> <text> remains for
per-keystroke sites. Verified live: an input wired with input/change/blur fired
'IICB' from one fill.
#26 (ergonomics):
- 'close <tab>' now closes just that tab and prints 'Tab [tN] closed'; bare
'close' still closes the browser. Previously 'close t12' ran a browser close
and alarmingly printed 'Browser closed'.
- new 'chrome-use current': prints the active tab's stable handle (tabId + CDP
targetId + url/title), refreshed live — so an agent holds the targetId (which
survives cross-process nav) instead of re-deriving 'which tab is live' from
'tabs' every step. The deeper tab-id churn is the #21/#23 stable-targetId story.
Tests cover fill events (live), close tab-vs-browser parse, and current.
A click on a target=_blank link / window.open opened a new tab, but the active
tab stayed put, so the post-click snapshot showed the OLD page — looking exactly
like the click failed. On the relay the new tab is discovered only via getTargets
(the relay doesn't push target events to the daemon), so it went unsurfaced.
handle_click now snapshots tracked targets before the click and, after, runs a
lightweight BrowserManager::adopt_newly_opened (one getTargets, attaches only the
new target — far cheaper than a full resync) to detect a freshly-opened tab. It's
reported as openedTab {tabId,url,title} in the response (and a '→ opened new tab
[tN] <url>' hint in text mode). Default keeps focus on the current tab (so
multi-tab flows aren't hijacked, per #7/#8.1); 'click <sel> --follow' switches to
the new tab. Verified live: clicking a _blank link prints
'→ opened new tab [t13] https://example.org/'.
Completes the #24 friction items (B/C/D shipped in 770708b).
Three CLI gaps surfaced driving a Mercari signup→checkout flow:
- #24-B (correctness): a bare label like 'click 購入手続きへ' was fed straight to
document.querySelector as CSS and failed as an invalid selector, even though
snapshot listed the button by that exact name. build_find_element_js now tries
CSS first, then falls back to matching an interactive element by visible text
(exact then contains) — nested and non-ASCII labels resolve. 'text=<label>'
forces the text path. CSS still wins when it matches.
- #24-D: 'get text' with no selector now returns the whole page (body).
- #24-C: 'tab <ref> --activate' (alias --front) switches to the tab AND raises it
to the foreground — to surface a specific tab for the human.
Tests cover the text fallback / text= / xpath builder, body default, activate
flag. The core stale-sessionId-after-cross-process-nav bug is the #20/#23 class,
already fixed in ext 0.4.8 — needs that extension deployed.
Dogfooding by driving a canvas game surfaced that per-action shell round-trips
(keydown; sleep; keyup) are the slowest, lowest-fidelity way to drive anything
timed — each is a process spawn + relay round-trip with ~250ms jitter, so a
'0.8s hold' is anything but.
- 'press <key> --hold <ms>': keyDown, wait, keyUp all inside the daemon, so the
hold duration is precise and it's one round-trip. For games (hold-to-move/
charge) and any press-and-hold.
- Documented the real driving pattern in the core skill + --help: script a timed
sequence in ONE round-trip with 'batch "press d --hold 900" "press j" "wait 200"'
(batch sends each step to the running daemon; --hold/wait block in-daemon), and
prefer reading engine state via main-world 'eval' over guessing from pixels.
Parser test covers plain/held/missing-duration. Builds on the keydown/keyup full
descriptor fix.
`keydown`/`keyup` dispatched a minimal Input.dispatchKeyEvent carrying only
{key}, so games/handlers that read event.code ("KeyD", "ArrowRight") or
event.keyCode saw nothing — a held key set no movement flag and the player
barely moved (dogfood: Dead Cell). They now build the same descriptor `press`
uses (key + code + windows/nativeVirtualKeyCode + printable text on down) via a
shared interaction::dispatch_single_key. Verified live: holding a direction now
drives continuous movement (player ran into an enemy and took damage), where
before it nudged ~80px.
Dogfooding a canvas game over the extension relay surfaced three issues:
1. (serious) A fresh relay session's first `open` navigated one of the USER's
existing tabs instead of opening its own — in testing it replaced a
half-filled form with the target site. On connect the daemon passively
attaches to the user's tabs and pinned one as active; navigate() then drove
it. Now: on the relay (agent_group set), if the active tab isn't one this
session created, navigate() opens its own tab in the session's group first.
Off the relay (a browser we launched) reusing the active tab stays correct.
Pure helper active_index_is_owned() + regression tests.
2. (discoverability) `keydown <key>` / `keyup <key>` (hold-to-move, essential
for games/shortcuts) already existed as commands+daemon handlers but were
absent from --help and the skill, so they were undiscoverable. Documented in
--help, the core skill, and the canvas-app hint.
3. (UX) Canvas/WebGL pages expose almost no a11y tree, so `snapshot` is empty
and agents get stuck hunting refs. snapshot now detects a viewport-dominating
canvas with a sparse tree and prints a hint pointing at the screenshot +
coordinate-click + keydown/keyup path.
Verified live over the relay: `open` now lands the game in its own new tab with
the user's tabs (incl. the Rakuten recovery form) untouched; the canvas hint
fires on the game page; `close` cleans up only the session's own tab.
Multi-session over one relayed Chrome had a tab-identity fracture: each daemon
discovered targets ONCE at connect and assigned its own t<N> indices, so a tab
filled in session A was unreachable from session B — B saw a disjoint/blank set
and rebinding via 'open' piled up duplicate tabs. A stranded, still-filled tab
could not be finished from any other session.
- 'tab list' now re-syncs the live target set on every call: adopts tabs other
sessions opened (or that re-attached after a cross-process nav), drops gone
ones (clears phantom rows), and refreshes url/title from each live tab via
Target.getTargetInfo (the relay only stamps target_info on attach, so it goes
stale/blank after navigation — which made rows indistinguishable).
- 'tab list --full' now prints each tab's stable CDP targetId. Unlike t<N>
(per-session, reassigned each connect), targetId is stable across every session
on the relayed Chrome.
- 'tab <targetId>' adopts a specific pre-existing tab — including another
session's — WITHOUT reloading, so a half-filled form survives. handle_tab_switch
resyncs first, then resolves a raw targetId before falling back to t<N>/label.
- 'open <url> --reuse-tab' (alias --reuse) switches to an existing tab already on
that URL (matched by origin+path, ignoring volatile query/fragment) instead of
spawning a duplicate.
Verified live over the extension relay: a fresh session's 'tab list --full' lists
the user's real tabs with correct titles + full URLs + targetIds, and
'tab <targetId>' lands on and reads the exact stranded Rakuten account-recovery
form from the report. Unit tests cover URL normalization + --reuse-tab parsing;
full suite green. Docs: --help Tabs section + core skill multi-session guidance.
Two SPA-SSO debugging gaps:
- The core skill referenced `bringToFront` but the CLI parser never mapped it
(the daemon handler existed) → 'Unknown command'. Wire up
bringToFront / bring-to-front / bringtofront → the existing action.
- 'stale sessionId — re-open your target URL' recovery was impossible because
`tab list` truncates long URLs with '…', cutting client_id/state out of SSO
links. Add `tab list --full` (also `tab --full`) to print untruncated URLs;
SKILL.md documents the recovery (full URL + re-open the stable entry URL).
The stale-session itself auto-recovers via the stable per-tab relay session id
(#17, extension 0.4.4). Parse tests for both new forms; verified live.
#16: handle_screenshot now returns a canonicalized ABSOLUTE path, so the
`✓ Screenshot saved to …` line is the same regardless of process cwd and the
agent can read the file without guessing the cwd.
#15: `session list` now reflects the extension-relay connection — when the relay
is up it shows the active session as `(relay/extension → live Chrome)` instead
of "No active sessions", and the --json output gains a `relay` bool. Stops agents
misjudging a live relay connection as down.
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Standalone product rename across the whole repo (issue: project identity):
- Binary/package/repo/skill/docs: agent-browser[-stealth] → chrome-use
(single binary name `chrome-use`; old aliases agent-browser/abs dropped).
- Version: 0.27.0-fork.51 → 1.0.0 (drop the upstream-fork counter).
- Native-messaging host: com.agent_browser.connect → com.leeguoo.chrome_use
(CLI + ab-connect extension in lockstep — this is a breaking handshake change,
extension bumped 0.4.2 → 0.5.0, needs a Web Store republish).
- Config dir: ~/.agent-browser → ~/.chrome-use.
- README/zh: reframed from "stealth fork of agent-browser" to a standalone
product with a small `originally based on vercel-labs/agent-browser` credit.
- Kept AGENT_BROWSER_* env vars working (63 vars across the codebase; renaming
them would break every existing script/skill for no user-facing gain).
Build green, 802 unit tests pass, fmt + clippy clean. Upstream attribution to
vercel-labs/agent-browser preserved.
Field report #8: in extension-relay sessions, reads (eval/screenshot/network)
could silently run against whatever tab drifted into focus, with no signal,
and network capture was intermittently empty.
- #8.1: screenshot and `network requests` now print `screenshot @ <url>` /
`network @ <url>` to stderr (mirrors the existing `eval @ <url>`), and the
responses carry `origin`. A read against the wrong/drifted tab — and the
"0 captured" vs "wrong page" ambiguity — is now obvious.
- #8.3: `network requests --clear` now enables Network capture immediately
instead of lazily on the next read, so requests fired between `--clear` and
the following read are tracked (fixes the "No requests captured" on first
try, works on retry" race). Extracted enable_request_tracking helper.
- #8.2: the daemon version-mismatch restart notice now spells out that
in-memory context (active tab, refs, captured requests) is reset and tells
the user to re-open the target URL if the next read looks blank/wrong.
Verified on a launched browser: coordinate clicks land, screenshot/network
stamps appear, and a fetch after --clear is captured on the first read.
Field-report ergonomics fixes so agents stop wasting a round on a wrong guess:
- Coordinate click is now first-class: `click <x> <y>`, `click <x>,<y>`,
and `click --coords <x>,<y>` dispatch a raw viewport-point click (no
element resolution), reusing the humanize trajectory + press dwell. Was
previously only reachable via eval(elementFromPoint(...).click()).
- Aliases: `tabs` (plural) → the `tab` subcommand tree; `get-text`/`get_text`
→ `get text <selector>`.
- `find <value> <action>` with a bare value (no locator keyword), e.g.
`find "I'm not a robot" click`, now errors with the corrected command
(`find text "I'm not a robot" click`) plus concrete examples, instead of
a bare "Valid options: role, text, ..." list.
Adds parse-layer regression tests for every form.
Local stealth verification with no external detector: reports mode (connect vs
launch), live fingerprint probes (navigator.webdriver / window.chrome / plugins /
UA-headless) as pass/fail, and an audit of the active overrides for the path
(incl. the iframe-proxy state from #4). `--json` for a stable shape agents can
gate a sensitive flow on. Distinct from `doctor` (install/env health).
The biggest manual-cost point in the dogfood reports: `select @ref` is a silent
no-op on non-native dropdowns, and click+wait+Enter on react-select/ARIA/portal
menus took ~20 turns of hand-written eval to get right.
New `pick <selector> --option "<text>"` does it atomically in one in-page async
routine: native <select> → set value + input/change; custom widget → focus +
open (pointer/mouse sequence), poll up to 2.5s for the option to render anywhere
(portals included), match by visible text, scroll it in, fire the full
pointer/mouse sequence. ERRORS loudly if the option never appears — no silent
success.
Verified headless: native <select> → "Gamma"; portal combobox → "欧洲"
(non-ASCII); missing option → explicit error. Documented in the skill.
- eval now prints `eval @ <url>` to stderr (stdout stays the raw value) so an
agent can catch tab drift — e.g. a logged-in fetch that hit the wrong origin —
before trusting the result. Mitigates the issue #2/#3 P0 safety concern. (eval
already returned the origin; the default output just never surfaced it.)
- `type --focused <text>`: type into the currently-focused element with no
selector, for custom widgets that move focus to a hidden input (issue #2 P3).
- AGENT_BROWSER_HUMANIZE set to an unrecognized value now warns once (like the
--humanize flag) instead of being silently ignored (Hermes #3).
Root cause behind Hermes #1 (CLICK_MODE=dom "does nothing") and #2 (--humanize
"does nothing"): both are env vars the daemon reads, but the daemon's env is
frozen at spawn — set them on a command to an already-running daemon and they
were silently ignored. (Confirmed: setting CLICK_MODE=dom at daemon spawn made
dom_click fire; setting it later did not.)
Fix: the client forwards AGENT_BROWSER_CLICK_MODE / AGENT_BROWSER_HUMANIZE in the
command envelope (_clickMode/_humanize); execute_command applies them per command
— mirrors CLICK_MODE into the process env (interaction::click reads it fresh) and
sets the humanize session level. Each command is authoritative.
Verified on an already-running daemon: CLICK_MODE=dom now fires dom_click
(hits 0→1); --humanize human typing applies.
Driving the user's real Chrome should not yank their view around. Now the agent
operates entirely in the background:
- New tabs are created with `background: true` (CreateTargetParams) so opening
one never foregrounds it (the ab-connect extension already used active:false;
this covers the raw-CDP path too).
- Dropped the two AUTO `Page.bringToFront` calls (auto-connect fresh tab, and the
internal active-page switch). The explicit `bringToFront` command is untouched —
surfacing a tab stays opt-in.
- enable_domains now sets `Emulation.setFocusEmulationEnabled(true)` so a
backgrounded agent tab still renders (screenshots work), isn't render-throttled,
and reports document.hasFocus()/visibilityState='visible' — which also removes
the "tab is hidden the whole session" bot tell.
Verified headless: hasFocus=true/visible while backgrounded; click + screenshot
still work. Default behaviour, no flag.
Completes the humanize suite:
- Clicks land on a jittered point inside the element's box (Fast/Human) instead
of its exact centre. `resolve_element_center` now also returns the element
width/height (box_model_dims); the CSS-selector path reports zero size → land
on centre (no jitter, no regression). Jitter is clamped to the inner box so the
click never misses.
- Wheel scrolls split into eased, jittered segments (humanize::scroll_segments,
unit-tested) instead of one instant jump.
- Drag follows the curved trajectory at Fast/Human (linear 10-step at Off).
Off is unchanged throughout. 9/9 unit tests; verified headless — jittered click
still lands (→ iana.org), segmented scroll moves the page.
After each navigation, probe the loaded page for known behavioural anti-bot
vendor fingerprints — cookies (_abck/Akamai, _px/PerimeterX, datadome,
reese84/Imperva, …), script URLs, and window globals — and escalate this
session to HumanizeLevel::Human when one is present, else fall back to the Off
baseline. So ordinary sites run at full speed (instant clicks) and only pages
actually guarded by behavioural detection pay for human-like motion.
`AGENT_BROWSER_HUMANIZE` still forces a fixed level and short-circuits the probe.
Best-effort: a failed probe leaves the level unchanged. Verified end-to-end
(headless --launch): a HUMANIZE=human click on example.com traverses the curved
trajectory and lands correctly (→ iana.org), identical outcome to Off.
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
- wait --url: the arg parser never read `--timeout`, so a non-matching pattern
waited the large default and wedged the daemon. Parse it. Also: matching was a
literal substring (`includes`) so globs never matched — convert `**`/`*`/`?`
globs to an anchored regex. And `poll_until_true` now bounds each probe with a
timeout and tolerates transient navigation errors, so a hung `Runtime.evaluate`
can never block past the deadline (un-wedges the daemon).
- find role <role> [--name]: the query was `[role="X"], X`, which matches a
literal <X> tag / explicit attribute but NOT implicit-role elements — so
`find role link` (<a href>) and `find role heading` (<h1>) never matched. Add a
proper ARIA-role → implicit-element map and broaden accessible-name matching
(aria-label/title/alt/value/text).
- click on a syntactically-invalid selector returned `✓ Done`: querySelector
throws, and Runtime.evaluate returned the thrown DOMException as an objectId
that was clicked as if it were the element. Check exception_details → error.
- output: a title-less page now prints `✓ <url>` instead of an empty title line.
- docs(skill): tab refs are `t2`, not `2` (SKILL.md, electron).
Verified live (isolated launch): wait --url glob matches instantly; non-matching
honors --timeout (2s) and leaves the daemon responsive; find role link/heading
match; invalid selector errors. Unit tests added for the glob + role map + parse.
The fork's CI had never been green. Pre-existing failures:
- version-sync: check-version-sync.js read packages/dashboard/package.json,
which doesn't exist in this fork (workspace is just "."). Drop the dashboard
comparison; check package.json vs cli/Cargo.toml only.
- Dashboard job: `pnpm install --filter dashboard` for a non-existent package.
Remove the job.
- Format check: repo was never `cargo fmt`-clean. Ran cargo fmt (mechanical).
- Clippy -D warnings (newly enforced on Rust 1.94 stable): manual_contains in
commands.rs (.iter().any()->.contains()), question_mark in element.rs
(if-let-Err -> ?), result_large_err on the tungstenite handshake callback in
connect.rs (allow — the Result type is fixed by the accept_hdr_async contract).
- rust-cross: lightpanda::waits_for_ready_without_logs spawns a real process +
binds a socket with timing assumptions; flaky in CI. Marked #[ignore].
Also: skill docs note fork.30's relay-preferred auto-connect (plain
`agent-browser open` is dialog-free once the ab-connect extension is loaded) and
the extension's new "agent-browser-stealth" display name.
After connect+grouping worked, follow-up eval/get-title/screenshot drifted to a
foreign tab: on a shared browser, Target.targetCreated events for tabs the user
or OTHER agent sessions open stream in and are drained on every command. The
drain path routed them through add_page(), which sets active_page_index to the
new page — so the session's active tab silently jumped to a foreign tab and its
commands landed there.
Add BrowserManager::add_background_page() (push without touching active, dedup by
target_id) and use it in the event-drain path. Explicit opens (tab new, the
add-and-switch paths) keep using add_page() and still focus the new tab.
Closes the last gap in concurrent multi-agent: each session now drives its OWN
tab regardless of other sessions'/the user's tab activity.
Borrow anti-detection hardening from Scrapling/patchright, preferring native
CDP/Chrome overrides over JS lies:
- Runtime.enable is now opt-in via AGENT_BROWSER_CAPTURE_CONSOLE (default off).
It was called on every session INCLUDING CdpAttach (the user's real Chrome),
leaking the patchright/rebrowser "runtime" CDP signal and undermining the
"real browser, no lies" guarantee. Runtime.evaluate/callFunctionOn and
runIfWaitingForDebugger work without it; only console/error capture needs it.
The console/errors commands now return a hint when capture is disabled.
- Timezone alignment via native Emulation.setTimezoneOverride, opt-in with
AGENT_BROWSER_TIMEZONE=<IANA>|auto (FullLaunch only). Intl and Date both
follow with no JS artifact.
- WebRTC IP-leak handling via the --force-webrtc-ip-handling-policy Chrome
flag: auto disable_non_proxied_udp when a proxy is set (so the real IP can't
leak past the proxy); AGENT_BROWSER_BLOCK_WEBRTC=1 hides the local IP when
there is no proxy; =0 opts out.
- Opt-in canvas/audio fingerprint noise via AGENT_BROWSER_HIDE_CANVAS=1
(FullLaunch only). Session-stable seed so reads stay consistent within a
session while differing from the headless-stable hash.
Adds 5 unit tests; full suite 751 passed, 0 failed.
Addresses the footgun raised in issue #1 follow-up: plain `--launch` silently
uses a temporary EMPTY profile (no cookies/login), and the connect-failure
error even recommended it — trapping agents into thinking they reused the
logged-in browser when they didn't.
- `--profile auto`: resolves to the Chrome profile last used (from Local State
`profile.last_used`), falling back to "Default", then the first profile. So
`--launch --profile auto open <url>` reuses real login state without naming
the profile. (--profile <name>/Default already worked.)
- connect-failure error now recommends `--launch --profile auto` and states
plainly that bare `--launch` is a temporary EMPTY profile — no cookies/login.
- bare `--launch` (no --profile, not CI) now prints a warning to that effect.
- README: fix Setup (relaunch with --remote-debugging-port, not chrome://inspect)
and split Standalone mode into throwaway vs. keep-your-login (`--profile auto`).
Tests: resolve_chrome_profile("auto") prefers last_used, falls back to Default.
handle_launch's fresh-launch path (the path `--launch open <url>` takes) never
called apply_stealth_to_browser — only the launch FLAGS were applied (e.g.
--disable-blink-features=AutomationControlled, which is why navigator.webdriver
was already false). As a result the 32 JS fingerprint patches and the
Emulation.setUserAgentOverride HeadlessChrome→Chrome UA strip NEVER ran on a
launched browser: navigator.userAgent kept the HeadlessChrome marker (a
longstanding bug — identical on the prior prebuilt binary).
Add the apply_stealth_to_browser call after launch (the auto_launch path
already had it; only the explicit-launch path was missing it).
Verified, FullLaunch headless:
- navigator.webdriver === false, navigator.userAgent => Chrome/<v> (no Headless)
- new tabs and the initial page both clean
- bot.sannysoft.com: 0 failed / 31 passed
- output: a success response with no data payload now prints "Done" instead of
nothing (a silent exit 0 looked like a no-op).
- commands: add top-level aliases for `get` status reads — `url`, `cdp-url`
(and `cdp_url`), `title`, `html`, `text`, `value`, `count`, `box`, `styles`,
`attr` — so `agent-browser url` no longer errors "Unknown command".
- connect errors now explain the Chrome 136+ realities:
- connect-failure mentions the "Allow remote debugging?" consent dialog and
that remote debugging is a startup flag, not a setting.
- no-Chrome error tells the user to relaunch Chrome with
--remote-debugging-port (auto-connect then works).
- --cdp discovery failure explains Chrome 136+ dropped the HTTP discovery
endpoints and to use the default auto-connect instead.
Stealth coverage (the fork's core value was leaking on secondary surfaces):
- stealth scripts are registered per CDP session, so new tabs (`tab new`) and
cross-origin iframe sessions created after the initial page had NO patches.
Extract apply_stealth_via_mgr/apply_stealth_to_session and re-apply on
tab_new and on iframe attach. Fixes automation markers (and FullLaunch UA)
leaking in new tabs / cross-origin frames.
Resource hygiene (temp profiles filled the disk):
- ChromeProcess::drop already cleans the temp user-data-dir on normal exit, but
a hard kill (kill -9 / version-mismatch restart / crash) skips Drop and leaks
~50MB per session. Add cleanup_orphaned_chrome_profiles() on daemon startup
that sweeps agent-browser-chrome-* temp dirs NOT referenced by any live
process (so an in-use profile is never deleted).
Cherry-picks upstream agent-browser #1396. Adds a configurable
--hide-scrollbars flag (AGENT_BROWSER_HIDE_SCROLLBARS env, hideScrollbars
config key, default true) that appends Chrome's --hide-scrollbars launch arg
for headless (non-extension) launches so native scrollbars aren't painted into
screenshots. Plumbed through flags.rs, connection.rs, main.rs, native/actions.rs
and native/cdp/chrome.rs; help text in output.rs + skill-data.
Fork adaptation:
- the arg lands in the headless && !has_extensions block, separate from the
stealth base args — no interaction with anti-detection.
- dropped upstream docs/, agent-browser.schema.json and README hunks (removed
or rewritten in this fork).
Verified: cargo check --tests passes.
Reported: a single `click @ref` could hang 5+ minutes, with multiple
queued click invocations adding up to 7+ minutes — worst case 30s
timeout × 3 CDP calls × N parallel processes:
- verify_ref_identity (Accessibility.getPartialAXTree) → default 30s
- resolveNode / getBoxModel → default 30s
- wait_for_paint_settled (Runtime.evaluate awaitPromise) → default 30s
The latter two are best-effort defenses added in fork.3-5 to fix SPA
race / DOM-reuse bugs. They should never block a real click for
30s — the unguarded code path was always faster than the guarded
path-that-hangs.
- verify_ref_identity capped at 1s (skips check on timeout)
- wait_for_paint_settled capped at 500ms (skips wait on timeout)
Both skip-on-timeout intentionally: the worst case is the click
behaves like fork.2 (race-prone but fast), which is strictly better
than the user pkilling stuck processes.
Also rewrites the misleading "Chrome 144+ chrome://inspect tip" in
the auto-connect failure message — the toggle exposes target
discovery only, not the /json/version HTTP API the auto-connect
flow expects (verified by user: lsof shows :9222 listening but
curl /json/version returns 404).
Two changes that pair with each other:
1. connect_auto_with_fresh_tab now does a Runtime.evaluate "1"
round-trip after creating the fresh tab. This catches the zombie
CDP socket case (process alive, websocket dead) where every step
up to that point reports success but the next user command would
silently no-op against a dead session. Failing here lets the
caller surface a proper "CDP session unresponsive" error instead
of returning Ok and letting `agent-browser open URL` exit 0 with
a still-blank tab.
2. handle_wait now recognizes @ref selectors (e.g. `wait @e8 --gone`).
It polls resolve_element_object_id, which already runs the
verify_ref_identity check from 007fd1b — so:
- `wait @e8` succeeds while the original element is
still mounted with its snapshot role+name
- `wait @e8 --gone` succeeds when the ref's identity changes
(modal closed, button re-textified, etc.)
This gives users the "assert modal still open" primitive that
prior versions could only approximate with screenshots.
Before: after `npm i -g` upgrade, the next agent-browser command would
detect daemon version mismatch, kill the old daemon, spawn a fresh one,
and connect to a brand-new about:blank tab. The user's previous
navigation state was silently lost — `get url` returned about:blank
even though the user's Chrome was still on the same page.
Now: before killing the old daemon, the CLI synchronously asks it for
its current URL via the existing socket. If non-empty and not
about:blank, it's persisted to a `.restore-url` sidecar in the socket
dir. After the new daemon spawns and auto-connects, it reads the
sidecar (read-and-delete), navigates the fresh tab to the saved URL,
and prints `⚠ Restored previous URL: <url>`.
Manual `agent-browser close` does NOT write the sidecar, so a clean
shutdown won't trigger surprise navigation. The sidecar is consumed on
read regardless of whether navigation succeeded, so a stale entry
can't haunt later auto-launches.
- CdpAttach mode: only removes navigator.webdriver (user's real Chrome
already has genuine fingerprint, heavy patches create detectable lies)
- FullLaunch mode: applies all 32 patches (new Chrome needs full coverage)
- Improved webdriver removal: uses Object.defineProperty to override CDP
getter on Navigator.prototype, not just delete
- CreepJS results: 0% stealth (was 20%), hasIframeProxy: gone
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Auto-connect is now ON by default (was opt-in via --auto-connect)
- Added --launch/--new flags to explicitly start a fresh browser
- CI environments (CI env var) automatically use --launch mode
- Friendly error message with platform-specific Chrome relaunch guide
- Mentions Chrome 144+ runtime CDP toggle (chrome://inspect)
- --cdp and --provider flags implicitly disable auto-connect
- AGENT_BROWSER_NO_AUTO_CONNECT=1 to disable, AGENT_BROWSER_FORCE_LAUNCH=1 to force
Track 3 of native-stealth migration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created cli/src/native/stealth.rs with stealth JS injection via CDP
- Extracted 32 patch IIFEs from TS stealth.ts into stealth_scripts.js
- Injected via Page.addScriptToEvaluateOnNewDocument on every launch/connect
- Added stealth Chrome args (disable AutomationControlled, use ANGLE GL)
- Auto-detects and cleans HeadlessChrome from User-Agent string
- Overrides navigator.userAgentData high-entropy hints
- Stealth enabled by default, disable with AGENT_BROWSER_STEALTH=0
Track 2 of native-stealth migration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(react): first-class React introspection, Web Vitals, and nextjs skill
Add React-general and web-universal features as first-class agent-browser verbs
(react tree/inspect/renders/suspense, vitals, pushstate). Genuinely Next.js-specific
workflows (PPR cookie protocol, /_next/mcp bridge, dev-server endpoints) ship as
a new `nextjs` skill that composes the primitives. No new runtime dependencies -
the React DevTools installHook.js is vendored (MIT) and include_str!'d into the
binary.
New commands:
react tree Full React component tree (depth id parent name)
react inspect <fiberId> Props, hooks, state, source for one fiber
react renders start|stop Fiber profiler with Insts/Mounts/Re-renders/Self/DOM
+ prev->next change details
react suspense Suspense boundaries + classifier (client-hook,
request-api, server-fetch, cache, stream, framework)
+ root-cause grouping + recommendations
vitals [url] LCP/CLS/TTFB/FCP/INP + React hydration phases
pushstate <url> Generic SPA client-side navigation
removeinitscript <id> Remove a script registered via addinitscript
New launch flags:
--init-script <path> Register init scripts before first navigation
(repeatable; env AGENT_BROWSER_INIT_SCRIPTS)
--enable <feature> Built-in init scripts; currently react-devtools
(repeatable; env AGENT_BROWSER_ENABLE)
Other primitives:
network route ... --resource-type <csv> Filter by CDP resource type
cookies set --curl <file> Auto-detects JSON/cURL/Cookie-header
* fixes
* fixes
* fixes
* fix(tabs): preserve refs across --tab peek and cover outer-tab-closed path
Follow-up to #1249 so `--tab <id>` is actually useful for agents:
- Save and restore the outer tab's `ref_map`, `iframe_sessions`, and
`active_frame_id` across a scoped command instead of clearing them.
`snapshot` → `--tab N <cmd>` → `click @e1` now keeps the outer tab's
refs intact. Scoped commands still see a clean slate so outer refs
can't resolve against the scoped tab's DOM.
- Close the coverage gap the Vercel review bot flagged on #1249: the
previous `e2e_tab_scoped_command_handles_outer_tab_closed` test used
`tab_close`, which is in the scoped-dispatch exclusion list, so it
never exercised the restore-skip branch it claimed to test. Renamed
to `e2e_tab_close_with_tab_id_closes_active_tab` with an honest
docstring, and added `e2e_tab_scoped_command_outer_tab_closed_mid_dispatch`
that actually hits the branch via `window.opener.close()` on a
script-opened intermediate tab.
- Add `e2e_tab_scoped_command_isolates_refs_from_outer_tab` pinning
that outer refs don't bleed into the scoped tab's DOM resolution.
- Rewrite `e2e_tab_scoped_command_clears_state_on_switch` as
`e2e_tab_scoped_command_preserves_outer_tab_state`, verifying the
restored @e1 still clicks end-to-end.
- Update the 52 `--help` entries for `--tab <id>` to describe peek /
restore semantics instead of a vague "Target specific tab ID".
- Update README, docs site, config schema, and the agent-facing
skills reference with working examples (refs survive the peek) and
a "when to use \`--tab <id>\` vs \`tab <id>\`" guide so agents pick
the right flag for their workflow.
* fix(tabs): use t<N> prefix for tab ids, add --label for named tabs
Follow-on to the tab work in #1249 and the prior commit, redesigning the
tab handle surface before release since nothing ships these features yet.
## Why
Incrementing integer tab ids (`1`, `2`, `3`) look indistinguishable from
positional indices in command output, LLM-generated scripts, and docs. In
the common single-agent case where position and id coincide, readers have
no visual cue for which mental model they're using. Positional indices
silently shift when unrelated tabs open/close, so misreading a handle as
an index is a correctness hazard.
## Changes
**Tab ids are now `t1`, `t2`, `t3` (strings).** Bare integer `tabId`
values are rejected with a teaching message rather than silently accepted.
The `t` prefix matches the `@e1` element-ref convention and makes ids
unmistakably non-positional at a glance.
**Labels.** Tabs can be created with a user-assigned label (e.g. `docs`,
`app`) via `tab new --label <name> [url]`. Labels are interchangeable
with `t<N>` ids everywhere a tab ref is accepted. They're never
auto-generated, never rewritten on navigation, and must be unique within
a session.
**Dashboard fix.** `packages/dashboard/src/types.ts` declared
`TabInfo.index: number` but the daemon has been sending `tabId` (not
`index`) since #892, making `tab.index` `undefined` and breaking the
dashboard's close/switch buttons silently. Updated the TS types and
usages to consume `tabId` (string) and optional `label`, restoring the
dashboard's tab interactions.
## Surface
- `cli/src/native/browser.rs`: `TabRef::parse` / `format_tab_id` /
`is_valid_label` / `PageInfo.label` / `BrowserManager::resolve_tab_ref`
/ `BrowserManager::has_label`. `tab_new` gains an optional label
argument with duplicate rejection. All JSON responses use the string
form and include the label.
- `cli/src/native/actions.rs`: scoped-command pre-dispatch and
`handle_tab_{switch,close,new}` parse string refs and resolve to
stable ids.
- `cli/src/{flags,commands,main,output}.rs`: `--tab` / config `tab`
are `String`; `tab` subcommand accepts `t<N>` or a label and supports
`tab new --label <name> [url]`. All 52 `--help` entries updated.
- `agent-browser.schema.json`: `tab` property type is now `string` with
a pattern matching `t<N>` or label form.
- `packages/dashboard`: `TabInfo.tabId: string` / `label?: string | null`;
`closeTabAtom`/`switchTabAtom` take `tabRef: string`; component props
updated.
- Docs: README, docs site (`commands/` and `configuration/`), and the
agent-facing skills reference rewritten with the new examples.
## Tests
- Added `TabRef::parse` / `format_tab_id` / `is_valid_label` unit tests
pinning the bare-integer rejection, the teaching error, label rules,
and round-tripping.
- Added `test_tab_switch_by_id` / `_by_label` / `test_tab_new_with_label`
/ `_with_label_and_url` / `_with_url_then_label` in `commands.rs`;
rewrote `test_tab_unknown_subcommand_errors` since labels make
`tab select` a legitimate ref.
- Added `e2e_tab_new_with_label_can_be_switched_and_peeked`,
`e2e_tab_new_with_duplicate_label_errors`,
`e2e_tab_scoped_command_rejects_bare_integer`.
- Migrated every existing tab e2e test (and one unit test) from
integer `tabId` to the string form.
`cargo fmt`, `cargo clippy -- -D warnings`, all 30 non-ignored tab unit
tests, all 13 tab e2e tests, and `tsc --noEmit` on the dashboard all
pass.
* refactor(tabs): drop --tab scoped peek flag; keep t<N> ids and labels
After fleshing out `--tab <id|label>` in the previous commits (scoped
pre/post-dispatch save/restore, ref preservation, outer-tab-closed edge
case, full e2e coverage), the machinery-to-value ratio makes the feature
hard to justify. Nixing it now while nothing has shipped.
## Why
- Every new daemon feature touching per-tab state has to reason about
scoped-dispatch interleaving. `ScopedRestore`, pre/post-dispatch hooks,
and the exclusion list add ongoing maintenance tax.
- Three separate PRs (#892, #1249, and this one pre-nix) were needed to
reach "works correctly." That's a smell.
- `tab <id|label>` switch + labels already cover the legible multi-tab
workflow case.
- `--tab` vs `tab <id>` have opposite lifecycle semantics but look
identical, teaching every agent two things where one would do.
- "Non-disruptive peek" isn't actually race-free: the daemon does swap
active tab during execution, so a concurrent client between pre- and
post-dispatch sees the scoped tab as active.
- Ref-based interaction with scoped tabs never worked ergonomically —
refs are per-tab, so `--tab N click @e1` requires `@e1` to already be
on tab N, which means a prior switch, which negates the peek.
- Adding a feature back is easy; removing shipped API is hard.
If per-tab caching (`HashMap<tab_id, RefMap>`) lands later, `--tab` can
be reintroduced essentially for free. That's the right time.
## Removed
- `--tab <id|label>` global flag (`cli/src/flags.rs`, `cli/src/main.rs`,
all 52 `--help` entries in `cli/src/output.rs`).
- `tab` property in `agent-browser.schema.json` and the config-options
row in `docs/src/app/configuration/page.mdx`.
- `ScopedRestore` struct, pre/post-dispatch save/restore in
`execute_command` (`cli/src/native/actions.rs`).
- `impl Default for RefMap` in `cli/src/native/element.rs` (only added
for `mem::take` in the scoped machinery).
- `e2e_tab_global_targeting`, `_snapshot`, `_snapshot_non_contiguous`,
`e2e_tab_scoped_command_preserves_outer_tab_state`,
`_isolates_refs_from_outer_tab`, `_restores_active_tab`,
`_outer_tab_closed_mid_dispatch`. 590 lines.
- The "When to use `--tab` vs `tab <id|label>`" sections in README,
docs site, and skills reference.
## Kept
- Stable tab ids (`t1`, `t2`, `t3`) with bare-integer rejection.
- User-assigned labels (`tab new --label docs [url]`), with duplicate
rejection and interchangeable use everywhere a tab ref is accepted.
- `BrowserManager::{active_tab_id, has_tab_id, resolve_tab_ref, has_label}`
accessors (still used by the remaining tab handlers).
- `TabRef::parse`, `format_tab_id`, `is_valid_label` and their unit
tests.
- Dashboard TS fix (`TabInfo.tabId` + `label`).
- `e2e_tab_close_with_tab_id_closes_active_tab` (renamed docstring to
drop the gone exclusion-list reference).
- `e2e_tab_new_with_label_can_be_switched_and_closed` (rewrite of the
previous `_and_peeked` test — now exercises only switch and close).
- `e2e_tab_switch_rejects_bare_integer` (rewrite targeting the
`tab_switch` daemon handler rather than the removed scoped path).
net: -900 lines across 12 files. `cargo fmt`, `cargo clippy -D warnings`,
all 25 non-ignored tab unit tests, all 6 tab e2e tests, and
`tsc --noEmit` on the dashboard all pass.
* fix(tabs): initialize tab_id on missing PageInfo sites
PR #892 added a required `tab_id: u32` field to `PageInfo` but missed two
initializer sites, which broke the build on the PR branch. CI never caught
this because the external-contributor workflow status was `action_required`
and never ran.
- `cli/src/native/browser.rs:395` — the `direct_page` branch of
`connect_cdp_inner` used by the cloud providers (Browserbase, Browserless,
Browser Use, Kernel, AgentCore). Use `assign_tab_id()` to get a fresh id.
- `cli/src/native/browser.rs:1580` — a unit test initializer. Use `tab_id: 1`
since the test doesn't exercise id assignment.
* feat(tabs): restore active tab and clear per-tab state for scoped --tab
Follow-up on PR #892's `--tab <id>` flag.
The original implementation called `tab_switch_by_id` directly from the
pre-dispatch block in `execute_command` but didn't touch the daemon's
per-tab state, and never restored the previously-active tab. Two concrete
issues this fixes:
1. `state.ref_map`, `state.iframe_sessions`, and `state.active_frame_id`
were left intact across the pre-dispatch switch, so `--tab N click @e1`
would try to resolve `@e1` against the scoped tab's DOM using a
backend-node id from the outer tab. In practice the click handler's
role+name fallback hid this as "element not found" errors, but on pages
where both tabs have similarly-labelled elements it could click the
wrong one.
2. The PR description promised scoped routing would "restore the previous
active tab", but the implementation permanently switched. `--tab 3
snapshot` would leave tab 3 as the active tab even after the command
returned, surprising subsequent non-scoped commands.
This change:
- Saves the current tab's stable `tab_id` (not its array index, which
would shift if the scoped command closed other tabs) before switching.
- Clears per-tab daemon state before the switch so refs/iframes/frame
context can't leak between tabs.
- After the action runs, restores the original active tab (also via
stable id) unless that tab was closed during the scoped command, in
which case we leave the scoped tab active.
- Adds `BrowserManager::active_tab_id()` and `has_tab_id()` accessors
to support the above without exposing the internal `pages` vector.
* test(tabs): regression tests for scoped --tab state clearing and restoration
Three new `#[ignore]` e2e tests pinning the fixed behavior:
- `e2e_tab_scoped_command_clears_state_on_switch` — populates `ref_map` on
tab 1, runs a `tabId: 2`-scoped command, asserts `ref_map`,
`iframe_sessions`, and `active_frame_id` are all cleared.
- `e2e_tab_scoped_command_restores_active_tab` — sets up two tabs, runs
a scoped command against the non-active one, asserts a subsequent
unscoped command reflects the originally-active tab.
- `e2e_tab_scoped_command_handles_outer_tab_closed` — runs a scoped
`tab_close` that kills the outer tab itself, asserts no error and the
scoped tab becomes active.
Also updates two misleading comments in the PR's existing
`e2e_tab_global_targeting*` tests to reflect restoration semantics; the
assertions themselves were already consistent with restoration.
* docs(tabs): document stable tab IDs and --tab scoped-command flag
Per AGENTS.md, changes that users or agents would need to know about must
land in every doc surface. Fills the gaps PR #892 left:
- `README.md` — new `--tab <id>` row in the Options table, rewrite the
tab command examples to use `<id>` instead of `<n>`, add a paragraph
explaining stable tab IDs and `--tab` peek semantics.
- `docs/src/app/commands/page.mdx` — same command-example rewrite plus a
new "Stable tab IDs and `--tab`" subsection.
- `docs/src/app/configuration/page.mdx` — add `tab` row to the config
options table so JSON config users can discover it.
- `agent-browser.schema.json` — add `tab` property with description,
matching the config schema.
- `skills/agent-browser/references/commands.md` — same command-example
rewrite plus a short paragraph for agents on when to use `--tab`.
Introduces stable per-tab IDs and a global `--tab <id>` flag for scoping individual commands to a specific tab.
Breaking change: response payloads for `tab_list`, `tab_new`, `tab_switch`, `tab_close`, and `window_new` now use `tabId` instead of `index`. `tab_close` returns `{tabId, closed: true}` instead of `{closed, activeIndex}`. `agent-browser tab <unknown>` now errors instead of silently listing tabs.
Follow-up PR to land immediately after this fixes a compile error on the provider direct-page path, clears per-tab daemon state around scoped switches, and implements active-tab restoration so `--tab N` is non-intrusive as intended.
Fix idleTimeout description to document human-friendly formats (30s,
5m, 1h) alongside raw milliseconds. Add trailing newline. Serve the
schema from the docs app at agent-browser.dev/schema.json via a
prebuild copy step, and update all $schema URLs to use the stable
docs-hosted URL instead of raw GitHub.
* fix: load storage state at launch when --state / AGENT_BROWSER_STATE is set
The `--state` flag and `AGENT_BROWSER_STATE` env var were documented as
restoring saved browser state (cookies + localStorage) at launch, but
`load_state()` was never called after the browser started. The feature
has been broken since it was introduced.
Adds `try_load_storage_state()` and calls it from every early-return
path in `auto_launch()` (lazy launch triggered by commands like
`navigate`) and from `handle_launch()` (explicit `launch` command).
Also adds 4 e2e tests covering all state-persistence paths:
- Explicit launch with `storageState` field
- Auto-launch via `AGENT_BROWSER_STATE` env var
- Session-name auto-restore via `try_auto_restore_state`
- Explicit `state_load` command (baseline sanity check)
Fixes#1164.
* style: apply cargo fmt to e2e_tests.rs
Reformats a single long format\! call to satisfy CI's rustfmt check.
No behavior change.
* fix: call try_load_storage_state in all handle_launch branches
The CDP URL, CDP port, auto-connect, and provider early-return branches
were skipping storage state loading because try_load_storage_state was
only called in the normal BrowserManager::launch() path at the bottom
of handle_launch().
Also compute storage_state_owned once and reuse it across all branches
rather than borrowing storage_state (a &str tied to cmd) in a helper
that needs an owned Option<String>.
* Fix storage state reload on reused launches
* Fix storage-state launch errors
* Fix storage state replay ordering
* Align storage-state errors across launch paths
* Fix storageState launch cleanup
* fix: inherit viewport dimensions in recording context
When `record start` creates a new browser context, it now re-applies the
current viewport settings (from `set viewport` or `set device`) so the
recording resolution matches what the user configured instead of falling
back to the default 1280×720.
Closes#1207
* style: apply cargo fmt to e2e test
* chore: remove obvious comments
* chore: remove obvious comments from e2e test
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>