1use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::atomic::{AtomicU32, Ordering};
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11
12use crate::git_utils;
13
14#[derive(Debug, Clone)]
16pub struct FooterData {
17 pub model_name: String,
19 pub provider_name: String,
21 pub thinking_level: String,
23 pub session_name: Option<String>,
25 pub git_branch: Option<String>,
27 pub pwd: Option<String>,
29 pub input_tokens: Arc<AtomicU32>,
31 pub output_tokens: Arc<AtomicU32>,
33 pub cache_read_tokens: Arc<AtomicU32>,
35 pub cache_write_tokens: Arc<AtomicU32>,
37 pub context_window_pct: f32,
39 pub total_cost: f64,
41 pub session_duration_secs: u64,
43 pub extension_statuses: HashMap<String, String>,
45}
46
47impl Default for FooterData {
48 fn default() -> Self {
49 Self {
50 model_name: String::new(),
51 provider_name: String::new(),
52 thinking_level: "off".to_string(),
53 session_name: None,
54 git_branch: None,
55 pwd: None,
56 input_tokens: Arc::new(AtomicU32::new(0)),
57 output_tokens: Arc::new(AtomicU32::new(0)),
58 cache_read_tokens: Arc::new(AtomicU32::new(0)),
59 cache_write_tokens: Arc::new(AtomicU32::new(0)),
60 context_window_pct: 0.0,
61 total_cost: 0.0,
62 session_duration_secs: 0,
63 extension_statuses: HashMap::new(),
64 }
65 }
66}
67
68impl FooterData {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn with_model(mut self, model_name: &str, provider_name: &str) -> Self {
76 self.model_name = model_name.to_string();
77 self.provider_name = provider_name.to_string();
78 self
79 }
80
81 pub fn with_thinking_level(mut self, level: &str) -> Self {
83 self.thinking_level = level.to_string();
84 self
85 }
86
87 pub fn with_session_name(mut self, name: Option<String>) -> Self {
89 self.session_name = name;
90 self
91 }
92
93 pub fn with_git_branch(mut self, cwd: &PathBuf) -> Self {
95 self.git_branch = git_utils::get_current_branch(cwd);
97 self
98 }
99
100 pub fn with_pwd(mut self, pwd: Option<String>) -> Self {
102 self.pwd = pwd;
103 self
104 }
105
106 pub fn get_input_tokens(&self) -> u32 {
108 self.input_tokens.load(Ordering::Relaxed)
109 }
110
111 pub fn get_output_tokens(&self) -> u32 {
113 self.output_tokens.load(Ordering::Relaxed)
114 }
115
116 pub fn get_cache_read_tokens(&self) -> u32 {
118 self.cache_read_tokens.load(Ordering::Relaxed)
119 }
120
121 pub fn get_cache_write_tokens(&self) -> u32 {
123 self.cache_write_tokens.load(Ordering::Relaxed)
124 }
125
126 pub fn update_tokens(&self, input: u32, output: u32) {
128 self.input_tokens.store(input, Ordering::Relaxed);
129 self.output_tokens.store(output, Ordering::Relaxed);
130 }
131
132 pub fn update_cache_tokens(&self, read: u32, write: u32) {
134 self.cache_read_tokens.store(read, Ordering::Relaxed);
135 self.cache_write_tokens.store(write, Ordering::Relaxed);
136 }
137
138 pub fn update_all_tokens(&self, input: u32, output: u32, cache_read: u32, cache_write: u32) {
140 self.update_tokens(input, output);
141 self.update_cache_tokens(cache_read, cache_write);
142 }
143
144 pub fn set_context_window_pct(&mut self, pct: f32) {
146 self.context_window_pct = pct.clamp(0.0, 100.0);
147 }
148
149 pub fn set_total_cost(&mut self, cost: f64) {
151 self.total_cost = cost;
152 }
153
154 pub fn set_session_duration(&mut self, secs: u64) {
156 self.session_duration_secs = secs;
157 }
158
159 pub fn set_extension_status(&mut self, key: &str, value: Option<&str>) {
161 if let Some(v) = value {
162 self.extension_statuses.insert(key.to_string(), v.to_string());
163 } else {
164 self.extension_statuses.remove(key);
165 }
166 }
167
168 pub fn clear_extension_statuses(&mut self) {
170 self.extension_statuses.clear();
171 }
172
173 pub fn format_tokens(&self) -> String {
175 let input = self.get_input_tokens();
176 let output = self.get_output_tokens();
177 let cache_read = self.get_cache_read_tokens();
178 let cache_write = self.get_cache_write_tokens();
179
180 let mut parts = Vec::new();
181 if input > 0 {
182 parts.push(format!("↑{}", Self::format_token_count(input)));
183 }
184 if output > 0 {
185 parts.push(format!("↓{}", Self::format_token_count(output)));
186 }
187 if cache_read > 0 {
188 parts.push(format!("R{}", Self::format_token_count(cache_read)));
189 }
190 if cache_write > 0 {
191 parts.push(format!("W{}", Self::format_token_count(cache_write)));
192 }
193 parts.join(" ")
194 }
195
196 fn format_token_count(count: u32) -> String {
198 if count < 1000 {
199 count.to_string()
200 } else if count < 10000 {
201 format!("{:.1}k", count as f32 / 1000.0)
202 } else if count < 1_000_000 {
203 format!("{}k", count / 1000)
204 } else {
205 format!("{:.1}M", count as f32 / 1_000_000.0)
206 }
207 }
208
209 pub fn format_context_window(&self) -> String {
211 if self.context_window_pct > 0.0 {
212 format!("{:.1}%", self.context_window_pct)
213 } else {
214 String::from("0%")
215 }
216 }
217
218 pub fn has_data(&self) -> bool {
220 self.model_name.is_empty() == false
221 || self.get_input_tokens() > 0
222 || self.get_output_tokens() > 0
223 || self.total_cost > 0.0
224 }
225
226 pub fn total_tokens(&self) -> u32 {
228 self.get_input_tokens() + self.get_output_tokens()
229 }
230
231 pub fn render_lines(&self, width: usize) -> Vec<String> {
234 let mut lines = Vec::new();
235
236 let mut pwd_parts = Vec::new();
238 if let Some(ref pwd) = self.pwd {
239 pwd_parts.push(pwd.clone());
240 }
241 if let Some(ref branch) = self.git_branch {
242 pwd_parts.push(format!("({})", branch));
243 }
244 if let Some(ref session) = self.session_name {
245 pwd_parts.push(format!("• {}", session));
246 }
247 let pwd_line = if pwd_parts.is_empty() {
248 String::new()
249 } else {
250 pwd_parts.join(" ")
251 };
252 lines.push(pwd_line);
253
254 let mut stats_parts = Vec::new();
256
257 let token_str = self.format_tokens();
259 if !token_str.is_empty() {
260 stats_parts.push(token_str);
261 }
262
263 if self.total_cost > 0.0 {
265 stats_parts.push(format!("${:.3}", self.total_cost));
266 }
267
268 if self.context_window_pct > 0.0 {
270 stats_parts.push(format!("ctx:{}", self.format_context_window()));
271 }
272
273 let mut right_parts = Vec::new();
275 if !self.model_name.is_empty() {
276 if self.provider_name.is_empty() {
277 right_parts.push(self.model_name.clone());
278 } else {
279 right_parts.push(format!("({}) {}", self.provider_name, self.model_name));
280 }
281 }
282
283 if self.thinking_level != "off" && !self.model_name.is_empty() {
285 right_parts.push(format!("thinking:{}", self.thinking_level));
286 }
287
288 let stats_line = if stats_parts.is_empty() && right_parts.is_empty() {
290 String::new()
291 } else if stats_parts.is_empty() {
292 right_parts.join(" ")
293 } else if right_parts.is_empty() {
294 stats_parts.join(" ")
295 } else {
296 format!("{} {}", stats_parts.join(" "), right_parts.join(" "))
297 };
298 lines.push(stats_line);
299
300 for (key, value) in &self.extension_statuses {
302 lines.push(format!("[{}] {}", key, value));
303 }
304
305 lines
306 }
307}
308
309#[derive(Debug, Clone)]
311pub struct KeybindingHint {
312 pub keys: String,
314 pub description: String,
316}
317
318impl KeybindingHint {
319 pub fn new(keys: &str, description: &str) -> Self {
320 Self {
321 keys: keys.to_string(),
322 description: description.to_string(),
323 }
324 }
325}
326
327pub struct SessionTimer {
329 start: Instant,
330}
331
332impl SessionTimer {
333 pub fn new() -> Self {
335 Self {
336 start: Instant::now(),
337 }
338 }
339
340 pub fn elapsed(&self) -> Duration {
342 self.start.elapsed()
343 }
344
345 pub fn reset(&mut self) {
347 self.start = Instant::now();
348 }
349}
350
351impl Default for SessionTimer {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357pub fn format_duration(duration: Duration) -> String {
359 let total_secs = duration.as_secs();
360
361 if total_secs < 60 {
362 return format!("{}s", total_secs);
363 }
364
365 let minutes = total_secs / 60;
366 if minutes < 60 {
367 let seconds = total_secs % 60;
368 return format!("{}m {}s", minutes, seconds);
369 }
370
371 let hours = minutes / 60;
372 let mins = minutes % 60;
373 if hours < 24 {
374 return format!("{}h {}m", hours, mins);
375 }
376
377 let days = hours / 24;
378 let hrs = hours % 24;
379 format!("{}d {}h", days, hrs)
380}
381
382pub struct CostEstimator {
385 input_price_per_m: HashMap<String, f64>,
387 output_price_per_m: HashMap<String, f64>,
389}
390
391impl CostEstimator {
392 pub fn new() -> Self {
394 let mut input_price_per_m = HashMap::new();
395 let mut output_price_per_m = HashMap::new();
396
397 input_price_per_m.insert("claude".to_string(), 3.0);
399 output_price_per_m.insert("claude".to_string(), 15.0);
400
401 input_price_per_m.insert("gpt-4".to_string(), 30.0);
403 output_price_per_m.insert("gpt-4".to_string(), 60.0);
404 input_price_per_m.insert("gpt-3.5".to_string(), 0.5);
405 output_price_per_m.insert("gpt-3.5".to_string(), 1.5);
406
407 input_price_per_m.insert("gemini".to_string(), 0.125);
409 output_price_per_m.insert("gemini".to_string(), 0.5);
410
411 Self {
412 input_price_per_m,
413 output_price_per_m,
414 }
415 }
416
417 pub fn estimate(&self, model: &str, input_tokens: u32, output_tokens: u32) -> Option<f64> {
419 let model_lower = model.to_lowercase();
421
422 let input_price = self
423 .input_price_per_m
424 .iter()
425 .find(|(name, _)| model_lower.contains(&name.to_lowercase()))
426 .map(|(_, price)| *price);
427
428 let output_price = self
429 .output_price_per_m
430 .iter()
431 .find(|(name, _)| model_lower.contains(&name.to_lowercase()))
432 .map(|(_, price)| *price);
433
434 match (input_price, output_price) {
435 (Some(inp), Some(outp)) => {
436 let input_cost = (input_tokens as f64 / 1_000_000.0) * inp;
437 let output_cost = (output_tokens as f64 / 1_000_000.0) * outp;
438 Some(input_cost + output_cost)
439 }
440 _ => None,
441 }
442 }
443}
444
445impl Default for CostEstimator {
446 fn default() -> Self {
447 Self::new()
448 }
449}
450
451pub trait FooterDataProvider: Send + Sync {
453 fn get_footer_data(&self) -> FooterData;
455
456 fn get_model_name(&self) -> Option<String>;
458
459 fn get_git_branch(&self) -> Option<String>;
461
462 fn get_token_counts(&self) -> (u32, u32);
464
465 fn get_session_duration(&self) -> Duration;
467
468 fn get_keybinding_hints(&self) -> Vec<KeybindingHint>;
470}
471
472pub struct SimpleFooterDataProvider {
474 model_name: Option<String>,
475 provider_name: Option<String>,
476 git_branch: Option<String>,
477 pwd: Option<String>,
478 input_tokens: Arc<AtomicU32>,
479 output_tokens: u32,
480 cache_read_tokens: u32,
481 cache_write_tokens: u32,
482 session_timer: SessionTimer,
483 keybinding_hints: Vec<KeybindingHint>,
484 extension_statuses: HashMap<String, String>,
485 available_providers: usize,
486 thinking_level: String,
487 session_name: Option<String>,
488}
489
490impl SimpleFooterDataProvider {
491 pub fn new() -> Self {
493 Self {
494 model_name: None,
495 provider_name: None,
496 git_branch: None,
497 pwd: None,
498 input_tokens: Arc::new(AtomicU32::new(0)),
499 output_tokens: 0,
500 cache_read_tokens: 0,
501 cache_write_tokens: 0,
502 session_timer: SessionTimer::new(),
503 keybinding_hints: Vec::new(),
504 extension_statuses: HashMap::new(),
505 available_providers: 0,
506 thinking_level: "off".to_string(),
507 session_name: None,
508 }
509 }
510
511 pub fn with_model(mut self, model: Option<String>, provider: Option<String>) -> Self {
513 self.model_name = model;
514 self.provider_name = provider;
515 self
516 }
517
518 pub fn with_git_branch(mut self, branch: Option<String>) -> Self {
520 self.git_branch = branch;
521 self
522 }
523
524 pub fn with_pwd(mut self, pwd: Option<String>) -> Self {
526 self.pwd = pwd;
527 self
528 }
529
530 pub fn with_tokens(mut self, input: u32, output: u32) -> Self {
532 self.input_tokens = Arc::new(AtomicU32::new(input));
533 self.output_tokens = output;
534 self
535 }
536
537 pub fn add_hint(mut self, keys: &str, description: &str) -> Self {
539 self.keybinding_hints.push(KeybindingHint::new(keys, description));
540 self
541 }
542
543 pub fn with_providers(mut self, count: usize) -> Self {
545 self.available_providers = count;
546 self
547 }
548
549 pub fn update_tokens(&mut self, input: u32, output: u32) {
551 self.input_tokens = Arc::new(AtomicU32::new(input));
552 self.output_tokens = output;
553 }
554
555 pub fn update_cache_tokens(&mut self, read: u32, write: u32) {
557 self.cache_read_tokens = read;
558 self.cache_write_tokens = write;
559 }
560
561 pub fn set_extension_status(&mut self, key: &str, status: Option<&str>) {
563 if let Some(s) = status {
564 self.extension_statuses.insert(key.to_string(), s.to_string());
565 } else {
566 self.extension_statuses.remove(key);
567 }
568 }
569
570 pub fn set_thinking_level(&mut self, level: &str) {
572 self.thinking_level = level.to_string();
573 }
574
575 pub fn set_session_name(&mut self, name: Option<String>) {
577 self.session_name = name;
578 }
579
580 pub fn set_available_providers(&mut self, count: usize) {
582 self.available_providers = count;
583 }
584}
585
586impl Default for SimpleFooterDataProvider {
587 fn default() -> Self {
588 Self::new()
589 }
590}
591
592impl FooterDataProvider for SimpleFooterDataProvider {
593 fn get_footer_data(&self) -> FooterData {
594 let mut data = FooterData {
595 model_name: self.model_name.clone().unwrap_or_default(),
596 provider_name: self.provider_name.clone().unwrap_or_default(),
597 thinking_level: self.thinking_level.clone(),
598 session_name: self.session_name.clone(),
599 git_branch: self.git_branch.clone(),
600 pwd: self.pwd.clone(),
601 input_tokens: Arc::clone(&self.input_tokens),
602 output_tokens: Arc::new(AtomicU32::new(self.output_tokens)),
603 cache_read_tokens: Arc::new(AtomicU32::new(self.cache_read_tokens)),
604 cache_write_tokens: Arc::new(AtomicU32::new(self.cache_write_tokens)),
605 context_window_pct: 0.0,
606 total_cost: 0.0,
607 session_duration_secs: self.session_timer.elapsed().as_secs(),
608 extension_statuses: self.extension_statuses.clone(),
609 };
610
611 if let Some(ref model) = self.model_name {
613 let cost_estimator = CostEstimator::new();
614 if let Some(cost) = cost_estimator.estimate(
615 model,
616 self.input_tokens.load(Ordering::Relaxed),
617 self.output_tokens,
618 ) {
619 data.total_cost = cost;
620 }
621 }
622
623 data
624 }
625
626 fn get_model_name(&self) -> Option<String> {
627 self.model_name.clone()
628 }
629
630 fn get_git_branch(&self) -> Option<String> {
631 self.git_branch.clone()
632 }
633
634 fn get_token_counts(&self) -> (u32, u32) {
635 (self.input_tokens.load(Ordering::Relaxed), self.output_tokens)
636 }
637
638 fn get_session_duration(&self) -> Duration {
639 self.session_timer.elapsed()
640 }
641
642 fn get_keybinding_hints(&self) -> Vec<KeybindingHint> {
643 self.keybinding_hints.clone()
644 }
645}
646
647pub struct ExtensionStatusTracker {
649 statuses: HashMap<String, String>,
650}
651
652impl ExtensionStatusTracker {
653 pub fn new() -> Self {
654 Self {
655 statuses: HashMap::new(),
656 }
657 }
658
659 pub fn set(&mut self, extension: &str, status: &str) {
660 self.statuses.insert(extension.to_string(), status.to_string());
661 }
662
663 pub fn clear(&mut self, extension: &str) {
664 self.statuses.remove(extension);
665 }
666
667 pub fn get_all(&self) -> &HashMap<String, String> {
668 &self.statuses
669 }
670}
671
672impl Default for ExtensionStatusTracker {
673 fn default() -> Self {
674 Self::new()
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 #[test]
683 fn test_footer_data_new() {
684 let data = FooterData::new();
685 assert!(data.model_name.is_empty());
686 assert!(data.provider_name.is_empty());
687 assert_eq!(data.get_input_tokens(), 0);
688 assert_eq!(data.get_output_tokens(), 0);
689 }
690
691 #[test]
692 fn test_footer_data_with_model() {
693 let data = FooterData::new().with_model("claude-3.5-sonnet", "anthropic");
694 assert_eq!(data.model_name, "claude-3.5-sonnet");
695 assert_eq!(data.provider_name, "anthropic");
696 }
697
698 #[test]
699 fn test_footer_data_with_thinking_level() {
700 let data = FooterData::new().with_thinking_level("medium");
701 assert_eq!(data.thinking_level, "medium");
702 }
703
704 #[test]
705 fn test_footer_data_with_session_name() {
706 let data = FooterData::new().with_session_name(Some("my-session".to_string()));
707 assert_eq!(data.session_name, Some("my-session".to_string()));
708 }
709
710 #[test]
711 fn test_footer_data_with_pwd() {
712 let data = FooterData::new().with_pwd(Some("~/projects/oxi".to_string()));
713 assert_eq!(data.pwd, Some("~/projects/oxi".to_string()));
714 }
715
716 #[test]
717 fn test_footer_data_update_tokens() {
718 let data = FooterData::new();
719 data.update_tokens(1000, 500);
720 assert_eq!(data.get_input_tokens(), 1000);
721 assert_eq!(data.get_output_tokens(), 500);
722 }
723
724 #[test]
725 fn test_footer_data_update_cache_tokens() {
726 let data = FooterData::new();
727 data.update_cache_tokens(2000, 100);
728 assert_eq!(data.get_cache_read_tokens(), 2000);
729 assert_eq!(data.get_cache_write_tokens(), 100);
730 }
731
732 #[test]
733 fn test_footer_data_update_all_tokens() {
734 let data = FooterData::new();
735 data.update_all_tokens(1000, 500, 2000, 100);
736 assert_eq!(data.get_input_tokens(), 1000);
737 assert_eq!(data.get_output_tokens(), 500);
738 assert_eq!(data.get_cache_read_tokens(), 2000);
739 assert_eq!(data.get_cache_write_tokens(), 100);
740 }
741
742 #[test]
743 fn test_footer_data_total_tokens() {
744 let data = FooterData::new();
745 data.update_tokens(100, 50);
746 assert_eq!(data.total_tokens(), 150);
747 }
748
749 #[test]
750 fn test_footer_data_format_tokens() {
751 let data = FooterData::new();
752 data.update_tokens(1500, 2500);
753 let formatted = data.format_tokens();
754 assert!(formatted.contains("↑1.5k") || formatted.contains("↑1500"));
755 assert!(formatted.contains("↓2.5k") || formatted.contains("↓2500"));
756 }
757
758 #[test]
759 fn test_footer_data_format_context_window() {
760 let mut data = FooterData::new();
761 data.set_context_window_pct(75.5);
762 assert_eq!(data.format_context_window(), "75.5%");
763 }
764
765 #[test]
766 fn test_footer_data_has_data() {
767 let mut data = FooterData::new();
768 assert!(!data.has_data());
769 data.model_name = "gpt-4".to_string();
770 assert!(data.has_data());
771 }
772
773 #[test]
774 fn test_footer_data_render_lines() {
775 let mut data = FooterData::new();
776 data.model_name = "gpt-4".to_string();
777 data.provider_name = "openai".to_string();
778 data.update_tokens(100, 50);
779 data.set_total_cost(0.01);
780 data.pwd = Some("~/projects".to_string());
781
782 let lines = data.render_lines(80);
783 assert!(lines.len() >= 2);
784 assert!(lines[0].contains("~/projects"));
785 assert!(lines[1].contains("gpt-4"));
786 }
787
788 #[test]
789 fn test_footer_data_extension_status() {
790 let mut data = FooterData::new();
791 data.set_extension_status("ext1", Some("working"));
792 assert_eq!(data.extension_statuses.get("ext1"), Some(&"working".to_string()));
793 data.set_extension_status("ext1", None);
794 assert!(data.extension_statuses.get("ext1").is_none());
795 }
796
797 #[test]
798 fn test_footer_data_set_context_window_pct() {
799 let mut data = FooterData::new();
800 data.set_context_window_pct(150.0); assert_eq!(data.context_window_pct, 100.0);
802 data.set_context_window_pct(-10.0); assert_eq!(data.context_window_pct, 0.0);
804 }
805
806 #[test]
807 fn test_footer_data_set_total_cost() {
808 let mut data = FooterData::new();
809 data.set_total_cost(1.234);
810 assert_eq!(data.total_cost, 1.234);
811 }
812
813 #[test]
814 fn test_footer_data_set_session_duration() {
815 let mut data = FooterData::new();
816 data.set_session_duration(3600);
817 assert_eq!(data.session_duration_secs, 3600);
818 }
819
820 #[test]
821 fn test_session_timer() {
822 let timer = SessionTimer::new();
823 std::thread::sleep(Duration::from_millis(10));
824 let elapsed = timer.elapsed();
825 assert!(elapsed.as_millis() >= 10);
826 }
827
828 #[test]
829 fn test_session_timer_reset() {
830 let mut timer = SessionTimer::new();
831 std::thread::sleep(Duration::from_millis(10));
832 timer.reset();
833 let elapsed = timer.elapsed();
834 assert!(elapsed.as_millis() < 10);
835 }
836
837 #[test]
838 fn test_format_duration() {
839 assert_eq!(format_duration(Duration::from_secs(30)), "30s");
840 assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
841 assert_eq!(format_duration(Duration::from_secs(3661)), "1h 1m");
842 assert_eq!(format_duration(Duration::from_secs(86401)), "1d 0h");
843 }
844
845 #[test]
846 fn test_cost_estimator() {
847 let estimator = CostEstimator::new();
848
849 let cost = estimator.estimate("claude-3.5-sonnet", 1_000_000, 1_000_000);
851 assert!(cost.is_some());
852 assert!(cost.unwrap() > 0.0);
853
854 let cost = estimator.estimate("unknown-model", 1000, 500);
856 assert!(cost.is_none());
857 }
858
859 #[test]
860 fn test_cost_estimator_gpt4() {
861 let estimator = CostEstimator::new();
862 let cost = estimator.estimate("gpt-4-turbo", 500_000, 200_000);
863 assert!(cost.is_some());
864 let val = cost.unwrap();
865 assert!((val - 27.0).abs() < 0.1);
868 }
869
870 #[test]
871 fn test_keybinding_hint() {
872 let hint = KeybindingHint::new("Ctrl+C", "Cancel");
873 assert_eq!(hint.keys, "Ctrl+C");
874 assert_eq!(hint.description, "Cancel");
875 }
876
877 #[test]
878 fn test_simple_provider() {
879 let provider = SimpleFooterDataProvider::new()
880 .with_model(Some("gpt-4".to_string()), Some("openai".to_string()))
881 .with_tokens(100, 50);
882
883 assert_eq!(provider.get_model_name(), Some("gpt-4".to_string()));
884 assert_eq!(provider.get_token_counts(), (100, 50));
885 }
886
887 #[test]
888 fn test_simple_provider_update() {
889 let mut provider = SimpleFooterDataProvider::new();
890 provider.update_tokens(200, 100);
891 provider.update_cache_tokens(500, 50);
892 assert_eq!(provider.get_token_counts(), (200, 100));
893 }
894
895 #[test]
896 fn test_simple_provider_footer_data() {
897 let provider = SimpleFooterDataProvider::new()
898 .with_model(Some("claude".to_string()), Some("anthropic".to_string()))
899 .with_tokens(1000, 500);
900
901 let footer = provider.get_footer_data();
902 assert_eq!(footer.model_name, "claude");
903 assert!(footer.total_cost > 0.0);
904 }
905
906 #[test]
907 fn test_extension_status_tracker() {
908 let mut tracker = ExtensionStatusTracker::new();
909
910 tracker.set("my-extension", "Working...");
911 assert_eq!(tracker.get_all().get("my-extension"), Some(&"Working...".to_string()));
912
913 tracker.clear("my-extension");
914 assert!(tracker.get_all().get("my-extension").is_none());
915 }
916
917 #[test]
918 fn test_simple_provider_thinking_level() {
919 let mut provider = SimpleFooterDataProvider::new();
920 provider.set_thinking_level("high");
921 let footer = provider.get_footer_data();
922 assert_eq!(footer.thinking_level, "high");
923 }
924
925 #[test]
926 fn test_simple_provider_session_name() {
927 let mut provider = SimpleFooterDataProvider::new();
928 provider.set_session_name(Some("my-session".to_string()));
929 let footer = provider.get_footer_data();
930 assert_eq!(footer.session_name, Some("my-session".to_string()));
931 }
932}