use crate::avatars::resolve_avatar;
use clap::Parser;
use colored::*;
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Parser, Clone)]
pub struct TreeArgs {
#[clap(skip)]
pub conversation_override: Option<String>,
}
pub fn run_tree(args: TreeArgs) {
let fur_dir = Path::new(".fur");
let index_path = fur_dir.join("index.json");
if !index_path.exists() {
eprintln!(
"{}",
"🚨 .fur/ not found. Run `fur new` first.".red().bold()
);
return;
}
let index_data: Value = serde_json::from_str(
&crate::security::io::read_text_file(&index_path)
.expect("❌ Project locked. Run `fur unlock`."),
)
.unwrap();
let conversation_id = if let Some(ref override_id) = args.conversation_override {
override_id
} else {
index_data["active_thread"].as_str().unwrap_or("")
};
let convo_path = fur_dir
.join("threads")
.join(format!("{}.json", conversation_id));
let conversation_data: Value = serde_json::from_str(
&crate::security::io::read_text_file(&convo_path)
.expect("❌ Project locked. Run `fur unlock`."),
)
.unwrap();
let avatars: Value = serde_json::from_str(
&fs::read_to_string(fur_dir.join("avatars.json")).unwrap_or_else(|_| "{}".to_string()),
)
.unwrap_or(serde_json::json!({}));
println!(
"{} {}",
"🌳 Conversation Tree:".bold().cyan(),
conversation_data["title"]
.as_str()
.unwrap_or("Untitled")
.green()
.bold()
);
if let Some(messages) = conversation_data["messages"].as_array() {
let id_to_message = load_conversation_messages(&fur_dir, &conversation_data);
for (idx, msg_id) in messages.iter().enumerate() {
if let Some(mid) = msg_id.as_str() {
render_message(&id_to_message, mid, "", idx == messages.len() - 1, &avatars);
}
}
}
}
fn load_conversation_messages(fur_dir: &Path, conversation: &Value) -> HashMap<String, Value> {
let mut id_to_message = HashMap::new();
let mut to_visit: Vec<String> = conversation["messages"]
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|id| id.as_str().map(|s| s.to_string()))
.collect();
while let Some(mid) = to_visit.pop() {
let path = fur_dir.join("messages").join(format!("{}.json", mid));
if let Some(content) = crate::security::io::read_text_file(&path) {
if let Ok(json) = serde_json::from_str::<Value>(&content) {
if let Some(children) = json["children"].as_array() {
for c in children {
if let Some(cid) = c.as_str() {
to_visit.push(cid.to_string());
}
}
}
if let Some(branches) = json["branches"].as_array() {
for block in branches {
if let Some(arr) = block.as_array() {
for c in arr {
if let Some(cid) = c.as_str() {
to_visit.push(cid.to_string());
}
}
}
}
}
id_to_message.insert(mid.clone(), json);
}
}
}
id_to_message
}
fn render_message(
id_to_message: &HashMap<String, Value>,
msg_id: &str,
prefix: &str,
is_last: bool,
avatars: &Value,
) {
if let Some(msg) = id_to_message.get(msg_id) {
let branch_symbol = if is_last { "└──" } else { "├──" };
let tree_prefix = format!("{}{}", prefix, branch_symbol.bright_green());
let avatar_key = msg["avatar"].as_str().unwrap_or("???");
let (name, emoji) = resolve_avatar(avatars, avatar_key);
let text = crate::renderer::utils::preview_of(msg, 60);
let id_display = msg_id[..8].to_string();
if msg.get("markdown").is_some() {
println!(
"{} {} {} {} {} {}",
tree_prefix,
"[Root]".cyan(),
emoji.yellow(),
format!("[{}]", name).bright_yellow(),
text.white(),
format!("📄 {}", id_display).magenta()
);
} else {
println!(
"{} {} {} {} {}",
tree_prefix,
"[Root]".cyan(),
emoji.yellow(),
format!("[{}]", name).bright_yellow(),
format!("{} {}", text.white(), id_display.bright_black())
);
}
let empty: Vec<Value> = Vec::new();
let children = msg["children"].as_array().unwrap_or(&empty);
let branches = msg["branches"].as_array().unwrap_or(&empty);
if !branches.is_empty() {
for (_b_idx, branch) in branches.iter().enumerate() {
if let Some(arr) = branch.as_array() {
for (i, child_id) in arr.iter().enumerate() {
if let Some(cid) = child_id.as_str() {
let new_prefix = format!(
"{}{} ",
prefix,
if is_last { " " } else { "│ " }.bright_green()
);
render_message(
id_to_message,
cid,
&new_prefix,
i == arr.len() - 1,
avatars,
);
}
}
}
}
} else {
for (i, child_id) in children.iter().enumerate() {
if let Some(cid) = child_id.as_str() {
let new_prefix = format!(
"{}{} ",
prefix,
if is_last { " " } else { "│ " }.bright_green()
);
render_message(
id_to_message,
cid,
&new_prefix,
i == children.len() - 1,
avatars,
);
}
}
}
}
}