1use super::Formatter;
7use crate::mem8::ConversationMemory;
8use crate::scanner::{FileNode, TreeStats};
9use anyhow::Result;
10use std::io::Write;
11use std::path::Path;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14const MAX_NODES_FOR_ITERATION: usize = 100_000;
16
17const MAX_NODES_TO_CHECK: usize = 10_000;
19
20pub struct ContextFormatter {
21 show_git: bool,
22 show_memories: bool,
23}
24
25impl Default for ContextFormatter {
26 fn default() -> Self {
27 Self {
28 show_git: true,
29 show_memories: true,
30 }
31 }
32}
33
34impl ContextFormatter {
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 fn get_git_context(&self, path: &Path) -> Option<String> {
41 if !self.show_git {
42 return None;
43 }
44
45 if let Ok(repo) = gix::discover(path) {
47 let mut git_info = Vec::new();
48
49 if let Ok(head) = repo.head_ref() {
51 if let Some(reference) = head {
52 let branch = reference.name().as_bstr().to_string();
53 git_info.push(format!(
54 "Branch: {}",
55 branch.strip_prefix("refs/heads/").unwrap_or(&branch)
56 ));
57 }
58 }
59
60 if let Ok(commit) = repo.head_commit() {
62 let id = commit.id().to_string();
63 let msg = commit
64 .message_raw_sloppy()
65 .to_string()
66 .lines()
67 .next()
68 .unwrap_or("No message")
69 .to_string();
70 git_info.push(format!("Last: {} - {}", &id[..8], msg));
71 }
72
73 if !git_info.is_empty() {
74 return Some(git_info.join("\n"));
75 }
76 }
77
78 None
79 }
80
81 fn get_memory_context(&self, path: &Path) -> Option<String> {
83 if !self.show_memories {
84 return None;
85 }
86
87 let project_name = path.file_name()?.to_str()?;
89
90 let memory = ConversationMemory::new().ok()?;
92
93 let conversations = memory.list_conversations().ok()?;
95 let related: Vec<_> = conversations
96 .iter()
97 .filter(|c| c.file_name.contains(project_name))
98 .take(3)
99 .collect();
100
101 if !related.is_empty() {
102 let mut output = vec!["🧠 Related memories:".to_string()];
103 for conv in related {
104 output.push(format!(
105 " • {} ({} messages)",
106 conv.file_name, conv.message_count
107 ));
108 }
109 return Some(output.join("\n"));
110 }
111
112 None
113 }
114}
115
116impl Formatter for ContextFormatter {
117 fn format(
118 &self,
119 writer: &mut dyn Write,
120 nodes: &[FileNode],
121 stats: &TreeStats,
122 root_path: &Path,
123 ) -> Result<()> {
124 let node_count = nodes.len();
126 let should_skip_iteration = node_count > MAX_NODES_FOR_ITERATION;
127
128 if should_skip_iteration {
129 eprintln!(
130 "⚠️ Warning: Large directory ({} files). Context mode will use summary data only.",
131 node_count
132 );
133 eprintln!(" Consider using --max-depth to limit the scan, or use --mode summary-ai instead.");
134 }
135
136 writeln!(writer, "=== Smart Tree Context ===")?;
137 writeln!(writer)?;
138
139 writeln!(writer, "📁 Project: {}", root_path.display())?;
141
142 if let Some(git_info) = self.get_git_context(root_path) {
144 writeln!(writer, "\n📍 Git Status:")?;
145 writeln!(writer, "{}", git_info)?;
146 }
147
148 writeln!(writer, "\n🌳 Structure:")?;
150 writeln!(writer, "SUMMARY_AI_V1:")?;
151 writeln!(writer, "PATH:{}", root_path.display())?;
152 writeln!(
153 writer,
154 "STATS:F{:x}D{:x}S{:x}",
155 stats.total_files, stats.total_dirs, stats.total_size
156 )?;
157
158 if !should_skip_iteration {
160 let mut ext_counts = std::collections::HashMap::new();
161 for node in nodes {
162 if !node.is_dir {
163 if let Some(ext) = node.path.extension() {
164 let ext_str = ext.to_string_lossy().to_string();
165 *ext_counts.entry(ext_str).or_insert(0) += 1;
166 }
167 }
168 }
169
170 let mut exts: Vec<_> = ext_counts.iter().collect();
171 exts.sort_by(|a, b| b.1.cmp(a.1));
172
173 let ext_str: Vec<_> = exts
174 .iter()
175 .take(10)
176 .map(|(ext, count)| format!("{}:{}", ext, count))
177 .collect();
178 if !ext_str.is_empty() {
179 writeln!(writer, "EXT:{}", ext_str.join(","))?;
180 }
181
182 let key_files = find_key_files(nodes);
184 if !key_files.is_empty() {
185 writeln!(writer, "KEY:{}", key_files.join(","))?;
186 }
187
188 let recent = find_recent_files(nodes, 86400); if !recent.is_empty() {
191 writeln!(writer, "\n⏰ Recent changes:")?;
192 for file in recent.iter().take(5) {
193 writeln!(writer, " • {}", file)?;
194 }
195 }
196 } else {
197 writeln!(
199 writer,
200 "\n⚠️ Detailed file analysis skipped due to large directory size"
201 )?;
202 writeln!(
203 writer,
204 " Total files: {}, Total dirs: {}",
205 stats.total_files, stats.total_dirs
206 )?;
207 }
208
209 if let Some(memories) = self.get_memory_context(root_path) {
211 writeln!(writer, "\n{}", memories)?;
212 }
213
214 writeln!(writer, "\n=== End Context ===")?;
215
216 Ok(())
217 }
218}
219
220fn find_key_files(nodes: &[FileNode]) -> Vec<String> {
222 let important = [
223 "Cargo.toml",
224 "package.json",
225 "README.md",
226 "CLAUDE.md",
227 "pyproject.toml",
228 "go.mod",
229 "Makefile",
230 ".env",
231 ];
232
233 let mut found = Vec::new();
234 let max_to_check = nodes.len().min(MAX_NODES_TO_CHECK);
236
237 for node in nodes.iter().take(max_to_check) {
238 if let Some(file_name) = node.path.file_name() {
239 let name = file_name.to_string_lossy();
240 if important.contains(&name.as_ref()) && !found.contains(&name.to_string()) {
241 found.push(name.to_string());
242 if found.len() >= 10 {
243 break;
244 }
245 }
246 }
247 }
248 found
249}
250
251fn find_recent_files(nodes: &[FileNode], seconds: u64) -> Vec<String> {
252 let now = SystemTime::now()
253 .duration_since(UNIX_EPOCH)
254 .unwrap_or_default()
255 .as_secs();
256
257 let mut recent = Vec::new();
258 let max_to_check = nodes.len().min(MAX_NODES_TO_CHECK);
260
261 for node in nodes.iter().take(max_to_check) {
262 if !node.is_dir {
263 if let Ok(duration) = node.modified.duration_since(UNIX_EPOCH) {
264 let file_time = duration.as_secs();
265 let age = now.saturating_sub(file_time);
266 if age < seconds {
267 recent.push(node.path.display().to_string());
268 if recent.len() >= 10 {
269 break;
270 }
271 }
272 }
273 }
274 }
275 recent
276}