1use std::fs::{self, OpenOptions};
7use std::io::{self, Write};
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::time::{Duration, Instant};
11
12#[cfg(unix)]
13use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
14
15const DEBUG_DIR_ENV: &str = "GIT_IRIS_DEBUG_DIR";
16
17static DEBUG_MODE: AtomicBool = AtomicBool::new(false);
19
20pub fn enable_debug_mode() {
22 DEBUG_MODE.store(true, Ordering::SeqCst);
23}
24
25pub fn disable_debug_mode() {
27 DEBUG_MODE.store(false, Ordering::SeqCst);
28}
29
30pub fn is_debug_enabled() -> bool {
32 DEBUG_MODE.load(Ordering::SeqCst)
33}
34
35fn debug_artifacts_dir() -> io::Result<PathBuf> {
37 let base = std::env::var_os(DEBUG_DIR_ENV)
38 .map(PathBuf::from)
39 .or_else(|| {
40 dirs::cache_dir().map(|mut dir| {
41 dir.push("git-iris");
42 dir.push("debug-artifacts");
43 dir
44 })
45 })
46 .unwrap_or_else(|| {
47 std::env::temp_dir()
48 .join("git-iris")
49 .join("debug-artifacts")
50 });
51
52 if !base.exists() {
53 fs::create_dir_all(&base)?;
54 }
55
56 #[cfg(unix)]
57 {
58 let _ = fs::set_permissions(&base, fs::Permissions::from_mode(0o700));
59 }
60
61 Ok(base)
62}
63
64pub fn write_debug_artifact(filename: &str, contents: &str) -> io::Result<PathBuf> {
66 let mut path = debug_artifacts_dir()?;
67 path.push(filename);
68
69 write_secure_file(&path, contents)?;
70 Ok(path)
71}
72
73fn write_secure_file(path: &PathBuf, contents: &str) -> io::Result<()> {
74 #[cfg(unix)]
75 {
76 let mut options = OpenOptions::new();
77 options.write(true).create(true).truncate(true).mode(0o600);
78 let mut file = options.open(path)?;
79 file.write_all(contents.as_bytes())?;
80 Ok(())
81 }
82
83 #[cfg(not(unix))]
84 {
85 let mut file = OpenOptions::new()
86 .write(true)
87 .create(true)
88 .truncate(true)
89 .open(path)?;
90 file.write_all(contents.as_bytes())
91 }
92}
93
94fn format_duration(duration: Duration) -> String {
96 if duration.as_secs() > 0 {
97 format!("{:.2}s", duration.as_secs_f64())
98 } else if duration.as_millis() > 0 {
99 format!("{}ms", duration.as_millis())
100 } else {
101 format!("{}μs", duration.as_micros())
102 }
103}
104
105fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
107 if s.len() <= max_bytes {
108 return s;
109 }
110 let mut end = max_bytes;
111 while end > 0 && !s.is_char_boundary(end) {
112 end -= 1;
113 }
114 &s[..end]
115}
116
117pub fn debug_header(title: &str) {
119 if !is_debug_enabled() {
120 return;
121 }
122 tracing::debug!(target: "iris", "══════════════════════════════════════════════════════════════════════════════");
123 tracing::debug!(target: "iris", "◆ {} ◆", title);
124 tracing::debug!(target: "iris", "══════════════════════════════════════════════════════════════════════════════");
125}
126
127pub fn debug_section(title: &str) {
129 if !is_debug_enabled() {
130 return;
131 }
132 tracing::debug!(target: "iris", "▸ {}", title);
133 tracing::debug!(target: "iris", "──────────────────────────────────────────────────────────────────────────────");
134}
135
136pub fn debug_tool_call(tool_name: &str, args: &str) {
138 if !is_debug_enabled() {
139 return;
140 }
141
142 tracing::debug!(target: "iris", "🔧 Tool Call: {}", tool_name);
143
144 if !args.is_empty() {
145 let truncated = if args.len() > 200 {
146 format!("{}...", truncate_at_char_boundary(args, 200))
147 } else {
148 args.to_string()
149 };
150 tracing::debug!(target: "iris", " Args: {}", truncated);
151 }
152}
153
154pub fn debug_tool_response(tool_name: &str, response: &str, duration: Duration) {
156 if !is_debug_enabled() {
157 return;
158 }
159
160 let truncated = if response.len() > 500 {
161 format!("{}...", truncate_at_char_boundary(response, 500))
162 } else {
163 response.to_string()
164 };
165
166 tracing::debug!(target: "iris", "✓ Tool Response: {} ({})", tool_name, format_duration(duration));
167 tracing::debug!(target: "iris", " {}", truncated);
168}
169
170pub fn debug_llm_request(prompt: &str, max_tokens: Option<usize>) {
172 if !is_debug_enabled() {
173 return;
174 }
175
176 let char_count = prompt.chars().count();
177 let word_count = prompt.split_whitespace().count();
178
179 tracing::debug!(target: "iris", "🧠 LLM Request: {} chars, {} words {}",
180 char_count,
181 word_count,
182 max_tokens.map(|t| format!("(max {} tokens)", t)).unwrap_or_default()
183 );
184
185 for line in prompt.lines().take(5) {
187 let truncated = if line.len() > 120 {
188 format!("{}...", truncate_at_char_boundary(line, 120))
189 } else {
190 line.to_string()
191 };
192 tracing::debug!(target: "iris", " {}", truncated);
193 }
194 if prompt.lines().count() > 5 {
195 tracing::debug!(target: "iris", " ... ({} more lines)", prompt.lines().count() - 5);
196 }
197
198 if let Ok(path) = write_debug_artifact("iris_last_prompt.txt", prompt) {
200 tracing::debug!(target: "iris", " Full prompt saved to: {}", path.display());
201 }
202}
203
204pub fn debug_stream_chunk(_chunk: &str, chunk_number: usize) {
206 if !is_debug_enabled() {
207 return;
208 }
209
210 if chunk_number.is_multiple_of(10) {
212 tracing::debug!(target: "iris", "▹ chunk #{}", chunk_number);
213 }
214}
215
216pub fn debug_llm_response(response: &str, duration: Duration, tokens_used: Option<usize>) {
218 if !is_debug_enabled() {
219 return;
220 }
221
222 let char_count = response.chars().count();
223 let word_count = response.split_whitespace().count();
224
225 tracing::debug!(target: "iris", "✨ LLM Response: {} chars, {} words ({})",
226 char_count,
227 word_count,
228 format_duration(duration)
229 );
230
231 if let Some(tokens) = tokens_used {
232 tracing::debug!(target: "iris", " Tokens: {}", tokens);
233 }
234
235 if let Ok(path) = write_debug_artifact("iris_last_response.txt", response) {
237 tracing::debug!(target: "iris", " Full response saved to: {}", path.display());
238 }
239
240 let truncated = if response.len() > 1000 {
242 format!(
243 "{}...\n\n... ({} more characters)",
244 truncate_at_char_boundary(response, 1000),
245 response.len() - 1000
246 )
247 } else {
248 response.to_string()
249 };
250 for line in truncated.lines() {
251 tracing::debug!(target: "iris", "{}", line);
252 }
253}
254
255pub fn debug_json_parse_attempt(json_str: &str) {
257 if !is_debug_enabled() {
258 return;
259 }
260
261 tracing::debug!(target: "iris", "📝 JSON Parse Attempt: {} chars", json_str.len());
262
263 let head = if json_str.len() > 500 {
265 format!("{}...", truncate_at_char_boundary(json_str, 500))
266 } else {
267 json_str.to_string()
268 };
269 tracing::debug!(target: "iris", "{}", head);
270
271 if json_str.len() > 700 {
273 tracing::debug!(target: "iris", "... truncated ...");
274 let mut tail_start = json_str.len().saturating_sub(200);
275 while tail_start < json_str.len() && !json_str.is_char_boundary(tail_start) {
276 tail_start += 1;
277 }
278 tracing::debug!(target: "iris", "{}", &json_str[tail_start..]);
279 }
280}
281
282pub fn debug_json_parse_success(type_name: &str) {
284 if !is_debug_enabled() {
285 return;
286 }
287 tracing::debug!(target: "iris", "✓ JSON Parsed: {}", type_name);
288}
289
290pub fn debug_json_parse_error(error: &str) {
292 if !is_debug_enabled() {
293 return;
294 }
295 tracing::warn!(target: "iris", "✗ JSON Parse Error: {}", error);
296}
297
298pub fn debug_context_management(action: &str, details: &str) {
300 if !is_debug_enabled() {
301 return;
302 }
303 tracing::debug!(target: "iris", "🔍 {} {}", action, details);
304}
305
306pub fn debug_error(error: &str) {
308 if !is_debug_enabled() {
309 return;
310 }
311 tracing::error!(target: "iris", "✗ Error: {}", error);
312}
313
314pub fn debug_warning(warning: &str) {
316 if !is_debug_enabled() {
317 return;
318 }
319 tracing::warn!(target: "iris", "⚠ {}", warning);
320}
321
322pub fn debug_phase_change(phase: &str) {
324 if !is_debug_enabled() {
325 return;
326 }
327 tracing::debug!(target: "iris", "◆ {}", phase);
328 tracing::debug!(target: "iris", "──────────────────────────────────────────────────────────────────────────────");
329}
330
331pub struct DebugTimer {
333 start: Instant,
334 operation: String,
335}
336
337impl DebugTimer {
338 pub fn start(operation: &str) -> Self {
339 if is_debug_enabled() {
340 tracing::debug!(target: "iris", "⏱ Started: {}", operation);
341 }
342
343 Self {
344 start: Instant::now(),
345 operation: operation.to_string(),
346 }
347 }
348
349 pub fn finish(self) {
350 if is_debug_enabled() {
351 let duration = self.start.elapsed();
352 tracing::debug!(target: "iris", "✓ Completed: {} ({})", self.operation, format_duration(duration));
353 }
354 }
355}