mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-07-26 17:12:15 +00:00
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:
parent
a19c1b5132
commit
4a300c8cb0
3 changed files with 149 additions and 4 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue