feat: implement --compact flag to collapse single-child unnamed nodes

Activate the existing --compact CLI flag (previously a no-op) to reduce
tree verbosity by collapsing pass-through container nodes that carry no
semantic information (no ref, no name, no value, no description, no
states, exactly one child).

Slack with -i --compact: 264 → 214 nodes, ~5,967 → ~5,117 tokens (14%
reduction). All 161 refs preserved. Finder unaffected (3% reduction).

Handles Electron's empty-string-vs-None pattern using is_none_or.
This commit is contained in:
Lahfir 2026-03-01 00:36:20 -08:00
parent a19c1b5132
commit 4a300c8cb0
3 changed files with 149 additions and 4 deletions

View file

@ -17,11 +17,12 @@ pub struct SnapshotArgs {
pub fn execute(args: SnapshotArgs, adapter: &dyn PlatformAdapter) -> Result<Value, AppError> {
tracing::debug!(
"tree: snapshot app={:?} window_id={:?} max_depth={} interactive_only={}",
"tree: snapshot app={:?} window_id={:?} max_depth={} interactive_only={} compact={}",
args.app.as_deref().unwrap_or("(focused)"),
args.window_id.as_deref().unwrap_or("(auto)"),
args.max_depth,
args.interactive_only
args.interactive_only,
args.compact
);
let opts = crate::adapter::TreeOptions {

View file

@ -99,6 +99,7 @@ pub fn build(
&mut refmap,
opts.include_bounds,
opts.interactive_only,
opts.compact,
window.pid,
Some(window.app.as_str()),
);
@ -142,16 +143,26 @@ pub fn append_surface_refs(
};
let raw_tree = adapter.get_tree(&window, &opts).ok()?;
let mut refmap = RefMap::load().ok()?;
let tree = allocate_refs(raw_tree, &mut refmap, false, true, pid, source_app);
let tree = allocate_refs(raw_tree, &mut refmap, false, true, false, pid, source_app);
refmap.save().ok()?;
Some(tree)
}
fn is_collapsible(node: &AccessibilityNode) -> bool {
node.ref_id.is_none()
&& node.name.as_deref().is_none_or(str::is_empty)
&& node.value.as_deref().is_none_or(str::is_empty)
&& node.description.as_deref().is_none_or(str::is_empty)
&& node.states.is_empty()
&& node.children.len() == 1
}
fn allocate_refs(
mut node: AccessibilityNode,
refmap: &mut RefMap,
include_bounds: bool,
interactive_only: bool,
compact: bool,
window_pid: i32,
source_app: Option<&str>,
) -> AccessibilityNode {
@ -185,9 +196,13 @@ fn allocate_refs(
refmap,
include_bounds,
interactive_only,
compact,
window_pid,
source_app,
);
if compact && is_collapsible(&child) {
return child.children.into_iter().next();
}
if interactive_only && child.ref_id.is_none() && child.children.is_empty() {
None
} else {
@ -211,3 +226,129 @@ fn actions_for_role(role: &str) -> Vec<String> {
_ => vec!["Click".into()],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::AccessibilityNode;
fn node(role: &str) -> AccessibilityNode {
AccessibilityNode {
ref_id: None,
role: role.into(),
name: None,
value: None,
description: None,
hint: None,
states: vec![],
bounds: None,
children: vec![],
}
}
fn run_compact(tree: AccessibilityNode) -> AccessibilityNode {
let mut refmap = RefMap::new();
allocate_refs(tree, &mut refmap, false, false, true, 1, Some("Test"))
}
fn run_compact_interactive(tree: AccessibilityNode) -> AccessibilityNode {
let mut refmap = RefMap::new();
allocate_refs(tree, &mut refmap, false, true, true, 1, Some("Test"))
}
#[test]
fn test_compact_collapses_single_child_chain() {
let mut btn = node("button");
btn.name = Some("Send".into());
let mut g1 = node("group");
g1.children = vec![btn];
let mut g2 = node("group");
g2.children = vec![g1];
let mut root = node("window");
root.children = vec![g2];
let result = run_compact(root);
assert_eq!(result.role, "window");
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].role, "button");
assert_eq!(result.children[0].name.as_deref(), Some("Send"));
}
#[test]
fn test_compact_preserves_named_containers() {
let btn = node("button");
let mut named = node("group");
named.name = Some("Sidebar".into());
named.children = vec![btn];
let mut root = node("window");
root.children = vec![named];
let result = run_compact(root);
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].role, "group");
assert_eq!(result.children[0].name.as_deref(), Some("Sidebar"));
}
#[test]
fn test_compact_preserves_description() {
let btn = node("button");
let mut desc_node = node("group");
desc_node.description = Some("toolbar".into());
desc_node.children = vec![btn];
let mut root = node("window");
root.children = vec![desc_node];
let result = run_compact(root);
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].role, "group");
assert_eq!(result.children[0].description.as_deref(), Some("toolbar"));
}
#[test]
fn test_compact_preserves_states() {
let btn = node("button");
let mut disabled = node("group");
disabled.states = vec!["disabled".into()];
disabled.children = vec![btn];
let mut root = node("window");
root.children = vec![disabled];
let result = run_compact(root);
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].role, "group");
assert_eq!(result.children[0].states, vec!["disabled"]);
}
#[test]
fn test_compact_preserves_multi_child() {
let btn = node("button");
let tf = node("textfield");
let mut group = node("group");
group.children = vec![btn, tf];
let mut root = node("window");
root.children = vec![group];
let result = run_compact(root);
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].role, "group");
assert_eq!(result.children[0].children.len(), 2);
}
#[test]
fn test_compact_with_interactive_only() {
let mut btn = node("button");
btn.name = Some("OK".into());
let text = node("statictext");
let mut g1 = node("group");
g1.children = vec![btn];
let mut g2 = node("group");
g2.children = vec![text];
let mut root = node("window");
root.children = vec![g1, g2];
let result = run_compact_interactive(root);
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].role, "button");
assert!(result.children[0].ref_id.is_some());
}
}

View file

@ -43,7 +43,10 @@ pub struct SnapshotArgs {
pub include_bounds: bool,
#[arg(long, short = 'i', help = "Include interactive elements only")]
pub interactive_only: bool,
#[arg(long, help = "Omit empty structural nodes from output")]
#[arg(
long,
help = "Collapse single-child unnamed nodes to reduce tree depth"
)]
pub compact: bool,
#[arg(long, value_enum, default_value_t = Surface::Window, help = "Surface to snapshot")]
pub surface: Surface,