1use crate::avatars::resolve_avatar;
2use clap::Parser;
3use colored::*;
4use serde_json::Value;
5use std::collections::HashMap;
6use std::fs;
7use std::path::Path;
8
9#[derive(Parser, Clone)]
10pub struct TreeArgs {
11 #[clap(skip)]
12 pub conversation_override: Option<String>,
13}
14
15pub fn run_tree(args: TreeArgs) {
16 let fur_dir = Path::new(".fur");
17 let index_path = fur_dir.join("index.json");
18
19 if !index_path.exists() {
20 eprintln!(
21 "{}",
22 "🚨 .fur/ not found. Run `fur new` first.".red().bold()
23 );
24 return;
25 }
26
27 let index_data: Value = serde_json::from_str(
29 &crate::security::io::read_text_file(&index_path)
30 .expect("❌ Project locked. Run `fur unlock`."),
31 )
32 .unwrap();
33
34 let conversation_id = if let Some(ref override_id) = args.conversation_override {
35 override_id
36 } else {
37 index_data["active_thread"].as_str().unwrap_or("")
38 };
39 let convo_path = fur_dir
40 .join("threads")
41 .join(format!("{}.json", conversation_id));
42 let conversation_data: Value = serde_json::from_str(
43 &crate::security::io::read_text_file(&convo_path)
44 .expect("❌ Project locked. Run `fur unlock`."),
45 )
46 .unwrap();
47
48 let avatars: Value = serde_json::from_str(
50 &fs::read_to_string(fur_dir.join("avatars.json")).unwrap_or_else(|_| "{}".to_string()),
51 )
52 .unwrap_or(serde_json::json!({}));
53
54 println!(
55 "{} {}",
56 "🌳 Conversation Tree:".bold().cyan(),
57 conversation_data["title"]
58 .as_str()
59 .unwrap_or("Untitled")
60 .green()
61 .bold()
62 );
63
64 if let Some(messages) = conversation_data["messages"].as_array() {
65 let id_to_message = load_conversation_messages(&fur_dir, &conversation_data);
66 for (idx, msg_id) in messages.iter().enumerate() {
67 if let Some(mid) = msg_id.as_str() {
68 render_message(&id_to_message, mid, "", idx == messages.len() - 1, &avatars);
69 }
70 }
71 }
72}
73
74fn load_conversation_messages(fur_dir: &Path, conversation: &Value) -> HashMap<String, Value> {
75 let mut id_to_message = HashMap::new();
76 let mut to_visit: Vec<String> = conversation["messages"]
77 .as_array()
78 .unwrap_or(&vec![])
79 .iter()
80 .filter_map(|id| id.as_str().map(|s| s.to_string()))
81 .collect();
82
83 while let Some(mid) = to_visit.pop() {
84 let path = fur_dir.join("messages").join(format!("{}.json", mid));
85 if let Some(content) = crate::security::io::read_text_file(&path) {
86 if let Ok(json) = serde_json::from_str::<Value>(&content) {
87 if let Some(children) = json["children"].as_array() {
89 for c in children {
90 if let Some(cid) = c.as_str() {
91 to_visit.push(cid.to_string());
92 }
93 }
94 }
95 if let Some(branches) = json["branches"].as_array() {
96 for block in branches {
97 if let Some(arr) = block.as_array() {
98 for c in arr {
99 if let Some(cid) = c.as_str() {
100 to_visit.push(cid.to_string());
101 }
102 }
103 }
104 }
105 }
106 id_to_message.insert(mid.clone(), json);
107 }
108 }
109 }
110 id_to_message
111}
112
113fn render_message(
114 id_to_message: &HashMap<String, Value>,
115 msg_id: &str,
116 prefix: &str,
117 is_last: bool,
118 avatars: &Value,
119) {
120 if let Some(msg) = id_to_message.get(msg_id) {
121 let branch_symbol = if is_last { "└──" } else { "├──" };
123 let tree_prefix = format!("{}{}", prefix, branch_symbol.bright_green());
124
125 let avatar_key = msg["avatar"].as_str().unwrap_or("???");
126 let (name, emoji) = resolve_avatar(avatars, avatar_key);
127
128 let text = crate::renderer::utils::preview_of(msg, 60);
129
130 let id_display = msg_id[..8].to_string();
131
132 if msg.get("markdown").is_some() {
133 println!(
134 "{} {} {} {} {} {}",
135 tree_prefix,
136 "[Root]".cyan(),
137 emoji.yellow(),
138 format!("[{}]", name).bright_yellow(),
139 text.white(),
140 format!("📄 {}", id_display).magenta()
141 );
142 } else {
143 println!(
144 "{} {} {} {} {}",
145 tree_prefix,
146 "[Root]".cyan(),
147 emoji.yellow(),
148 format!("[{}]", name).bright_yellow(),
149 format!("{} {}", text.white(), id_display.bright_black())
150 );
151 }
152
153 let empty: Vec<Value> = Vec::new();
155 let children = msg["children"].as_array().unwrap_or(&empty);
156 let branches = msg["branches"].as_array().unwrap_or(&empty);
157
158 if !branches.is_empty() {
160 for (_b_idx, branch) in branches.iter().enumerate() {
161 if let Some(arr) = branch.as_array() {
162 for (i, child_id) in arr.iter().enumerate() {
163 if let Some(cid) = child_id.as_str() {
164 let new_prefix = format!(
165 "{}{} ",
166 prefix,
167 if is_last { " " } else { "│ " }.bright_green()
168 );
169 render_message(
170 id_to_message,
171 cid,
172 &new_prefix,
173 i == arr.len() - 1,
174 avatars,
175 );
176 }
177 }
178 }
179 }
180 } else {
181 for (i, child_id) in children.iter().enumerate() {
182 if let Some(cid) = child_id.as_str() {
183 let new_prefix = format!(
184 "{}{} ",
185 prefix,
186 if is_last { " " } else { "│ " }.bright_green()
187 );
188 render_message(
189 id_to_message,
190 cid,
191 &new_prefix,
192 i == children.len() - 1,
193 avatars,
194 );
195 }
196 }
197 }
198 }
199}