use crate::analyzer::TreeNode;
use crate::parser::models::NodeType;
use crate::parser::ExecutionNode;
use std::collections::HashMap;
use std::rc::Rc;
fn dedup_agent_progress_by_agent_id(nodes: Vec<ExecutionNode>) -> Vec<ExecutionNode> {
use std::collections::HashMap;
let mut first_idx: HashMap<String, usize> = HashMap::new();
for (i, node) in nodes.iter().enumerate() {
if node.node_type == NodeType::Progress {
if let Some(data) = node.extra.as_ref().and_then(|e| e.get("data")) {
if data.get("type").and_then(|t| t.as_str()) == Some("agent_progress") {
if let Some(agent_id) = data.get("agentId").and_then(|a| a.as_str()) {
first_idx.entry(agent_id.to_string()).or_insert(i);
}
}
}
}
}
nodes
.into_iter()
.enumerate()
.filter(|(i, node)| {
if node.node_type == NodeType::Progress {
if let Some(data) = node.extra.as_ref().and_then(|e| e.get("data")) {
if data.get("type").and_then(|t| t.as_str()) == Some("agent_progress") {
if let Some(agent_id) = data.get("agentId").and_then(|a| a.as_str()) {
return first_idx.get(agent_id) == Some(i);
}
}
}
}
true
})
.map(|(_, node)| node)
.collect()
}
fn is_local_command_node(node: &ExecutionNode) -> bool {
if node.node_type != NodeType::User {
return false;
}
let text = match node.message.as_ref() {
Some(m) => m.text_content(),
None => return false,
};
crate::analyzer::prompt_detect::is_local_command_text(&text)
}
fn filter_local_commands(nodes: Vec<ExecutionNode>) -> Vec<ExecutionNode> {
nodes.into_iter().filter(|n| !is_local_command_node(n)).collect()
}
pub fn build_simple_tree(nodes: Vec<ExecutionNode>) -> Vec<TreeNode> {
let nodes = filter_local_commands(nodes);
let nodes = dedup_agent_progress_by_agent_id(nodes);
let rc_nodes: Vec<Rc<ExecutionNode>> = nodes.into_iter().map(Rc::new).collect();
let mut node_map: HashMap<String, Rc<ExecutionNode>> = HashMap::new();
let mut children_map: HashMap<String, Vec<Rc<ExecutionNode>>> = HashMap::new();
let mut root_nodes: Vec<Rc<ExecutionNode>> = Vec::new();
for rc_node in rc_nodes {
if let Some(ref uuid) = rc_node.uuid {
node_map.insert(uuid.clone(), rc_node);
}
}
for rc_node in node_map.values() {
match rc_node.parent_uuid.as_deref() {
Some(pid) if node_map.contains_key(pid) => {
children_map
.entry(pid.to_string())
.or_default()
.push(Rc::clone(rc_node));
}
_ => {
root_nodes.push(Rc::clone(rc_node));
}
}
}
for children in children_map.values_mut() {
children.sort_by(|a, b| {
let ts_a = a.timestamp.unwrap_or(0);
let ts_b = b.timestamp.unwrap_or(0);
ts_a.cmp(&ts_b)
});
}
root_nodes.retain(|n| {
if n.node_type == NodeType::Progress {
if let Some(ref uuid) = n.uuid {
return children_map.contains_key(uuid);
}
return false;
}
true
});
root_nodes.sort_by(|a, b| {
let ts_a = a.timestamp.unwrap_or(0);
let ts_b = b.timestamp.unwrap_or(0);
ts_a.cmp(&ts_b)
});
root_nodes
.into_iter()
.map(|rc_node| build_tree_node(&rc_node, &children_map, 0))
.collect()
}
fn build_tree_node(
rc_node: &Rc<ExecutionNode>,
children_map: &HashMap<String, Vec<Rc<ExecutionNode>>>,
depth: usize,
) -> TreeNode {
let children = if let Some(ref uuid) = rc_node.uuid {
if let Some(child_nodes) = children_map.get(uuid) {
child_nodes
.iter()
.map(|child_rc| build_tree_node(child_rc, children_map, depth + 1))
.collect()
} else {
Vec::new()
}
} else {
Vec::new()
};
TreeNode {
node: Rc::clone(rc_node),
children,
depth,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ExecutionNode;
fn create_test_node(uuid: &str, parent_uuid: Option<&str>, node_type: NodeType) -> ExecutionNode {
ExecutionNode {
uuid: Some(uuid.to_string()),
parent_uuid: parent_uuid.map(|s| s.to_string()),
timestamp: Some(1000),
node_type,
is_sidechain: None,
session_id: None,
cwd: None,
message: None,
tool_use: None,
tool_result: None,
tool_use_result: None,
thinking: None,
progress: None,
token_usage: None,
extra: None,
}
}
#[test]
fn test_build_simple_tree_single_node() {
let nodes = vec![create_test_node("root", None, NodeType::User)];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 1);
assert_eq!(tree[0].depth, 0);
assert_eq!(tree[0].children.len(), 0);
assert_eq!(tree[0].node.node_type, NodeType::User);
}
#[test]
fn test_build_simple_tree_with_children() {
let nodes = vec![
create_test_node("root", None, NodeType::User),
create_test_node("child1", Some("root"), NodeType::Assistant),
create_test_node("child2", Some("root"), NodeType::Unknown),
];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 1);
assert_eq!(tree[0].depth, 0);
assert_eq!(tree[0].children.len(), 2);
assert_eq!(tree[0].children[0].depth, 1);
assert_eq!(tree[0].children[1].depth, 1);
}
#[test]
fn test_build_simple_tree_deep_hierarchy() {
let nodes = vec![
create_test_node("root", None, NodeType::User),
create_test_node("level1", Some("root"), NodeType::Assistant),
create_test_node("level2", Some("level1"), NodeType::Unknown),
create_test_node("level3", Some("level2"), NodeType::Unknown),
];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 1);
assert_eq!(tree[0].depth, 0);
assert_eq!(tree[0].children[0].depth, 1);
assert_eq!(tree[0].children[0].children[0].depth, 2);
assert_eq!(tree[0].children[0].children[0].children[0].depth, 3);
}
#[test]
fn test_build_simple_tree_multiple_roots() {
let nodes = vec![
create_test_node("root1", None, NodeType::User),
create_test_node("root2", None, NodeType::Assistant),
];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 2);
assert_eq!(tree[0].depth, 0);
assert_eq!(tree[1].depth, 0);
}
#[test]
fn test_rc_sharing() {
let nodes = vec![
create_test_node("root", None, NodeType::User),
create_test_node("child", Some("root"), NodeType::Assistant),
];
let tree = build_simple_tree(nodes);
assert_eq!(Rc::strong_count(&tree[0].node), 1);
assert_eq!(Rc::strong_count(&tree[0].children[0].node), 1);
}
#[test]
fn test_orphan_nodes_promoted_to_roots() {
let nodes = vec![
create_test_node("root", None, NodeType::User),
create_test_node("orphan", Some("deleted"), NodeType::Assistant),
];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 2, "orphan must be promoted to root");
let types: Vec<NodeType> = tree.iter().map(|t| t.node.node_type).collect();
assert!(types.contains(&NodeType::User));
assert!(types.contains(&NodeType::Assistant));
}
#[test]
fn test_orphan_with_own_children_preserved() {
let nodes = vec![
create_test_node("orphan", Some("deleted"), NodeType::Assistant),
create_test_node("grandchild", Some("orphan"), NodeType::User),
];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 1, "orphan is the only root");
assert_eq!(tree[0].node.node_type, NodeType::Assistant);
assert_eq!(tree[0].children.len(), 1);
assert_eq!(tree[0].children[0].node.node_type, NodeType::User);
}
#[test]
fn test_childless_progress_roots_are_filtered() {
let nodes = vec![
create_test_node("user-root", None, NodeType::User),
create_test_node("prog-noise", None, NodeType::Progress),
];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 1);
assert_eq!(tree[0].node.node_type, NodeType::User);
}
#[test]
fn test_progress_root_with_children_is_kept() {
let nodes = vec![
create_test_node("prog-root", None, NodeType::Progress),
create_test_node("child", Some("prog-root"), NodeType::User),
];
let tree = build_simple_tree(nodes);
assert_eq!(tree.len(), 1, "progress root with children must be kept");
assert_eq!(tree[0].node.node_type, NodeType::Progress);
assert_eq!(tree[0].children.len(), 1);
}
fn create_agent_progress_node(uuid: &str, agent_id: &str, prompt: &str) -> ExecutionNode {
let mut extra = std::collections::HashMap::new();
extra.insert(
"data".to_string(),
serde_json::json!({
"type": "agent_progress",
"agentId": agent_id,
"prompt": prompt,
}),
);
ExecutionNode {
uuid: Some(uuid.to_string()),
parent_uuid: None,
timestamp: Some(1000),
node_type: NodeType::Progress,
is_sidechain: None,
session_id: None,
cwd: None,
message: None,
tool_use: None,
tool_result: None,
tool_use_result: None,
thinking: None,
progress: None,
token_usage: None,
extra: Some(extra),
}
}
#[test]
fn test_agent_progress_dedup_keeps_first_per_agent_id() {
let nodes = vec![
create_agent_progress_node("a1", "agent-X", "Explore code"),
create_test_node("user1", None, NodeType::User),
create_agent_progress_node("a2", "agent-X", ""),
create_agent_progress_node("a3", "agent-X", ""),
create_agent_progress_node("b1", "agent-Y", "Run tests"),
];
let deduped = dedup_agent_progress_by_agent_id(nodes);
let agent_progress: Vec<&ExecutionNode> = deduped
.iter()
.filter(|n| n.node_type == NodeType::Progress)
.collect();
assert_eq!(agent_progress.len(), 2, "one per agentId");
assert_eq!(agent_progress[0].uuid.as_deref(), Some("a1"), "first agent-X kept");
assert_eq!(agent_progress[1].uuid.as_deref(), Some("b1"), "first agent-Y kept");
}
#[test]
fn test_agent_progress_dedup_preserves_non_agent_progress() {
let mut hook_extra = std::collections::HashMap::new();
hook_extra.insert(
"data".to_string(),
serde_json::json!({ "type": "hook_progress", "hookName": "SessionStart" }),
);
let hook_node = ExecutionNode {
uuid: Some("hook1".to_string()),
parent_uuid: None,
timestamp: Some(1000),
node_type: NodeType::Progress,
is_sidechain: None,
session_id: None,
cwd: None,
message: None,
tool_use: None,
tool_result: None,
tool_use_result: None,
thinking: None,
progress: None,
token_usage: None,
extra: Some(hook_extra),
};
let nodes = vec![
hook_node,
create_agent_progress_node("a1", "agent-X", "task"),
create_agent_progress_node("a2", "agent-X", ""),
];
let deduped = dedup_agent_progress_by_agent_id(nodes);
assert_eq!(deduped.len(), 2, "hook_progress preserved, agent-X collapsed to 1");
assert_eq!(deduped[0].uuid.as_deref(), Some("hook1"));
assert_eq!(deduped[1].uuid.as_deref(), Some("a1"));
}
}