1use std::cell::RefCell;
2use std::sync::atomic::{AtomicUsize, Ordering};
3
4static SESSION_ORIGINAL: AtomicUsize = AtomicUsize::new(0);
5static SESSION_SAVED: AtomicUsize = AtomicUsize::new(0);
6static SESSION_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
7
8thread_local! {
9 static CURRENT_MODE: RefCell<Option<String>> = const { RefCell::new(None) };
10 static CURRENT_DETAIL: RefCell<Option<String>> = const { RefCell::new(None) };
11}
12
13pub struct SavingsInfo<'a> {
14 pub original: usize,
15 pub compressed: usize,
16 pub mode: Option<&'a str>,
17 pub detail: Option<&'a str>,
18}
19
20pub struct ModeGuard;
21
22impl ModeGuard {
23 pub fn new(mode: &str) -> Self {
24 CURRENT_MODE.with(|m| *m.borrow_mut() = Some(mode.to_string()));
25 Self
26 }
27
28 pub fn with_detail(mode: &str, detail: &str) -> Self {
29 CURRENT_MODE.with(|m| *m.borrow_mut() = Some(mode.to_string()));
30 CURRENT_DETAIL.with(|d| *d.borrow_mut() = Some(detail.to_string()));
31 Self
32 }
33}
34
35impl Drop for ModeGuard {
36 fn drop(&mut self) {
37 CURRENT_MODE.with(|m| {
41 if let Ok(mut slot) = m.try_borrow_mut() {
42 *slot = None;
43 }
44 });
45 CURRENT_DETAIL.with(|d| {
46 if let Ok(mut slot) = d.try_borrow_mut() {
47 *slot = None;
48 }
49 });
50 }
51}
52
53fn current_mode() -> Option<String> {
54 CURRENT_MODE.with(|m| m.borrow().clone())
55}
56
57fn current_detail() -> Option<String> {
58 CURRENT_DETAIL.with(|d| d.borrow().clone())
59}
60
61pub fn record_savings(original: usize, saved: usize) {
62 SESSION_ORIGINAL.fetch_add(original, Ordering::Relaxed);
63 SESSION_SAVED.fetch_add(saved, Ordering::Relaxed);
64 SESSION_CALL_COUNT.fetch_add(1, Ordering::Relaxed);
65}
66
67pub fn session_totals() -> (usize, usize, usize) {
68 (
69 SESSION_ORIGINAL.load(Ordering::Relaxed),
70 SESSION_SAVED.load(Ordering::Relaxed),
71 SESSION_CALL_COUNT.load(Ordering::Relaxed),
72 )
73}
74
75pub fn reset_session() {
76 SESSION_ORIGINAL.store(0, Ordering::Relaxed);
77 SESSION_SAVED.store(0, Ordering::Relaxed);
78 SESSION_CALL_COUNT.store(0, Ordering::Relaxed);
79}
80
81fn format_number(n: usize) -> String {
82 if n >= 1_000_000 {
83 let m = n as f64 / 1_000_000.0;
84 format!("{m:.1}M")
85 } else if n >= 10_000 {
86 let k = n as f64 / 1_000.0;
87 format!("{k:.1}k")
88 } else if n >= 1_000 {
89 let whole = n / 1_000;
90 format!("{whole},{:03}", n % 1_000)
91 } else {
92 n.to_string()
93 }
94}
95
96fn is_explicitly_enabled() -> bool {
97 matches!(std::env::var("LEAN_CTX_SHOW_SAVINGS"), Ok(v) if v.trim() == "1")
98}
99
100fn is_ultra_suppressed() -> bool {
101 if is_explicitly_enabled() {
102 return false;
103 }
104 let level = super::config::CompressionLevel::effective(&super::config::Config::load());
105 matches!(level, super::config::CompressionLevel::Max)
106}
107
108pub fn format_footer(info: &SavingsInfo<'_>) -> String {
109 if !super::protocol::savings_footer_visible() {
110 return String::new();
111 }
112 if is_ultra_suppressed() {
113 return String::new();
114 }
115 format_footer_inner(info)
116}
117
118fn format_footer_inner(info: &SavingsInfo<'_>) -> String {
119 if info.original == 0 {
120 return String::new();
121 }
122 let saved = info.original.saturating_sub(info.compressed);
123 if saved == 0 {
124 return String::new();
125 }
126 let pct = (saved as f64 / info.original as f64 * 100.0).round() as usize;
127
128 let annotation = super::config::CompressionAnnotation::effective();
129 let threshold = super::config::Config::load().annotation_threshold_pct as usize;
130
131 if matches!(annotation, super::config::CompressionAnnotation::None) {
132 record_savings(info.original, saved);
133 return String::new();
134 }
135
136 if pct < threshold {
137 record_savings(info.original, saved);
138 return String::new();
139 }
140
141 let orig_str = format_number(info.original);
142 let comp_str = format_number(info.compressed);
143
144 let pct_display = match annotation {
145 super::config::CompressionAnnotation::Quantized => {
146 let quantized = ((pct + 5) / 10) * 10;
147 format!("~{quantized}")
148 }
149 _ => pct.to_string(),
150 };
151
152 let mut parts = vec![format!(
153 "{orig_str} \u{2192} {comp_str} tok (\u{2193}{pct_display}%)"
154 )];
155
156 if let Some(mode) = info.mode {
157 parts.push(format!("mode: {mode}"));
158 }
159 if let Some(detail) = info.detail {
160 parts.push(detail.to_string());
161 }
162
163 record_savings(info.original, saved);
164
165 let body = parts.join(" | ");
166 format!("\u{2500}\u{2500}\u{2500} {body} \u{2500}\u{2500}\u{2500}")
167}
168
169pub fn format_footer_basic(original: usize, compressed: usize) -> String {
170 let mode = current_mode();
171 let detail = current_detail();
172 format_footer(&SavingsInfo {
173 original,
174 compressed,
175 mode: mode.as_deref(),
176 detail: detail.as_deref(),
177 })
178}
179
180pub fn append_footer(output: &str, info: &SavingsInfo<'_>) -> String {
181 let footer = format_footer(info);
182 if footer.is_empty() {
183 output.to_string()
184 } else {
185 format!("{output}\n{footer}")
186 }
187}
188
189pub fn append_footer_basic(output: &str, original: usize, compressed: usize) -> String {
190 let footer = format_footer_basic(original, compressed);
191 if footer.is_empty() {
192 output.to_string()
193 } else {
194 format!("{output}\n{footer}")
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn format_number_small() {
204 assert_eq!(format_number(42), "42");
205 assert_eq!(format_number(999), "999");
206 }
207
208 #[test]
209 fn format_number_thousands() {
210 assert_eq!(format_number(1_000), "1,000");
211 assert_eq!(format_number(4_200), "4,200");
212 assert_eq!(format_number(9_999), "9,999");
213 }
214
215 #[test]
216 fn format_number_large() {
217 assert_eq!(format_number(12_300), "12.3k");
218 assert_eq!(format_number(45_200), "45.2k");
219 }
220
221 #[test]
222 fn format_number_millions() {
223 assert_eq!(format_number(1_500_000), "1.5M");
224 }
225
226 #[test]
227 fn basic_footer_format() {
228 let _lock = crate::core::data_dir::test_env_lock();
229 crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
230
231 let info = SavingsInfo {
232 original: 4200,
233 compressed: 840,
234 mode: Some("map"),
235 detail: None,
236 };
237 let result = format_footer_inner(&info);
238 assert!(
239 result.starts_with("\u{2500}\u{2500}\u{2500} "),
240 "should start with box-drawing: {result}"
241 );
242 assert!(
243 result.ends_with(" \u{2500}\u{2500}\u{2500}"),
244 "should end with box-drawing: {result}"
245 );
246 assert!(
247 result.contains("4,200"),
248 "should contain formatted original: {result}"
249 );
250 assert!(
251 result.contains("840"),
252 "should contain compressed: {result}"
253 );
254 assert!(
255 result.contains("\u{2193}80%"),
256 "should contain percentage: {result}"
257 );
258 assert!(
259 result.contains("mode: map"),
260 "should contain mode: {result}"
261 );
262
263 crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
264 }
265
266 #[test]
267 fn footer_with_detail() {
268 let _lock = crate::core::data_dir::test_env_lock();
269 crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
270
271 let info = SavingsInfo {
272 original: 12300,
273 compressed: 620,
274 mode: None,
275 detail: Some("3 patterns matched"),
276 };
277 let result = format_footer_inner(&info);
278 assert!(
279 result.contains("3 patterns matched"),
280 "detail missing: {result}"
281 );
282 assert!(
283 result.contains("12.3k"),
284 "should format large numbers: {result}"
285 );
286
287 crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
288 }
289
290 #[test]
291 fn footer_returns_empty_when_no_savings() {
292 let result = format_footer_inner(&SavingsInfo {
293 original: 100,
294 compressed: 100,
295 mode: None,
296 detail: None,
297 });
298 assert!(
299 result.is_empty(),
300 "should be empty with 0 savings: {result}"
301 );
302 }
303
304 #[test]
305 fn footer_returns_empty_when_zero_original() {
306 let result = format_footer_inner(&SavingsInfo {
307 original: 0,
308 compressed: 0,
309 mode: None,
310 detail: None,
311 });
312 assert!(
313 result.is_empty(),
314 "should be empty with 0 original: {result}"
315 );
316 }
317
318 #[test]
319 fn visibility_gated_tests() {
320 let _lock = crate::core::data_dir::test_env_lock();
321
322 crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "0");
323 crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "never");
324 let result = format_footer_basic(100, 50);
325 assert!(
326 result.is_empty(),
327 "should be empty with never mode: {result}"
328 );
329
330 let result = append_footer_basic("hello", 100, 50);
331 assert_eq!(result, "hello");
332
333 crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "1");
334 crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "always");
335 crate::test_env::remove_var("LEAN_CTX_QUIET");
336 super::super::protocol::set_mcp_context(false);
337
338 let result = append_footer_basic("hello", 100, 50);
339 assert!(
340 result.starts_with("hello\n"),
341 "should start with original: {result}"
342 );
343 assert!(
344 result.contains("\u{2500}\u{2500}\u{2500}"),
345 "should contain box-drawing: {result}"
346 );
347
348 crate::test_env::remove_var("LEAN_CTX_SHOW_SAVINGS");
351 crate::test_env::remove_var("LEAN_CTX_SAVINGS_FOOTER");
352 }
353
354 #[test]
355 fn session_accumulator_tracks() {
356 reset_session();
357 record_savings(100, 50);
358 record_savings(200, 80);
359 let (orig, saved, calls) = session_totals();
360 assert_eq!(orig, 300);
361 assert_eq!(saved, 130);
362 assert_eq!(calls, 2);
363 reset_session();
364 }
365
366 #[test]
367 fn session_counter_removed_from_footer() {
368 let _lock = crate::core::data_dir::test_env_lock();
369 crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
370
371 reset_session();
372 for _ in 0..20 {
373 record_savings(100, 50);
374 }
375 let info = SavingsInfo {
376 original: 100,
377 compressed: 50,
378 mode: None,
379 detail: None,
380 };
381 let result = format_footer_inner(&info);
382 assert!(
383 !result.contains("session:"),
384 "session counter must not appear in footer (breaks prefix stability): {result}"
385 );
386 reset_session();
387 crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
388 }
389
390 #[test]
391 fn quantized_mode_rounds_to_nearest_10() {
392 let _lock = crate::core::data_dir::test_env_lock();
393 crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "quantized");
394
395 let info = SavingsInfo {
396 original: 100,
397 compressed: 58,
398 mode: None,
399 detail: None,
400 };
401 let result = format_footer_inner(&info);
402 assert!(
403 result.contains("~40%"),
404 "42% should quantize to ~40%: {result}"
405 );
406
407 let info = SavingsInfo {
408 original: 100,
409 compressed: 13,
410 mode: None,
411 detail: None,
412 };
413 let result = format_footer_inner(&info);
414 assert!(
415 result.contains("~90%"),
416 "87% should quantize to ~90%: {result}"
417 );
418
419 crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
420 }
421
422 #[test]
423 fn none_mode_suppresses_all_annotations() {
424 let _lock = crate::core::data_dir::test_env_lock();
425 crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "none");
426
427 let info = SavingsInfo {
428 original: 100,
429 compressed: 20,
430 mode: Some("map"),
431 detail: None,
432 };
433 let result = format_footer_inner(&info);
434 assert!(result.is_empty(), "none mode should suppress all: {result}");
435
436 crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
437 }
438
439 #[test]
440 fn threshold_suppresses_small_savings() {
441 let _lock = crate::core::data_dir::test_env_lock();
442 crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
443
444 let info = SavingsInfo {
445 original: 100,
446 compressed: 97,
447 mode: None,
448 detail: None,
449 };
450 let result = format_footer_inner(&info);
451 assert!(
452 result.is_empty(),
453 "3% savings (below default 5% threshold) should be suppressed: {result}"
454 );
455
456 let info = SavingsInfo {
457 original: 100,
458 compressed: 90,
459 mode: None,
460 detail: None,
461 };
462 let result = format_footer_inner(&info);
463 assert!(
464 result.contains("10%"),
465 "10% savings (above 5% threshold) should appear: {result}"
466 );
467
468 crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
469 }
470
471 #[test]
472 fn mode_guard_sets_and_clears() {
473 assert!(current_mode().is_none());
474 {
475 let _guard = ModeGuard::new("map");
476 assert_eq!(current_mode().as_deref(), Some("map"));
477 }
478 assert!(current_mode().is_none());
479 }
480
481 #[test]
482 fn mode_guard_with_detail() {
483 {
484 let _guard = ModeGuard::with_detail("shell", "3 patterns");
485 assert_eq!(current_mode().as_deref(), Some("shell"));
486 assert_eq!(current_detail().as_deref(), Some("3 patterns"));
487 }
488 assert!(current_mode().is_none());
489 assert!(current_detail().is_none());
490 }
491}