ai_agent/services/
diagnostic_tracking.rs1use std::collections::HashMap;
6
7pub const MAX_DIAGNOSTICS_SUMMARY_CHARS: usize = 4000;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum DiagnosticSeverity {
11 Error,
12 Warning,
13 Info,
14 Hint,
15}
16
17#[derive(Debug, Clone)]
18pub struct DiagnosticRange {
19 pub start: DiagnosticPosition,
20 pub end: DiagnosticPosition,
21}
22
23#[derive(Debug, Clone)]
24pub struct DiagnosticPosition {
25 pub line: u32,
26 pub character: u32,
27}
28
29#[derive(Debug, Clone)]
30pub struct Diagnostic {
31 pub message: String,
32 pub severity: DiagnosticSeverity,
33 pub range: DiagnosticRange,
34 pub source: Option<String>,
35 pub code: Option<String>,
36}
37
38#[derive(Debug, Clone)]
39pub struct DiagnosticFile {
40 pub uri: String,
41 pub diagnostics: Vec<Diagnostic>,
42}
43
44pub struct DiagnosticTrackingService {
45 initialized: bool,
46 baseline: HashMap<String, Vec<Diagnostic>>,
47 right_file_diagnostics_state: HashMap<String, Vec<Diagnostic>>,
48 last_processed_timestamps: HashMap<String, u64>,
49}
50
51impl DiagnosticTrackingService {
52 pub fn new() -> Self {
53 Self {
54 initialized: false,
55 baseline: HashMap::new(),
56 right_file_diagnostics_state: HashMap::new(),
57 last_processed_timestamps: HashMap::new(),
58 }
59 }
60
61 pub fn initialize(&mut self) {
62 self.initialized = true;
63 }
64
65 pub fn shutdown(&mut self) {
66 self.initialized = false;
67 self.baseline.clear();
68 self.right_file_diagnostics_state.clear();
69 self.last_processed_timestamps.clear();
70 }
71
72 pub fn reset(&mut self) {
73 self.baseline.clear();
74 self.right_file_diagnostics_state.clear();
75 self.last_processed_timestamps.clear();
76 }
77
78 fn normalize_file_uri(&self, file_uri: &str) -> String {
79 let prefixes = ["file://", "_claude_fs_right:", "_claude_fs_left:"];
80
81 let mut normalized = file_uri.to_string();
82 for prefix in &prefixes {
83 if file_uri.starts_with(prefix) {
84 normalized = file_uri
85 .strip_prefix(prefix)
86 .unwrap_or(file_uri)
87 .to_string();
88 break;
89 }
90 }
91
92 normalized
93 }
94
95 pub fn before_file_edited(&mut self, file_path: &str, timestamp: u64) {
96 if !self.initialized {
97 return;
98 }
99
100 let normalized_path = self.normalize_file_uri(file_path);
101 self.baseline.insert(normalized_path.clone(), Vec::new());
102 self.last_processed_timestamps
103 .insert(normalized_path, timestamp);
104 }
105
106 pub fn set_baseline_diagnostics(&mut self, file_path: &str, diagnostics: Vec<Diagnostic>) {
107 let normalized_path = self.normalize_file_uri(file_path);
108 self.baseline.insert(normalized_path, diagnostics);
109 }
110
111 pub fn get_baseline(&self, file_path: &str) -> Option<&Vec<Diagnostic>> {
112 let normalized_path = self.normalize_file_uri(file_path);
113 self.baseline.get(&normalized_path)
114 }
115
116 fn are_diagnostics_equal(&self, a: &Diagnostic, b: &Diagnostic) -> bool {
117 a.message == b.message
118 && a.severity == b.severity
119 && a.source == b.source
120 && a.code == b.code
121 && a.range.start.line == b.range.start.line
122 && a.range.start.character == b.range.start.character
123 && a.range.end.line == b.range.end.line
124 && a.range.end.character == b.range.end.character
125 }
126
127 fn are_diagnostic_arrays_equal(&self, a: &[Diagnostic], b: &[Diagnostic]) -> bool {
128 if a.len() != b.len() {
129 return false;
130 }
131
132 a.iter().all(|diag_a| {
133 b.iter()
134 .any(|diag_b| self.are_diagnostics_equal(diag_a, diag_b))
135 }) && b.iter().all(|diag_b| {
136 a.iter()
137 .any(|diag_a| self.are_diagnostics_equal(diag_a, diag_b))
138 })
139 }
140
141 pub fn format_diagnostics_summary(files: &[DiagnosticFile]) -> String {
142 let truncation_marker = "…[truncated]";
143
144 let result: String = files
145 .iter()
146 .map(|file| {
147 let filename = file.uri.split('/').last().unwrap_or(&file.uri);
148 let diagnostics: String = file
149 .diagnostics
150 .iter()
151 .map(|d| {
152 let severity_symbol = Self::get_severity_symbol(&d.severity);
153 let code_str = d
154 .code
155 .as_ref()
156 .map(|c| format!(" [{}]", c))
157 .unwrap_or_default();
158 let source_str = d
159 .source
160 .as_ref()
161 .map(|s| format!(" ({})", s))
162 .unwrap_or_default();
163 format!(
164 " {} [Line {}:{}] {}{}{}",
165 severity_symbol,
166 d.range.start.line + 1,
167 d.range.start.character + 1,
168 d.message,
169 code_str,
170 source_str
171 )
172 })
173 .collect::<Vec<_>>()
174 .join("\n");
175
176 format!("{}:\n{}", filename, diagnostics)
177 })
178 .collect::<Vec<_>>()
179 .join("\n\n");
180
181 if result.len() > MAX_DIAGNOSTICS_SUMMARY_CHARS {
182 return result[..MAX_DIAGNOSTICS_SUMMARY_CHARS - truncation_marker.len()].to_string()
183 + truncation_marker;
184 }
185
186 result
187 }
188
189 pub fn get_severity_symbol(severity: &DiagnosticSeverity) -> &'static str {
190 match severity {
191 DiagnosticSeverity::Error => "✗",
192 DiagnosticSeverity::Warning => "⚠",
193 DiagnosticSeverity::Info => "ℹ",
194 DiagnosticSeverity::Hint => "★",
195 }
196 }
197}
198
199impl Default for DiagnosticTrackingService {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_normalize_file_uri() {
211 let service = DiagnosticTrackingService::new();
212
213 assert_eq!(
214 service.normalize_file_uri("file:///path/to/file.ts"),
215 "/path/to/file.ts"
216 );
217 assert_eq!(
218 service.normalize_file_uri("_claude_fs_right:/path/to/file.ts"),
219 "/path/to/file.ts"
220 );
221 }
222
223 #[test]
224 fn test_severity_symbols() {
225 assert_eq!(
226 DiagnosticTrackingService::get_severity_symbol(&DiagnosticSeverity::Error),
227 "✗"
228 );
229 assert_eq!(
230 DiagnosticTrackingService::get_severity_symbol(&DiagnosticSeverity::Warning),
231 "⚠"
232 );
233 assert_eq!(
234 DiagnosticTrackingService::get_severity_symbol(&DiagnosticSeverity::Info),
235 "ℹ"
236 );
237 assert_eq!(
238 DiagnosticTrackingService::get_severity_symbol(&DiagnosticSeverity::Hint),
239 "★"
240 );
241 }
242
243 #[test]
244 fn test_diagnostic_tracking_service() {
245 let mut service = DiagnosticTrackingService::new();
246
247 assert!(!service.initialized);
248
249 service.initialize();
250 assert!(service.initialized);
251
252 service.reset();
253 assert!(service.baseline.is_empty());
254
255 service.shutdown();
256 assert!(!service.initialized);
257 }
258}