From af8823b27bcce6694487ea3e12a30cd079cb74f0 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Mon, 15 Jun 2026 15:46:23 +0900 Subject: [PATCH] feat(text): 'get text --pierce' reads through CLOSED shadow DOM (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cli/src/commands.rs | 19 +++++++++ cli/src/native/actions.rs | 9 ++++ cli/src/native/element.rs | 88 +++++++++++++++++++++++++++++++++++++++ cli/src/output.rs | 1 + skill-data/core/SKILL.md | 7 ++++ 5 files changed, 124 insertions(+) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index bfa68e7..fae0468 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -2455,6 +2455,15 @@ fn parse_get(rest: &[&str], id: &str) -> Result { if all_frames { return Ok(json!({ "id": id, "action": "gettext", "allFrames": true })); } + // `get text --pierce` reads through CLOSED shadow DOM / child docs + // via the CDP DOM tree — content eval/innerText can't reach, e.g. an + // extension's injected panel in a closed shadow root (issue #30). + let pierce = rest[1..] + .iter() + .any(|a| matches!(*a, "--pierce" | "--shadow" | "--deep")); + if pierce { + return Ok(json!({ "id": id, "action": "gettext", "pierce": true })); + } // `get text --main` returns the main-content region (readability), // skipping header/nav/footer/sidebar boilerplate (issue #27). let main = rest[1..] @@ -4897,6 +4906,16 @@ mod tests { } } + #[test] + fn test_get_text_pierce() { + for variant in ["get text --pierce", "get text --shadow", "text --deep"] { + let cmd = parse_command(&args(variant), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "gettext", "{variant}"); + assert_eq!(cmd["pierce"], true, "{variant}"); + assert!(cmd.get("selector").is_none(), "{variant}"); + } + } + #[test] fn test_tab_activate_flag() { let plain = parse_command(&args("tab t3"), &default_flags()).unwrap(); diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index eb436c6..02f770a 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -3628,6 +3628,15 @@ async fn handle_gettext(cmd: &Value, state: &mut DaemonState) -> Result Resu .to_string()) } +// Text nodes whose parent is one of these carry no visible content. +fn is_noise_tag(name: &str) -> bool { + matches!(name, "SCRIPT" | "STYLE" | "NOSCRIPT" | "TEMPLATE" | "HEAD") +} + +// Walk a CDP DOM.Node tree, collecting text-node values. Unlike `innerText` +// (JS, blocked by CLOSED shadow roots), the CDP DOM tree from +// `DOM.getDocument(pierce:true)` includes closed shadow roots and child +// documents — so this reaches text JS can't. `parent_noise` carries whether an +// ancestor was