From 3ac69e822acf2689f823c96471e50266ed6ed452 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Thu, 11 Jun 2026 23:15:37 +0900 Subject: [PATCH] fix: keep interactive nodes in snapshot -c; better stale-ref guidance (issue #2/#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - snapshot -c (compact) now always keeps lines with an interactive ARIA role (button/link/textbox/combobox/option/…), not only `ref=`/`": "` lines — so a clickable control can't vanish from compact output and leave the agent clicking an empty ref (issue #2 P1). Additive: only ever keeps more. compact tests green. - stale-ref error now leads with "take a fresh snapshot" and points to the `eval` fallback for ref-churning SPAs, and demotes AGENT_BROWSER_VERIFY_REF=0 to a flagged last resort instead of presenting it as the fix (issue #3 P1). --- cli/src/native/element.rs | 8 ++++++-- cli/src/native/snapshot.rs | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/cli/src/native/element.rs b/cli/src/native/element.rs index 6d6035d..ae2a284 100644 --- a/cli/src/native/element.rs +++ b/cli/src/native/element.rs @@ -556,8 +556,12 @@ async fn verify_ref_identity( Err(format!( "Ref {} no longer matches its snapshot. Was [{} \"{}\"], now [{} \"{}\"].\n\ The DOM mutated between snapshot and interaction (typical with React/Vue \ - reusing nodes during re-render). Take a fresh snapshot, then re-target.\n\ - To bypass this guard set AGENT_BROWSER_VERIFY_REF=0.", + reusing nodes during re-render). Fix: take a fresh `snapshot` and re-target \ + with the new ref. For SPAs where refs churn every interaction, drive the \ + element directly with `eval` (e.g. `eval \"document.querySelector(...).click()\"`), \ + which doesn't depend on refs.\n\ + (Last resort: AGENT_BROWSER_VERIFY_REF=0 disables this safety check — only \ + if you accept clicks may land on a re-rendered/wrong node.)", ref_id, expected_role, expected_name, actual_role, actual_name, )) } diff --git a/cli/src/native/snapshot.rs b/cli/src/native/snapshot.rs index c899b05..c5ef6db 100644 --- a/cli/src/native/snapshot.rs +++ b/cli/src/native/snapshot.rs @@ -1305,6 +1305,39 @@ fn render_tree( } } +/// True if a snapshot line names an interactive ARIA role. Compaction keeps +/// these even without a `ref=`/`": "` marker, so a clickable control never gets +/// dropped from `-c` output (the dogfood reports saw a button present in the full +/// snapshot vanish from compact, leaving the agent clicking an empty ref). +fn is_interactive_line(line: &str) -> bool { + const ROLES: &[&str] = &[ + "button", + "link", + "textbox", + "checkbox", + "radio", + "combobox", + "listbox", + "menuitem", + "menuitemcheckbox", + "menuitemradio", + "option", + "switch", + "slider", + "spinbutton", + "searchbox", + "tab ", + "clickable", + "focusable", + "editable", + ]; + let t = line.trim_start(); + // Lines look like `- button "Label" [ref=e1]`; match the role token after the + // leading "- " marker. + let t = t.strip_prefix("- ").unwrap_or(t); + ROLES.iter().any(|r| t.starts_with(r)) +} + fn compact_tree(tree: &str, interactive: bool) -> String { let lines: Vec<&str> = tree.lines().collect(); if lines.is_empty() { @@ -1314,7 +1347,7 @@ fn compact_tree(tree: &str, interactive: bool) -> String { let mut keep = vec![false; lines.len()]; for (i, line) in lines.iter().enumerate() { - if line.contains("ref=") || line.contains(": ") { + if line.contains("ref=") || line.contains(": ") || is_interactive_line(line) { keep[i] = true; // Mark ancestors let my_indent = count_indent(line);