Compare commits

...
Author SHA1 Message Date
leeguooooo 64140879d5 chore(release): bump to 0.27.0-fork.5 — attach-mode UX + zombie-CDP probe + wait @ref 2026-05-09 04:11:09 +09:00
leeguooooo d3bfd76c96 fix(connect): liveness probe + wait @ref support
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.
2026-05-09 04:10:48 +09:00
leeguooooo 47dfe760be fix(cli): better message when only --headed is ignored in attach mode
In CDP-attach mode (the default since 0.24.0-fork.1), --headed has no
effect — the user's existing Chrome is already visible, and the
generic "use 'agent-browser close' first to restart" advice doesn't
help (the new daemon attaches right back). Explicitly say --headed is
moot and point to --launch as the actual escape hatch.

Other ignored flags (--profile, --proxy, etc.) keep the existing
"close + reopen" message because for those it IS the right advice.
2026-05-09 04:10:46 +09:00
5 changed files with 95 additions and 8 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser-stealth"
version = "0.27.0-fork.4"
version = "0.27.0-fork.5"
dependencies = [
"aes-gcm",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.27.0-fork.4"
version = "0.27.0-fork.5"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
+18 -5
View File
@@ -822,11 +822,24 @@ fn main() {
.collect();
if !ignored_flags.is_empty() && !flags.json {
eprintln!(
"{} {} ignored: daemon already running. Use 'agent-browser close' first to restart with new options.",
color::warning_indicator(),
ignored_flags.join(", ")
);
// Special case: --headed is irrelevant in CDP-attach mode
// (your existing Chrome is always already visible). The
// "agent-browser close + reopen" advice doesn't help because
// the new daemon will attach right back to the same Chrome.
// Don't suggest a useless workaround.
if ignored_flags == ["--headed"] {
eprintln!(
"{} --headed has no effect when attached to your running Chrome (it's already visible). \
Pass --launch to spawn a separate browser if you need to control headedness.",
color::warning_indicator(),
);
} else {
eprintln!(
"{} {} ignored: daemon already running. Use 'agent-browser close' first to restart with new options.",
color::warning_indicator(),
ignored_flags.join(", ")
);
}
}
}
+74
View File
@@ -1510,6 +1510,28 @@ async fn connect_auto_with_fresh_tab() -> Result<BrowserManager, String> {
.client
.send_command("Page.bringToFront", None, Some(&session_id))
.await;
// Liveness probe: confirm the CDP session can actually round-trip
// before returning success. Without this, a zombie CDP socket (process
// alive, websocket dead) would let `connect_auto` and `tab_new` succeed,
// we'd return Ok, the next user command would silently no-op, and
// `agent-browser open URL` would exit 0 with the browser still on
// about:blank. Failing here lets the caller surface the real error.
if let Err(e) = mgr
.client
.send_command("Runtime.evaluate", Some(serde_json::json!({
"expression": "1",
"returnByValue": true,
})), Some(&session_id))
.await
{
return Err(format!(
"CDP session is unresponsive after attaching ({}). \
The browser may have lost its DevTools connection. \
Try: agent-browser close, then re-run.",
e
));
}
Ok(mgr)
}
@@ -3105,6 +3127,15 @@ async fn handle_wait(cmd: &Value, state: &mut DaemonState) -> Result<Value, Stri
.get("state")
.and_then(|v| v.as_str())
.unwrap_or("visible");
// @-ref support: if the selector is `@e12` style, poll the ref map +
// accessibility tree instead of `document.querySelector`. This makes
// `wait @e8 --gone` a usable "assert modal still mounted" primitive
// for SPA flows where the only stable identity is the AX role+name
// captured at snapshot time.
if selector.starts_with('@') {
wait_for_ref(state, selector, state_str, timeout_ms).await?;
return Ok(json!({ "waited": "ref", "ref": selector, "state": state_str }));
}
wait_for_selector(&mgr.client, &session_id, selector, state_str, timeout_ms).await?;
return Ok(json!({ "waited": "selector", "selector": selector }));
}
@@ -3316,6 +3347,49 @@ async fn handle_reload(state: &mut DaemonState) -> Result<Value, String> {
// Wait helpers
// ---------------------------------------------------------------------------
/// Poll-based wait for a ref-identified element. Resolves the @-ref by
/// re-running the ref-identity verification each iteration. The supported
/// states mirror selector-based waits:
///
/// - "visible" / "attached" — succeed when the ref resolves to a node
/// whose AX role + name still match the snapshot entry
/// - "detached" / "hidden" — succeed when the ref no longer matches
/// (node removed OR re-textified to something else)
///
/// Times out with a "ref X did not become {state}" error.
async fn wait_for_ref(
state: &mut DaemonState,
ref_selector: &str,
desired_state: &str,
timeout_ms: u64,
) -> Result<(), String> {
let want_present = !matches!(desired_state, "detached" | "hidden");
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
loop {
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
let session_id = mgr.active_session_id()?.to_string();
let resolved = super::element::resolve_element_object_id(
&mgr.client,
&session_id,
&state.ref_map,
ref_selector,
&state.iframe_sessions,
)
.await;
let present = resolved.is_ok();
if present == want_present {
return Ok(());
}
if std::time::Instant::now() >= deadline {
return Err(format!(
"Timeout: ref {} did not become {} within {}ms",
ref_selector, desired_state, timeout_ms
));
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
}
async fn wait_for_selector(
client: &super::cdp::client::CdpClient,
session_id: &str,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-browser-stealth",
"version": "0.27.0-fork.4",
"version": "0.27.0-fork.5",
"description": "Browser automation CLI for AI agents — stealth fork with anti-detection",
"type": "module",
"files": [