* feat: add network request detail and filtering for request tracking
- Add `network request <requestId>` command to view full request/response
details including response body via CDP Network.getResponseBody
- Add --type, --method, --status filter flags to `network requests`
- --type: comma-separated resource types (xhr,fetch,document)
- --method: filter by HTTP method
- --status: supports exact (200), class (2xx), range (400-499)
- Extend TrackedRequest with request_id, post_data, status,
response_headers, mime_type fields
- Update Network.responseReceived handler to also populate
tracked_requests (previously only updated HAR entries)
- Add tests for parse commands and matches_status_filter
- Update README, SKILL.md, docs, and help text
Closes#932
* fix: show request ID and status in network requests output
* fix: add ref for cursor-interactive content roles
* fix: format
* feat: always include cursor-interactive elements in snapshot, -C is deprecated
* feat: process StaticText aggregation and deduplication
* update test
* clean up
* fix: escape text of elements in snapshot
* fix: redundant slicing
* fix: cargo fmt
* feat: deduplicate redundant StaticText
---------
Co-authored-by: 羲洋 <lipengyang.lpy@alibaba-inc.com>
* fix: prevent state commands from starting daemon without session_name
(#677)
State management commands (state_list, state_show, state_clear,
state_clean, state_rename) are pure file operations that don't need a
running daemon. Previously, these commands would trigger daemon
startup
via ensure_daemon(), and if AGENT_BROWSER_SESSION_NAME was exported
after the first command (e.g. `state clear --all`), the daemon would
start without session_name. Subsequent open/close commands would
reuse
that daemon, causing close to skip state persistence entirely.
Fix: execute state management commands locally in the CLI process
before
ensure_daemon() is called. This is done via a new
dispatch_state_command() function in state.rs that centralizes the
command routing, used by both the CLI (local path) and the daemon
(batch/IPC path).
Also:
- Add OutputOptions::from_flags() helper to deduplicate construction
- Add unit tests for dispatch_state_command routing and error
handling
* style: fix fmt and clippy warnings
- Remove redundant closure in dispatch_state_command (clippy::redundant_closure)
- Remove needless borrow in run_batch (clippy::needless_borrow)
- Fix trailing blank lines (rustfmt)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove dead code and unused variables in actions.rs
Remove `daemon_state_from_env` (superseded by `DaemonState::new`) and
`resolve_semantic_locator` (superseded by `handle_semantic_locator`),
both of which had zero call sites. Also remove unused `_session_id`
bindings in `handle_find` and `handle_multiselect`, and a no-op
`let _ = sid` in `handle_getbyrole`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: simplify `_sid` to `_` per review feedback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: use `is_ok()` instead of `if let Ok(_)` for idiomatic Rust
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Fixed an issue where custom viewport settings were ignored in WebSocket streaming, causing status messages to always report hardcoded dimensions (1280x720) instead of the actual viewport size.
## Changes Made
- **Added viewport tracking to StreamServer**: New `viewport_width` and `viewport_height` fields to store current dimensions
- **Updated viewport synchronization**: Modified `handle_viewport()` and `handle_device()` to update the stream server when viewport changes
- **Fixed status message broadcasting**: Replaced hardcoded dimensions with actual viewport values in `broadcast_status()` calls
- **Improved screencast defaults**: Changed screencast to use stored viewport dimensions as defaults instead of hardcoded 1280x720
- **Added viewport getter methods**: New `set_viewport()` and `viewport()` methods on StreamServer for dimension management
This ensures that WebSocket clients receive consistent viewport dimensions across all message types (status and frame messages), matching the actual browser viewport settings.
Fixes#950
Navigate with load, then wait for username/password/submit selectors using the default action timeout. This avoids networkidle hangs on pages with continuous background requests.
* fix: support xpath= selector prefix in element resolution
Resolves#907. When a selector starts with "xpath=", use
document.evaluate() instead of document.querySelector() so that
XPath expressions like "xpath=//button" work correctly.
* test: replace overlapping test with edge case tests
Replace test_build_selector_js_xpath_strips_prefix (which overlapped
with the xpath test) with two edge case tests: empty xpath and
selector starting with "xpath" without "=" delimiter.
* fix: support xpath= selector in resolve_element_object_id
Apply the same xpath= handling to resolve_element_object_id, which is
used by type, fill, focus, hover, check, select, screenshot, drag,
and all other selector-based commands beyond basic click.
* refactor: extract build_find_element_js to deduplicate xpath/css logic
The xpath= vs querySelector branching was duplicated in both
build_selector_js and resolve_element_object_id. Extract the shared
logic into build_find_element_js and reuse it in both places.
* fix: support xpath= selector in get_element_count
Use ORDERED_NODE_SNAPSHOT_TYPE with snapshotLength for XPath counting,
matching the querySelectorAll().length behavior for CSS selectors.
* refactor: rename find to find_expr for clarity
* refactor: extract build_count_elements_js and add regression tests
Extract element counting JS generation into build_count_elements_js
helper (matching the pattern of build_find_element_js) and add tests
for both CSS and XPath counting paths.
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
The v0.20.0 Rust rewrite dropped two keepalive mechanisms that the
Node.js/Playwright daemon provided, causing CDP connections to remote
Browserless instances to silently die between commands when traversing
multi-hop proxy topologies (Istio Envoy, OpenResty, etc.).
Restore parity with v0.19.0 and improve on it:
1. TCP SO_KEEPALIVE (v0.19.0 parity): Playwright's WebSocketTransport
used HTTP agents with keepAlive: true, which set SO_KEEPALIVE on the
underlying TCP socket. Restored via socket2::SockRef on the
tokio_tungstenite stream before splitting.
2. WebSocket Ping frames (improvement): Send Ping frames every 30s on
idle connections. This goes beyond v0.19.0 because L7 proxies (Envoy,
nginx, OpenResty) can see WebSocket pings but not TCP keepalive
probes, making this effective through application-layer proxy hops.
The keepalive task is coordinated with the reader loop via a watch
channel and stops automatically when the connection closes.
Fixes#934
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* fix: handle relative URLs in domain filter WebSocket script
Pass location.href as base URL to the URL constructor so relative URLs
(e.g. "/path" or "//host/path") resolve correctly instead of throwing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Affirm-Skill: commit-and-push
* fix: apply domain filter review followups
Use location.href as base in native WebSocket handler to match
EventSource/sendBeacon, remove redundant comment, add test coverage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* fix: restore origin-scoped --headers persistence across commands
In the v0.20 Rust rewrite, headers passed via --headers on open were
only applied to that single navigation via Network.setExtraHTTPHeaders,
which did not persist them for subsequent commands. In v0.19
(Playwright-based), these headers persisted for all subsequent
same-origin requests.
This restores the v0.19 behavior using CDP Fetch interception:
- A background task processes Fetch.requestPaused events in real-time,
injecting origin-scoped headers into matching requests and continuing
non-matching requests unmodified. This avoids the deadlock that occurs
when Fetch interception pauses requests during Page.navigate or
Runtime.evaluate (which block waiting for completion).
- The same background task also handles domain filtering and route
interception, replacing the previous drain-between-commands approach
that couldn't process events during navigation or script evaluation.
Fixes:
- --headers persist for same-origin navigations without re-passing flag
- --headers persist for in-page fetch/XHR to the same origin
- --headers do not leak to cross-origin navigations or sub-resources
- `set headers` (global) is unaffected and stacks with --headers
- Domain filter Fetch interception no longer deadlocks during navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: apply cargo fmt formatting
* revert inaccurate comment change
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* Fix Chrome headless launch failures by adding --disable-gpu flag
Fixes silent Chrome crashes in headless mode when GPU drivers are unavailable or restricted (common in VMs, containers, and cloud environments).
## Changes Made
- **Auto-add `--disable-gpu` flag**: Automatically includes `--disable-gpu` when launching Chrome in headless mode to prevent GPU initialization crashes
- **Improved error reporting**: Enhanced error messages to include Chrome's exit code when it crashes before writing DevToolsActivePort
- **Better user guidance**: Added helpful hints in error messages suggesting `--no-sandbox` and `--disable-gpu` flags for troubleshooting
- **Updated tests**: Added test coverage for the new `--disable-gpu` flag behavior
## Implementation Details
The `--disable-gpu` flag is only added in headless mode (when `options.headless && !has_extensions`), preserving GPU acceleration for non-headless usage. The error handling now captures Chrome's exit code and provides actionable debugging information when Chrome fails silently.
Fixes#914
* Use --enable-unsafe-swiftshader instead of --disable-gpu for Playwright parity
--disable-gpu disables all GPU acceleration and breaks WebGL on Chrome 130+.
Playwright uses --enable-unsafe-swiftshader to enable CPU-based software
rendering via SwiftShader, which prevents GPU-driver crashes while preserving
WebGL support. This matches the behavior from v0.19 (Playwright-based daemon).
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* feat: enhance HAR entries with timings, cookies, postData, and protocol normalisation
Extends #874's HarEntry struct and CDP handlers with richer capture:
- wall_time (f64) replaces pre-formatted started_date_time, preserving
sub-second precision from CDP wallTime for accurate RFC 3339 output
- request_headers / response_headers changed from Value to Vec<(String,String)>
for typed access without re-parsing JSON objects
- post_data captured from Network.requestWillBeSent for HAR postData
- cdp_timing and loading_finished_timestamp captured from responseReceived
and loadingFinished respectively, enabling har_compute_timings to produce
accurate blocked/dns/connect/ssl/send/wait/receive phases
- har_cdp_protocol_to_http_version normalises CDP protocol strings
(h2 -> HTTP/2.0, h3 -> HTTP/3.0, etc.)
- Request cookies parsed from Cookie header; response cookies from Set-Cookie
with ';'-first split to correctly strip Path/HttpOnly attributes before '='
- har_wall_time_to_rfc3339 replaces har_started_date_time, using the time
crate directly on the f64 epoch value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: apply cargo fmt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add iframe support for CLI interactions and snapshots
This PR adds comprehensive iframe support to the agent browser CLI, allowing users to interact with elements inside iframes seamlessly.
## Problem
Users couldn't interact with elements inside iframes via the command line. The existing `frame` command was non-functional as it set `active_frame_id` but no other code read this value.
## Changes Made
### Enhanced Frame Context Tracking
- Added `frame_id` field to `RefEntry` to track which frame each element reference belongs to
- Updated `RefMap::add` and related methods to accept and store frame context
- Modified element resolution functions to use frame context from ref entries
### Improved Frame Command
- Fixed the existing `frame` command to actually work by threading `active_frame_id` through snapshot operations
- Added support for iframe element references (e.g., `frame @e2`) in addition to CSS selectors
- Enhanced frame detection to work with both named frames and iframe elements
### Updated Snapshot Behavior
- Modified `take_snapshot` to accept optional frame context parameter
- Updated all snapshot call sites to pass appropriate frame context
- Maintained backward compatibility while enabling frame-scoped operations
### Element Resolution Updates
- Updated `resolve_element_center` and `resolve_element_object_id` to use frame context from ref entries
- Modified `find_node_id_by_role_name` to support frame-specific element lookup
- Ensured all interaction functions work correctly within iframe contexts
## Implementation Details
- Frame context is now properly propagated through the entire element interaction pipeline
- The `frame` command can accept both CSS selectors and element references
- All existing functionality remains intact while adding iframe capabilities
- Added `Iframe` to interactive roles for better element discovery
Fixes#863
* docs: add iframe support documentation
Document the new iframe capabilities across all documentation surfaces:
- Auto-inlining of iframe content in snapshots
- Direct interaction with iframe element refs
- frame command support for element refs (@e3)
- Scoped snapshots via frame switching
* fix: pass active frame context to diff snapshots and fix nameless iframe lookup
- handle_diff_snapshot now respects active_frame_id instead of always
passing None, so diff snapshots work correctly inside iframes
- Nameless/id-less iframes now fall back to src URL (or null) instead of
the literal string 'frame' which never matched any frame in the tree
* fix: resolve iframe frame ID via DOM.describeNode and reduce code duplication
- handle_frame: Use DOM.describeNode + contentDocument.frameId to resolve
iframe frame IDs directly, fixing failures for nameless iframes that
lack name/id/src attributes
- element.rs: Deduplicate add() by delegating to add_with_frame()
- snapshot.rs: Guard against out-of-bounds insert_str when iframe marker
is on the last line without a trailing newline
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* feat: fall back to ws://host:port/devtools/browser when HTTP discovery
fails
Chrome 136+ with UI-based remote debugging (chrome://inspect) exposes
CDP over WebSocket but does not serve /json/version or /json/list HTTP
endpoints. Add a third fallback in discover_cdp_url_with_timeout() that
connects directly to ws://host:port/devtools/browser and verifies the
endpoint with Browser.getVersion.
Fixes#870
* chore
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Expose HAR recording as a CLI subcommand under the existing `network`
command so users can capture and export network traffic without a
separate tool or opening the browser twice.
- Parse `network har start` and `network har stop [path]` in commands.rs
- Enrich HarEntry with request/response headers, timestamps, status text,
resource type, HTTP version, and body sizes from CDP events
- Produce HAR 1.2 output with creator/browser metadata, query strings,
and proper header arrays compatible with Chrome DevTools HAR viewer
- Auto-generate output path under ~/.agent-browser/tmp/har/ when omitted
- Add har_stop to skip_launch list so export works without a live browser
- Update help text, README, docs site, SKILL.md, and security policy docs
- Add unit tests for parsing, HAR entry serialization, and stop behavior
* fix: improve Chrome launch process by enhancing user-data-dir handling and adding timeout for DevToolsActivePort
* fix: enhance Chrome launch process by improving user data directory handling and timeout management for DevToolsActivePort
* fix: remove unused wait_for_ws_url function to streamline Chrome launch process
* fix: fall back to /json/list when /json/version is unavailable
Chrome's UI-based remote debugging mode (the permission dialog flow)
only exposes a WebSocket endpoint and does not serve /json/version.
Discovery now tries /json/version first, then falls back to /json/list
to find the browser target's WebSocket URL.
Fixes#628
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve original error message when /json/list fallback fails
When both /json/version and /json/list fail, return the original
/json/version error instead of a wrapped message. This preserves the
error format expected by callers like lightpanda's timeout test.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Generic elements (e.g. <div>) with cursor:pointer/onclick have empty ARIA
names because their text lives in StaticText children, which get filtered
in interactive mode. Fall back to the JS-collected textContent so the text
appears on the rendered tree line.
Fixes e2e_snapshot_cursor_many_elements CI failure from #855.
* fix: Windows auto-connect profiling (#835)
Fix three interrelated bugs causing `--auto-connect` to fail on Windows,
plus a UX issue where auto-connect hijacked existing tabs:
1. Stale DevToolsActivePort — add TCP port liveness check before returning
M144+ WebSocket URL; remove stale files when port is dead.
2. Missing Windows error codes — add os error 10061 (WSAECONNREFUSED) and
10054 (WSAECONNRESET) to is_transient_error() so daemon startup races
are retried on Windows.
3. --auto-connect not propagated to daemon — add auto_connect to
DaemonOptions, set AGENT_BROWSER_AUTO_CONNECT env var via
apply_daemon_env(), and guard the headed launch block so it doesn't
send a second launch that overrides the auto-connect.
4. Auto-connect opens a fresh tab — after connecting to an existing
Chrome, create a new about:blank tab and bring it to front so
navigations don't hijack the user's existing tabs.
Made-with: Cursor
* fix: address review feedback — cargo fmt, shared helper, Windows tests
- Run cargo fmt on is_port_reachable() formatting
- Extract duplicated auto-connect-with-fresh-tab logic into
connect_auto_with_fresh_tab() helper used by both handle_launch()
and auto_launch()
- Add unit tests for Windows WSAECONNREFUSED (os error 10061) and
WSAECONNRESET (os error 10054) in is_transient_error()
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* feat: embed cursor-interactive elements into snapshot tree
* optimize format
* fix: address review feedback for e2e_snapshot_cursor_interactive unitest
---------
Co-authored-by: 羲洋 <lipengyang.lpy@alibaba-inc.com>
* fix: support remote host in CDP discovery (#851)
`discover_cdp_url` now accepts a host parameter instead of hardcoding
127.0.0.1, allowing `connect "http://<remote-ip>:<port>"` to query the
correct remote `/json/version` endpoint. The returned webSocketDebuggerUrl
is rewritten to match the requested host and port, since Chrome always
reports 127.0.0.1 regardless of the interface it was reached through.
* style: apply cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: unify discover_cdp_url and discover_cdp_url_with_request_timeout
Merge the two discovery functions into discover_cdp_url(host, port) and
discover_cdp_url_with_timeout(host, port, timeout), eliminating duplicated
logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: merge discover_cdp_url into single function with optional timeout
Replace discover_cdp_url + discover_cdp_url_with_timeout with a single
discover_cdp_url(host, port, timeout) where timeout is Option<Duration>.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace Option<Duration> with separate discover_cdp_url_with_timeout
Split back into two functions for cleaner call sites:
- discover_cdp_url(host, port) for default timeout
- discover_cdp_url_with_timeout(host, port, timeout) for custom timeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: bracket IPv6 addresses in CDP discovery HTTP URL
Extract bracket_ipv6 helper and apply it in fetch_cdp_info to produce
valid URLs like http://[::1]:9222/json/version instead of malformed
http://::1:9222/json/version.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `wait --load networkidle` command was incorrectly returning immediately when pages were served from cache, causing subsequent commands to fail. This happened because the network idle logic would return instantly when no network requests were pending, without observing any idle period.
## Changes Made
- Extract network idle polling logic into a separate `poll_network_idle` function for better testability
- Fix the timeout handling to start a 500ms idle timer when no requests are pending, instead of returning immediately
- Add comprehensive unit tests covering the regression case and normal network request flows
- Ensure the function always observes at least 500ms of network inactivity before resolving, even for cached pages
## Key Fix
The critical change is in the timeout branch: when no CDP events arrive within 600ms, we now start the idle timer if no requests are pending, rather than returning `Ok(())` immediately. This prevents false-positive idle detection for pages that load entirely from cache.
Fixes#846
* fix: resolve snapshot -C and screenshot --annotate hang over WSS (#841)
Root cause: sequential CDP round-trips per element in
find_cursor_interactive_elements() and collect_annotations() caused
timeouts over high-latency WSS connections (~200ms × 200+ elements
exceeds the 30s CDP timeout).
Fix:
- snapshot -C: Replace per-element CDP calls with a single JS eval
that detects cursor:pointer/onclick/tabindex elements in-browser,
then batch-resolve via DOM.querySelectorAll + concurrent
DOM.describeNode calls using join_all
- screenshot --annotate: Replace sequential DOM.resolveNode +
getRect calls with concurrent join_all, matching v0.19.0's
Promise.all() pattern
Behavioral parity with v0.19.0 (Node.js/Playwright):
- cursor:pointer detection via getComputedStyle
- Inherited cursor:pointer dedup (skip children of pointer parents)
- interactiveTags and interactive ARIA roles exclusion
- Role differentiation: clickable vs focusable
- Text dedup against ARIA tree ref names and quoted strings
- Edge case: -i -C shows cursor elements even when ARIA tree is empty
Tests:
- 5 unit tests for build_dedup_set() helper
- 3 e2e regression tests: cursor-interactive detection, annotation
scaling to 50 elements, cursor scaling to 100 elements
* fix: add hidden/aria-hidden filtering, contentEditable support, and cleanup robustness
- Restore hidden/aria-hidden element filtering in cursor-interactive JS
(was present in old code, dropped during rewrite)
- Add contentEditable detection with 'editable' role and hint
- Replace fire-and-forget cleanup with warning on failure
- Simplify build_dedup_set to use ref_map only (eliminates fragile
tree-text quote parsing; ref_map already has all ref-bearing names)
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* fix: use correct Windows virtual-key codes for punctuation in type command
The `type` command was dropping punctuation characters like `.`, `'`, and
`#` because `char_to_key_info()` used raw ASCII codes as the
`windowsVirtualKeyCode` in CDP `Input.dispatchKeyEvent` calls. For
punctuation the ASCII value collides with unrelated VK codes — most
critically '.' (ASCII 46) equals VK_DELETE (0x2E), causing Chrome to
interpret periods as Delete key presses.
Changes:
- Add `punctuation_key_info()` with correct VK_OEM_* codes matching
Playwright's USKeyboardLayout (e.g. Period=190, Slash=191, Semicolon=186)
- Fall back to `Input.insertText` for characters without a US keyboard
mapping (emoji, CJK, etc.), matching Playwright's `keyboard.type()`
- Update e2e test to use `type` instead of `fill` workaround for email
- Add unit tests verifying VK code parity with Playwright's layout
Fixes#833
* style: fix rustfmt formatting for InsertTextParams
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
The v0.20.0 migration from Playwright to the native Rust daemon introduced
two regressions in checkbox/radio handling:
1. `is_element_checked` only read `this.checked`, which is undefined on
non-input elements. Material Design and ARIA controls use wrapper divs
with `role="checkbox"` and `aria-checked`, or hide the native input
off-screen inside a label. The function now mirrors Playwright's
`getChecked()` with follow-label retargeting: native `.checked`,
`aria-checked` for ARIA roles, `label.control` traversal, and nested
input lookup.
2. `check`/`uncheck` accepted the coordinate-based CDP click result
without verifying the state actually changed. When the AX tree's
`backendDOMNodeId` points to a hidden off-screen input (common in
Material Design), `Input.dispatchMouseEvent` hits nothing. The actions
now re-check state after clicking and fall back to a JS `.click()` on
the resolved input — matching Playwright's `_setChecked` verify step.
Adds e2e regression test covering Material Design (hidden input + ripple
overlay), ARIA-only, and native checkbox patterns.
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
When using --auto-connect, discover_and_attach_targets() was selecting
Chrome internal pages (chrome://, chrome-extension://, devtools://) as
the active target. Follow-up commands like `get url` and `snapshot`
would then return data from targets like chrome://omnibox-popup.top-chrome/
instead of the actual application tab.
Add is_internal_chrome_target() filter to exclude internal Chrome targets
from the discovery results. If no user-facing targets remain after
filtering, the existing "create a new tab" fallback handles it.
Fixes#813
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* fix: restore WebSocket streaming in native daemon
The v0.20.0 Rust rewrite broke WebSocket streaming — connections opened
but received zero messages before closing. Multiple issues contributed:
1. StreamServer was dropped immediately after creation in daemon.rs,
closing the broadcast channel and killing all WS connections.
2. Screencast frames were only processed during command polling
(drain_cdp_events) instead of in real-time, unlike the 0.19.0
TypeScript cdp.on('Page.screencastFrame') callback.
3. Auto-start/stop screencast on WS client connect/disconnect was
missing from the Rust implementation.
4. Screencast CDP commands used the wrong session ID (daemon session
name instead of the CDP page session from Target.attachToTarget).
5. Broadcast channel Lagged errors killed WS connections instead of
being handled gracefully.
The fix adds a background CDP event loop in StreamServer that subscribes
to Chrome events and broadcasts screencast frames in real-time, properly
tracks the CDP page session ID, restores auto-screencast lifecycle, and
keeps the StreamServer alive in DaemonState.
Fixes#820
* fix: use actual CDP session ID for input dispatch in stream WebSocket
Pass the real cdp_session_id (from Target.attachToTarget) through to
handle_ws_client instead of an empty string. Previously, input commands
(mouse, keyboard, touch) were sent with `"sessionId": ""` which Chrome
silently rejects. Now the correct page session ID is read at dispatch
time, and when no session ID is set yet (before browser launch),
the field is omitted entirely via `None` so Chrome uses browser-level
dispatch.
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* fix: snapshot --selector scopes to the matched element subtree
The native Rust daemon accepted the --selector flag but never used it —
the full accessibility tree was always returned regardless of the
selector. This restores the 0.19.0 behaviour where snapshot --selector
returns only the subtree rooted at the matched CSS selector.
The implementation resolves the selector via Runtime.evaluate, fetches
the full DOM subtree with DOM.describeNode(depth: -1) to collect all
descendant backendNodeIds, then filters the AX tree to render only the
nodes whose backendDOMNodeId falls within that set. This correctly
handles elements like <body> that don't map to a direct AX node.
Also fixes handle_snapshot reading "depth" instead of "maxDepth" from
the command JSON, which caused --depth to be silently ignored.
Fixes#822
* style: run cargo fmt on snapshot.rs
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* fix: add appium: vendor prefix to iOS capabilities for Appium v3
Appium v3 enforces the W3C WebDriver spec strictly, requiring
non-standard capabilities to use vendor prefixes. The iOS provider
was sending capabilities like `automationName`, `noReset`, `deviceName`,
`platformVersion`, and `udid` without the required `appium:` prefix,
causing session creation to fail with InvalidArgumentError.
This change prefixes all non-standard capabilities with `appium:` while
leaving standard W3C capabilities (`platformName`, `browserName`)
unprefixed. Backwards-compatible with Appium v2, which accepts both
formats.
Fixes#629
* fix: extract build_ios_capabilities for testable production code path
Addresses review feedback: removes unused `mut manager` warning and
validates the actual capability-building logic instead of reconstructing
JSON inline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace screenshot polling with screencast-based piped ffmpeg recording
Recording previously used Page.captureScreenshot polling at 10fps,
which was CPU-heavy and produced inconsistent results. Now uses
Page.startScreencast with throttled acks (35ms interval) to receive
frames event-driven from Chrome, and pipes JPEG data directly to
ffmpeg stdin in real-time instead of saving temp files.
- Spawn ffmpeg at recording start with piped stdin (image2pipe)
- Background task receives screencast frames, interpolates gaps by
repeating the last frame based on timestamps, targets 25fps
- Ack throttling controls Chrome's frame push rate
- Fix: current frame was never written after the first one
- Fix: frame count was read before task finished padding
- Remove tokio-util dependency (replaced CancellationToken with oneshot)
- Add tokio "process" feature for async child process stdin pipe
- Extract start/stop_recording_task helpers on DaemonState
- Add tests for restart, ffmpeg codec selection, and stop without task
* fmt
* chore
* fix: switch WebM codec from VP9 to VP8 for correct framerate and browser
compatibility
VP9 realtime encoder ignored input framerate, producing 10fps output
instead of 25fps. This caused inconsistent playback in browsers.
VP8 respects -framerate 25 and has wider browser playback support.
* fmt
* fix: add kill_on_drop to ffmpeg process to prevent zombie on task panic
* fix: switch from screencast to screenshot polling for reliable recording duration
Screencast only pushes frames on visual changes, producing short videos
on static pages. Screenshot polling captures at a fixed 10fps interval
regardless of page activity, guaranteeing duration matches wall-clock time.
ffmpeg piped stdin architecture is preserved — no temp files.
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Brave Browser is Chromium-based and uses the same DevToolsActivePort
mechanism. Add its user-data-dir paths to get_chrome_user_data_dirs()
and its executable paths to find_chrome() on all three platforms
(macOS, Linux, Windows).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: gracefully fall back to role/name lookup when backend_node_id is stale
When the DOM changes between snapshot and click (common with SPAs and
dynamic UIs), the stored backend_node_id becomes invalid. Previously,
DOM.getBoxModel and DOM.resolveNode failures propagated as hard errors,
bypassing the role/name fallback path entirely. Now these failures are
caught and the code falls through to a JS-based element lookup.
Also adds resolve_object_id_by_role_name so that resolve_element_object_id
has a fallback for ref-based lookups (previously it had none), and
improves the role matching JS to correctly map implicit ARIA roles
(e.g. <input type="submit"> → "button", <a href> → "link").
Closes#805
* test: add e2e regression test for stale ref click fallback (#805)
Verifies that clicking a ref whose backend_node_id has become stale
(because the DOM was replaced by JavaScript) falls back to role/name
lookup instead of failing with "Could not compute box model".
* fix: use accessibility tree for stale ref fallback instead of JS heuristic
Replace the hand-rolled JS role/name matching (getImplicitRole,
getAccessibleName) with a re-query of Accessibility.getFullAXTree —
the same data source that built the ref map during snapshot. This
guarantees role/name matching is identical to what was stored,
preventing silent wrong-element clicks from name computation
divergence (e.g. aria-labelledby, <label for>, alt text).
Matches v0.19.0 (Playwright) behavior where getByRole always
re-queried the live accessibility tree.
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
Replace all `eprintln!` calls in daemon-context code with
`let _ = writeln!(std::io::stderr(), ...)` so that broken pipe errors
on stderr are silently ignored instead of panicking.
The CLI client spawns the daemon with piped stderr to capture startup
errors, then drops the pipe handle once the daemon is ready. Any
subsequent `eprintln!` in the daemon panics because Rust's `eprintln!`
macro internally unwraps the write result. This caused the reported
"failed printing to stderr: Broken pipe (os error 32)" panic during
Chrome launch on Linux.
Closes#799
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
The CDP event broadcast channel (capacity 256) can overflow on slow CI
runners when Chrome emits many events during navigation. Previously,
RecvError::Lagged was treated the same as RecvError::Closed, causing
spurious "Event stream closed" errors even though Chrome was still
running. Now all 5 event-receiving loops correctly continue on Lagged
instead of breaking.
Chrome uses /dev/shm for shared memory, which is typically limited to
64MB on CI runners and containers. When Chrome exhausts this, it crashes
mid-session with "Event stream closed" errors. Auto-detect CI/container
environments and pass --disable-dev-shm-usage to use /tmp instead.
The CDP WebSocket client had three issues causing snapshot to hang
indefinitely when connected to remote browsers via WSS:
1. Binary WebSocket frames were silently dropped — remote CDP proxies
(Browserless, Browserbase, etc.) may send large responses like
Accessibility.getFullAXTree as Binary frames instead of Text frames.
2. Default tungstenite size limits (16 MiB frame / 64 MiB message)
could be exceeded by large accessibility tree responses, causing the
WebSocket connection to error out and the reader task to die.
3. When the reader task died, pending commands waited for the full
30-second timeout instead of failing immediately.
Fixes#788
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
Chrome occasionally crashes during startup on CI runners before
printing the DevTools URL, causing random e2e test failures across
different tests each run. Retry the launch with a 500ms delay to
handle these transient crashes.
- Restore the `refs` dictionary in `--json` snapshot output, matching the documented API contract
- The `refs` field was silently dropped during the Node.js to Rust rewrite (v0.20), causing consumers parsing `data.refs` for programmatic element interaction to receive no structured ref data
Fixes#785
* fix: use VP9 codec for webm recording output
The recording command hardcoded libx264 (H.264) which is incompatible
with the WebM container format. WebM only supports VP8/VP9/AV1 codecs,
causing ffmpeg to fail when users specify a .webm output file.
Select codec based on output file extension: libvpx-vp9 for .webm,
libx264 for other formats.
Fixes#778
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: use CRF mode for VP9 webm encoding
Switch from bitrate target (-b:v 2M) to constant quality mode (-crf 30),
which is the standard approach for screen recording (used by Puppeteer
and recommended by ffmpeg VP9 guide). CRF adapts bitrate to scene
complexity for more consistent quality.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add -b:v 0 for true constant quality VP9 encoding
Without -b:v 0, libvpx-vp9 uses its default bitrate target alongside
-crf, resulting in constrained quality mode instead of true constant
quality mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pad video dimensions to even numbers for h264 compatibility
libx264 requires width and height to be divisible by 2, but CDP
screencast can capture frames with odd dimensions (e.g. 1280x577).
Add pad filter to ensure even dimensions for all codecs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Regenerate pnpm-lock.yaml to match the cleaned-up package.json (only
@changesets/cli remains). Add CI environment detection to
should_disable_sandbox() so Chrome launches with --no-sandbox on GitHub
Actions runners where AppArmor blocks unprivileged user namespaces.