1use chrono::{DateTime, Utc};
13use ratatui::Frame;
14use ratatui::layout::{Constraint, Layout, Rect};
15use ratatui::style::{Modifier, Style};
16use ratatui::text::{Line, Span};
17use ratatui::widgets::Paragraph;
18use ratatui_bubbletea_components::{Progress, Spinner, SpinnerFrames};
19use ratatui_bubbletea_theme::BubbleTheme;
20
21use crate::balance::{self, DisplayPrefs, MetricHeadline};
22use crate::countdown;
23use crate::format::{
24 clamp_pct, cursor_credit_label, cursor_credit_meter, local_time_hms, money, reset_credit_lines,
25 usd,
26};
27use crate::pacing::{self, PaceSeverity};
28use crate::pango::severity_for;
29use crate::theme::Theme;
30use crate::tui::app::TabState;
31use crate::tui::style::{bubble_theme, color, progress_theme, severity_color};
32use crate::usage::VendorSnapshot;
33
34pub enum Section {
37 Title { left: String, right: Option<String> },
42 Metric {
44 label: String,
45 pct: u16,
46 severity: PaceSeverity,
47 value_label: String,
48 footnote: String,
49 },
50 Text { label: String, value: String },
52 Block { label: String, body: Vec<String> },
54 Spacer,
56}
57
58pub(crate) struct SectionProjection {
63 pub section: Section,
64 pub reset_at: Option<DateTime<Utc>>,
65 pub window: Option<chrono::Duration>,
70 pub group: Option<&'static str>,
75 pub model_pool: bool,
80 pub headline: MetricHeadline,
87 pub used_cents: Option<i64>,
92 pub limit_cents: Option<i64>,
95 pub regen: Option<String>,
100 pub percent_precise: Option<f64>,
105 pub reading: Option<&'static str>,
110}
111
112struct SectionBuilder(Vec<SectionProjection>);
113
114impl SectionBuilder {
115 fn projection(section: Section) -> SectionProjection {
116 SectionProjection {
117 section,
118 reset_at: None,
119 window: None,
120 group: None,
121 model_pool: false,
122 headline: MetricHeadline::Percent,
123 used_cents: None,
124 limit_cents: None,
125 regen: None,
126 percent_precise: None,
127 reading: None,
128 }
129 }
130
131 fn new(sections: Vec<Section>) -> Self {
132 Self(
133 sections
134 .into_iter()
135 .map(|section| {
136 assert!(
137 !matches!(section, Section::Metric { .. }),
138 "metric sections must declare reset metadata with push_metric"
139 );
140 Self::projection(section)
141 })
142 .collect(),
143 )
144 }
145
146 fn push(&mut self, section: Section) {
147 assert!(
148 !matches!(section, Section::Metric { .. }),
149 "metric sections must declare reset metadata with push_metric"
150 );
151 self.0.push(Self::projection(section));
152 }
153
154 fn push_metric(&mut self, section: Section, reset_at: Option<DateTime<Utc>>) {
157 assert!(matches!(section, Section::Metric { .. }));
158 let mut row = Self::projection(section);
159 row.reset_at = reset_at;
160 self.0.push(row);
161 }
162
163 fn regen_last_metric(&mut self, amount: String) {
170 let Some(last) = self.0.last_mut() else {
171 return;
172 };
173 last.regen = Some(amount.clone());
174 last.reading = Some("left");
175 if let Section::Metric { footnote, .. } = &mut last.section
176 && let Some(rest) = footnote.strip_prefix("Resets in ")
177 {
178 *footnote = format!("Regenerates {amount} in {rest}");
179 }
180 }
181
182 fn precise_last_metric(&mut self, pct: f64) {
187 let Some(last) = self.0.last_mut() else {
188 return;
189 };
190 last.percent_precise = Some(pct);
191 if let Section::Metric { value_label, .. } = &mut last.section {
192 *value_label = format_precise_pct(pct);
193 }
194 }
195
196 fn push_metric_with_headline(
201 &mut self,
202 section: Section,
203 reset_at: Option<DateTime<Utc>>,
204 headline: MetricHeadline,
205 ) {
206 assert!(matches!(section, Section::Metric { .. }));
207 let mut row = Self::projection(section);
208 row.reset_at = reset_at;
209 row.headline = headline;
210 self.0.push(row);
211 }
212
213 fn push_metric_in_window(
216 &mut self,
217 section: Section,
218 reset_at: Option<DateTime<Utc>>,
219 window: chrono::Duration,
220 ) {
221 assert!(matches!(section, Section::Metric { .. }));
222 let mut row = Self::projection(section);
223 row.reset_at = reset_at;
224 row.window = Some(window);
225 self.0.push(row);
226 }
227
228 fn push_metric_in_group(&mut self, section: Section, group: &'static str) {
233 assert!(matches!(section, Section::Metric { .. }));
234 let mut row = Self::projection(section);
235 row.group = Some(group);
236 self.0.push(row);
237 }
238 fn mark_model_pool(&mut self) {
241 let row = self.0.last_mut().expect("a metric was just pushed");
242 assert!(matches!(row.section, Section::Metric { .. }));
243 row.model_pool = true;
244 }
245
246 fn push_on_demand(&mut self, used_cents: i64, limit_cents: Option<i64>) {
251 let value = match limit_cents {
252 Some(limit) => format!(
253 "{} / {}",
254 crate::usage::fmt_minor(used_cents, 2, Some("USD")),
255 crate::usage::fmt_minor(limit, 2, Some("USD"))
256 ),
257 None => crate::usage::fmt_minor(used_cents, 2, Some("USD")),
258 };
259 let mut row = Self::projection(Section::Text {
260 label: "On-Demand".into(),
261 value,
262 });
263 row.used_cents = Some(used_cents);
264 row.limit_cents = limit_cents.filter(|limit| *limit > 0);
265 self.0.push(row);
266 }
267}
268
269fn push_balance_headline(
282 v: &mut SectionBuilder,
283 label: &str,
284 remaining: f64,
285 currency: &str,
286 money_severity: PaceSeverity,
287 api_limit: Option<f64>,
288 prefs: DisplayPrefs,
289) {
290 let amount = money(remaining, currency);
291 let Some(limit) = balance::denominator(api_limit, prefs.display_limit) else {
292 v.push(Section::Text {
295 label: label.into(),
296 value: amount,
297 });
298 return;
299 };
300
301 let pct = balance::consumed_pct(limit, remaining);
302 let headline = balance::resolve_headline(prefs.headline, Some(limit));
303 let of_limit = money(limit, currency);
304 let (value_label, footnote) = match headline {
305 MetricHeadline::Percent => (
306 format!("{pct}%"),
307 format!("{amount} of {of_limit} left ({pct}% used)"),
308 ),
309 MetricHeadline::Value => (
310 amount.clone(),
311 format!("{pct}% of {of_limit} used ({amount} left)"),
312 ),
313 };
314 v.push_metric_with_headline(
315 Section::Metric {
316 label: label.into(),
317 pct,
318 severity: money_severity.max(severity_for(i32::from(pct))),
319 value_label,
320 footnote,
321 },
322 None,
323 headline,
324 );
325}
326
327pub fn compact_cells(snapshot: &VendorSnapshot) -> (String, Vec<(String, PaceSeverity)>) {
333 let pct = |label: &str, p: i32| (format!("{label} {p}%"), severity_for(p));
334 let usd_cell = |v: f64| (usd(v), PaceSeverity::Low);
337 let money_cell = |v: f64, c: &str| (money(v, c), PaceSeverity::Low);
338 let (plan, mut cells) = match snapshot {
339 VendorSnapshot::Anthropic(s) => {
340 let mut cells = vec![
341 pct("S", s.session.utilization_pct),
342 pct("W", s.weekly.utilization_pct),
343 ];
344 if let Some(sonnet) = &s.sonnet {
345 cells.push(pct("Son", sonnet.utilization_pct));
346 }
347 (s.plan.clone(), cells)
348 }
349 VendorSnapshot::AnthropicApi(s) => {
350 let cell = match s.pct() {
351 Some(p) => pct("spend", p),
352 None => (format!("{}/mo", usd(s.spent)), PaceSeverity::Low),
353 };
354 (String::new(), vec![cell])
355 }
356 VendorSnapshot::Openai(s) => {
357 let mut cells = Vec::new();
358 if let Some(w) = &s.session {
359 cells.push(pct("5h", w.utilization_pct));
360 }
361 if let Some(w) = &s.weekly {
362 cells.push(pct("7d", w.utilization_pct));
363 }
364 if cells.is_empty() {
365 cells.push(("—".into(), PaceSeverity::Low));
366 }
367 (s.plan.clone(), cells)
368 }
369 VendorSnapshot::MuseCode(s) => {
370 let mut cells = Vec::new();
371 if let Some(window) = &s.session {
372 cells.push(pct("S", window.utilization_pct));
373 }
374 if let Some(window) = &s.weekly {
375 cells.push(pct("W", window.utilization_pct));
376 }
377 (s.plan.clone(), cells)
378 }
379 VendorSnapshot::Copilot(s) => (
380 s.plan.clone(),
381 s.quotas()
382 .map(|(label, quota)| pct(label, quota.used_pct()))
383 .collect(),
384 ),
385 VendorSnapshot::Zai(s) => {
386 let mut cells = Vec::new();
387 if let Some(w) = &s.session {
388 cells.push(pct("S", w.utilization_pct));
389 }
390 if let Some(w) = &s.weekly {
391 cells.push(pct("W", w.utilization_pct));
392 }
393 if cells.is_empty() {
394 cells.push(("—".into(), PaceSeverity::Low));
395 }
396 (s.plan.clone(), cells)
397 }
398 VendorSnapshot::Openrouter(s) => (String::new(), vec![usd_cell(s.balance())]),
399 VendorSnapshot::OrcaRouter(s) => (
400 String::new(),
401 vec![match s.remaining_usd() {
402 Some(remaining) => usd_cell(remaining),
403 None => usd_cell(s.spent_usd()),
404 }],
405 ),
406 VendorSnapshot::Deepseek(s) => (String::new(), vec![money_cell(s.balance, &s.currency)]),
407 VendorSnapshot::Deepinfra(s) => (
408 String::new(),
409 vec![(usd(s.balance), crate::deepinfra::vendor::severity(s))],
410 ),
411 VendorSnapshot::Kimi(s) => {
412 let mut cells = vec![pct("5h", s.window_pct())];
413 if s.has_weekly {
414 cells.push(pct("wk", s.weekly_pct()));
415 }
416 if let Some(monthly) = s.monthly_pct {
417 cells.push(pct("mo", monthly));
418 }
419 (s.plan.clone().unwrap_or_default(), cells)
420 }
421 VendorSnapshot::Kilo(s) => (String::new(), vec![usd_cell(s.balance)]),
422 VendorSnapshot::Novita(s) => (String::new(), vec![usd_cell(s.available)]),
423 VendorSnapshot::Synthetic(s) => {
424 (String::new(), vec![pct("week", s.weekly.utilization_pct)])
425 }
426 VendorSnapshot::Lyceum(s) => (
427 String::new(),
428 vec![(usd(s.available_credits), PaceSeverity::Low)],
429 ),
430 VendorSnapshot::Moonshot(s) => (String::new(), vec![money_cell(s.available, &s.currency)]),
431 VendorSnapshot::Grok(s) => (String::new(), vec![usd_cell(s.balance)]),
432 VendorSnapshot::SuperGrok(s) => (s.plan.clone(), vec![pct(s.period.short(), s.weekly_pct)]),
433 VendorSnapshot::Grokbot(s) => {
434 let cells = if s.has_included_allowance {
436 vec![pct("wk", s.weekly_pct)]
437 } else {
438 vec![("—".into(), PaceSeverity::Low)]
439 };
440 (s.display_plan().to_string(), cells)
441 }
442 VendorSnapshot::ModelStudio(s) => {
443 let cells = [
445 ("5h", s.session.as_ref()),
446 ("wk", s.weekly.as_ref()),
447 ("mo", s.monthly.as_ref()),
448 ]
449 .into_iter()
450 .filter_map(|(label, window)| window.map(|w| pct(label, w.utilization_pct)))
451 .collect::<Vec<_>>();
452 (
453 crate::vendor::VendorId::ModelStudio
454 .display_name()
455 .to_string(),
456 cells,
457 )
458 }
459 VendorSnapshot::Devin(s) => {
460 let mut cells = [("D", s.daily.as_ref()), ("W", s.weekly.as_ref())]
461 .into_iter()
462 .filter_map(|(label, window)| window.map(|w| pct(label, w.utilization_pct)))
463 .collect::<Vec<_>>();
464 if let Some(balance) = s.overage_balance_micros {
465 cells.push((
466 crate::usage::fmt_minor(balance, crate::devin::vendor::BALANCE_DECIMALS, None),
467 PaceSeverity::Low,
468 ));
469 }
470 (String::new(), cells)
473 }
474 VendorSnapshot::Droid(s) => {
475 let mut cells = [
476 ("5h", s.session.as_ref()),
477 ("wk", s.weekly.as_ref()),
478 ("mo", s.monthly.as_ref()),
479 ]
480 .into_iter()
481 .filter_map(|(label, window)| window.map(|w| pct(label, w.utilization_pct)))
482 .collect::<Vec<_>>();
483 if let Some(balance) = s.extra_balance_cents {
484 cells.push((
485 crate::droid::vendor::extra_balance(balance),
486 PaceSeverity::Low,
487 ));
488 }
489 (String::new(), cells)
492 }
493 VendorSnapshot::Antigravity(s) => (
494 s.plan.clone(),
495 [
496 s.session.as_ref().map(|w| pct("S", w.utilization_pct)),
497 s.weekly.as_ref().map(|w| pct("W", w.utilization_pct)),
498 ]
499 .into_iter()
500 .flatten()
501 .collect(),
502 ),
503 VendorSnapshot::Cursor(s) => (
504 s.plan.clone(),
505 vec![pct("auto", s.auto_pct), pct("premium", s.api_pct)],
506 ),
507 VendorSnapshot::Minimax(s) => (
508 s.plan.clone(),
509 vec![
510 pct("S", s.session.utilization_pct),
511 pct("W", s.weekly.utilization_pct),
512 ],
513 ),
514 VendorSnapshot::Kiro(s) => (s.plan.clone(), vec![pct("credits", s.pct())]),
515 VendorSnapshot::NousResearch(s) => {
516 let cell = s
517 .usage_percent()
518 .map(|value| pct("usage", i32::from(clamp_pct(value))))
519 .unwrap_or_else(|| ("—".into(), PaceSeverity::Low));
520 (s.plan.clone().unwrap_or_default(), vec![cell])
521 }
522 VendorSnapshot::CommandCode(s) => {
523 let cells = [
524 ("session", s.five_hour.as_ref()),
525 ("weekly", s.weekly.as_ref()),
526 ("monthly", s.monthly_window().as_ref()),
527 ]
528 .into_iter()
529 .filter_map(|(label, window)| window.map(|window| pct(label, window.pct())))
530 .collect();
531 (s.plan.clone().unwrap_or_default(), cells)
532 }
533 VendorSnapshot::Cline(s) => {
534 let cells = s
535 .windows()
536 .into_iter()
537 .filter_map(|(label, window)| window.map(|window| pct(label, window.pct())))
538 .collect();
539 (s.plan.clone().unwrap_or_else(|| "Cline".into()), cells)
540 }
541 VendorSnapshot::OpenCodeGo(s) => {
542 let cells = [
543 ("rolling", s.rolling.as_ref()),
544 ("weekly", s.weekly.as_ref()),
545 ("monthly", s.monthly.as_ref()),
546 ]
547 .into_iter()
548 .filter_map(|(label, window)| {
549 window.map(|window| pct(label, i32::from(clamp_pct(window.percent))))
550 })
551 .collect();
552 ("OpenCode Go".into(), cells)
553 }
554 VendorSnapshot::Ollama(s) => {
555 let cells = [
556 ("5h", s.session.as_ref()),
557 ("wk", s.weekly.as_ref()),
558 ("mo", s.monthly.as_ref()),
559 ]
560 .into_iter()
561 .filter_map(|(label, window)| {
562 window.map(|window| pct(label, window.utilization_pct.clamp(0, 100)))
563 })
564 .collect();
565 (s.plan.clone(), cells)
566 }
567 VendorSnapshot::Custom(s) => (
568 s.plan.clone().unwrap_or_default(),
569 s.metrics
570 .iter()
571 .take(3)
572 .map(|metric| pct(&metric.label, i32::from(metric.pct)))
573 .collect(),
574 ),
575 };
576
577 for (text, _) in &mut cells {
578 *text = crate::display::sanitize_untrusted_field(text);
579 }
580 (crate::display::sanitize_untrusted_field(&plan), cells)
581}
582
583pub fn headline_pct(snapshot: &VendorSnapshot) -> Option<i32> {
588 match snapshot {
589 VendorSnapshot::Anthropic(s) => [
590 Some(s.session.utilization_pct),
591 Some(s.weekly.utilization_pct),
592 s.sonnet.as_ref().map(|w| w.utilization_pct),
593 ]
594 .into_iter()
595 .flatten()
596 .max(),
597 VendorSnapshot::AnthropicApi(s) => s.pct(),
598 VendorSnapshot::Openai(s) => [
599 s.session.as_ref().map(|w| w.utilization_pct),
600 s.weekly.as_ref().map(|w| w.utilization_pct),
601 ]
602 .into_iter()
603 .flatten()
604 .max(),
605 VendorSnapshot::Copilot(s) => s.quotas().map(|(_, quota)| quota.used_pct()).max(),
606 VendorSnapshot::Zai(s) => [
607 s.session.as_ref().map(|w| w.utilization_pct),
608 s.weekly.as_ref().map(|w| w.utilization_pct),
609 ]
610 .into_iter()
611 .flatten()
612 .max(),
613 VendorSnapshot::Kimi(s) => Some(s.worst_pct()),
614 VendorSnapshot::Antigravity(s) => [
615 s.session.as_ref().map(|w| w.utilization_pct),
616 s.weekly.as_ref().map(|w| w.utilization_pct),
617 s.third_party_session.as_ref().map(|w| w.utilization_pct),
618 s.third_party_weekly.as_ref().map(|w| w.utilization_pct),
619 ]
620 .into_iter()
621 .flatten()
622 .chain(s.model_pools.iter().map(|p| p.window.utilization_pct))
623 .max(),
624 VendorSnapshot::Cursor(s) => (!s.unlimited).then_some(s.total_pct),
625 VendorSnapshot::Minimax(s) => Some(s.session.utilization_pct.max(s.weekly.utilization_pct)),
626 VendorSnapshot::Kiro(s) => Some(s.pct()),
627 VendorSnapshot::NousResearch(s) => {
628 s.usage_percent().map(|value| i32::from(clamp_pct(value)))
629 }
630 VendorSnapshot::CommandCode(s) => {
631 let worst = s.worst_pct();
632 (s.five_hour.is_some() || s.weekly.is_some()).then_some(worst)
633 }
634 VendorSnapshot::Cline(s) => s.worst_pct(),
635 VendorSnapshot::MuseCode(s) => [s.session.as_ref(), s.weekly.as_ref()]
636 .into_iter()
637 .flatten()
638 .map(|window| window.utilization_pct)
639 .max(),
640 VendorSnapshot::Synthetic(s) => Some(s.weekly.utilization_pct),
641 VendorSnapshot::OpenCodeGo(s) => [
642 s.rolling
643 .as_ref()
644 .map(|window| window.percent.round() as i32),
645 s.weekly
646 .as_ref()
647 .map(|window| window.percent.round() as i32),
648 s.monthly
649 .as_ref()
650 .map(|window| window.percent.round() as i32),
651 ]
652 .into_iter()
653 .flatten()
654 .max(),
655 VendorSnapshot::SuperGrok(s) => Some(s.weekly_pct),
656 VendorSnapshot::Grokbot(s) => s.has_included_allowance.then_some(s.weekly_pct),
657 VendorSnapshot::ModelStudio(s) => {
658 [s.session.as_ref(), s.weekly.as_ref(), s.monthly.as_ref()]
659 .into_iter()
660 .flatten()
661 .map(|w| w.utilization_pct)
662 .max()
663 }
664 VendorSnapshot::Devin(s) => [
665 s.daily.as_ref().map(|window| window.utilization_pct),
666 s.weekly.as_ref().map(|window| window.utilization_pct),
667 ]
668 .into_iter()
669 .flatten()
670 .max(),
671 VendorSnapshot::Droid(s) => crate::droid::vendor::headline_utilization(s),
672 VendorSnapshot::Ollama(s) => [
673 s.session.as_ref().map(|w| w.utilization_pct),
674 s.weekly.as_ref().map(|w| w.utilization_pct),
675 s.monthly.as_ref().map(|w| w.utilization_pct),
676 ]
677 .into_iter()
678 .flatten()
679 .max(),
680 VendorSnapshot::Custom(s) => s.metrics.first().map(|metric| i32::from(metric.pct)),
681 VendorSnapshot::Openrouter(_)
682 | VendorSnapshot::OrcaRouter(_)
683 | VendorSnapshot::Deepseek(_)
684 | VendorSnapshot::Deepinfra(_)
685 | VendorSnapshot::Kilo(_)
686 | VendorSnapshot::Novita(_)
687 | VendorSnapshot::Lyceum(_)
688 | VendorSnapshot::Moonshot(_)
689 | VendorSnapshot::Grok(_) => None,
690 }
691}
692
693pub fn sections_for(tab: &TabState, now: DateTime<Utc>, pace_tolerance: u32) -> Vec<Section> {
695 sections_with_metadata_for(tab, now, pace_tolerance)
696 .into_iter()
697 .map(|projected| projected.section)
698 .collect()
699}
700
701pub(crate) fn sections_with_metadata_for(
704 tab: &TabState,
705 now: DateTime<Utc>,
706 pace_tolerance: u32,
707) -> Vec<SectionProjection> {
708 let mut sections = match tab {
709 TabState::Loading => SectionBuilder::new(vec![
710 Section::Spacer,
711 Section::Text {
712 label: "".into(),
713 value: " Loading…".into(),
714 },
715 ]),
716 TabState::Error { message: e, plan } => {
717 let mut rows = Vec::new();
718 if let Some(plan) = plan {
719 rows.push(Section::Title {
720 left: plan.clone(),
721 right: None,
722 });
723 }
724 rows.extend([
725 Section::Spacer,
726 Section::Text {
727 label: "Error".into(),
728 value: e.clone(),
729 },
730 Section::Spacer,
731 Section::Text {
732 label: "".into(),
733 value: "Press `r` to retry, `q` to quit.".into(),
734 },
735 ]);
736 SectionBuilder::new(rows)
737 }
738 TabState::Ready(r) => {
739 let snapshot = &r.snapshot;
740 let last_error = &r.last_error;
741 let prefs = r.display;
742 let mut sections = match snapshot {
743 VendorSnapshot::Anthropic(s) => anthropic_sections(s, now, pace_tolerance),
744 VendorSnapshot::AnthropicApi(s) => anthropic_api_sections(s),
745 VendorSnapshot::Openai(s) => openai_sections(s, now, pace_tolerance),
746 VendorSnapshot::Copilot(s) => copilot_sections(s, now),
747 VendorSnapshot::Zai(s) => zai_sections(s, now, pace_tolerance),
748 VendorSnapshot::Openrouter(s) => openrouter_sections(s, prefs),
749 VendorSnapshot::OrcaRouter(s) => orcarouter_sections(s, now),
750 VendorSnapshot::Deepseek(s) => deepseek_sections(s, prefs),
751 VendorSnapshot::Deepinfra(s) => deepinfra_sections(s, prefs),
752 VendorSnapshot::Kimi(s) => kimi_sections(s, now, pace_tolerance),
753 VendorSnapshot::Kilo(s) => kilo_sections(s, prefs),
754 VendorSnapshot::Novita(s) => novita_sections(s, prefs),
755 VendorSnapshot::Synthetic(s) => synthetic_sections(s, now, pace_tolerance),
756 VendorSnapshot::Lyceum(s) => lyceum_sections(s),
757 VendorSnapshot::Moonshot(s) => moonshot_sections(s, prefs),
758 VendorSnapshot::Grok(s) => grok_sections(s, prefs),
759 VendorSnapshot::SuperGrok(s) => supergrok_sections(s, now, pace_tolerance),
760 VendorSnapshot::Grokbot(s) => grokbot_sections(s, now, pace_tolerance),
761 VendorSnapshot::ModelStudio(s) => modelstudio_sections(s, now, pace_tolerance),
762 VendorSnapshot::Devin(s) => devin_sections(s, now, pace_tolerance),
763 VendorSnapshot::Droid(s) => droid_sections(s, now, pace_tolerance),
764 VendorSnapshot::MuseCode(s) => muse_sections(s, now, pace_tolerance),
765 VendorSnapshot::Antigravity(s) => antigravity_sections(s, now),
766 VendorSnapshot::Cursor(s) => cursor_sections(s, now, pace_tolerance),
767 VendorSnapshot::Minimax(s) => minimax_sections(s, now, pace_tolerance),
768 VendorSnapshot::Kiro(s) => kiro_sections(s, now),
769 VendorSnapshot::NousResearch(s) => nous_sections(s, now, prefs),
770 VendorSnapshot::OpenCodeGo(s) => opencode_go_sections(s, now, pace_tolerance),
771 VendorSnapshot::CommandCode(s) => commandcode_sections(s, now),
772 VendorSnapshot::Ollama(s) => ollama_sections(s, now, pace_tolerance),
773 VendorSnapshot::Custom(s) => custom_sections(s),
774 VendorSnapshot::Cline(s) => cline_sections(s, now),
775 };
776 let updated = match r.fetched_at {
780 Some(at) => format!("Updated {}", local_time_hms(at)),
781 None => "Updated —".to_string(),
782 };
783 if let Some(SectionProjection {
784 section: Section::Title { right, .. },
785 ..
786 }) = sections.0.first_mut()
787 {
788 *right = Some(updated);
789 }
790 if let Some((label, msg)) = warning_label(snapshot, last_error) {
792 sections.push(Section::Spacer);
793 sections.push(Section::Text { label, value: msg });
794 }
795 sections
796 }
797 };
798 for projected in &mut sections.0 {
799 sanitize_section(&mut projected.section);
800 }
801 sections.0
802}
803
804fn sanitize_section(section: &mut Section) {
807 let clean = |value: &mut String| {
808 *value = crate::display::sanitize_untrusted_field(value);
809 };
810 match section {
811 Section::Title { left, right } => {
812 clean(left);
813 if let Some(right) = right {
814 clean(right);
815 }
816 }
817 Section::Metric {
818 label,
819 value_label,
820 footnote,
821 ..
822 } => {
823 clean(label);
824 clean(value_label);
825 clean(footnote);
826 }
827 Section::Text { label, value } => {
828 clean(label);
829 clean(value);
830 }
831 Section::Block { label, body } => {
832 clean(label);
833 for line in body {
834 clean(line);
835 }
836 }
837 Section::Spacer => {}
838 }
839}
840
841fn warning_label(
845 snapshot: &VendorSnapshot,
846 last_error: &Option<(u16, String)>,
847) -> Option<(String, String)> {
848 let (code, message) = last_error.as_ref()?;
849 if *code != 0 {
850 return Some((format!("HTTP {code}"), message.clone()));
851 }
852 if message.is_empty() {
853 return None;
854 }
855 let label = if matches!(snapshot, VendorSnapshot::Kimi(_))
856 && matches!(
857 crate::kimi::vendor::warning_kind(*code, message),
858 crate::kimi::vendor::WarningKind::SchemaDrift
859 ) {
860 "Kimi API schema drift"
861 } else {
862 "Warning"
863 };
864 let value = if label == message {
867 String::new()
868 } else {
869 message.clone()
870 };
871 Some((label.into(), value))
872}
873
874fn anthropic_api_sections(s: &crate::usage::AnthropicApiSnapshot) -> SectionBuilder {
875 let mut v = SectionBuilder::new(vec![Section::Title {
876 left: "Anthropic API".into(),
877 right: None,
878 }]);
879 match (s.limit.filter(|l| *l > 0.0), s.pct()) {
884 (Some(limit), Some(pct)) => {
885 let p = pct.clamp(0, 100) as u16;
886 v.push_metric(
887 Section::Metric {
888 label: "Spend (mo)".into(),
889 pct: p,
890 severity: severity_for(pct),
891 value_label: format!("{} of ${:.0}", usd(s.spent), limit),
892 footnote: format!("{pct}% of monthly limit"),
893 },
894 None,
895 );
896 }
897 _ => {
898 v.push(Section::Text {
899 label: "Spend (mo)".into(),
900 value: usd(s.spent),
901 });
902 }
903 }
904 if let Some(day) = s.covered_through_day() {
905 v.push(Section::Text {
906 label: "Through".into(),
907 value: day,
908 });
909 }
910 v.push(Section::Spacer);
911 v.push(Section::Text {
912 label: "".into(),
913 value: "Only completed daily buckets from the Admin usage API.".into(),
914 });
915 v.push(Section::Text {
916 label: "".into(),
917 value: "Prepaid credit balance is Console-only (no API).".into(),
918 });
919 v.push(Section::Text {
920 label: "".into(),
921 value: "Excludes Priority Tier cost (not reported by this API).".into(),
922 });
923 v
924}
925
926fn anthropic_sections(
927 s: &crate::usage::AnthropicSnapshot,
928 now: DateTime<Utc>,
929 tol: u32,
930) -> SectionBuilder {
931 let mut v = SectionBuilder::new(vec![Section::Title {
932 left: format!("Claude {}", s.plan),
933 right: None,
934 }]);
935
936 push_window(&mut v, "Session (5h)", &s.session, now, tol, true);
937 push_window(&mut v, "Weekly (7d)", &s.weekly, now, tol, true);
938 if let Some(w) = &s.sonnet {
939 push_window(&mut v, "Sonnet only", w, now, tol, false);
940 }
941 for sw in &s.scoped {
942 push_window(
943 &mut v,
944 &format!("{} (7d)", sw.label),
945 &sw.window,
946 now,
947 tol,
948 false,
949 );
950 }
951 if let Some(e) = &s.extra {
952 v.push(Section::Spacer);
953 let pct = e.percent().clamp(0, 100) as u16;
954 let (value_label, mut footnote) = match e.fmt_limit() {
958 Some(l) => (
959 format!("{} of {}", e.fmt_spent(), l),
960 format!("{pct}% of monthly limit consumed"),
961 ),
962 None => (e.fmt_spent(), "no monthly limit reported".to_string()),
963 };
964 if let Some(note) = e.status_note() {
965 footnote.push_str(" · ");
966 footnote.push_str(note);
967 }
968 v.push_metric(
969 Section::Metric {
970 label: "Extra usage".into(),
971 pct,
972 severity: severity_for(pct as i32),
973 value_label,
974 footnote,
975 },
976 None,
977 );
978 }
979 if let Some(c) = &s.cloud_credits {
980 v.push(Section::Spacer);
981 let (pct, _, mut footnote) =
990 crate::format::cursor_credit_meter(c.remaining.0, c.limit.0, c.expires_at, now);
991 let value = crate::usage::fmt_minor(c.remaining.0, 2, Some("USD"));
992 if c.locked {
993 footnote.push_str(" · locked");
996 }
997 v.push_metric_with_headline(
1000 Section::Metric {
1001 label: "Cloud session credits".into(),
1002 pct,
1003 severity: severity_for(i32::from(pct)),
1004 value_label: value,
1005 footnote,
1006 },
1007 None,
1008 MetricHeadline::Value,
1009 );
1010 }
1011 if let Some(u) = &s.usage_credits {
1012 v.push(Section::Spacer);
1013 let mut body = vec![format!(
1014 "balance: {}",
1015 crate::usage::fmt_minor(u.balance.0, u.decimal_places, u.currency.as_deref())
1016 )];
1017 if let Some(auto_reload) = u.auto_reload {
1020 body.push(if auto_reload {
1021 "auto-reload: on".into()
1022 } else {
1023 "auto-reload: off".into()
1024 });
1025 }
1026 v.push(Section::Block {
1027 label: "Usage credits".into(),
1028 body,
1029 });
1030 }
1031 push_reset_credits(&mut v, &s.reset_credits, now);
1032 v
1033}
1034
1035fn openai_sections(
1036 s: &crate::usage::OpenAiSnapshot,
1037 now: DateTime<Utc>,
1038 tol: u32,
1039) -> SectionBuilder {
1040 let mut v = SectionBuilder::new(vec![Section::Title {
1041 left: s.plan.clone(),
1042 right: None,
1043 }]);
1044 if let Some(session) = &s.session {
1045 push_window(&mut v, "Codex 5h", session, now, tol, true);
1046 } else if s.weekly.is_some() {
1047 push_unreported_window(&mut v, "Codex 5h");
1048 }
1049 if let Some(weekly) = &s.weekly {
1050 push_window(&mut v, "Codex weekly", weekly, now, tol, true);
1051 } else if s.session.is_some() {
1052 push_unreported_window(&mut v, "Codex weekly");
1053 }
1054 if s.session.is_none() && s.weekly.is_none() {
1055 v.push(Section::Spacer);
1056 v.push(Section::Text {
1057 label: "".into(),
1058 value: " no usage windows reported".into(),
1059 });
1060 }
1061 if let Some(cr) = &s.code_review {
1062 push_window(&mut v, "Code review", cr, now, tol, false);
1063 }
1064 for limit in &s.additional_limits {
1067 if let Some(w) = &limit.session {
1068 push_window(&mut v, &format!("{} (5h)", limit.name), w, now, tol, false);
1069 }
1070 if let Some(w) = &limit.weekly {
1071 push_window(&mut v, &format!("{} (7d)", limit.name), w, now, tol, false);
1072 }
1073 }
1074 if !s.unavailable_models.is_empty() {
1077 v.push(Section::Spacer);
1078 v.push(Section::Block {
1079 label: "Unavailable".into(),
1080 body: s
1081 .unavailable_models
1082 .iter()
1083 .map(|m| match m.available_at {
1084 Some(at) => format!("{} — back {}", m.model, countdown::format(Some(at), now)),
1085 None => format!("{} — at capacity", m.model),
1086 })
1087 .collect(),
1088 });
1089 }
1090 if let Some(c) = &s.credits {
1091 v.push(Section::Spacer);
1092 let body = vec![crate::openai::vendor::credit_balance_label(c)];
1093 v.push(Section::Block {
1094 label: "Credits".into(),
1095 body,
1096 });
1097 }
1098 if let Some(credits) = &s.reset_credits {
1099 push_reset_block(&mut v, credits, now);
1100 }
1101 v
1102}
1103
1104fn push_unreported_window(sections: &mut SectionBuilder, label: &str) {
1105 sections.push(Section::Spacer);
1106 sections.push(Section::Text {
1107 label: label.into(),
1108 value: "Not reported".into(),
1109 });
1110}
1111
1112fn copilot_sections(s: &crate::copilot::types::Snapshot, now: DateTime<Utc>) -> SectionBuilder {
1113 let mut sections = SectionBuilder::new(vec![Section::Title {
1114 left: format!("GitHub Copilot {}", s.plan),
1115 right: None,
1116 }]);
1117 for (label, quota) in s.quotas() {
1118 let pct = quota.used_pct();
1119 let detail = if quota.unlimited {
1120 "Unlimited".to_string()
1121 } else {
1122 quota
1123 .used_and_entitlement()
1124 .map(|(used, entitlement)| format!("{used} of {entitlement} used"))
1125 .unwrap_or_else(|| format!("{}% remaining", quota.percent_remaining))
1126 };
1127 sections.push(Section::Spacer);
1128 sections.push_metric(
1129 Section::Metric {
1130 label: label.to_string(),
1131 pct: pct.clamp(0, 100) as u16,
1132 severity: severity_for(pct),
1133 value_label: if quota.unlimited {
1134 "Unlimited".to_string()
1135 } else {
1136 format!("{pct}%")
1137 },
1138 footnote: detail,
1139 },
1140 s.reset_at,
1141 );
1142 }
1143 sections.push(Section::Spacer);
1144 sections.push(Section::Text {
1145 label: "Resets".into(),
1146 value: countdown::format(s.reset_at, now),
1147 });
1148 sections
1149}
1150
1151fn zai_sections(s: &crate::usage::ZaiSnapshot, now: DateTime<Utc>, tol: u32) -> SectionBuilder {
1152 let mut v = SectionBuilder::new(vec![Section::Title {
1153 left: s.plan.clone(),
1154 right: None,
1155 }]);
1156 if let Some(w) = &s.session {
1157 push_window(&mut v, "Session (5h)", w, now, tol, true);
1158 }
1159 if let Some(w) = &s.weekly {
1160 push_window(&mut v, "Weekly", w, now, tol, true);
1161 }
1162 if let Some(w) = &s.mcp {
1163 push_window(&mut v, "MCP tools (monthly)", w, now, tol, true);
1164 }
1165 if s.session.is_none() && s.weekly.is_none() && s.mcp.is_none() {
1166 v.push(Section::Spacer);
1167 v.push(Section::Text {
1168 label: "".into(),
1169 value: " no usage windows reported".into(),
1170 });
1171 }
1172 v
1173}
1174
1175fn openrouter_sections(
1176 s: &crate::usage::OpenRouterSnapshot,
1177 prefs: DisplayPrefs,
1178) -> SectionBuilder {
1179 let mut v = SectionBuilder::new(vec![Section::Title {
1180 left: s.label.clone(),
1181 right: None,
1182 }]);
1183 let pct = s.consumed_pct().clamp(0, 100) as u16;
1184 let denominator = balance::denominator(Some(s.total_credits), None);
1193 v.push(Section::Spacer);
1194 v.push_metric_with_headline(
1195 Section::Metric {
1196 label: "Credit balance".into(),
1197 pct,
1198 severity: crate::openrouter::vendor::severity(s),
1202 value_label: usd(s.balance()),
1203 footnote: format!(
1204 "{} of {} used ({pct}%)",
1205 usd(s.total_usage),
1206 usd(s.total_credits)
1207 ),
1208 },
1209 None,
1210 balance::resolve_headline(prefs.headline, denominator),
1211 );
1212 v.push(Section::Spacer);
1213 v.push(Section::Block {
1214 label: "Usage by period".into(),
1215 body: vec![format!(
1216 "today ${:.2} · week ${:.2} · month ${:.2}",
1217 s.usage_daily, s.usage_weekly, s.usage_monthly
1218 )],
1219 });
1220 if let (Some(limit), Some(rem)) = (s.limit, s.limit_remaining) {
1221 v.push(Section::Spacer);
1222 v.push(Section::Block {
1223 label: "Per-key limit".into(),
1224 body: vec![format!("{} of {} remaining", usd(rem), usd(limit))],
1225 });
1226 }
1227 v.push(Section::Spacer);
1228 v.push(Section::Block {
1229 label: "Tier".into(),
1230 body: vec![if s.is_free_tier {
1231 "free tier".into()
1232 } else {
1233 "paid tier".into()
1234 }],
1235 });
1236 if !s.recent_models.is_empty() {
1237 v.push(Section::Spacer);
1238 v.push(Section::Block {
1239 label: "Recent models".into(),
1240 body: s.recent_models.clone(),
1241 });
1242 }
1243 v
1244}
1245
1246fn orcarouter_sections(s: &crate::usage::OrcaRouterSnapshot, now: DateTime<Utc>) -> SectionBuilder {
1251 let mut v = SectionBuilder::new(vec![
1252 Section::Title {
1253 left: "OrcaRouter".into(),
1254 right: None,
1255 },
1256 Section::Spacer,
1257 ]);
1258 match (s.limit_usd().filter(|l| *l > 0.0), s.consumed_pct()) {
1259 (Some(_), Some(pct)) => {
1260 let p = pct.clamp(0, 100) as u16;
1261 v.push_metric(
1262 Section::Metric {
1263 label: "Credit".into(),
1264 pct: p,
1265 severity: crate::orcarouter::vendor::severity(s),
1266 value_label: usd(s.remaining_usd().unwrap_or_default()),
1267 footnote: format!(
1268 "{} of {} used ({pct}%)",
1269 usd(s.spent_usd()),
1270 usd(s.limit_usd().unwrap_or_default())
1271 ),
1272 },
1273 s.access_until,
1276 );
1277 }
1278 _ => {
1279 v.push(Section::Text {
1280 label: "Spent".into(),
1281 value: usd(s.spent_usd()),
1282 });
1283 v.push(Section::Spacer);
1284 v.push(Section::Text {
1285 label: "".into(),
1286 value: "no credit limit on this key".into(),
1287 });
1288 }
1289 }
1290 if let Some(access_until) = s.access_until {
1291 v.push(Section::Spacer);
1292 v.push(Section::Text {
1293 label: "Key expires".into(),
1294 value: format!(
1295 "{} ({})",
1296 countdown::format(Some(access_until), now),
1297 crate::format::local_date_hm(access_until)
1298 ),
1299 });
1300 }
1301 v
1302}
1303
1304fn antigravity_sections(
1310 s: &crate::usage::AntigravitySnapshot,
1311 now: DateTime<Utc>,
1312) -> SectionBuilder {
1313 let mut v = SectionBuilder::new(vec![Section::Title {
1314 left: s.plan.clone(),
1315 right: None,
1316 }]);
1317 for (heading, weekly) in [("Session", false), ("Weekly", true)] {
1318 let rows = crate::antigravity::vendor::rows(s, weekly);
1319 if rows.is_empty() {
1322 continue;
1323 }
1324 v.push(Section::Spacer);
1325 v.push(Section::Text {
1326 label: heading.into(),
1327 value: String::new(),
1328 });
1329 for row in rows {
1330 push_window(&mut v, row.label, row.window, now, 5, false);
1331 if row.model_pool {
1332 v.mark_model_pool();
1333 }
1334 }
1335 }
1336 if s.source == crate::usage::AntigravitySource::Remote {
1339 v.push(Section::Spacer);
1340 v.push(Section::Text {
1341 label: "Source".into(),
1342 value: "Google API".into(),
1343 });
1344 }
1345 v
1346}
1347
1348fn push_cursor_pool(v: &mut SectionBuilder, section: Section, s: &crate::usage::CursorSnapshot) {
1352 match s.cycle_window() {
1353 Some(window) => v.push_metric_in_window(section, s.reset_at, window),
1354 None => v.push_metric(section, s.reset_at),
1355 }
1356}
1357
1358fn cursor_pool_footnote(
1363 base: String,
1364 pct: i32,
1365 s: &crate::usage::CursorSnapshot,
1366 now: DateTime<Utc>,
1367 tol: u32,
1368) -> String {
1369 let Some(window) = s.cycle_window() else {
1370 return base;
1371 };
1372 let pace = pacing::calc(pct, s.reset_at, now, window, tol);
1373 format!(
1374 "{base} · {}% elapsed · {}",
1375 pace.elapsed_pct, pace.point_label
1376 )
1377}
1378
1379fn cursor_sections(
1380 s: &crate::usage::CursorSnapshot,
1381 now: DateTime<Utc>,
1382 tol: u32,
1383) -> SectionBuilder {
1384 let mut v = SectionBuilder::new(vec![Section::Title {
1385 left: format!("Cursor {}", s.plan),
1386 right: None,
1387 }]);
1388 if s.unlimited {
1389 v.push(Section::Spacer);
1390 v.push(Section::Text {
1391 label: "Plan".into(),
1392 value: "Unlimited — pools don't cap".into(),
1393 });
1394 } else {
1395 v.push(Section::Spacer);
1397 push_cursor_pool(
1398 &mut v,
1399 Section::Metric {
1400 label: "Cursor Models".into(),
1401 pct: s.auto_pct.clamp(0, 100) as u16,
1402 severity: severity_for(s.auto_pct),
1403 value_label: format!("{}%", s.auto_pct),
1404 footnote: cursor_pool_footnote("Auto + Composer".into(), s.auto_pct, s, now, tol),
1405 },
1406 s,
1407 );
1408 v.push(Section::Spacer);
1409 push_cursor_pool(
1410 &mut v,
1411 Section::Metric {
1412 label: "Other Models".into(),
1413 pct: s.api_pct.clamp(0, 100) as u16,
1414 severity: severity_for(s.api_pct),
1415 value_label: format!("{}%", s.api_pct),
1416 footnote: cursor_pool_footnote(
1417 format!(
1418 "Named / API models · on-demand {}",
1419 if s.on_demand_enabled { "on" } else { "off" }
1420 ),
1421 s.api_pct,
1422 s,
1423 now,
1424 tol,
1425 ),
1426 },
1427 s,
1428 );
1429 if let Some(used) = s.on_demand_used_cents {
1430 v.push(Section::Spacer);
1431 v.push_on_demand(used, s.on_demand_limit_cents);
1432 }
1433 }
1434 for grant in &s.credits {
1435 let name = cursor_credit_label(&crate::display::sanitize_untrusted_line(
1436 grant.display_name.trim(),
1437 ));
1438 let (pct, value, footnote) = cursor_credit_meter(
1439 grant.remaining_cents,
1440 grant.total_cents,
1441 grant.expires_at,
1442 now,
1443 );
1444 v.push(Section::Spacer);
1445 v.push_metric_with_headline(
1448 Section::Metric {
1449 label: name,
1450 pct,
1451 severity: severity_for(i32::from(pct)),
1452 value_label: value,
1453 footnote,
1454 },
1455 None,
1456 MetricHeadline::Value,
1457 );
1458 }
1459 v.push(Section::Spacer);
1460 v.push(Section::Text {
1461 label: "Resets".into(),
1462 value: countdown::format(s.reset_at, now),
1463 });
1464 v
1465}
1466
1467fn nous_sections(
1468 s: &crate::nous::types::AccountSnapshot,
1469 now: DateTime<Utc>,
1470 prefs: DisplayPrefs,
1471) -> SectionBuilder {
1472 let mut sections = SectionBuilder::new(vec![Section::Title {
1473 left: "Nous Research".into(),
1474 right: None,
1475 }]);
1476 if let Some(value) = s.usage_percent() {
1477 let pct = clamp_pct(value);
1478 let balance = s
1483 .total_usable_credits
1484 .or(s.purchased_credits_remaining)
1485 .or(s.credits_remaining);
1486 let headline = match (prefs.headline, balance) {
1487 (crate::balance::Headline::Amount, Some(_)) => MetricHeadline::Value,
1488 _ => MetricHeadline::Percent,
1489 };
1490 let (label, value_label, footnote) = match headline {
1491 MetricHeadline::Value => (
1492 "Credits remaining".into(),
1493 usd(balance.unwrap_or_default()),
1494 "usable now".into(),
1495 ),
1496 MetricHeadline::Percent => ("Usage".into(), format!("{pct}%"), "current period".into()),
1497 };
1498 sections.push_metric_with_headline(
1499 Section::Metric {
1500 label,
1501 pct,
1502 severity: severity_for(i32::from(pct)),
1503 value_label,
1504 footnote,
1505 },
1506 s.current_period_end,
1507 headline,
1508 );
1509 }
1510 sections.push(Section::Spacer);
1511 if let Some(remaining) = s.credits_remaining {
1512 sections.push(Section::Text {
1513 label: "Subscription credits".into(),
1514 value: format!("{} remaining", usd(remaining)),
1515 });
1516 }
1517 if let Some(purchased) = s.purchased_credits_remaining {
1518 sections.push(Section::Text {
1519 label: "Top-up credits".into(),
1520 value: format!("{} remaining", usd(purchased)),
1521 });
1522 }
1523 if let Some(total_usable) = s.total_usable_credits {
1524 sections.push(Section::Text {
1525 label: "Total usable credits".into(),
1526 value: usd(total_usable),
1527 });
1528 }
1529 if let Some(period_end) = s.current_period_end {
1530 sections.push(Section::Text {
1531 label: "Renews".into(),
1532 value: countdown::format(Some(period_end), now),
1533 });
1534 }
1535 sections
1536}
1537
1538fn commandcode_sections(
1539 s: &crate::commandcode::types::Snapshot,
1540 now: DateTime<Utc>,
1541) -> SectionBuilder {
1542 let title = match s.plan.as_deref() {
1543 Some(plan) if !plan.is_empty() => format!("Command Code {plan}"),
1544 _ => "Command Code".to_string(),
1545 };
1546 let mut sections = SectionBuilder::new(vec![Section::Title {
1547 left: title,
1548 right: None,
1549 }]);
1550 for (label, window) in [
1551 ("Session (5h)", s.five_hour.as_ref()),
1552 ("Weekly", s.weekly.as_ref()),
1553 ("Monthly", s.monthly_window().as_ref()),
1554 ] {
1555 if let Some(window) = window {
1556 let pct = window.pct();
1557 sections.push_metric(
1558 Section::Metric {
1559 label: label.into(),
1560 pct: pct.clamp(0, 100) as u16,
1561 severity: severity_for(pct),
1562 value_label: format!("{pct}%"),
1563 footnote: format!("{} of {}", usd(window.used), usd(window.cap)),
1564 },
1565 window.resets_at,
1566 );
1567 sections.push(Section::Text {
1568 label: "Resets".into(),
1569 value: countdown::format(window.resets_at, now),
1570 });
1571 }
1572 }
1573 if let Some(credits) = s.credits.as_ref() {
1574 sections.push(Section::Spacer);
1575 sections.push(Section::Text {
1576 label: "Credits".into(),
1577 value: usd(credits.remaining()),
1578 });
1579 }
1580 sections
1581}
1582
1583fn cline_sections(s: &crate::cline::types::Snapshot, now: DateTime<Utc>) -> SectionBuilder {
1586 let mut sections = SectionBuilder::new(vec![Section::Title {
1587 left: s.plan.clone().unwrap_or_else(|| "Cline".into()),
1588 right: None,
1589 }]);
1590 let mut any = false;
1591 for (label, window) in [
1592 ("5 hours", s.five_hour.as_ref()),
1593 ("Weekly", s.seven_day.as_ref()),
1594 ("Monthly", s.thirty_day.as_ref()),
1595 ] {
1596 let Some(window) = window else {
1597 continue;
1598 };
1599 any = true;
1600 let pct = window.pct();
1601 sections.push_metric(
1602 Section::Metric {
1603 label: label.into(),
1604 pct: pct.clamp(0, 100) as u16,
1605 severity: severity_for(pct),
1606 value_label: format!("{pct}%"),
1607 footnote: String::new(),
1608 },
1609 window.resets_at(),
1610 );
1611 sections.push(Section::Text {
1612 label: "Resets in".into(),
1613 value: window
1614 .resets_at()
1615 .map(|at| countdown::format(Some(at), now))
1616 .unwrap_or_else(|| "—".into()),
1617 });
1618 }
1619 if !any {
1620 sections.push(Section::Text {
1621 label: "Quota".into(),
1622 value: "this plan has no ClinePass quota".into(),
1623 });
1624 }
1625 if let Some(period_end) = s.period_end {
1626 sections.push(Section::Spacer);
1627 sections.push(Section::Text {
1628 label: "Renews".into(),
1629 value: period_end.format("%Y-%m-%d").to_string(),
1630 });
1631 }
1632 sections
1633}
1634
1635fn opencode_go_sections(
1636 s: &crate::opencode_go::types::Usage,
1637 now: DateTime<Utc>,
1638 tol: u32,
1639) -> SectionBuilder {
1640 use crate::opencode_go::vendor::{ROLLING_WINDOW, WEEKLY_WINDOW};
1641
1642 let mut sections = SectionBuilder::new(vec![Section::Title {
1643 left: "OpenCode Go".into(),
1644 right: None,
1645 }]);
1646 let mut any = false;
1647 for (label, window, duration) in [
1648 ("Rolling (5h)", s.rolling.as_ref(), ROLLING_WINDOW),
1649 ("Weekly (7d)", s.weekly.as_ref(), WEEKLY_WINDOW),
1650 ] {
1651 let Some(window) = window else {
1652 continue;
1653 };
1654 any = true;
1655 let pct = i32::from(clamp_pct(window.percent));
1656 let projected = crate::usage::UsageWindow {
1657 utilization_pct: pct,
1658 resets_at: Some(window.resets_at),
1659 window_duration: duration,
1660 };
1661 push_window(&mut sections, label, &projected, now, tol, true);
1662 }
1663 if let Some(window) = s.monthly.as_ref() {
1668 any = true;
1669 let pct = clamp_pct(window.percent);
1670 sections.push_metric(
1671 Section::Metric {
1672 label: "Monthly".into(),
1673 pct,
1674 severity: severity_for(i32::from(pct)),
1675 value_label: format!("{pct}%"),
1676 footnote: format!(
1677 "Resets in {}",
1678 countdown::format(Some(window.resets_at), now)
1679 ),
1680 },
1681 Some(window.resets_at),
1682 );
1683 }
1684 if !any {
1685 sections.push(Section::Spacer);
1686 sections.push(Section::Text {
1687 label: "".into(),
1688 value: " no usage windows reported".into(),
1689 });
1690 }
1691 sections
1692}
1693
1694fn kiro_sections(s: &crate::usage::KiroSnapshot, now: DateTime<Utc>) -> SectionBuilder {
1699 let pct = s.pct();
1700 let mut v = SectionBuilder::new(vec![
1701 Section::Title {
1702 left: format!("Kiro {}", s.plan),
1703 right: None,
1704 },
1705 Section::Spacer,
1706 ]);
1707 v.push_metric(
1708 Section::Metric {
1709 label: "Credits".into(),
1710 pct: pct.clamp(0, 100) as u16,
1711 severity: severity_for(pct),
1712 value_label: format!("{pct}%"),
1713 footnote: format!("{:.2} of {:.0}", s.used, s.limit),
1714 },
1715 s.reset_at,
1716 );
1717 v.push(Section::Spacer);
1718 v.push(Section::Text {
1719 label: "Resets".into(),
1720 value: countdown::format(s.reset_at, now),
1721 });
1722 v
1723}
1724
1725fn minimax_sections(
1730 s: &crate::usage::MinimaxSnapshot,
1731 now: DateTime<Utc>,
1732 tol: u32,
1733) -> SectionBuilder {
1734 use crate::minimax::vendor::{POOL_GENERAL, POOL_VIDEO};
1735
1736 let mut v = SectionBuilder::new(vec![Section::Title {
1737 left: s.plan.clone(),
1738 right: None,
1739 }]);
1740 for (heading, general, video) in [
1741 ("Session", &s.session, s.video_session.as_ref()),
1742 ("Weekly", &s.weekly, s.video_weekly.as_ref()),
1743 ] {
1744 v.push(Section::Spacer);
1745 v.push(Section::Text {
1746 label: heading.into(),
1747 value: String::new(),
1748 });
1749 push_window(&mut v, POOL_GENERAL, general, now, tol, true);
1750 if let Some(w) = video {
1751 push_window(&mut v, POOL_VIDEO, w, now, tol, true);
1752 }
1753 }
1754 v
1755}
1756
1757fn kilo_sections(s: &crate::usage::KiloSnapshot, prefs: DisplayPrefs) -> SectionBuilder {
1758 let mut v = SectionBuilder::new(vec![
1759 Section::Title {
1760 left: s.label.clone(),
1761 right: None,
1762 },
1763 Section::Spacer,
1764 ]);
1765 push_balance_headline(
1766 &mut v,
1767 "Balance",
1768 s.balance,
1769 "USD",
1770 crate::kilo::vendor::severity(s),
1771 None,
1772 prefs,
1773 );
1774 v
1775}
1776
1777fn novita_sections(s: &crate::usage::NovitaSnapshot, prefs: DisplayPrefs) -> SectionBuilder {
1778 let mut v = SectionBuilder::new(vec![
1779 Section::Title {
1780 left: "Novita".into(),
1781 right: None,
1782 },
1783 Section::Spacer,
1784 ]);
1785 push_balance_headline(
1788 &mut v,
1789 "Balance",
1790 s.available,
1791 "USD",
1792 crate::novita::vendor::severity(s),
1793 None,
1794 prefs,
1795 );
1796 v.push(Section::Block {
1797 label: "Breakdown".into(),
1798 body: vec![format!(
1799 "top-up ${:.2} · credit limit ${:.2}",
1800 s.cash, s.credit_limit
1801 )],
1802 });
1803 if s.outstanding > 0.0 {
1804 v.push(Section::Spacer);
1805 v.push(Section::Block {
1806 label: "Owed".into(),
1807 body: vec![usd(s.outstanding)],
1808 });
1809 }
1810 v
1811}
1812
1813fn synthetic_sections(
1814 s: &crate::usage::SyntheticSnapshot,
1815 now: DateTime<Utc>,
1816 tolerance: u32,
1817) -> SectionBuilder {
1818 let mut sections = SectionBuilder::new(vec![Section::Title {
1819 left: s.plan.clone().unwrap_or_else(|| "Synthetic".to_string()),
1820 right: None,
1821 }]);
1822 push_window(
1823 &mut sections,
1824 "Weekly credits",
1825 &s.weekly,
1826 now,
1827 tolerance,
1828 false,
1829 );
1830 let regen_amount = match &s.credits {
1833 Some(credits) if credits.next_regen > 0.0 => {
1834 Some(format!("{} credits", credits_number(credits.next_regen)))
1835 }
1836 _ => s
1837 .next_regen_percent
1838 .filter(|p| *p > 0.0)
1839 .map(|p| format!("{}%", credits_number(p))),
1840 };
1841 if let Some(amount) = regen_amount {
1842 sections.regen_last_metric(amount);
1843 }
1844 sections.precise_last_metric(s.utilization_precise);
1847 if let Some(credits) = &s.credits {
1848 sections.push(Section::Text {
1849 label: "Credits".into(),
1850 value: format!(
1851 "{} / {}",
1852 credits_number(credits.used),
1853 credits_number(credits.max)
1854 ),
1855 });
1856 }
1857 sections
1858}
1859
1860fn lyceum_sections(s: &crate::usage::LyceumSnapshot) -> SectionBuilder {
1861 SectionBuilder::new(vec![
1862 Section::Title {
1863 left: crate::vendor::VendorId::Lyceum.display_name().into(),
1864 right: None,
1865 },
1866 Section::Spacer,
1867 Section::Text {
1868 label: "Available balance".into(),
1869 value: usd(s.available_credits),
1870 },
1871 Section::Text {
1872 label: "Amount used".into(),
1873 value: usd(s.used_credits),
1874 },
1875 ])
1876}
1877
1878fn moonshot_sections(s: &crate::usage::MoonshotSnapshot, prefs: DisplayPrefs) -> SectionBuilder {
1879 let cur = &s.currency;
1880 let fmt = |v: f64| money(v, cur);
1881 let mut v = SectionBuilder::new(vec![
1882 Section::Title {
1883 left: "Kimi (Moonshot)".into(),
1884 right: None,
1885 },
1886 Section::Spacer,
1887 ]);
1888 push_balance_headline(
1889 &mut v,
1890 "Balance",
1891 s.available,
1892 cur,
1893 crate::moonshot::vendor::severity(s),
1894 None,
1895 prefs,
1896 );
1897 v.push(Section::Block {
1898 label: "Breakdown".into(),
1899 body: vec![format!("cash {} · voucher {}", fmt(s.cash), fmt(s.voucher))],
1900 });
1901 v
1902}
1903
1904fn grok_sections(s: &crate::usage::GrokSnapshot, prefs: DisplayPrefs) -> SectionBuilder {
1905 let mut v = SectionBuilder::new(vec![
1906 Section::Title {
1907 left: "Grok (xAI)".into(),
1908 right: None,
1909 },
1910 Section::Spacer,
1911 ]);
1912 push_balance_headline(
1913 &mut v,
1914 "Prepaid balance",
1915 s.balance,
1916 "USD",
1917 crate::grok::vendor::severity(s),
1918 None,
1919 prefs,
1920 );
1921 v
1922}
1923
1924fn custom_sections(s: &crate::custom::types::CustomSnapshot) -> SectionBuilder {
1930 let mut v = SectionBuilder::new(vec![
1931 Section::Title {
1932 left: s.plan.clone().unwrap_or_default(),
1933 right: None,
1934 },
1935 Section::Spacer,
1936 ]);
1937 for metric in &s.metrics {
1938 let pct = metric.pct.min(100);
1939 let section = Section::Metric {
1940 label: metric.label.clone(),
1941 pct,
1942 severity: severity_for(i32::from(pct)),
1943 value_label: format!("{pct}%"),
1944 footnote: metric.footnote.clone(),
1945 };
1946 match metric.window_secs {
1947 Some(secs) => v.push_metric_in_window(
1948 section,
1949 metric.resets_at,
1950 chrono::Duration::seconds(i64::try_from(secs).unwrap_or(i64::MAX)),
1951 ),
1952 None => v.push_metric(section, metric.resets_at),
1953 }
1954 }
1955 if !s.texts.is_empty() {
1956 v.push(Section::Spacer);
1957 for text in &s.texts {
1958 v.push(Section::Text {
1959 label: text.label.clone(),
1960 value: text.value.clone(),
1961 });
1962 }
1963 }
1964 v
1965}
1966
1967fn supergrok_sections(
1968 s: &crate::usage::SuperGrokSnapshot,
1969 now: DateTime<Utc>,
1970 tol: u32,
1971) -> SectionBuilder {
1972 let pct = s.weekly_pct;
1973 let mut v = SectionBuilder::new(vec![
1974 Section::Title {
1975 left: s.plan.clone(),
1976 right: None,
1977 },
1978 Section::Spacer,
1979 ]);
1980 let footnote = match (s.period, s.reset_at) {
1984 (crate::usage::SuperGrokPeriod::Weekly, Some(_)) => {
1985 let pace = pacing::calc(pct, s.reset_at, now, chrono::Duration::days(7), tol);
1986 format!(
1987 "Resets in {} · {}% elapsed · {}",
1988 countdown::format(s.reset_at, now),
1989 pace.elapsed_pct,
1990 pace.point_label
1991 )
1992 }
1993 (_, Some(_)) => format!("Resets in {}", countdown::format(s.reset_at, now)),
1994 (_, None) => String::new(),
1995 };
1996 let metric = Section::Metric {
1997 label: format!("{} usage", s.period.label()),
1998 pct: pct.clamp(0, 100) as u16,
1999 severity: severity_for(pct),
2000 value_label: format!("{pct}%"),
2001 footnote,
2002 };
2003 if s.period == crate::usage::SuperGrokPeriod::Weekly {
2006 v.push_metric_in_window(metric, s.reset_at, chrono::Duration::days(7));
2007 } else {
2008 v.push_metric(metric, s.reset_at);
2009 }
2010 for product in &s.products {
2011 v.push_metric_in_group(
2016 Section::Metric {
2017 label: product.label.clone(),
2018 pct: product.percent.clamp(0, 100) as u16,
2019 severity: PaceSeverity::Low,
2020 value_label: format!("{}%", product.percent),
2021 footnote: String::new(),
2022 },
2023 "Breakdown",
2024 );
2025 }
2026 if let Some(extra) = s.additional_usage_text() {
2031 v.push(Section::Spacer);
2032 v.push(Section::Text {
2033 label: "Additional usage".into(),
2034 value: extra,
2035 });
2036 }
2037 if let Some(bal) = s.prepaid_balance.filter(|bal| *bal > 0.0) {
2038 v.push(Section::Spacer);
2039 v.push(Section::Text {
2040 label: "Prepaid API".into(),
2041 value: usd(bal),
2042 });
2043 }
2044 push_reset_credits(&mut v, &s.reset_credits, now);
2045 v
2046}
2047
2048fn ollama_sections(
2049 s: &crate::usage::OllamaSnapshot,
2050 now: DateTime<Utc>,
2051 pace_tolerance: u32,
2052) -> SectionBuilder {
2053 let mut v = SectionBuilder::new(vec![Section::Title {
2054 left: format!("Ollama Cloud {}", s.plan),
2055 right: None,
2056 }]);
2057 if let Some(w) = &s.session {
2058 push_window(&mut v, "Session (5h)", w, now, pace_tolerance, true);
2059 }
2060 if let Some(w) = &s.weekly {
2061 push_window(&mut v, "Weekly", w, now, pace_tolerance, true);
2062 }
2063 if let Some(w) = &s.monthly {
2064 push_window(&mut v, "Monthly", w, now, pace_tolerance, true);
2065 }
2066 push_top_models(&mut v, &s.session_models, "Top models (5h)");
2067 push_top_models(&mut v, &s.weekly_models, "Top models (weekly)");
2068 push_top_models(&mut v, &s.monthly_models, "Top models (monthly)");
2069 if let Some(credits) = &s.credits {
2070 let mut body = vec![format!(
2071 "{} remaining of {}",
2072 credits.balance, credits.allowance
2073 )];
2074 if let Some(purchased) = &credits.purchased {
2075 body.push(format!("Purchased credits: {purchased}"));
2076 }
2077 v.push(Section::Block {
2078 label: "Credits".into(),
2079 body,
2080 });
2081 }
2082 if let Some(cost) = &s.activity_cost {
2083 v.push(Section::Spacer);
2084 v.push(Section::Block {
2085 label: "Activity".into(),
2086 body: vec![format!(
2087 "{} · {}",
2088 usd_str(cost),
2089 s.activity_period.as_deref().unwrap_or("last 4 weeks")
2090 )],
2091 });
2092 }
2093 v
2094}
2095
2096fn push_top_models(
2097 sections: &mut SectionBuilder,
2098 models: &[crate::usage::OllamaModelUsage],
2099 label: &str,
2100) {
2101 if models.is_empty() {
2102 return;
2103 }
2104 let mut sorted: Vec<&crate::usage::OllamaModelUsage> = models.iter().collect();
2105 sorted.sort_by_key(|m| std::cmp::Reverse(m.request_count));
2106 let body: Vec<String> = sorted
2107 .into_iter()
2108 .take(5)
2109 .map(|m| format!("{}: {} requests", m.name, m.request_count))
2110 .collect();
2111 sections.push(Section::Spacer);
2112 sections.push(Section::Block {
2113 label: label.into(),
2114 body,
2115 });
2116}
2117
2118fn usd_str(cost: &str) -> String {
2119 cost.parse::<f64>()
2120 .map(usd)
2121 .unwrap_or_else(|_| cost.to_string())
2122}
2123
2124fn credits_number(value: f64) -> String {
2126 if value.fract() == 0.0 {
2127 format!("{value:.0}")
2128 } else {
2129 format!("{value:.2}")
2130 }
2131}
2132
2133fn push_reset_credits(
2134 v: &mut SectionBuilder,
2135 credits: &crate::usage::ResetCredits,
2136 now: DateTime<Utc>,
2137) {
2138 if credits.is_empty() {
2139 return;
2140 }
2141 push_reset_block(v, credits, now);
2142}
2143
2144fn push_reset_block(
2148 v: &mut SectionBuilder,
2149 credits: &crate::usage::ResetCredits,
2150 now: DateTime<Utc>,
2151) {
2152 v.push(Section::Spacer);
2153 v.push(Section::Block {
2154 label: "Reset credits".into(),
2155 body: reset_credit_lines(credits, now),
2156 });
2157}
2158
2159fn deepseek_sections(s: &crate::usage::DeepseekSnapshot, prefs: DisplayPrefs) -> SectionBuilder {
2160 let currency = &s.currency;
2161 let fmt = |v: f64| money(v, currency);
2162 let avail = if s.is_available {
2163 "available"
2164 } else {
2165 "unavailable"
2166 };
2167 let mut v = SectionBuilder::new(vec![Section::Title {
2168 left: "DeepSeek".into(),
2169 right: None,
2170 }]);
2171 v.push(Section::Spacer);
2172 push_balance_headline(
2175 &mut v,
2176 "Balance",
2177 s.balance,
2178 currency,
2179 crate::deepseek::vendor::severity(s),
2180 None,
2181 prefs,
2182 );
2183 v.push(Section::Block {
2184 label: "Breakdown".into(),
2185 body: vec![format!(
2186 "granted {} · topped-up {}",
2187 fmt(s.granted),
2188 fmt(s.topped_up)
2189 )],
2190 });
2191 v.push(Section::Spacer);
2192 v.push(Section::Block {
2193 label: "API".into(),
2194 body: vec![avail.into()],
2195 });
2196 v
2197}
2198
2199fn deepinfra_sections(
2200 snapshot: &crate::usage::DeepInfraSnapshot,
2201 prefs: DisplayPrefs,
2202) -> SectionBuilder {
2203 let mut sections = SectionBuilder::new(vec![Section::Title {
2204 left: "DeepInfra".into(),
2205 right: None,
2206 }]);
2207 sections.push(Section::Spacer);
2208 push_balance_headline(
2209 &mut sections,
2210 "Balance",
2211 snapshot.balance,
2212 "USD",
2213 crate::deepinfra::vendor::severity(snapshot),
2214 None,
2215 prefs,
2216 );
2217 sections.push(Section::Spacer);
2218 match snapshot.monthly_limit {
2219 Some(limit) if limit > 0.0 => {
2220 let percent = snapshot.monthly_consumed_pct().unwrap_or_default();
2221 sections.push_metric(
2225 Section::Metric {
2226 label: "Monthly usage".into(),
2227 pct: percent.clamp(0, 100) as u16,
2228 severity: severity_for(percent),
2229 value_label: format!("{} / {}", usd(snapshot.monthly_spend), usd(limit)),
2230 footnote: format!("{percent}% used in {}", snapshot.period),
2231 },
2232 None,
2233 );
2234 }
2235 Some(limit) => sections.push(Section::Text {
2236 label: "Monthly usage".into(),
2237 value: format!("{} / {}", usd(snapshot.monthly_spend), usd(limit)),
2238 }),
2239 None => sections.push(Section::Text {
2240 label: "Monthly usage".into(),
2241 value: format!("{} / no limit", usd(snapshot.monthly_spend)),
2242 }),
2243 }
2244 sections.push(Section::Text {
2245 label: "Period".into(),
2246 value: snapshot.period.clone(),
2247 });
2248 sections
2249}
2250
2251fn kimi_sections(s: &crate::usage::KimiSnapshot, now: DateTime<Utc>, tol: u32) -> SectionBuilder {
2256 use crate::kimi::vendor::{ROLLING_WINDOW, WEEKLY_WINDOW};
2257
2258 let plan = s.plan.as_deref().unwrap_or("Kimi");
2259 let mut v = SectionBuilder::new(vec![Section::Title {
2260 left: plan.into(),
2261 right: None,
2262 }]);
2263 let window = |pct, resets_at, window_duration| crate::usage::UsageWindow {
2264 utilization_pct: pct,
2265 resets_at,
2266 window_duration,
2267 };
2268
2269 if s.window_limit > 0 {
2270 let w = window(s.window_pct(), s.window_reset_at, ROLLING_WINDOW);
2271 push_window(&mut v, "Rolling window (5h)", &w, now, tol, false);
2272 }
2273 if s.has_weekly {
2274 let w = window(s.weekly_pct(), s.weekly_reset_at, WEEKLY_WINDOW);
2275 push_window(&mut v, "Weekly quota", &w, now, tol, false);
2276 }
2277 if let Some(monthly_pct) = s.monthly_pct {
2278 v.push(Section::Spacer);
2282 v.push_metric(
2283 Section::Metric {
2284 label: "Monthly".into(),
2285 pct: monthly_pct.clamp(0, 100) as u16,
2286 severity: severity_for(monthly_pct),
2287 value_label: format!("{monthly_pct}%"),
2288 footnote: format!("Resets in {}", countdown::format(s.monthly_reset_at, now)),
2289 },
2290 s.monthly_reset_at,
2291 );
2292 }
2293
2294 v
2295}
2296
2297fn grokbot_sections(
2302 s: &crate::usage::GrokbotSnapshot,
2303 now: DateTime<Utc>,
2304 tol: u32,
2305) -> SectionBuilder {
2306 let mut v = SectionBuilder::new(vec![Section::Title {
2307 left: s.display_plan().to_string(),
2308 right: None,
2309 }]);
2310 v.push(Section::Spacer);
2311 if !s.has_included_allowance {
2312 v.push(Section::Text {
2313 label: "Included usage".into(),
2314 value: "no included allowance on this account".into(),
2315 });
2316 return v;
2317 }
2318 let footnote = match s.window {
2319 Some(window) if window.num_seconds() > 0 && s.reset_at.is_some() => {
2320 let pace = pacing::calc(s.weekly_pct, s.reset_at, now, window, tol);
2321 format!(
2322 "Resets in {} · {}% elapsed · {}",
2323 countdown::format(s.reset_at, now),
2324 pace.elapsed_pct,
2325 pace.point_label
2326 )
2327 }
2328 _ => format!("Resets in {}", countdown::format(s.reset_at, now)),
2329 };
2330 let metric = Section::Metric {
2331 label: "Weekly".into(),
2332 pct: s.weekly_pct.clamp(0, 100) as u16,
2333 severity: severity_for(s.weekly_pct),
2334 value_label: format!("{}%", s.weekly_pct),
2335 footnote,
2336 };
2337 match s.window {
2338 Some(window) => v.push_metric_in_window(metric, s.reset_at, window),
2339 None => v.push_metric(metric, s.reset_at),
2340 }
2341 if let Some(note) = s.on_demand_note() {
2344 v.push(Section::Spacer);
2345 v.push(Section::Text {
2346 label: "On-demand".into(),
2347 value: note.into(),
2348 });
2349 }
2350 v
2351}
2352
2353fn modelstudio_sections(
2360 s: &crate::usage::ModelStudioSnapshot,
2361 now: DateTime<Utc>,
2362 tol: u32,
2363) -> SectionBuilder {
2364 let mut v = SectionBuilder::new(vec![Section::Title {
2365 left: crate::modelstudio::vendor::PLAN_LABEL.into(),
2366 right: None,
2367 }]);
2368 v.push(Section::Spacer);
2369 if let Some(session) = s.session.as_ref() {
2370 push_window(&mut v, "Token Plan 5h", session, now, tol, true);
2371 }
2372 if let Some(weekly) = s.weekly.as_ref() {
2373 push_window(&mut v, "Token Plan 7d", weekly, now, tol, true);
2374 }
2375 if let Some(monthly) = s.monthly.as_ref() {
2376 if s.session.is_some() || s.weekly.is_some() {
2377 v.push(Section::Spacer);
2378 }
2379 let reset_text = countdown::format(monthly.resets_at, now);
2380 v.push_metric(
2381 Section::Metric {
2382 label: "Token Plan 30d".into(),
2383 pct: monthly.utilization_pct.clamp(0, 100) as u16,
2384 severity: severity_for(monthly.utilization_pct),
2385 value_label: format!("{}%", monthly.utilization_pct),
2386 footnote: format!("Resets in {reset_text}"),
2387 },
2388 monthly.resets_at,
2389 );
2390 }
2391 if s.session.is_none() && s.weekly.is_none() && s.monthly.is_none() {
2392 v.push(Section::Text {
2393 label: "Usage".into(),
2394 value: "no usage windows reported".into(),
2395 });
2396 }
2397 v
2398}
2399
2400fn muse_sections(
2401 s: &crate::usage::MuseSnapshot,
2402 now: DateTime<Utc>,
2403 tolerance: u32,
2404) -> SectionBuilder {
2405 let mut sections = SectionBuilder::new(vec![Section::Title {
2406 left: s.plan.clone(),
2407 right: None,
2408 }]);
2409 if let Some(window) = &s.session {
2410 push_window(&mut sections, "Session quota", window, now, tolerance, true);
2411 }
2412 if let Some(window) = &s.weekly {
2413 push_window(&mut sections, "Weekly quota", window, now, tolerance, true);
2414 }
2415 sections
2416}
2417
2418fn devin_sections(
2419 snapshot: &crate::usage::DevinSnapshot,
2420 now: DateTime<Utc>,
2421 tol: u32,
2422) -> SectionBuilder {
2423 let mut sections = SectionBuilder::new(vec![Section::Title {
2424 left: crate::vendor::VendorId::Devin.display_name().into(),
2425 right: None,
2426 }]);
2427 if let Some(daily) = snapshot.daily.as_ref() {
2428 push_window(&mut sections, "Daily quota", daily, now, tol, true);
2429 }
2430 if let Some(weekly) = snapshot.weekly.as_ref() {
2431 push_window(&mut sections, "Weekly quota", weekly, now, tol, true);
2432 }
2433 if snapshot.daily.is_none()
2434 && snapshot.weekly.is_none()
2435 && snapshot.overage_balance_micros.is_none()
2436 {
2437 sections.push(Section::Text {
2438 label: "Usage".into(),
2439 value: "no quota fields reported".into(),
2440 });
2441 }
2442 if let Some(balance) = snapshot.overage_balance_micros {
2443 sections.push(Section::Spacer);
2444 sections.push(Section::Text {
2445 label: "Overage balance".into(),
2446 value: crate::usage::fmt_minor(balance, crate::devin::vendor::BALANCE_DECIMALS, None),
2447 });
2448 sections.push(Section::Text {
2449 label: "Balance note".into(),
2450 value: "Microunit-to-USD display matches the tested account; currency contract is unverified.".into(),
2451 });
2452 }
2453 sections
2454}
2455
2456fn droid_sections(
2457 snapshot: &crate::usage::DroidSnapshot,
2458 now: DateTime<Utc>,
2459 tol: u32,
2460) -> SectionBuilder {
2461 let mut sections = SectionBuilder::new(vec![Section::Title {
2462 left: crate::vendor::VendorId::Droid.display_name().into(),
2463 right: None,
2464 }]);
2465 let standard = [
2466 ("5-hour quota", snapshot.session.as_ref()),
2467 ("Weekly quota", snapshot.weekly.as_ref()),
2468 ("Monthly quota", snapshot.monthly.as_ref()),
2469 ];
2470 if standard.iter().any(|(_, w)| w.is_some()) {
2471 sections.push(Section::Spacer);
2472 sections.push(Section::Text {
2473 label: "Standard".into(),
2474 value: String::new(),
2475 });
2476 for (label, window) in standard {
2477 if let Some(window) = window {
2478 push_droid_window(&mut sections, label, window, now, tol);
2479 }
2480 }
2481 }
2482 let core = [
2483 ("5-hour quota", snapshot.core_session.as_ref()),
2484 ("Weekly quota", snapshot.core_weekly.as_ref()),
2485 ("Monthly quota", snapshot.core_monthly.as_ref()),
2486 ];
2487 let core_idle = core
2488 .iter()
2489 .flat_map(|(_, w)| w)
2490 .all(|w| w.resets_at.is_none() && w.utilization_pct == 0);
2491 if core.iter().any(|(_, w)| w.is_some()) && core_idle {
2492 sections.push(Section::Spacer);
2495 sections.push(Section::Text {
2496 label: "Droid Core".into(),
2497 value: "not used yet".into(),
2498 });
2499 } else if core.iter().any(|(_, w)| w.is_some()) {
2500 sections.push(Section::Spacer);
2501 sections.push(Section::Text {
2502 label: "Droid Core".into(),
2503 value: String::new(),
2504 });
2505 for (label, window) in core {
2506 if let Some(window) = window {
2507 push_droid_window(&mut sections, label, window, now, tol);
2508 }
2509 }
2510 }
2511 if standard.iter().all(|(_, w)| w.is_none())
2512 && core.iter().all(|(_, w)| w.is_none())
2513 && snapshot.extra_balance_cents.is_none()
2514 {
2515 sections.push(Section::Text {
2516 label: "Usage".into(),
2517 value: "no quota fields reported".into(),
2518 });
2519 }
2520 if let Some(balance) = snapshot.extra_balance_cents {
2521 sections.push(Section::Spacer);
2522 sections.push(Section::Text {
2523 label: "Extra usage balance".into(),
2524 value: crate::droid::vendor::extra_balance(balance),
2525 });
2526 }
2527 sections
2528}
2529
2530fn push_droid_window(
2534 sections: &mut SectionBuilder,
2535 label: &str,
2536 w: &crate::usage::UsageWindow,
2537 now: DateTime<Utc>,
2538 tol: u32,
2539) {
2540 if w.resets_at.is_some() {
2541 return push_window(sections, label, w, now, tol, true);
2542 }
2543 let pct = w.utilization_pct.clamp(0, 100) as u16;
2544 sections.push(Section::Spacer);
2545 sections.push_metric_in_window(
2546 Section::Metric {
2547 label: label.into(),
2548 pct,
2549 severity: severity_for(pct as i32),
2550 value_label: format!("{pct}%"),
2551 footnote: "Not started".into(),
2552 },
2553 None,
2554 w.window_duration,
2555 );
2556}
2557
2558fn format_precise_pct(pct: f64) -> String {
2560 let rounded = (pct * 10.0).round() / 10.0;
2561 if rounded == rounded.trunc() {
2562 format!("{rounded:.0}%")
2563 } else {
2564 format!("{rounded}%")
2565 }
2566}
2567
2568fn push_window(
2569 sections: &mut SectionBuilder,
2570 label: &str,
2571 w: &crate::usage::UsageWindow,
2572 now: DateTime<Utc>,
2573 tol: u32,
2574 show_pacing: bool,
2575) {
2576 let pct = w.utilization_pct.clamp(0, 100) as u16;
2577 let reset_text = countdown::format(w.resets_at, now);
2578 let footnote = if show_pacing {
2579 let p = pacing::calc(w.utilization_pct, w.resets_at, now, w.window_duration, tol);
2580 format!(
2581 "Resets in {} · {}% elapsed · {}",
2582 reset_text, p.elapsed_pct, p.point_label
2583 )
2584 } else {
2585 format!("Resets in {}", reset_text)
2586 };
2587 sections.push(Section::Spacer);
2588 sections.push_metric_in_window(
2589 Section::Metric {
2590 label: label.into(),
2591 pct,
2592 severity: severity_for(pct as i32),
2593 value_label: format!("{pct}%"),
2594 footnote,
2595 },
2596 w.resets_at,
2597 w.window_duration,
2598 );
2599}
2600
2601pub fn render(f: &mut Frame, area: Rect, theme: &Theme, sections: &[Section]) {
2609 if sections.is_empty() {
2610 return;
2611 }
2612 let bubble = bubble_theme(theme);
2613 let pin_last =
2616 matches!(sections.last(), Some(Section::Text { value, .. }) if value.contains("Updated"));
2617
2618 let body_end = if pin_last {
2619 sections.len() - 1
2620 } else {
2621 sections.len()
2622 };
2623 let mut constraints: Vec<Constraint> =
2624 sections[..body_end].iter().map(section_height).collect();
2625
2626 if pin_last {
2627 constraints.push(Constraint::Min(0)); constraints.push(section_height(sections.last().unwrap()));
2629 } else {
2630 constraints.push(Constraint::Min(0));
2631 }
2632
2633 let chunks = Layout::default()
2634 .direction(ratatui::layout::Direction::Vertical)
2635 .constraints(constraints)
2636 .split(area);
2637
2638 for (i, s) in sections[..body_end].iter().enumerate() {
2639 render_section(f, chunks[i], theme, &bubble, s);
2640 }
2641 if pin_last {
2642 render_section(
2643 f,
2644 chunks[chunks.len() - 1],
2645 theme,
2646 &bubble,
2647 sections.last().unwrap(),
2648 );
2649 }
2650}
2651
2652fn section_height(s: &Section) -> Constraint {
2653 match s {
2654 Section::Title { .. } => Constraint::Length(2),
2655 Section::Metric { .. } => Constraint::Length(3),
2656 Section::Text { .. } => Constraint::Length(1),
2657 Section::Block { body, .. } => Constraint::Length(1 + body.len() as u16),
2658 Section::Spacer => Constraint::Length(1),
2659 }
2660}
2661
2662fn render_section(f: &mut Frame, area: Rect, theme: &Theme, bubble: &BubbleTheme, s: &Section) {
2663 match s {
2664 Section::Title { left, right } => {
2665 let left_line = Line::from(Span::styled(
2668 format!(" {} {left}", bubble.symbols.selected),
2669 bubble.title,
2670 ));
2671 f.render_widget(Paragraph::new(left_line), area);
2672 if let Some(rt) = right {
2673 let right_line =
2674 Line::from(Span::styled(format!("{rt} "), bubble.muted)).right_aligned();
2675 f.render_widget(Paragraph::new(right_line), area);
2676 }
2677 }
2678 Section::Metric {
2679 label,
2680 pct,
2681 severity,
2682 value_label,
2683 footnote,
2684 } => render_metric(
2685 f,
2686 area,
2687 theme,
2688 bubble,
2689 label,
2690 *pct,
2691 *severity,
2692 value_label,
2693 footnote,
2694 ),
2695 Section::Text { label, value } => {
2696 if label.is_empty() && value.contains("Loading") {
2697 render_loading(f, area, bubble);
2698 return;
2699 }
2700 if label == "Error" {
2701 let line = Line::from(vec![
2702 bubble.error(format!(" {} ", bubble.symbols.cross)),
2703 Span::styled(value.clone(), bubble.error.add_modifier(Modifier::BOLD)),
2704 ]);
2705 f.render_widget(Paragraph::new(line), area);
2706 return;
2707 }
2708 let mut spans = Vec::new();
2709 if !label.is_empty() {
2710 spans.push(Span::styled(
2711 format!(" {label} "),
2712 bubble.text.add_modifier(Modifier::BOLD),
2713 ));
2714 }
2715 spans.push(Span::styled(value.clone(), bubble.muted));
2716 f.render_widget(Paragraph::new(Line::from(spans)), area);
2717 }
2718 Section::Block { label, body } => render_block(f, area, bubble, label, body),
2719 Section::Spacer => {}
2720 }
2721}
2722
2723fn render_loading(f: &mut Frame, area: Rect, bubble: &BubbleTheme) {
2724 let frames = SpinnerFrames::DOTS;
2725 let frame_count = frames.frames().len().max(1);
2726 let frame = chrono::Utc::now().timestamp_millis().unsigned_abs() as usize / 120;
2727 let mut spinner = Spinner::new()
2728 .frames(frames)
2729 .label("Fetching usage data")
2730 .theme(*bubble);
2731 for _ in 0..(frame % frame_count) {
2732 spinner.tick();
2733 }
2734 f.render_widget(&spinner, area);
2735}
2736
2737#[allow(clippy::too_many_arguments)]
2738fn render_metric(
2739 f: &mut Frame,
2740 area: Rect,
2741 theme: &Theme,
2742 bubble: &BubbleTheme,
2743 label: &str,
2744 pct: u16,
2745 severity: PaceSeverity,
2746 value_label: &str,
2747 footnote: &str,
2748) {
2749 let bar_color = severity_color(theme, bubble, severity);
2750 let bar_empty = color(&theme.bar_empty).unwrap_or(bubble.palette.selected_background);
2751
2752 let inner = Layout::default()
2753 .direction(ratatui::layout::Direction::Vertical)
2754 .constraints([
2755 Constraint::Length(1),
2756 Constraint::Length(1),
2757 Constraint::Length(1),
2758 ])
2759 .split(area);
2760
2761 let label_line = Line::from(Span::styled(
2763 format!(" {label}"),
2764 bubble.text.add_modifier(Modifier::BOLD),
2765 ));
2766 f.render_widget(Paragraph::new(label_line), inner[0]);
2767
2768 let row = inner[1];
2770 let value_w = crate::display::text_width(value_label) as u16 + 2;
2771 let gauge_area = Rect {
2772 x: row.x + 2,
2773 y: row.y,
2774 width: row.width.saturating_sub(value_w + 4),
2775 height: 1,
2776 };
2777 let value_area = Rect {
2778 x: gauge_area.x + gauge_area.width + 1,
2779 y: row.y,
2780 width: value_w,
2781 height: 1,
2782 };
2783 let progress_theme = progress_theme(*bubble, bar_color, bar_empty);
2784 let progress = Progress::from_percent(pct)
2785 .theme(progress_theme)
2786 .show_percentage(false);
2787 f.render_widget(&progress, gauge_area);
2788 let value = Paragraph::new(Line::from(Span::styled(
2789 value_label.to_string(),
2790 Style::default().fg(bar_color).add_modifier(Modifier::BOLD),
2791 )));
2792 f.render_widget(value, value_area);
2793
2794 let foot = Line::from(Span::styled(format!(" {footnote}"), bubble.muted));
2796 f.render_widget(Paragraph::new(foot), inner[2]);
2797}
2798
2799fn render_block(f: &mut Frame, area: Rect, bubble: &BubbleTheme, label: &str, body: &[String]) {
2800 let mut lines = vec![Line::from(Span::styled(
2801 format!(" {label}"),
2802 bubble.text.add_modifier(Modifier::BOLD),
2803 ))];
2804 for b in body {
2805 lines.push(Line::from(Span::styled(format!(" {b}"), bubble.muted)));
2806 }
2807 f.render_widget(Paragraph::new(lines), area);
2808}
2809
2810#[cfg(test)]
2811mod tests {
2812 use super::*;
2813 use crate::usage::{
2814 AnthropicSnapshot, Cents, CloudCredits, ExtraUsage, KimiSnapshot, OpenAiCredits,
2815 OpenAiSnapshot, OpenAiSource, OpenRouterSnapshot, ResetCredit, ResetCredits, UsageCredits,
2816 UsageWindow, ZaiSnapshot,
2817 };
2818 use chrono::TimeZone;
2819
2820 fn now() -> DateTime<Utc> {
2821 Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap()
2822 }
2823
2824 #[test]
2825 fn lyceum_panel_shows_usd_balance_without_a_quota_percentage() {
2826 let snapshot = VendorSnapshot::Lyceum(crate::usage::LyceumSnapshot {
2827 available_credits: 42.5,
2828 used_credits: 7.5,
2829 total_credits_used: 7.5,
2830 remaining_credits: 42.5,
2831 monthly_free_credits: 0.0,
2832 purchased_credits: 50.0,
2833 });
2834 let sections = sections_for(&ready(snapshot.clone()), now(), 5);
2835 assert!(sections.iter().any(|section| matches!(section, Section::Text { label, value } if label == "Available balance" && value == "$42.50")));
2836 assert!(
2837 !sections
2838 .iter()
2839 .any(|section| matches!(section, Section::Metric { .. }))
2840 );
2841 assert_eq!(headline_pct(&snapshot), None);
2842 let (_, cells) = compact_cells(&snapshot);
2843 assert_eq!(cells[0].0, "$42.50");
2844 }
2845
2846 #[test]
2850 fn anthropic_api_spend_row_keeps_its_label_as_the_covered_day_moves() {
2851 fn rows(limit: Option<f64>, covered_through: Option<chrono::NaiveDate>) -> Vec<String> {
2852 let snapshot = VendorSnapshot::AnthropicApi(crate::usage::AnthropicApiSnapshot {
2853 spent: 1.34,
2854 limit,
2855 covered_through,
2856 });
2857 sections_for(&ready(snapshot), now(), 5)
2858 .into_iter()
2859 .filter_map(|section| match section {
2860 Section::Metric { label, .. } => Some(label),
2861 Section::Text { label, value } if !label.is_empty() => {
2862 Some(format!("{label} {value}"))
2863 }
2864 _ => None,
2865 })
2866 .collect()
2867 }
2868 let oct = |day| chrono::NaiveDate::from_ymd_opt(2026, 10, day);
2869 assert_eq!(
2870 rows(Some(1000.0), oct(17)),
2871 ["Spend (mo)", "Through Oct 17"]
2872 );
2873 assert_eq!(
2874 rows(Some(1000.0), oct(18)),
2875 ["Spend (mo)", "Through Oct 18"]
2876 );
2877 assert_eq!(rows(None, oct(18)), ["Spend (mo) $1.34", "Through Oct 18"]);
2878 assert_eq!(rows(None, None), ["Spend (mo) $1.34"]);
2880 }
2881
2882 fn ready(snapshot: VendorSnapshot) -> TabState {
2883 ready_with(snapshot, DisplayPrefs::default())
2884 }
2885
2886 fn ready_with(snapshot: VendorSnapshot, display: DisplayPrefs) -> TabState {
2887 TabState::Ready(Box::new(crate::tui::app::ReadyTab {
2888 snapshot,
2889 stale: false,
2890 last_error: None,
2891 fetched_at: Some(now() - chrono::Duration::seconds(15)),
2892 display,
2893 }))
2894 }
2895
2896 fn supergrok(period: crate::usage::SuperGrokPeriod) -> VendorSnapshot {
2897 VendorSnapshot::SuperGrok(crate::usage::SuperGrokSnapshot {
2898 plan: "SuperGrok".into(),
2899 account: "digest".into(),
2900 weekly_pct: 40,
2901 period,
2902 reset_at: Some(now() + chrono::Duration::days(2)),
2903 prepaid_balance: None,
2904 on_demand_used_cents: None,
2905 on_demand_cap_cents: None,
2906 reset_credits: crate::usage::ResetCredits::default(),
2907 products: Vec::new(),
2908 })
2909 }
2910
2911 fn only_metric(sections: &[SectionProjection]) -> &SectionProjection {
2912 let mut metrics = sections
2913 .iter()
2914 .filter(|projection| matches!(projection.section, Section::Metric { .. }));
2915 let metric = metrics.next().expect("one metric row");
2916 assert!(metrics.next().is_none(), "expected exactly one metric row");
2917 metric
2918 }
2919
2920 fn nous_snapshot() -> crate::nous::types::AccountSnapshot {
2921 crate::nous::types::AccountSnapshot {
2922 plan: Some("Ultra".into()),
2923 tier: Some(9),
2924 monthly_credits: Some(220.0),
2925 credits_remaining: Some(0.0),
2926 purchased_credits_remaining: Some(12.93),
2927 total_usable_credits: Some(12.93),
2928 rollover_credits: Some(10.0),
2929 current_period_end: None,
2930 }
2931 }
2932
2933 #[test]
2936 fn nous_bar_headline_can_be_the_credits_balance() {
2937 let percent = sections_with_metadata_for(
2938 &ready(VendorSnapshot::NousResearch(nous_snapshot())),
2939 now(),
2940 5,
2941 );
2942 let metric = only_metric(&percent);
2943 assert_eq!(metric.headline, MetricHeadline::Percent);
2944 match &metric.section {
2945 Section::Metric { value_label, .. } => assert_eq!(value_label, "100%"),
2946 _ => unreachable!(),
2947 }
2948
2949 let prefs = DisplayPrefs::balance(None, crate::balance::Headline::Amount);
2950 let balance = sections_with_metadata_for(
2951 &ready_with(VendorSnapshot::NousResearch(nous_snapshot()), prefs),
2952 now(),
2953 5,
2954 );
2955 let metric = only_metric(&balance);
2956 assert_eq!(metric.headline, MetricHeadline::Value);
2957 match &metric.section {
2958 Section::Metric {
2959 pct, value_label, ..
2960 } => {
2961 assert_eq!(*pct, 100);
2962 assert_eq!(value_label, "$12.93");
2963 }
2964 _ => unreachable!(),
2965 }
2966 }
2967
2968 #[test]
2969 fn nous_sections_render_credit_rows_as_usd() {
2970 let sections = sections_with_metadata_for(
2971 &ready(VendorSnapshot::NousResearch(nous_snapshot())),
2972 now(),
2973 5,
2974 );
2975 assert!(sections.iter().any(|s| matches!(
2976 &s.section,
2977 Section::Text { label, value } if label == "Subscription credits" && value == "$0.00 remaining"
2978 )));
2979 assert!(sections.iter().any(|s| matches!(
2980 &s.section,
2981 Section::Text { label, value } if label == "Top-up credits" && value == "$12.93 remaining"
2982 )));
2983 assert!(sections.iter().any(|s| matches!(
2984 &s.section,
2985 Section::Text { label, value } if label == "Total usable credits" && value == "$12.93"
2986 )));
2987 }
2988
2989 #[test]
2993 fn window_length_is_reported_only_when_exact() {
2994 use crate::usage::SuperGrokPeriod;
2995
2996 let weekly =
2997 sections_with_metadata_for(&ready(supergrok(SuperGrokPeriod::Weekly)), now(), 5);
2998 assert_eq!(only_metric(&weekly).window, Some(chrono::Duration::days(7)));
2999
3000 let monthly =
3001 sections_with_metadata_for(&ready(supergrok(SuperGrokPeriod::Monthly)), now(), 5);
3002 assert_eq!(only_metric(&monthly).window, None);
3003
3004 let unknown =
3005 sections_with_metadata_for(&ready(supergrok(SuperGrokPeriod::Unknown)), now(), 5);
3006 assert_eq!(only_metric(&unknown).window, None);
3007
3008 let kimi = sections_with_metadata_for(
3009 &ready(VendorSnapshot::Kimi(KimiSnapshot {
3010 plan: None,
3011 weekly_limit: 100,
3012 weekly_used: 10,
3013 weekly_remaining: 90,
3014 weekly_reset_at: Some(now() + chrono::Duration::days(3)),
3015 has_weekly: true,
3016 monthly_pct: None,
3017 monthly_reset_at: None,
3018 window_limit: 0,
3019 window_used: 0,
3020 window_remaining: 0,
3021 window_reset_at: None,
3022 })),
3023 now(),
3024 5,
3025 );
3026 assert_eq!(
3027 only_metric(&kimi).window,
3028 Some(crate::kimi::vendor::WEEKLY_WINDOW)
3029 );
3030 }
3031
3032 #[test]
3033 fn cursor_pools_carry_the_billing_cycle_window_only_when_it_is_exact() {
3034 let mut snap = cursor_snap();
3035 snap.cycle_start = Some(now() - chrono::Duration::days(22));
3036 let exact =
3037 sections_with_metadata_for(&ready(VendorSnapshot::Cursor(snap.clone())), now(), 5);
3038 let windows: Vec<_> = exact
3039 .iter()
3040 .filter(|p| matches!(p.section, Section::Metric { .. }))
3041 .map(|p| p.window)
3042 .collect();
3043 assert_eq!(
3044 windows,
3045 vec![
3046 Some(chrono::Duration::days(31)),
3047 Some(chrono::Duration::days(31))
3048 ]
3049 );
3050
3051 snap.cycle_start = None;
3056 let unknown = sections_with_metadata_for(&ready(VendorSnapshot::Cursor(snap)), now(), 5);
3057 let pools: Vec<_> = unknown
3058 .iter()
3059 .filter(|p| matches!(p.section, Section::Metric { .. }))
3060 .collect();
3061 assert_eq!(pools.len(), 2);
3062 assert!(
3063 pools.iter().all(|p| p.window.is_none()),
3064 "an unstated billing cycle must not report a window length"
3065 );
3066 assert!(
3067 pools.iter().all(|p| p.reset_at.is_some()),
3068 "the reset time still travels with the row"
3069 );
3070 }
3071
3072 #[test]
3073 fn cursor_pool_footnotes_pace_each_pool_against_an_exact_billing_cycle() {
3074 let snap = crate::usage::CursorSnapshot {
3075 auto_pct: 70,
3076 api_pct: 30,
3077 reset_at: Some(now() + chrono::Duration::days(5)),
3078 cycle_start: Some(now() - chrono::Duration::days(5)),
3079 ..cursor_snap()
3080 };
3081 let sections = sections_for(&ready(VendorSnapshot::Cursor(snap)), now(), 5);
3082 let footnotes: Vec<&str> = sections
3083 .iter()
3084 .filter_map(|section| match section {
3085 Section::Metric { footnote, .. } => Some(footnote.as_str()),
3086 _ => None,
3087 })
3088 .collect();
3089 assert_eq!(
3090 footnotes,
3091 [
3092 "Auto + Composer · 50% elapsed · 20pts ahead",
3093 "Named / API models · on-demand off · 50% elapsed · 20pts under",
3094 ]
3095 );
3096 }
3097
3098 #[test]
3099 fn cursor_pool_footnotes_skip_the_pace_when_the_cycle_is_not_exact() {
3100 let sections = sections_for(&ready(VendorSnapshot::Cursor(cursor_snap())), now(), 5);
3103 for section in §ions {
3104 if let Section::Metric { footnote, .. } = section {
3105 assert!(!footnote.contains("elapsed"), "{footnote}");
3106 }
3107 }
3108 }
3109
3110 #[test]
3111 fn deepinfra_monthly_usage_carries_no_reset_when_a_limit_is_set() {
3112 let snapshot = VendorSnapshot::Deepinfra(crate::usage::DeepInfraSnapshot {
3116 balance: 12.5,
3117 monthly_spend: 10.0,
3118 monthly_limit: Some(50.0),
3119 period: "2026.09".into(),
3120 });
3121 let rows = sections_with_metadata_for(&ready(snapshot), now(), 5);
3122 let metrics: Vec<_> = rows
3123 .iter()
3124 .filter(|p| matches!(p.section, Section::Metric { .. }))
3125 .collect();
3126 assert_eq!(metrics.len(), 1);
3127 assert!(
3128 metrics[0].reset_at.is_none(),
3129 "a vendor billing period has no reset instant"
3130 );
3131 }
3132
3133 #[test]
3134 fn copilot_sections_carry_quota_reset_metadata() {
3135 let reset_at = now() + chrono::Duration::days(4);
3136 let snapshot = VendorSnapshot::Copilot(crate::copilot::types::Snapshot {
3137 plan: "Pro".into(),
3138 premium: Some(crate::copilot::types::Quota {
3139 percent_remaining: 25,
3140 entitlement: Some(300),
3141 remaining: Some(75),
3142 unlimited: false,
3143 }),
3144 chat: None,
3145 completions: None,
3146 reset_at: Some(reset_at),
3147 });
3148 let sections = sections_with_metadata_for(&ready(snapshot), now(), 5);
3149 assert!(matches!(
3150 §ions[0].section,
3151 Section::Title { left, .. } if left == "GitHub Copilot Pro"
3152 ));
3153 let metric = sections
3154 .iter()
3155 .find(|projection| matches!(&projection.section, Section::Metric { .. }))
3156 .expect("premium metric");
3157 assert_eq!(metric.reset_at, Some(reset_at));
3158 assert!(matches!(
3159 &metric.section,
3160 Section::Metric { label, pct, value_label, footnote, .. }
3161 if label == "Premium requests"
3162 && *pct == 75
3163 && value_label == "75%"
3164 && footnote == "225 of 300 used"
3165 ));
3166 }
3167
3168 #[test]
3169 fn anthropic_sections_include_all_three_windows_when_present() {
3170 let snap = AnthropicSnapshot {
3171 plan: "Max 20x".into(),
3172 session: UsageWindow {
3173 utilization_pct: 60,
3174 resets_at: Some(now() + chrono::Duration::hours(1)),
3175 window_duration: chrono::Duration::hours(5),
3176 },
3177 weekly: UsageWindow {
3178 utilization_pct: 30,
3179 resets_at: Some(now() + chrono::Duration::days(3)),
3180 window_duration: chrono::Duration::days(7),
3181 },
3182 sonnet: Some(UsageWindow {
3183 utilization_pct: 5,
3184 resets_at: Some(now() + chrono::Duration::hours(2)),
3185 window_duration: chrono::Duration::days(7),
3186 }),
3187 scoped: vec![],
3188 extra: Some(ExtraUsage {
3189 limit: Some(Cents(5000)),
3190 spent: Cents(250),
3191 currency: None,
3192 decimal_places: Some(2),
3193 status: crate::usage::ExtraUsageStatus::Active,
3194 }),
3195 reset_credits: Default::default(),
3196 cloud_credits: None,
3197 usage_credits: None,
3198 };
3199 let sections = sections_for(&ready(VendorSnapshot::Anthropic(snap)), now(), 5);
3200 assert_eq!(sections.len(), 9);
3203 assert!(matches!(sections[0], Section::Title { .. }));
3204 if let Section::Title { right, .. } = §ions[0] {
3206 assert!(right.as_deref().is_some_and(|r| r.starts_with("Updated ")));
3207 } else {
3208 panic!("expected first section to be Title");
3209 }
3210 let metric_count = sections
3211 .iter()
3212 .filter(|s| matches!(s, Section::Metric { .. }))
3213 .count();
3214 assert_eq!(metric_count, 4);
3215 }
3216
3217 #[test]
3218 fn anthropic_uncapped_extra_shows_spend_without_a_denominator() {
3219 let snap = AnthropicSnapshot {
3222 plan: "Pro".into(),
3223 session: UsageWindow {
3224 utilization_pct: 10,
3225 resets_at: None,
3226 window_duration: chrono::Duration::hours(5),
3227 },
3228 weekly: UsageWindow {
3229 utilization_pct: 20,
3230 resets_at: None,
3231 window_duration: chrono::Duration::days(7),
3232 },
3233 sonnet: None,
3234 scoped: vec![],
3235 extra: Some(ExtraUsage {
3236 limit: None,
3237 spent: Cents(14157),
3238 currency: Some("BRL".into()),
3239 decimal_places: Some(2),
3240 status: crate::usage::ExtraUsageStatus::OutOfCredits,
3241 }),
3242 reset_credits: Default::default(),
3243 cloud_credits: None,
3244 usage_credits: None,
3245 };
3246 let sections = sections_for(&ready(VendorSnapshot::Anthropic(snap)), now(), 5);
3247 let extra = sections
3248 .iter()
3249 .find_map(|s| match s {
3250 Section::Metric {
3251 label,
3252 pct,
3253 value_label,
3254 footnote,
3255 ..
3256 } if label == "Extra usage" => Some((*pct, value_label.clone(), footnote.clone())),
3257 _ => None,
3258 })
3259 .expect("uncapped extra usage must still render a section");
3260 assert_eq!(extra.0, 0);
3261 assert_eq!(extra.1, "R$141.57");
3263 assert!(
3264 !extra.1.contains(" of "),
3265 "no denominator to show: {}",
3266 extra.1
3267 );
3268 assert_eq!(extra.2, "no monthly limit reported · out of credits");
3269 }
3270
3271 #[test]
3272 fn anthropic_omits_sonnet_and_extra_when_absent() {
3273 let snap = AnthropicSnapshot {
3274 plan: "Pro".into(),
3275 session: UsageWindow {
3276 utilization_pct: 10,
3277 resets_at: None,
3278 window_duration: chrono::Duration::hours(5),
3279 },
3280 weekly: UsageWindow {
3281 utilization_pct: 5,
3282 resets_at: None,
3283 window_duration: chrono::Duration::days(7),
3284 },
3285 sonnet: None,
3286 scoped: vec![],
3287 extra: None,
3288 reset_credits: Default::default(),
3289 cloud_credits: None,
3290 usage_credits: None,
3291 };
3292 let sections = sections_for(&ready(VendorSnapshot::Anthropic(snap)), now(), 5);
3293 let metric_count = sections
3294 .iter()
3295 .filter(|s| matches!(s, Section::Metric { .. }))
3296 .count();
3297 assert_eq!(metric_count, 2);
3298 }
3299
3300 fn claude_snapshot_with_credits(
3301 cloud: Option<CloudCredits>,
3302 usage: Option<UsageCredits>,
3303 ) -> VendorSnapshot {
3304 VendorSnapshot::Anthropic(AnthropicSnapshot {
3305 plan: "Max 5x".into(),
3306 session: UsageWindow {
3307 utilization_pct: 12,
3308 resets_at: Some(now() + chrono::Duration::hours(2)),
3309 window_duration: chrono::Duration::hours(5),
3310 },
3311 weekly: UsageWindow {
3312 utilization_pct: 34,
3313 resets_at: None,
3314 window_duration: chrono::Duration::days(7),
3315 },
3316 sonnet: None,
3317 scoped: vec![],
3318 extra: None,
3319 reset_credits: Default::default(),
3320 cloud_credits: cloud,
3321 usage_credits: usage,
3322 })
3323 }
3324
3325 #[test]
3326 fn anthropic_cloud_session_credits_render_as_a_metric() {
3327 let snap = claude_snapshot_with_credits(
3332 Some(CloudCredits {
3333 limit: Cents(25000),
3334 remaining: Cents(25000),
3335 expires_at: Some("2026-11-05T07:59:00Z".parse().unwrap()),
3336 locked: false,
3337 }),
3338 None,
3339 );
3340 let tab = ready(snap);
3341 let sections = sections_for(&tab, now(), 5);
3342 let metric = sections
3343 .iter()
3344 .find_map(|s| match s {
3345 Section::Metric {
3346 label,
3347 pct,
3348 severity,
3349 value_label,
3350 footnote,
3351 ..
3352 } if label == "Cloud session credits" => {
3353 Some((*pct, *severity, value_label.clone(), footnote.clone()))
3354 }
3355 _ => None,
3356 })
3357 .expect("a cloud session credits metric under the Claude account");
3358 assert_eq!(metric.0, 0);
3359 assert_eq!(metric.1, severity_for(0));
3360 assert_eq!(metric.2, "$250.00");
3362 assert!(
3366 metric
3367 .3
3368 .starts_with("$0.00 of $250.00 used (0%) · expires "),
3369 "{:?}",
3370 metric.3
3371 );
3372 assert!(!metric.3.contains("locked"), "{:?}", metric.3);
3373 let with_meta = sections_with_metadata_for(&tab, now(), 5);
3377 let projected = with_meta
3378 .iter()
3379 .find(|p| {
3380 matches!(&p.section, Section::Metric { label, .. } if label == "Cloud session credits")
3381 })
3382 .expect("the metric in the projection");
3383 assert_eq!(projected.headline, MetricHeadline::Value);
3384 assert!(projected.reset_at.is_none());
3385 }
3386
3387 #[test]
3388 fn anthropic_cloud_credits_headline_the_reported_balance_above_the_limit() {
3389 let snap = claude_snapshot_with_credits(
3391 Some(CloudCredits {
3392 limit: Cents(100),
3393 remaining: Cents(200),
3394 expires_at: None,
3395 locked: false,
3396 }),
3397 None,
3398 );
3399 let sections = sections_for(&ready(snap), now(), 5);
3400 let (pct, value) = sections
3401 .iter()
3402 .find_map(|s| match s {
3403 Section::Metric {
3404 label,
3405 pct,
3406 value_label,
3407 ..
3408 } if label == "Cloud session credits" => Some((*pct, value_label.clone())),
3409 _ => None,
3410 })
3411 .expect("a cloud session credits metric");
3412 assert_eq!(pct, 0);
3413 assert_eq!(value, "$2.00");
3414 }
3415
3416 #[test]
3417 fn anthropic_cloud_credits_meter_the_used_share() {
3418 let snap = claude_snapshot_with_credits(
3420 Some(CloudCredits {
3421 limit: Cents(25000),
3422 remaining: Cents(15000),
3423 expires_at: None,
3424 locked: false,
3425 }),
3426 None,
3427 );
3428 let sections = sections_for(&ready(snap), now(), 5);
3429 let metric = sections
3430 .iter()
3431 .find_map(|s| match s {
3432 Section::Metric {
3433 label,
3434 pct,
3435 severity,
3436 value_label,
3437 ..
3438 } if label == "Cloud session credits" => {
3439 Some((*pct, *severity, value_label.clone()))
3440 }
3441 _ => None,
3442 })
3443 .expect("a cloud session credits metric");
3444 assert_eq!(metric.0, 40);
3445 assert_eq!(metric.1, severity_for(40));
3446 assert_eq!(metric.2, "$150.00");
3449 let footnote = sections
3450 .iter()
3451 .find_map(|s| match s {
3452 Section::Metric {
3453 label, footnote, ..
3454 } if label == "Cloud session credits" => Some(footnote.clone()),
3455 _ => None,
3456 })
3457 .expect("the footnote");
3458 assert_eq!(footnote, "$100.00 of $250.00 used (40%)");
3460 }
3461
3462 #[test]
3463 fn anthropic_locked_cloud_credits_without_an_expiry_says_locked() {
3464 let snap = claude_snapshot_with_credits(
3466 Some(CloudCredits {
3467 limit: Cents(25000),
3468 remaining: Cents(25000),
3469 expires_at: None,
3470 locked: true,
3471 }),
3472 None,
3473 );
3474 let sections = sections_for(&ready(snap), now(), 5);
3475 let footnote = sections
3476 .iter()
3477 .find_map(|s| match s {
3478 Section::Metric {
3479 label, footnote, ..
3480 } if label == "Cloud session credits" => Some(footnote.clone()),
3481 _ => None,
3482 })
3483 .expect("a cloud session credits metric");
3484 assert_eq!(footnote, "$0.00 of $250.00 used (0%) · locked");
3487 }
3488
3489 #[test]
3490 fn anthropic_cloud_and_usage_credits_sit_between_extra_usage_and_resets() {
3491 let credits = ResetCredits {
3492 available: 1,
3493 credits: vec![ResetCredit {
3494 title: Some("Opus launch reset".into()),
3495 expires_at: Some(now() + chrono::Duration::days(20)),
3496 }],
3497 };
3498 let mut snap = claude_snapshot_with_credits(
3499 Some(CloudCredits {
3500 limit: Cents(25000),
3501 remaining: Cents(25000),
3502 expires_at: Some(now() + chrono::Duration::days(10)),
3503 locked: false,
3504 }),
3505 Some(UsageCredits {
3506 balance: Cents(1500),
3507 decimal_places: 2,
3508 currency: Some("USD".into()),
3509 auto_reload: Some(true),
3510 }),
3511 );
3512 if let VendorSnapshot::Anthropic(a) = &mut snap {
3513 a.extra = Some(ExtraUsage {
3514 limit: Some(Cents(5000)),
3515 spent: Cents(250),
3516 currency: None,
3517 decimal_places: Some(2),
3518 status: crate::usage::ExtraUsageStatus::Active,
3519 });
3520 a.reset_credits = credits;
3521 }
3522 let sections = sections_for(&ready(snap), now(), 5);
3523 let index_of = |probe: &str| {
3524 sections
3525 .iter()
3526 .position(|s| {
3527 matches!(
3528 s,
3529 Section::Metric { label, .. }
3530 | Section::Block { label, .. } if label == probe
3531 )
3532 })
3533 .unwrap_or_else(|| panic!("missing section {probe}"))
3534 };
3535 let extra = index_of("Extra usage");
3536 let cloud = index_of("Cloud session credits");
3537 let usage = index_of("Usage credits");
3538 let resets = index_of("Reset credits");
3539 assert!(
3540 extra < cloud && cloud < usage && usage < resets,
3541 "order must be Extra usage < Cloud < Usage credits < Reset credits: {extra} < {cloud} < {usage} < {resets}"
3542 );
3543 }
3544
3545 #[test]
3546 fn anthropic_usage_credits_render_as_a_block_with_the_balance_and_flag() {
3547 for (auto_reload, expected) in [
3548 (Some(true), vec!["balance: $15.00", "auto-reload: on"]),
3549 (Some(false), vec!["balance: $15.00", "auto-reload: off"]),
3550 (None, vec!["balance: $15.00"]),
3551 ] {
3552 let snap = claude_snapshot_with_credits(
3553 None,
3554 Some(UsageCredits {
3555 balance: Cents(1500),
3556 decimal_places: 2,
3557 currency: Some("USD".into()),
3558 auto_reload,
3559 }),
3560 );
3561 let sections = sections_for(&ready(snap), now(), 5);
3562 let body = sections
3563 .iter()
3564 .find_map(|s| match s {
3565 Section::Block { label, body } if label == "Usage credits" => {
3566 Some(body.clone())
3567 }
3568 _ => None,
3569 })
3570 .unwrap_or_else(|| panic!("usage credits block, auto_reload {auto_reload:?}"));
3571 assert_eq!(body, expected, "auto_reload {auto_reload:?}");
3572 }
3573
3574 let snap = claude_snapshot_with_credits(
3577 None,
3578 Some(UsageCredits {
3579 balance: Cents(0),
3580 decimal_places: 0,
3581 currency: Some("KRW".into()),
3582 auto_reload: None,
3583 }),
3584 );
3585 let sections = sections_for(&ready(snap), now(), 5);
3586 let body = sections
3587 .iter()
3588 .find_map(|s| match s {
3589 Section::Block { label, body } if label == "Usage credits" => Some(body.clone()),
3590 _ => None,
3591 })
3592 .expect("a reported balance renders even at zero");
3593 assert_eq!(body, vec!["balance: 0 KRW"]);
3594 }
3595
3596 #[test]
3597 fn anthropic_omits_credit_sections_when_the_response_brought_none() {
3598 let snap = claude_snapshot_with_credits(None, None);
3599 let sections = sections_for(&ready(snap), now(), 5);
3600 assert!(!sections.iter().any(|s| matches!(
3601 s,
3602 Section::Metric { label, .. }
3603 if label == "Cloud session credits"
3604 )));
3605 assert!(!sections.iter().any(|s| matches!(
3606 s,
3607 Section::Block { label, .. } if label == "Usage credits"
3608 )));
3609 }
3610
3611 #[test]
3612 fn openrouter_always_has_balance_metric_and_period_block() {
3613 let snap = OpenRouterSnapshot {
3614 label: "OR".into(),
3615 total_credits: 100.0,
3616 total_usage: 25.0,
3617 usage_daily: 1.0,
3618 usage_weekly: 5.0,
3619 usage_monthly: 25.0,
3620 is_free_tier: false,
3621 limit: None,
3622 limit_remaining: None,
3623 recent_models: Vec::new(),
3624 };
3625 let sections = sections_for(&ready(VendorSnapshot::Openrouter(snap)), now(), 5);
3626 assert!(matches!(sections[0], Section::Title { .. }));
3627 assert!(
3628 sections
3629 .iter()
3630 .any(|s| matches!(s, Section::Metric { label, .. } if label == "Credit balance"))
3631 );
3632 assert!(
3633 sections
3634 .iter()
3635 .any(|s| matches!(s, Section::Block { label, .. } if label == "Usage by period"))
3636 );
3637 }
3638
3639 #[test]
3640 fn openrouter_recent_models_block_renders_only_when_present() {
3641 let snap = |recent_models: Vec<String>| OpenRouterSnapshot {
3642 label: "OR".into(),
3643 total_credits: 100.0,
3644 total_usage: 25.0,
3645 usage_daily: 1.0,
3646 usage_weekly: 5.0,
3647 usage_monthly: 25.0,
3648 is_free_tier: false,
3649 limit: None,
3650 limit_remaining: None,
3651 recent_models,
3652 };
3653 let models = vec!["gpt-5-codex ($1.25 · 42 reqs)".to_string()];
3654 let sections = sections_for(
3655 &ready(VendorSnapshot::Openrouter(snap(models.clone()))),
3656 now(),
3657 5,
3658 );
3659 let block = sections.iter().find_map(|s| match s {
3660 Section::Block { label, body } if label == "Recent models" => Some(body),
3661 _ => None,
3662 });
3663 assert_eq!(block, Some(&models));
3664
3665 let sections = sections_for(
3666 &ready(VendorSnapshot::Openrouter(snap(Vec::new()))),
3667 now(),
3668 5,
3669 );
3670 assert!(
3671 !sections
3672 .iter()
3673 .any(|s| matches!(s, Section::Block { label, .. } if label == "Recent models"))
3674 );
3675 }
3676
3677 #[test]
3681 fn openrouter_debt_reaches_the_panel_row_red_and_signed() {
3682 let snap = OpenRouterSnapshot {
3683 label: "OR".into(),
3684 total_credits: 0.0,
3685 total_usage: 5.71,
3686 usage_daily: 1.0,
3687 usage_weekly: 5.0,
3688 usage_monthly: 5.71,
3689 is_free_tier: false,
3690 limit: None,
3691 limit_remaining: None,
3692 recent_models: Vec::new(),
3693 };
3694 let sections = sections_for(&ready(VendorSnapshot::Openrouter(snap.clone())), now(), 5);
3695 let metric = sections
3696 .iter()
3697 .find_map(|s| match s {
3698 Section::Metric {
3699 label,
3700 value_label,
3701 severity,
3702 footnote,
3703 ..
3704 } if label == "Credit balance" => Some((value_label, severity, footnote)),
3705 _ => None,
3706 })
3707 .expect("no credit balance metric");
3708 assert_eq!(metric.0, "-$5.71");
3709 assert_eq!(*metric.1, PaceSeverity::Critical);
3710 assert_eq!(metric.2, "$5.71 of $0.00 used (0%)");
3711
3712 let (_, cells) = compact_cells(&VendorSnapshot::Openrouter(snap));
3714 assert_eq!(cells[0].0, "-$5.71");
3715 }
3716
3717 #[test]
3721 fn zai_windows_are_paced_like_every_other_percentage_vendor() {
3722 let window = |pct: i32, hours: i64, span: chrono::Duration| crate::usage::UsageWindow {
3723 utilization_pct: pct,
3724 resets_at: Some(now() + chrono::Duration::hours(hours)),
3725 window_duration: span,
3726 };
3727 let snap = ZaiSnapshot {
3728 plan: "GLM Coding Pro".into(),
3729 session: Some(window(40, 2, chrono::Duration::hours(5))),
3730 weekly: Some(window(60, 48, chrono::Duration::days(7))),
3731 mcp: Some(window(10, 200, chrono::Duration::days(30))),
3732 };
3733
3734 let footnotes: Vec<String> = sections_for(&ready(VendorSnapshot::Zai(snap)), now(), 5)
3735 .into_iter()
3736 .filter_map(|section| match section {
3737 Section::Metric { footnote, .. } => Some(footnote),
3738 _ => None,
3739 })
3740 .collect();
3741
3742 assert_eq!(footnotes.len(), 3, "{footnotes:?}");
3743 for footnote in &footnotes {
3744 assert!(footnote.contains("% elapsed"), "{footnote}");
3745 }
3746 assert_eq!(
3748 footnotes[0], "Resets in 2h 00m · 60% elapsed · 20pts under",
3749 "{footnotes:?}"
3750 );
3751 }
3752
3753 #[test]
3754 fn zai_no_windows_renders_message() {
3755 let snap = ZaiSnapshot {
3756 plan: "GLM".into(),
3757 session: None,
3758 weekly: None,
3759 mcp: None,
3760 };
3761 let sections = sections_for(&ready(VendorSnapshot::Zai(snap)), now(), 5);
3762 assert!(sections.iter().any(|s| matches!(
3763 s,
3764 Section::Text { value, .. } if value.contains("no usage windows reported")
3765 )));
3766 }
3767
3768 #[test]
3769 fn openai_no_windows_renders_message() {
3770 let snap = OpenAiSnapshot {
3771 plan: "ChatGPT Plus".into(),
3772 session: None,
3773 weekly: None,
3774 code_review: None,
3775 additional_limits: Vec::new(),
3776 unavailable_models: Vec::new(),
3777 credits: None,
3778 reset_credits: None,
3779 source: OpenAiSource::CodexOauth,
3780 };
3781 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
3782 assert!(sections.iter().any(|s| matches!(
3783 s,
3784 Section::Text { value, .. } if value.contains("no usage windows reported")
3785 )));
3786 }
3787
3788 #[test]
3789 fn loading_state_yields_loading_section() {
3790 let sections = sections_for(&TabState::Loading, now(), 5);
3791 assert!(sections.iter().any(|s| matches!(
3792 s,
3793 Section::Text { value, .. } if value.contains("Loading")
3794 )));
3795 }
3796
3797 #[test]
3798 fn error_state_includes_retry_hint() {
3799 let sections = sections_for(&TabState::error("token expired"), now(), 5);
3800 assert!(sections.iter().any(|s| matches!(
3801 s,
3802 Section::Text { value, .. } if value.contains("token expired")
3803 )));
3804 assert!(sections.iter().any(|s| matches!(
3805 s,
3806 Section::Text { value, .. } if value.contains("`r` to retry")
3807 )));
3808 }
3809
3810 #[test]
3811 fn error_state_keeps_oauth_plan_as_title() {
3812 let sections = sections_for(
3813 &TabState::error_with_plan("HTTP 401", Some("Claude Max 5x".into())),
3814 now(),
3815 5,
3816 );
3817 assert!(matches!(
3818 §ions[0],
3819 Section::Title { left, .. } if left == "Claude Max 5x"
3820 ));
3821 assert!(sections.iter().any(|s| matches!(
3822 s,
3823 Section::Text { value, .. } if value.contains("HTTP 401")
3824 )));
3825 assert!(!sections.iter().any(|s| matches!(s, Section::Metric { .. })));
3826 }
3827
3828 #[test]
3829 fn openai_with_credits_renders_block() {
3830 let snap = OpenAiSnapshot {
3831 plan: "ChatGPT Plus".into(),
3832 session: Some(UsageWindow {
3833 utilization_pct: 1,
3834 resets_at: None,
3835 window_duration: chrono::Duration::hours(5),
3836 }),
3837 weekly: Some(UsageWindow {
3838 utilization_pct: 0,
3839 resets_at: None,
3840 window_duration: chrono::Duration::days(7),
3841 }),
3842 code_review: None,
3843 additional_limits: Vec::new(),
3844 unavailable_models: Vec::new(),
3845 credits: Some(OpenAiCredits {
3846 balance: "62500".into(),
3847 has_credits: true,
3848 unlimited: false,
3849 approx_local_messages: Some((100, 200)),
3850 approx_cloud_messages: Some((30, 50)),
3851 }),
3852 reset_credits: None,
3853 source: OpenAiSource::CodexOauth,
3854 };
3855 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
3856 let body = sections.iter().find_map(|s| match s {
3857 Section::Block { label, body } if label == "Credits" => Some(body.clone()),
3858 _ => None,
3859 });
3860 assert_eq!(body, Some(vec!["62,500".to_string()]));
3865 }
3866
3867 #[test]
3868 fn openai_credits_block_omits_message_estimate_lines_when_both_ranges_are_reported() {
3869 let snap = OpenAiSnapshot {
3870 plan: "ChatGPT Plus".into(),
3871 session: None,
3872 weekly: None,
3873 code_review: None,
3874 additional_limits: Vec::new(),
3875 unavailable_models: Vec::new(),
3876 credits: Some(OpenAiCredits {
3877 balance: "62500".into(),
3878 has_credits: true,
3879 unlimited: false,
3880 approx_local_messages: Some((100, 200)),
3881 approx_cloud_messages: Some((30, 50)),
3882 }),
3883 reset_credits: None,
3884 source: OpenAiSource::CodexOauth,
3885 };
3886 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
3887 let body = sections
3888 .iter()
3889 .find_map(|s| match s {
3890 Section::Block { label, body } if label == "Credits" => Some(body.clone()),
3891 _ => None,
3892 })
3893 .expect("the Credits block is rendered");
3894 assert!(body.iter().any(|line| line.contains("62,500")));
3897 assert!(
3898 !body
3899 .iter()
3900 .any(|line| line.contains("local messages") || line.contains("cloud messages"))
3901 );
3902 }
3903
3904 #[test]
3905 fn openai_non_numeric_balance_is_shown_without_a_unit() {
3906 let snap = OpenAiSnapshot {
3907 plan: "ChatGPT Plus".into(),
3908 session: None,
3909 weekly: None,
3910 code_review: None,
3911 additional_limits: Vec::new(),
3912 unavailable_models: Vec::new(),
3913 credits: Some(OpenAiCredits {
3914 balance: "$5.00".into(),
3915 has_credits: true,
3916 unlimited: false,
3917 approx_local_messages: None,
3918 approx_cloud_messages: None,
3919 }),
3920 reset_credits: None,
3921 source: OpenAiSource::CodexOauth,
3922 };
3923 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
3924 let body = sections.iter().find_map(|s| match s {
3925 Section::Block { label, body } if label == "Credits" => Some(body.clone()),
3926 _ => None,
3927 });
3928 assert_eq!(
3930 body.as_ref().and_then(|b| b.first()),
3931 Some(&"$5.00".to_string())
3932 );
3933 }
3934
3935 #[test]
3936 fn openai_weekly_only_marks_the_session_window_as_not_reported() {
3937 let snap = OpenAiSnapshot {
3938 plan: "ChatGPT Prolite".into(),
3939 session: None,
3940 weekly: Some(UsageWindow {
3941 utilization_pct: 66,
3942 resets_at: None,
3943 window_duration: chrono::Duration::days(7),
3944 }),
3945 code_review: None,
3946 additional_limits: Vec::new(),
3947 unavailable_models: Vec::new(),
3948 credits: None,
3949 reset_credits: None,
3950 source: OpenAiSource::CodexOauth,
3951 };
3952 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
3953 assert!(sections.iter().any(|section| matches!(
3954 section,
3955 Section::Metric { label, .. } if label == "Codex weekly"
3956 )));
3957 assert!(!sections.iter().any(|section| matches!(
3958 section,
3959 Section::Metric { label, .. } if label == "Codex 5h"
3960 )));
3961 assert!(sections.iter().any(|section| matches!(
3962 section,
3963 Section::Text { label, value }
3964 if label == "Codex 5h" && value == "Not reported"
3965 )));
3966 }
3967
3968 #[test]
3969 fn openai_session_only_marks_the_weekly_window_as_not_reported() {
3970 let snap = OpenAiSnapshot {
3971 plan: "ChatGPT Plus".into(),
3972 session: Some(UsageWindow {
3973 utilization_pct: 12,
3974 resets_at: None,
3975 window_duration: chrono::Duration::hours(5),
3976 }),
3977 weekly: None,
3978 code_review: None,
3979 additional_limits: Vec::new(),
3980 unavailable_models: Vec::new(),
3981 credits: None,
3982 reset_credits: None,
3983 source: OpenAiSource::CodexOauth,
3984 };
3985 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
3986 assert!(sections.iter().any(|section| matches!(
3987 section,
3988 Section::Metric { label, .. } if label == "Codex 5h"
3989 )));
3990 assert!(sections.iter().any(|section| matches!(
3991 section,
3992 Section::Text { label, value }
3993 if label == "Codex weekly" && value == "Not reported"
3994 )));
3995 }
3996
3997 #[test]
4002 fn banked_resets_reach_the_panel_for_every_provider() {
4003 let now = now();
4004 let credits = ResetCredits {
4005 available: 2,
4006 credits: vec![
4007 ResetCredit {
4008 title: Some("Full reset (Weekly + 5 hr)".into()),
4009 expires_at: Some(now + chrono::Duration::days(13)),
4010 },
4011 ResetCredit {
4012 title: Some("Full reset (Weekly + 5 hr)".into()),
4013 expires_at: Some(now + chrono::Duration::days(13) + chrono::Duration::hours(6)),
4014 },
4015 ],
4016 };
4017 let codex = OpenAiSnapshot {
4018 plan: "ChatGPT Plus".into(),
4019 session: None,
4020 weekly: None,
4021 code_review: None,
4022 additional_limits: Vec::new(),
4023 unavailable_models: Vec::new(),
4024 credits: None,
4025 reset_credits: Some(credits.clone()),
4026 source: OpenAiSource::CodexOauth,
4027 };
4028 let supergrok = crate::usage::SuperGrokSnapshot {
4029 plan: "SuperGrok".into(),
4030 account: "scope".into(),
4031 weekly_pct: 30,
4032 period: crate::usage::SuperGrokPeriod::Weekly,
4033 reset_at: Some(now + chrono::Duration::days(3)),
4034 prepaid_balance: None,
4035 on_demand_used_cents: None,
4036 on_demand_cap_cents: None,
4037 reset_credits: credits.clone(),
4038 products: Vec::new(),
4039 };
4040 let claude = AnthropicSnapshot {
4041 plan: "Max 20x".into(),
4042 session: UsageWindow {
4043 utilization_pct: 2,
4044 resets_at: Some(now + chrono::Duration::hours(1)),
4045 window_duration: chrono::Duration::hours(5),
4046 },
4047 weekly: UsageWindow {
4048 utilization_pct: 63,
4049 resets_at: Some(now + chrono::Duration::days(1)),
4050 window_duration: chrono::Duration::days(7),
4051 },
4052 sonnet: None,
4053 scoped: Vec::new(),
4054 extra: None,
4055 reset_credits: credits,
4056 cloud_credits: None,
4057 usage_credits: None,
4058 };
4059
4060 for snapshot in [
4061 VendorSnapshot::Openai(codex),
4062 VendorSnapshot::SuperGrok(supergrok),
4063 VendorSnapshot::Anthropic(claude),
4064 ] {
4065 let sections = sections_for(&ready(snapshot), now, 5);
4066 let body = sections.iter().find_map(|section| match section {
4067 Section::Block { label, body } if label == "Reset credits" => Some(body.clone()),
4068 _ => None,
4069 });
4070 let body = body.expect("reset credits block");
4071 assert_eq!(body.len(), 2, "{body:?}");
4072 assert!(
4073 body.iter()
4074 .all(|line| line.contains("Full reset (Weekly + 5 hr)")),
4075 "{body:?}"
4076 );
4077 }
4078 }
4079
4080 #[test]
4083 fn a_provider_with_no_banked_resets_shows_no_reset_row() {
4084 let snap = OpenAiSnapshot {
4085 plan: "ChatGPT Plus".into(),
4086 session: None,
4087 weekly: None,
4088 code_review: None,
4089 additional_limits: Vec::new(),
4090 unavailable_models: Vec::new(),
4091 credits: None,
4092 reset_credits: None,
4093 source: OpenAiSource::CodexOauth,
4094 };
4095 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
4096 assert!(!sections.iter().any(|section| matches!(
4097 section,
4098 Section::Block { label, .. } if label == "Reset credits"
4099 )));
4100 }
4101
4102 #[test]
4103 fn a_codex_account_that_states_zero_resets_shows_none_available() {
4104 let snap = OpenAiSnapshot {
4105 plan: "ChatGPT Pro".into(),
4106 session: None,
4107 weekly: None,
4108 code_review: None,
4109 additional_limits: Vec::new(),
4110 unavailable_models: Vec::new(),
4111 credits: None,
4112 reset_credits: Some(ResetCredits::default()),
4113 source: OpenAiSource::CodexOauth,
4114 };
4115 let sections = sections_for(&ready(VendorSnapshot::Openai(snap)), now(), 5);
4116 assert!(sections.iter().any(|section| matches!(
4117 section,
4118 Section::Block { label, body }
4119 if label == "Reset credits" && body == &vec!["None available".to_string()]
4120 )));
4121 }
4122
4123 #[test]
4124 fn supergrok_does_not_repeat_the_window_reset_as_its_own_section() {
4125 let now = now();
4126 let snap = crate::usage::SuperGrokSnapshot {
4127 plan: "SuperGrok".into(),
4128 account: "scope".into(),
4129 weekly_pct: 0,
4130 period: crate::usage::SuperGrokPeriod::Weekly,
4131 reset_at: Some(now + chrono::Duration::days(6)),
4132 prepaid_balance: Some(0.0),
4133 on_demand_used_cents: None,
4134 on_demand_cap_cents: None,
4135 reset_credits: ResetCredits::default(),
4136 products: Vec::new(),
4137 };
4138 let sections = sections_for(&ready(VendorSnapshot::SuperGrok(snap)), now, 5);
4139 assert!(!sections.iter().any(|section| matches!(
4140 section,
4141 Section::Text { label, .. } if label == "Resets"
4142 )));
4143 assert!(!sections.iter().any(|section| matches!(
4146 section,
4147 Section::Text { label, .. } if label == "Prepaid API"
4148 )));
4149 }
4150
4151 #[test]
4155 fn supergrok_hides_a_zero_prepaid_balance_but_keeps_a_real_one() {
4156 let now = now();
4157 let base = |prepaid: Option<f64>| crate::usage::SuperGrokSnapshot {
4158 plan: "SuperGrok".into(),
4159 account: "scope".into(),
4160 weekly_pct: 40,
4161 period: crate::usage::SuperGrokPeriod::Weekly,
4162 reset_at: Some(now + chrono::Duration::days(6)),
4163 prepaid_balance: prepaid,
4164 on_demand_used_cents: None,
4165 on_demand_cap_cents: None,
4166 reset_credits: ResetCredits::default(),
4167 products: Vec::new(),
4168 };
4169 let labels = |snap| {
4170 sections_for(&ready(VendorSnapshot::SuperGrok(snap)), now, 5)
4171 .into_iter()
4172 .filter_map(|section| match section {
4173 Section::Text { label, value, .. } => Some((label, value)),
4174 _ => None,
4175 })
4176 .collect::<Vec<_>>()
4177 };
4178 assert!(
4179 !labels(base(Some(0.0)))
4180 .iter()
4181 .any(|(label, _)| label == "Prepaid API")
4182 );
4183 assert_eq!(
4184 labels(base(Some(4.22))).last(),
4185 Some(&("Prepaid API".to_string(), "$4.22".to_string()))
4186 );
4187 }
4188
4189 #[test]
4192 fn supergrok_lists_additional_usage_when_the_billing_document_has_it() {
4193 let now = now();
4194 let mut snap = crate::usage::SuperGrokSnapshot {
4195 plan: "SuperGrok".into(),
4196 account: "scope".into(),
4197 weekly_pct: 40,
4198 period: crate::usage::SuperGrokPeriod::Weekly,
4199 reset_at: Some(now + chrono::Duration::days(6)),
4200 prepaid_balance: None,
4201 reset_credits: ResetCredits::default(),
4202 products: Vec::new(),
4203 on_demand_used_cents: Some(0),
4204 on_demand_cap_cents: Some(0),
4205 };
4206 let labels = |snap: crate::usage::SuperGrokSnapshot| {
4207 sections_for(&ready(VendorSnapshot::SuperGrok(snap)), now, 5)
4208 .into_iter()
4209 .filter_map(|section| match section {
4210 Section::Text { label, value, .. } => Some((label, value)),
4211 _ => None,
4212 })
4213 .collect::<Vec<_>>()
4214 };
4215 assert!(labels(snap.clone()).is_empty());
4216 snap.on_demand_used_cents = Some(150);
4217 snap.on_demand_cap_cents = Some(2000);
4218 assert_eq!(
4219 labels(snap),
4220 vec![("Additional usage".to_string(), "$1.50 / $20.00".to_string())]
4221 );
4222 }
4223
4224 #[test]
4225 fn supergrok_lists_product_slices_beside_the_overall_meter() {
4226 let now = now();
4227 let snap = crate::usage::SuperGrokSnapshot {
4228 plan: "SuperGrok".into(),
4229 account: "scope".into(),
4230 weekly_pct: 90,
4231 period: crate::usage::SuperGrokPeriod::Weekly,
4232 reset_at: Some(now + chrono::Duration::days(3)),
4233 prepaid_balance: None,
4234 on_demand_used_cents: None,
4235 on_demand_cap_cents: None,
4236 reset_credits: ResetCredits::default(),
4237 products: vec![
4238 crate::usage::SuperGrokProduct {
4239 label: "Grok Build".into(),
4240 percent: 87,
4241 },
4242 crate::usage::SuperGrokProduct {
4243 label: "Grok Chat".into(),
4244 percent: 3,
4245 },
4246 ],
4247 };
4248 let labels: Vec<_> = sections_for(&ready(VendorSnapshot::SuperGrok(snap)), now, 5)
4249 .into_iter()
4250 .filter_map(|section| match section {
4251 Section::Metric { label, pct, .. } => Some((label, pct)),
4252 _ => None,
4253 })
4254 .collect();
4255 assert_eq!(
4256 labels,
4257 vec![
4258 ("Weekly usage".into(), 90),
4259 ("Grok Build".into(), 87),
4260 ("Grok Chat".into(), 3),
4261 ]
4262 );
4263 }
4264
4265 #[test]
4269 fn supergrok_product_slices_carry_the_breakdown_group() {
4270 let now = now();
4271 let snap = crate::usage::SuperGrokSnapshot {
4272 plan: "SuperGrok".into(),
4273 account: "scope".into(),
4274 weekly_pct: 90,
4275 period: crate::usage::SuperGrokPeriod::Weekly,
4276 reset_at: Some(now + chrono::Duration::days(3)),
4277 prepaid_balance: None,
4278 on_demand_used_cents: None,
4279 on_demand_cap_cents: None,
4280 reset_credits: ResetCredits::default(),
4281 products: vec![crate::usage::SuperGrokProduct {
4282 label: "Grok Build".into(),
4283 percent: 87,
4284 }],
4285 };
4286 let projected = sections_with_metadata_for(&ready(VendorSnapshot::SuperGrok(snap)), now, 5);
4287 let mut metrics = projected
4288 .iter()
4289 .filter(|p| matches!(p.section, Section::Metric { .. }));
4290 let overall = metrics.next().expect("overall usage metric");
4291 let build = metrics.next().expect("one product slice metric");
4292 assert!(metrics.next().is_none(), "expected exactly two metric rows");
4293 assert_eq!(overall.group, None);
4294 assert!(overall.reset_at.is_some());
4295 assert_eq!(build.group, Some("Breakdown"));
4296 assert_eq!(build.reset_at, None);
4297 assert_eq!(build.window, None);
4298 }
4299
4300 #[test]
4304 fn supergrok_weekly_usage_row_carries_the_countdown_and_pace() {
4305 use crate::usage::SuperGrokPeriod;
4306
4307 let sections =
4308 sections_with_metadata_for(&ready(supergrok(SuperGrokPeriod::Weekly)), now(), 5);
4309 let Section::Metric { footnote, .. } = &only_metric(§ions).section else {
4310 unreachable!()
4311 };
4312 assert_eq!(footnote, "Resets in 2d 0h · 71% elapsed · 31pts under");
4314 }
4315
4316 #[test]
4319 fn supergrok_monthly_and_unknown_usage_rows_carry_the_countdown_alone() {
4320 use crate::usage::SuperGrokPeriod;
4321
4322 for period in [SuperGrokPeriod::Monthly, SuperGrokPeriod::Unknown] {
4323 let sections = sections_with_metadata_for(&ready(supergrok(period)), now(), 5);
4324 let Section::Metric { footnote, .. } = &only_metric(§ions).section else {
4325 unreachable!()
4326 };
4327 assert_eq!(footnote, "Resets in 2d 0h", "{period:?}");
4328 }
4329 }
4330
4331 #[test]
4334 fn supergrok_usage_row_without_a_reset_has_no_footnote() {
4335 let now = now();
4336 let snap = crate::usage::SuperGrokSnapshot {
4337 plan: "SuperGrok".into(),
4338 account: "scope".into(),
4339 weekly_pct: 40,
4340 period: crate::usage::SuperGrokPeriod::Weekly,
4341 reset_at: None,
4342 prepaid_balance: None,
4343 on_demand_used_cents: None,
4344 on_demand_cap_cents: None,
4345 reset_credits: ResetCredits::default(),
4346 products: Vec::new(),
4347 };
4348 let projected = sections_with_metadata_for(&ready(VendorSnapshot::SuperGrok(snap)), now, 5);
4349 let Section::Metric { footnote, .. } = &only_metric(&projected).section else {
4350 unreachable!()
4351 };
4352 assert_eq!(footnote, "");
4353 }
4354
4355 #[test]
4358 fn supergrok_product_slices_keep_an_empty_footnote() {
4359 let now = now();
4360 let snap = crate::usage::SuperGrokSnapshot {
4361 plan: "SuperGrok".into(),
4362 account: "scope".into(),
4363 weekly_pct: 90,
4364 period: crate::usage::SuperGrokPeriod::Weekly,
4365 reset_at: Some(now + chrono::Duration::days(3)),
4366 prepaid_balance: None,
4367 on_demand_used_cents: None,
4368 on_demand_cap_cents: None,
4369 reset_credits: ResetCredits::default(),
4370 products: vec![crate::usage::SuperGrokProduct {
4371 label: "Grok Build".into(),
4372 percent: 87,
4373 }],
4374 };
4375 let projected = sections_with_metadata_for(&ready(VendorSnapshot::SuperGrok(snap)), now, 5);
4376 let footnotes: Vec<&str> = projected
4377 .iter()
4378 .filter_map(|p| match &p.section {
4379 Section::Metric { footnote, .. } => Some(footnote.as_str()),
4380 _ => None,
4381 })
4382 .collect();
4383 assert_eq!(
4384 footnotes,
4385 ["Resets in 3d 0h · 57% elapsed · 33pts ahead", ""]
4386 );
4387 }
4388
4389 #[test]
4390 fn kimi_sections_include_weekly_and_window_with_used_over_limit() {
4391 let now = now();
4392 let snap = KimiSnapshot {
4393 plan: Some("LEVEL_INTERMEDIATE".into()),
4394 weekly_limit: 100,
4395 weekly_used: 26,
4396 weekly_remaining: 74,
4397 weekly_reset_at: Some(now + chrono::Duration::days(4)),
4398 has_weekly: true,
4399 monthly_pct: None,
4400 monthly_reset_at: None,
4401 window_limit: 100,
4402 window_used: 15,
4403 window_remaining: 85,
4404 window_reset_at: Some(now + chrono::Duration::hours(2)),
4405 };
4406 let sections = sections_for(&ready(VendorSnapshot::Kimi(snap)), now, 5);
4407 let metrics: Vec<_> = sections
4408 .iter()
4409 .filter(|s| matches!(s, Section::Metric { .. }))
4410 .collect();
4411 assert_eq!(metrics.len(), 2);
4412 assert!(sections.iter().any(|s| matches!(
4413 s,
4414 Section::Metric { label, .. } if label == "Weekly quota"
4415 )));
4416 assert!(sections.iter().any(|s| matches!(
4417 s,
4418 Section::Metric { label, .. } if label == "Rolling window (5h)"
4419 )));
4420
4421 let find_footnote = |label: &str| -> (String, String) {
4422 sections
4423 .iter()
4424 .find_map(|s| match s {
4425 Section::Metric {
4426 label: l,
4427 value_label,
4428 footnote,
4429 ..
4430 } if l == label => Some((value_label.clone(), footnote.clone())),
4431 _ => None,
4432 })
4433 .unwrap_or_else(|| panic!("missing metric {label}"))
4434 };
4435
4436 let (weekly_value, weekly_footnote) = find_footnote("Weekly quota");
4440 assert_eq!(weekly_value, "26%");
4441 assert_eq!(weekly_footnote, "Resets in 4d 0h");
4442
4443 let (window_value, window_footnote) = find_footnote("Rolling window (5h)");
4444 assert_eq!(window_value, "15%");
4445 assert_eq!(window_footnote, "Resets in 2h 00m");
4446 }
4447
4448 #[test]
4455 fn kimi_leads_with_the_rolling_window_like_every_other_two_window_vendor() {
4456 let now = now();
4457 let snap = KimiSnapshot {
4458 plan: Some("LEVEL_INTERMEDIATE".into()),
4459 weekly_limit: 100,
4460 weekly_used: 26,
4461 weekly_remaining: 74,
4462 weekly_reset_at: Some(now + chrono::Duration::days(4)),
4463 has_weekly: true,
4464 monthly_pct: None,
4465 monthly_reset_at: None,
4466 window_limit: 100,
4467 window_used: 15,
4468 window_remaining: 85,
4469 window_reset_at: Some(now + chrono::Duration::hours(2)),
4470 };
4471 let sections = sections_for(&ready(VendorSnapshot::Kimi(snap.clone())), now, 5);
4472 let labels: Vec<&str> = sections
4473 .iter()
4474 .filter_map(|s| match s {
4475 Section::Metric { label, .. } => Some(label.as_str()),
4476 _ => None,
4477 })
4478 .collect();
4479 assert_eq!(labels, ["Rolling window (5h)", "Weekly quota"]);
4480
4481 let (_, cells) = compact_cells(&VendorSnapshot::Kimi(snap));
4482 let texts: Vec<&str> = cells.iter().map(|(text, _)| text.as_str()).collect();
4483 assert_eq!(texts, ["5h 15%", "wk 26%"]);
4484 }
4485
4486 #[test]
4487 fn kimi_sections_omit_window_when_limit_zero() {
4488 let snap = KimiSnapshot {
4489 plan: None,
4490 weekly_limit: 100,
4491 weekly_used: 10,
4492 weekly_remaining: 90,
4493 weekly_reset_at: None,
4494 has_weekly: true,
4495 monthly_pct: None,
4496 monthly_reset_at: None,
4497 window_limit: 0,
4498 window_used: 0,
4499 window_remaining: 0,
4500 window_reset_at: None,
4501 };
4502 let sections = sections_for(&ready(VendorSnapshot::Kimi(snap)), now(), 5);
4503 let metric_count = sections
4504 .iter()
4505 .filter(|s| matches!(s, Section::Metric { .. }))
4506 .count();
4507 assert_eq!(metric_count, 1);
4508 }
4509
4510 #[test]
4515 fn kimi_sections_on_the_monthly_shape_drop_weekly_and_add_monthly() {
4516 let now = now();
4517 let snap = KimiSnapshot {
4518 plan: Some("Allegretto".into()),
4519 weekly_limit: 0,
4520 weekly_used: 0,
4521 weekly_remaining: 0,
4522 weekly_reset_at: None,
4523 has_weekly: false,
4524 monthly_pct: Some(42),
4525 monthly_reset_at: Some(now + chrono::Duration::days(30)),
4526 window_limit: 100,
4527 window_used: 15,
4528 window_remaining: 85,
4529 window_reset_at: Some(now + chrono::Duration::hours(2)),
4530 };
4531 let projected =
4532 sections_with_metadata_for(&ready(VendorSnapshot::Kimi(snap.clone())), now, 5);
4533 let metrics: Vec<_> = projected
4534 .iter()
4535 .filter(|p| matches!(p.section, Section::Metric { .. }))
4536 .collect();
4537 assert_eq!(metrics.len(), 2);
4538 let monthly = metrics
4539 .iter()
4540 .find(|p| matches!(&p.section, Section::Metric { label, .. } if label == "Monthly"))
4541 .expect("a Monthly metric");
4542 assert!(matches!(
4543 &monthly.section,
4544 Section::Metric { value_label, footnote, .. }
4545 if value_label == "42%" && footnote == "Resets in 30d 0h"
4546 ));
4547 assert_eq!(monthly.reset_at, Some(now + chrono::Duration::days(30)));
4548 assert_eq!(monthly.window, None, "no fixed window length, no pacing");
4549 assert!(!metrics.iter().any(
4550 |p| matches!(&p.section, Section::Metric { label, .. } if label == "Weekly quota")
4551 ));
4552
4553 let (_, cells) = compact_cells(&VendorSnapshot::Kimi(snap));
4555 let texts: Vec<&str> = cells.iter().map(|(text, _)| text.as_str()).collect();
4556 assert_eq!(texts, ["5h 15%", "mo 42%"]);
4557 }
4558
4559 fn cursor_snap() -> crate::usage::CursorSnapshot {
4560 crate::usage::CursorSnapshot {
4561 plan: "Ultra".into(),
4562 auto_pct: 98,
4563 api_pct: 100,
4564 total_pct: 99,
4565 unlimited: false,
4566 on_demand_enabled: false,
4567 on_demand_used_cents: None,
4568 on_demand_limit_cents: None,
4569 reset_at: Some(now() + chrono::Duration::days(9)),
4570 cycle_start: None,
4571 credits: Vec::new(),
4572 }
4573 }
4574
4575 #[test]
4576 fn compact_cells_flatten_key_metrics_for_the_overview() {
4577 let (plan, cells) = compact_cells(&VendorSnapshot::Cursor(cursor_snap()));
4579 assert_eq!(plan, "Ultra");
4580 assert_eq!(cells[0].0, "auto 98%");
4581 assert_eq!(cells[1].0, "premium 100%");
4582 assert_eq!(cells[1].1, PaceSeverity::Critical); let (plan, cells) = compact_cells(&VendorSnapshot::Kilo(crate::usage::KiloSnapshot {
4586 label: "Kilo".into(),
4587 balance: 8.42,
4588 }));
4589 assert!(plan.is_empty());
4590 assert_eq!(cells, vec![("$8.42".to_string(), PaceSeverity::Low)]);
4591 }
4592
4593 #[test]
4594 fn terminal_controls_are_removed_from_detail_and_overview_fields() {
4595 let error = TabState::error("bad\x1b]52;c;Y2FuYXJ5\x07 value");
4596 let sections = sections_for(&error, now(), 5);
4597 assert!(matches!(
4598 §ions[1],
4599 Section::Text { value, .. }
4600 if value == "bad]52;c;Y2FuYXJ5 value"
4601 && !value.chars().any(|ch| ch.is_control())
4602 ));
4603
4604 let mut snapshot = cursor_snap();
4605 snapshot.plan = "Ultra\x1b[2J\x07".into();
4606 let (plan, _) = compact_cells(&VendorSnapshot::Cursor(snapshot));
4607 assert_eq!(plan, "Ultra[2J");
4608 assert!(!plan.chars().any(char::is_control));
4609 }
4610
4611 #[test]
4612 fn headline_pct_is_the_worst_window_or_combined_total() {
4613 assert_eq!(
4615 headline_pct(&VendorSnapshot::Cursor(cursor_snap())),
4616 Some(99)
4617 );
4618
4619 let kilo = VendorSnapshot::Kilo(crate::usage::KiloSnapshot {
4621 label: "Kilo".into(),
4622 balance: 8.42,
4623 });
4624 assert_eq!(headline_pct(&kilo), None);
4625 }
4626
4627 #[test]
4628 fn droid_headline_counts_only_started_standard_windows() {
4629 let window = |pct: i32, started: bool| {
4630 Some(crate::usage::UsageWindow {
4631 utilization_pct: pct,
4632 resets_at: started.then(|| now() + chrono::Duration::hours(2)),
4633 window_duration: chrono::Duration::hours(5),
4634 })
4635 };
4636 let droid = |session, weekly, monthly, core| {
4637 VendorSnapshot::Droid(crate::usage::DroidSnapshot {
4638 session,
4639 weekly,
4640 monthly,
4641 core_session: core,
4642 core_weekly: None,
4643 core_monthly: None,
4644 extra_balance_cents: Some(250),
4645 })
4646 };
4647
4648 let snapshot = droid(
4650 window(2, true),
4651 window(0, false),
4652 window(0, false),
4653 window(100, true),
4654 );
4655 assert_eq!(headline_pct(&snapshot), Some(2));
4656 let (_, cells) = compact_cells(&snapshot);
4657 assert!(cells.iter().any(|(label, _)| label == "$2.50"));
4658
4659 let unstarted = droid(
4661 window(0, false),
4662 window(0, false),
4663 window(0, false),
4664 window(100, true),
4665 );
4666 assert_eq!(headline_pct(&unstarted), None);
4667 }
4668
4669 #[test]
4670 fn droid_unstarted_windows_say_so_and_an_idle_core_is_one_row() {
4671 let window = |pct: i32, started: bool| {
4672 Some(crate::usage::UsageWindow {
4673 utilization_pct: pct,
4674 resets_at: started.then(|| now() + chrono::Duration::hours(2)),
4675 window_duration: chrono::Duration::hours(5),
4676 })
4677 };
4678 let snapshot = |core_session| crate::usage::DroidSnapshot {
4679 session: window(0, false),
4680 weekly: window(23, true),
4681 monthly: None,
4682 core_session,
4683 core_weekly: window(0, false),
4684 core_monthly: window(0, false),
4685 extra_balance_cents: Some(0),
4686 };
4687 let footnotes = |sections: &[Section]| {
4688 sections
4689 .iter()
4690 .filter_map(|s| match s {
4691 Section::Metric {
4692 label, footnote, ..
4693 } => Some((label.clone(), footnote.clone())),
4694 _ => None,
4695 })
4696 .collect::<Vec<_>>()
4697 };
4698
4699 let idle = sections_for(
4700 &ready(VendorSnapshot::Droid(snapshot(window(0, false)))),
4701 now(),
4702 5,
4703 );
4704 let rows = footnotes(&idle);
4705 assert_eq!(rows.len(), 2, "Standard only: {rows:?}");
4706 assert_eq!(rows[0], ("5-hour quota".into(), "Not started".into()));
4707 assert!(rows[1].1.starts_with("Resets in"), "{rows:?}");
4708 assert!(idle.iter().any(|s| matches!(
4709 s,
4710 Section::Text { label, value } if label == "Droid Core" && value == "not used yet"
4711 )));
4712
4713 let used = sections_for(
4715 &ready(VendorSnapshot::Droid(snapshot(window(4, true)))),
4716 now(),
4717 5,
4718 );
4719 let rows = footnotes(&used);
4720 assert_eq!(rows.len(), 5, "{rows:?}");
4721 assert_eq!(rows[3].1, "Not started");
4722 assert!(!used.iter().any(|s| matches!(
4723 s,
4724 Section::Text { value, .. } if value == "not used yet"
4725 )));
4726 }
4727
4728 #[test]
4729 fn cursor_sections_show_both_pools_and_reset() {
4730 let mut snapshot = cursor_snap();
4731 snapshot.on_demand_enabled = true;
4732 snapshot.on_demand_used_cents = Some(1785);
4733 snapshot.on_demand_limit_cents = Some(35000);
4734 let sections = sections_for(&ready(VendorSnapshot::Cursor(snapshot)), now(), 5);
4735 let metrics: Vec<_> = sections
4736 .iter()
4737 .filter_map(|s| match s {
4738 Section::Metric {
4739 label, value_label, ..
4740 } => Some((label.clone(), value_label.clone())),
4741 _ => None,
4742 })
4743 .collect();
4744 assert_eq!(metrics.len(), 2, "two pools");
4745 assert!(
4746 metrics
4747 .iter()
4748 .any(|(l, v)| l == "Cursor Models" && v == "98%")
4749 );
4750 assert!(
4751 metrics
4752 .iter()
4753 .any(|(l, v)| l == "Other Models" && v == "100%")
4754 );
4755 assert!(sections.iter().any(|section| matches!(
4756 section,
4757 Section::Text { label, value }
4758 if label == "On-Demand" && value == "$17.85 / $350.00"
4759 )));
4760 assert!(sections.iter().any(|s| matches!(
4761 s,
4762 Section::Text { label, value } if label == "Resets" && value.contains("9d")
4763 )));
4764 }
4765
4766 #[test]
4767 fn cursor_credit_grant_is_a_meter_like_on_demand() {
4768 let mut snapshot = cursor_snap();
4769 snapshot.credits.push(crate::usage::CursorCreditGrant {
4770 remaining_cents: 2100,
4771 total_cents: 2500,
4772 expires_at: Some(now() + chrono::Duration::days(30)),
4773 display_name: "Power user grant".into(),
4774 });
4775 let sections = sections_for(&ready(VendorSnapshot::Cursor(snapshot)), now(), 5);
4776 assert!(sections.iter().any(|section| matches!(
4777 section,
4778 Section::Metric { label, pct, value_label, footnote, .. }
4779 if label == "Credits"
4780 && *pct == 16
4781 && value_label == "$21.00"
4782 && footnote.contains("$4.00 of $25.00 used (16%)")
4783 && footnote.contains("30d 0h")
4784 )));
4785 }
4786
4787 #[test]
4788 fn cursor_on_demand_projection_keeps_the_cents() {
4789 let mut snapshot = cursor_snap();
4790 snapshot.on_demand_enabled = true;
4791 snapshot.on_demand_used_cents = Some(1785);
4792 snapshot.on_demand_limit_cents = Some(35000);
4793 let projected =
4794 sections_with_metadata_for(&ready(VendorSnapshot::Cursor(snapshot.clone())), now(), 5);
4795 let row = projected
4796 .iter()
4797 .find(|row| {
4798 matches!(
4799 &row.section,
4800 Section::Text { label, .. } if label == "On-Demand"
4801 )
4802 })
4803 .expect("on-demand row");
4804 assert_eq!(row.used_cents, Some(1785));
4805 assert_eq!(row.limit_cents, Some(35000));
4806
4807 snapshot.on_demand_limit_cents = Some(0);
4809 let bare = sections_with_metadata_for(&ready(VendorSnapshot::Cursor(snapshot)), now(), 5);
4810 let bare_row = bare
4811 .iter()
4812 .find(|row| {
4813 matches!(
4814 &row.section,
4815 Section::Text { label, value }
4816 if label == "On-Demand" && value == "$17.85 / $0.00"
4817 )
4818 })
4819 .expect("on-demand row without a cap");
4820 assert_eq!(bare_row.used_cents, Some(1785));
4821 assert_eq!(bare_row.limit_cents, None);
4822 }
4823
4824 #[test]
4825 fn cursor_unlimited_plan_shows_no_pool_bars() {
4826 let mut snap = cursor_snap();
4827 snap.unlimited = true;
4828 let sections = sections_for(&ready(VendorSnapshot::Cursor(snap)), now(), 5);
4829 let metric_count = sections
4830 .iter()
4831 .filter(|s| matches!(s, Section::Metric { .. }))
4832 .count();
4833 assert_eq!(metric_count, 0);
4834 assert!(sections.iter().any(|s| matches!(
4835 s,
4836 Section::Text { value, .. } if value.contains("Unlimited")
4837 )));
4838 }
4839
4840 fn kiro_snap() -> crate::usage::KiroSnapshot {
4841 crate::usage::KiroSnapshot {
4842 plan: "KIRO POWER".into(),
4843 used: 9943.38,
4844 limit: 10000.0,
4845 reset_at: Some(now() + chrono::Duration::days(1)),
4846 }
4847 }
4848
4849 #[test]
4850 fn kiro_compact_cell_shows_the_credit_percentage() {
4851 let (plan, cells) = compact_cells(&VendorSnapshot::Kiro(kiro_snap()));
4852 assert_eq!(plan, "KIRO POWER");
4853 assert_eq!(
4854 cells,
4855 vec![("credits 99%".to_string(), PaceSeverity::Critical)]
4856 );
4857 }
4858
4859 #[test]
4860 fn kiro_headline_pct_is_the_credit_percentage() {
4861 assert_eq!(headline_pct(&VendorSnapshot::Kiro(kiro_snap())), Some(99));
4862 }
4863
4864 #[test]
4865 fn kiro_sections_show_the_credit_metric_and_reset() {
4866 let sections = sections_for(&ready(VendorSnapshot::Kiro(kiro_snap())), now(), 5);
4867 let metrics: Vec<_> = sections
4868 .iter()
4869 .filter_map(|s| match s {
4870 Section::Metric {
4871 label, value_label, ..
4872 } => Some((label.clone(), value_label.clone())),
4873 _ => None,
4874 })
4875 .collect();
4876 assert_eq!(metrics, vec![("Credits".to_string(), "99%".to_string())]);
4877 assert!(sections.iter().any(|s| matches!(
4878 s,
4879 Section::Text { label, value } if label == "Resets" && value.contains("1d")
4880 )));
4881 }
4882
4883 fn grokbot_snap() -> crate::usage::GrokbotSnapshot {
4884 crate::usage::GrokbotSnapshot {
4885 plan: "Grok Bot Plan".into(),
4886 billed_by: None,
4887 has_included_allowance: true,
4888 weekly_pct: 42,
4889 has_available_usage: true,
4890 on_demand_enabled: false,
4891 period_start: Some(now() - chrono::Duration::days(3)),
4892 reset_at: Some(now() + chrono::Duration::days(4)),
4893 window: Some(chrono::Duration::days(7)),
4894 }
4895 }
4896
4897 #[test]
4898 fn grokbot_sections_show_one_weekly_meter_with_the_derived_window() {
4899 let sections =
4900 sections_with_metadata_for(&ready(VendorSnapshot::Grokbot(grokbot_snap())), now(), 5);
4901 let metric = only_metric(§ions);
4902 let Section::Metric {
4903 label,
4904 value_label,
4905 footnote,
4906 ..
4907 } = &metric.section
4908 else {
4909 unreachable!()
4910 };
4911 assert_eq!(label, "Weekly");
4912 assert_eq!(value_label, "42%");
4913 assert!(footnote.contains("Resets in"), "{footnote}");
4914 assert!(footnote.contains("42% elapsed · on track"), "{footnote}");
4916 assert_eq!(metric.window, Some(chrono::Duration::days(7)));
4917 assert_eq!(metric.reset_at, grokbot_snap().reset_at);
4918 }
4919
4920 #[test]
4921 fn grokbot_pacing_details_use_variable_periods_and_skip_missing_bounds() {
4922 for (reset_at, window, expected) in [
4923 (
4924 Some(now() + chrono::Duration::days(5)),
4925 Some(chrono::Duration::days(10)),
4926 Some("50% elapsed · 8pts under"),
4927 ),
4928 (None, Some(chrono::Duration::days(10)), None),
4929 (Some(now() + chrono::Duration::days(5)), None, None),
4930 (
4931 Some(now() + chrono::Duration::days(5)),
4932 Some(chrono::Duration::zero()),
4933 None,
4934 ),
4935 ] {
4936 let snap = crate::usage::GrokbotSnapshot {
4937 reset_at,
4938 window,
4939 ..grokbot_snap()
4940 };
4941 let sections =
4942 sections_with_metadata_for(&ready(VendorSnapshot::Grokbot(snap)), now(), 5);
4943 let Section::Metric { footnote, .. } = &only_metric(§ions).section else {
4944 panic!("expected a weekly metric");
4945 };
4946 if let Some(expected) = expected {
4947 assert!(footnote.contains(expected), "{footnote}");
4948 } else {
4949 assert!(!footnote.contains("elapsed"), "{footnote}");
4950 }
4951 }
4952 }
4953
4954 #[test]
4955 fn grokbot_no_allowance_state_is_a_text_row_not_a_meter() {
4956 let snap = crate::usage::GrokbotSnapshot {
4957 has_included_allowance: false,
4958 weekly_pct: 0,
4959 ..grokbot_snap()
4960 };
4961 let sections = sections_for(&ready(VendorSnapshot::Grokbot(snap.clone())), now(), 5);
4962 assert!(
4963 sections
4964 .iter()
4965 .all(|s| !matches!(s, Section::Metric { .. })),
4966 "no meter without an included allowance"
4967 );
4968 assert!(sections.iter().any(|s| matches!(
4969 s,
4970 Section::Text { value, .. } if value.contains("no included allowance")
4971 )));
4972 assert_eq!(headline_pct(&VendorSnapshot::Grokbot(snap)), None);
4973 let (_, cells) = compact_cells(&VendorSnapshot::Grokbot(grokbot_snap()));
4974 assert_eq!(cells.len(), 1);
4975 assert!(cells[0].0.contains("42%"), "{cells:?}");
4976 }
4977
4978 #[test]
4979 fn grokbot_names_the_subscription_that_bills_it_over_the_generic_plan() {
4980 let (plan, _) = compact_cells(&VendorSnapshot::Grokbot(grokbot_snap()));
4983 assert_eq!(plan, "Grok Bot Plan");
4984 let billed = crate::usage::GrokbotSnapshot {
4985 billed_by: Some("Cursor Ultra".into()),
4986 ..grokbot_snap()
4987 };
4988 let (plan, _) = compact_cells(&VendorSnapshot::Grokbot(billed.clone()));
4989 assert_eq!(plan, "Cursor Ultra");
4990 let sections = sections_for(&ready(VendorSnapshot::Grokbot(billed)), now(), 5);
4992 assert!(matches!(
4993 sections.first(),
4994 Some(Section::Title { left, .. }) if left == "Cursor Ultra"
4995 ));
4996 }
4997
4998 #[test]
4999 fn grokbot_on_demand_footnote_is_a_text_row_when_it_applies() {
5000 let mut snap = grokbot_snap();
5001 snap.weekly_pct = 100;
5002 snap.has_available_usage = true;
5003 snap.on_demand_enabled = true;
5004 let sections = sections_for(&ready(VendorSnapshot::Grokbot(snap)), now(), 5);
5005 assert!(sections.iter().any(|s| matches!(
5006 s,
5007 Section::Text { label, value } if label == "On-demand" && value.contains("on-demand")
5008 )));
5009
5010 let mut snap = grokbot_snap();
5011 snap.weekly_pct = 100;
5012 snap.has_available_usage = true;
5013 snap.on_demand_enabled = false;
5014 let sections = sections_for(&ready(VendorSnapshot::Grokbot(snap)), now(), 5);
5015 assert!(
5016 sections
5017 .iter()
5018 .all(|s| !matches!(s, Section::Text { label, .. } if label == "On-demand")),
5019 "on-demand off: no footnote"
5020 );
5021 }
5022
5023 #[test]
5024 fn grokbot_without_a_period_start_reports_no_window() {
5025 let mut snap = grokbot_snap();
5026 snap.period_start = None;
5027 snap.window = None;
5028 let sections = sections_with_metadata_for(&ready(VendorSnapshot::Grokbot(snap)), now(), 5);
5029 assert_eq!(only_metric(§ions).window, None);
5030 }
5031
5032 fn modelstudio_snap() -> crate::usage::ModelStudioSnapshot {
5033 crate::usage::ModelStudioSnapshot {
5034 session: Some(crate::usage::UsageWindow {
5035 utilization_pct: 42,
5036 resets_at: Some(now() + chrono::Duration::hours(2)),
5037 window_duration: chrono::Duration::hours(5),
5038 }),
5039 weekly: Some(crate::usage::UsageWindow {
5040 utilization_pct: 74,
5041 resets_at: Some(now() + chrono::Duration::days(3)),
5042 window_duration: chrono::Duration::days(7),
5043 }),
5044 monthly: None,
5045 }
5046 }
5047
5048 #[test]
5051 fn modelstudio_sections_show_both_windows_with_resets() {
5052 let sections = sections_with_metadata_for(
5053 &ready(VendorSnapshot::ModelStudio(modelstudio_snap())),
5054 now(),
5055 5,
5056 );
5057 let metrics: Vec<_> = sections
5058 .iter()
5059 .filter_map(|projected| match &projected.section {
5060 Section::Metric {
5061 label, value_label, ..
5062 } => Some((label.clone(), value_label.clone())),
5063 _ => None,
5064 })
5065 .collect();
5066 assert_eq!(
5067 metrics,
5068 vec![
5069 ("Token Plan 5h".to_string(), "42%".to_string()),
5070 ("Token Plan 7d".to_string(), "74%".to_string()),
5071 ]
5072 );
5073 let with_meta: Vec<_> = sections
5074 .iter()
5075 .filter(|p| matches!(p.section, Section::Metric { .. }))
5076 .map(|p| (p.window, p.reset_at))
5077 .collect();
5078 assert_eq!(
5079 with_meta[0],
5080 (
5081 Some(chrono::Duration::hours(5)),
5082 modelstudio_snap().session.unwrap().resets_at
5083 )
5084 );
5085 assert_eq!(
5086 with_meta[1],
5087 (
5088 Some(chrono::Duration::days(7)),
5089 modelstudio_snap().weekly.unwrap().resets_at
5090 )
5091 );
5092 }
5093
5094 #[test]
5097 fn modelstudio_absent_windows_are_text_rows_never_zero_meters() {
5098 let snap = crate::usage::ModelStudioSnapshot {
5099 session: None,
5100 weekly: None,
5101 monthly: None,
5102 };
5103 let sections = sections_for(&ready(VendorSnapshot::ModelStudio(snap.clone())), now(), 5);
5104 assert!(
5105 sections
5106 .iter()
5107 .all(|s| !matches!(s, Section::Metric { .. })),
5108 "no meter without a reported window"
5109 );
5110 assert!(sections.iter().any(|s| matches!(
5111 s,
5112 Section::Text { value, .. } if value.contains("no usage windows reported")
5113 )));
5114 assert_eq!(
5115 headline_pct(&VendorSnapshot::ModelStudio(snap.clone())),
5116 None
5117 );
5118 let (_, cells) = compact_cells(&VendorSnapshot::ModelStudio(snap));
5119 assert!(cells.is_empty(), "{cells:?}");
5120
5121 let snap = crate::usage::ModelStudioSnapshot {
5123 session: None,
5124 weekly: modelstudio_snap().weekly,
5125 monthly: None,
5126 };
5127 let (_, cells) = compact_cells(&VendorSnapshot::ModelStudio(snap.clone()));
5128 assert_eq!(cells.len(), 1);
5129 assert!(cells[0].0.contains("74%"), "{cells:?}");
5130 assert_eq!(headline_pct(&VendorSnapshot::ModelStudio(snap)), Some(74));
5131 }
5132
5133 #[test]
5138 fn modelstudio_monthly_only_renders_a_meter_with_reset_and_no_window_length() {
5139 let snap = crate::usage::ModelStudioSnapshot {
5140 session: None,
5141 weekly: None,
5142 monthly: Some(crate::usage::UsageWindow {
5143 utilization_pct: 100,
5144 resets_at: Some(now() + chrono::Duration::days(21)),
5145 window_duration: chrono::Duration::days(30),
5146 }),
5147 };
5148 let sections =
5149 sections_with_metadata_for(&ready(VendorSnapshot::ModelStudio(snap.clone())), now(), 5);
5150 let metrics: Vec<_> = sections
5151 .iter()
5152 .filter_map(|projected| match &projected.section {
5153 Section::Metric { label, .. } => Some(label.clone()),
5154 _ => None,
5155 })
5156 .collect();
5157 assert_eq!(metrics, vec!["Token Plan 30d".to_string()]);
5158 let metric = sections
5159 .iter()
5160 .find(|p| matches!(p.section, Section::Metric { .. }))
5161 .unwrap();
5162 assert_eq!(
5163 metric.reset_at,
5164 Some(now() + chrono::Duration::days(21)),
5165 "the exact reset travels with the row"
5166 );
5167 assert_eq!(
5168 metric.window, None,
5169 "a plan-dependent monthly cycle declares no window length"
5170 );
5171 assert_eq!(headline_pct(&VendorSnapshot::ModelStudio(snap)), Some(100));
5172
5173 let (_, cells) = compact_cells(&crate::usage::VendorSnapshot::ModelStudio(
5175 crate::usage::ModelStudioSnapshot {
5176 session: None,
5177 weekly: None,
5178 monthly: Some(crate::usage::UsageWindow {
5179 utilization_pct: 100,
5180 resets_at: Some(now() + chrono::Duration::days(21)),
5181 window_duration: chrono::Duration::days(30),
5182 }),
5183 },
5184 ));
5185 assert_eq!(cells.len(), 1, "{cells:?}");
5186 }
5187
5188 #[test]
5189 fn devin_daily_and_weekly_windows_keep_reset_metadata_and_no_fake_session() {
5190 let daily_reset = now() + chrono::Duration::hours(4);
5191 let weekly_reset = now() + chrono::Duration::days(2);
5192 let snapshot = crate::usage::DevinSnapshot {
5193 daily: Some(UsageWindow {
5194 utilization_pct: 35,
5195 resets_at: Some(daily_reset),
5196 window_duration: chrono::Duration::days(1),
5197 }),
5198 weekly: Some(UsageWindow {
5199 utilization_pct: 72,
5200 resets_at: Some(weekly_reset),
5201 window_duration: chrono::Duration::days(7),
5202 }),
5203 overage_balance_micros: Some(9_168_615),
5204 };
5205 let vendor = VendorSnapshot::Devin(snapshot.clone());
5206 let sections = sections_with_metadata_for(&ready(vendor.clone()), now(), 5);
5207 let metrics: Vec<_> = sections
5208 .iter()
5209 .filter_map(|projection| match &projection.section {
5210 Section::Metric {
5211 label, value_label, ..
5212 } => Some((
5213 label.as_str(),
5214 value_label.as_str(),
5215 projection.reset_at,
5216 projection.window,
5217 )),
5218 _ => None,
5219 })
5220 .collect();
5221 assert_eq!(metrics.len(), 2);
5222 assert_eq!(
5223 metrics[0],
5224 (
5225 "Daily quota",
5226 "35%",
5227 Some(daily_reset),
5228 Some(chrono::Duration::days(1))
5229 )
5230 );
5231 assert_eq!(
5232 metrics[1],
5233 (
5234 "Weekly quota",
5235 "72%",
5236 Some(weekly_reset),
5237 Some(chrono::Duration::days(7))
5238 )
5239 );
5240 assert_eq!(headline_pct(&vendor), Some(72));
5241 let (plan, cells) = compact_cells(&vendor);
5242 assert_eq!(plan, "", "the caller prints the vendor name");
5243 assert_eq!(cells.len(), 3, "daily, weekly and text balance only");
5244 assert_eq!(cells[2].0, "$9.168615");
5245
5246 let no_data = VendorSnapshot::Devin(crate::usage::DevinSnapshot {
5247 daily: None,
5248 weekly: None,
5249 overage_balance_micros: None,
5250 });
5251 let sections = sections_for(&ready(no_data.clone()), now(), 5);
5252 assert!(sections.iter().any(|section| matches!(
5253 section,
5254 Section::Text { value, .. } if value == "no quota fields reported"
5255 )));
5256 assert_eq!(headline_pct(&no_data), None);
5257 assert!(compact_cells(&no_data).1.is_empty());
5258
5259 let balance_only = VendorSnapshot::Devin(crate::usage::DevinSnapshot {
5260 daily: None,
5261 weekly: None,
5262 overage_balance_micros: Some(1),
5263 });
5264 assert_eq!(headline_pct(&balance_only), None);
5265 assert_eq!(compact_cells(&balance_only).1.len(), 1);
5266 }
5267
5268 #[test]
5269 fn schema_drift_and_generic_code_zero_diagnostics_are_visible_without_http_labels() {
5270 let snap = KimiSnapshot {
5271 plan: None,
5272 weekly_limit: 100,
5273 weekly_used: 10,
5274 weekly_remaining: 90,
5275 weekly_reset_at: None,
5276 has_weekly: true,
5277 monthly_pct: None,
5278 monthly_reset_at: None,
5279 window_limit: 0,
5280 window_used: 0,
5281 window_remaining: 0,
5282 window_reset_at: None,
5283 };
5284 let mut schema = ready(VendorSnapshot::Kimi(snap.clone()));
5285 let TabState::Ready(tab) = &mut schema else {
5286 unreachable!()
5287 };
5288 tab.last_error = Some((0, crate::kimi::fetch::SCHEMA_DRIFT_MESSAGE.into()));
5289 let schema_sections = sections_for(&schema, now(), 5);
5290 assert!(schema_sections.iter().any(|section| matches!(
5291 section,
5292 Section::Text { label, value } if label == "Kimi API schema drift" && value.is_empty()
5293 )));
5294
5295 let mut generic = ready(VendorSnapshot::Kimi(snap));
5296 let TabState::Ready(tab) = &mut generic else {
5297 unreachable!()
5298 };
5299 tab.last_error = Some((0, "cache lock unavailable".into()));
5300 let generic_sections = sections_for(&generic, now(), 5);
5301 assert!(generic_sections.iter().any(|section| matches!(
5302 section,
5303 Section::Text { label, value } if label == "Warning" && value == "cache lock unavailable"
5304 )));
5305 assert!(!generic_sections.iter().any(|section| matches!(
5306 section,
5307 Section::Text { label, .. } if label.starts_with("HTTP")
5308 )));
5309
5310 let http = warning_label(
5311 &VendorSnapshot::Kimi(KimiSnapshot {
5312 plan: None,
5313 weekly_limit: 0,
5314 weekly_used: 0,
5315 weekly_remaining: 0,
5316 weekly_reset_at: None,
5317 has_weekly: true,
5318 monthly_pct: None,
5319 monthly_reset_at: None,
5320 window_limit: 0,
5321 window_used: 0,
5322 window_remaining: 0,
5323 window_reset_at: None,
5324 }),
5325 &Some((503, "service unavailable".into())),
5326 );
5327 assert_eq!(
5328 http,
5329 Some(("HTTP 503".into(), "service unavailable".into()))
5330 );
5331 }
5332
5333 fn antigravity_snap(source: crate::usage::AntigravitySource) -> VendorSnapshot {
5334 VendorSnapshot::Antigravity(crate::usage::AntigravitySnapshot {
5335 plan: "Pro".into(),
5336 account: "acct:test".into(),
5337 source,
5338 session: Some(UsageWindow {
5339 utilization_pct: 43,
5340 resets_at: Some(now() + chrono::Duration::hours(2)),
5341 window_duration: chrono::Duration::hours(5),
5342 }),
5343 weekly: None,
5344 third_party_session: None,
5345 third_party_weekly: None,
5346 model_pools: Vec::new(),
5347 })
5348 }
5349
5350 #[test]
5353 fn antigravity_model_pool_gets_its_own_row_under_its_cadence() {
5354 let VendorSnapshot::Antigravity(mut snap) =
5355 antigravity_snap(crate::usage::AntigravitySource::Local)
5356 else {
5357 unreachable!()
5358 };
5359 snap.model_pools.push(crate::usage::ModelPoolWindow {
5360 name: "Gemini 4 Argon".into(),
5361 window: UsageWindow {
5362 utilization_pct: 60,
5363 resets_at: Some(now() + chrono::Duration::days(3)),
5364 window_duration: chrono::Duration::days(7),
5365 },
5366 });
5367 let sections = sections_for(&ready(VendorSnapshot::Antigravity(snap)), now(), 5);
5368 let weekly = sections
5369 .iter()
5370 .position(|s| matches!(s, Section::Text { label, .. } if label == "Weekly"))
5371 .expect("Weekly heading");
5372 let argon = sections
5373 .iter()
5374 .position(|s| {
5375 matches!(s, Section::Metric { label, pct: 60, .. } if label == "Gemini 4 Argon")
5376 })
5377 .expect("Argon row");
5378 assert!(argon > weekly);
5379 }
5380
5381 #[test]
5384 fn antigravity_names_the_remote_source_and_only_that() {
5385 use crate::usage::AntigravitySource;
5386
5387 let remote = sections_for(
5388 &ready(antigravity_snap(AntigravitySource::Remote)),
5389 now(),
5390 5,
5391 );
5392 let n = remote.len();
5393 assert!(matches!(remote[n - 2], Section::Spacer));
5394 assert!(matches!(
5395 &remote[n - 1],
5396 Section::Text { label, value }
5397 if label == "Source" && value == "Google API"
5398 ));
5399
5400 let local = sections_for(&ready(antigravity_snap(AntigravitySource::Local)), now(), 5);
5401 assert!(
5402 !local
5403 .iter()
5404 .any(|s| matches!(s, Section::Text { label, .. } if label == "Source"))
5405 );
5406
5407 for source in [AntigravitySource::Statusline, AntigravitySource::Agy] {
5408 let sections = sections_for(&ready(antigravity_snap(source)), now(), 5);
5409 assert!(
5410 !sections
5411 .iter()
5412 .any(|s| matches!(s, Section::Text { label, .. } if label == "Source")),
5413 "{source:?}"
5414 );
5415 }
5416 }
5417
5418 #[test]
5422 fn custom_sections_follow_declaration_order_and_carry_reset_metadata() {
5423 use crate::custom::types::{CustomMetric, CustomSnapshot, CustomText};
5424
5425 let session_reset = now() + chrono::Duration::hours(3);
5426 let monthly_reset = now() + chrono::Duration::days(12);
5427 let snapshot = VendorSnapshot::Custom(CustomSnapshot {
5428 plan: Some("Team".into()),
5429 metrics: vec![
5430 CustomMetric {
5431 label: "Session".into(),
5432 pct: 40,
5433 footnote: "40 of 100".into(),
5434 resets_at: Some(session_reset),
5435 window_secs: Some(18_000),
5436 },
5437 CustomMetric {
5438 label: "Monthly".into(),
5439 pct: 120,
5440 footnote: String::new(),
5441 resets_at: Some(monthly_reset),
5442 window_secs: None,
5443 },
5444 ],
5445 texts: vec![CustomText {
5446 label: "Region".into(),
5447 value: "eu".into(),
5448 }],
5449 });
5450
5451 let sections = sections_with_metadata_for(&ready(snapshot.clone()), now(), 5);
5452 assert!(matches!(
5453 §ions[0].section,
5454 Section::Title { left, right } if left == "Team" && right.is_some()
5455 ));
5456 assert!(matches!(sections[1].section, Section::Spacer));
5457 assert!(matches!(
5458 §ions[2].section,
5459 Section::Metric { label, pct, value_label, footnote, .. }
5460 if label == "Session" && *pct == 40 && value_label == "40%" && footnote == "40 of 100"
5461 ));
5462 assert_eq!(sections[2].reset_at, Some(session_reset));
5463 assert_eq!(sections[2].window, Some(chrono::Duration::hours(5)));
5464 assert!(matches!(
5466 §ions[3].section,
5467 Section::Metric { label, pct, value_label, .. }
5468 if label == "Monthly" && *pct == 100 && value_label == "100%"
5469 ));
5470 assert_eq!(sections[3].reset_at, Some(monthly_reset));
5471 assert_eq!(sections[3].window, None);
5472 assert!(matches!(sections[4].section, Section::Spacer));
5473 assert!(matches!(
5474 §ions[5].section,
5475 Section::Text { label, value } if label == "Region" && value == "eu"
5476 ));
5477 assert_eq!(sections.len(), 6);
5478
5479 let (plan, cells) = compact_cells(&snapshot);
5480 assert_eq!(plan, "Team");
5481 assert_eq!(cells[0].0, "Session 40%");
5482 assert_eq!(cells[1].0, "Monthly 120%");
5483 assert_eq!(headline_pct(&snapshot), Some(40));
5484
5485 let bare = VendorSnapshot::Custom(CustomSnapshot {
5487 plan: None,
5488 metrics: vec![],
5489 texts: vec![],
5490 });
5491 let sections = sections_with_metadata_for(&ready(bare.clone()), now(), 5);
5492 assert!(matches!(§ions[0].section, Section::Title { left, .. } if left.is_empty()));
5493 assert_eq!(sections.len(), 2);
5494 assert_eq!(compact_cells(&bare), (String::new(), vec![]));
5495 assert_eq!(headline_pct(&bare), None);
5496 }
5497
5498 fn deepseek(balance: f64) -> VendorSnapshot {
5501 VendorSnapshot::Deepseek(crate::usage::DeepseekSnapshot {
5502 is_available: true,
5503 balance,
5504 granted: balance,
5505 topped_up: 0.0,
5506 currency: "USD".into(),
5507 })
5508 }
5509
5510 fn balance_row(sections: &[SectionProjection]) -> &SectionProjection {
5511 sections
5512 .iter()
5513 .find(|projection| {
5514 matches!(&projection.section,
5515 Section::Metric { label, .. } | Section::Text { label, .. }
5516 if label == "Balance" || label == "Prepaid balance")
5517 })
5518 .expect("a balance row")
5519 }
5520
5521 #[test]
5524 fn a_balance_vendor_without_a_tank_keeps_its_plain_text_row() {
5525 let sections = sections_with_metadata_for(&ready(deepseek(5.50)), now(), 5);
5526 assert!(matches!(
5527 &balance_row(§ions).section,
5528 Section::Text { value, .. } if value == "$5.50"
5529 ));
5530 }
5531
5532 #[test]
5536 fn a_tank_meters_the_balance_without_moving_the_money_off_the_bar() {
5537 let prefs = DisplayPrefs::balance(Some(200.0), crate::balance::Headline::Amount);
5538 let sections = sections_with_metadata_for(&ready_with(deepseek(50.0), prefs), now(), 5);
5539 let row = balance_row(§ions);
5540 assert_eq!(row.headline, MetricHeadline::Value);
5541 match &row.section {
5542 Section::Metric {
5543 pct,
5544 value_label,
5545 footnote,
5546 ..
5547 } => {
5548 assert_eq!(*pct, 75);
5549 assert_eq!(value_label, "$50.00");
5550 assert_eq!(footnote, "75% of $200.00 used ($50.00 left)");
5551 }
5552 _ => panic!("expected a metric"),
5553 }
5554 }
5555
5556 #[test]
5559 fn choosing_percent_swaps_which_number_is_the_headline() {
5560 let prefs = DisplayPrefs::balance(Some(200.0), crate::balance::Headline::Percent);
5561 let sections = sections_with_metadata_for(&ready_with(deepseek(50.0), prefs), now(), 5);
5562 let row = balance_row(§ions);
5563 assert_eq!(row.headline, MetricHeadline::Percent);
5564 match &row.section {
5565 Section::Metric {
5566 pct,
5567 value_label,
5568 footnote,
5569 ..
5570 } => {
5571 assert_eq!(*pct, 75);
5572 assert_eq!(value_label, "75%");
5573 assert_eq!(footnote, "$50.00 of $200.00 left (75% used)");
5574 }
5575 _ => panic!("expected a metric"),
5576 }
5577 }
5578
5579 #[test]
5582 fn percent_without_a_tank_keeps_the_amount_on_the_bar() {
5583 let prefs = DisplayPrefs::balance(None, crate::balance::Headline::Percent);
5584 let sections = sections_with_metadata_for(&ready_with(deepseek(5.50), prefs), now(), 5);
5585 assert!(matches!(
5586 &balance_row(§ions).section,
5587 Section::Text { value, .. } if value == "$5.50"
5588 ));
5589 }
5590
5591 #[test]
5594 fn a_balance_over_the_cap_reads_as_zero_percent_with_the_money_in_the_detail() {
5595 let prefs = DisplayPrefs::balance(Some(20.0), crate::balance::Headline::Percent);
5596 let sections = sections_with_metadata_for(&ready_with(deepseek(50.0), prefs), now(), 5);
5597 match &balance_row(§ions).section {
5598 Section::Metric {
5599 pct,
5600 value_label,
5601 footnote,
5602 severity,
5603 ..
5604 } => {
5605 assert_eq!(*pct, 0);
5606 assert_eq!(value_label, "0%");
5607 assert!(footnote.contains("$50.00"), "{footnote}");
5608 assert_eq!(*severity, PaceSeverity::Low);
5609 }
5610 _ => panic!("expected a metric"),
5611 }
5612 }
5613
5614 #[test]
5617 fn severity_is_the_worse_of_the_money_tier_and_the_percentage_tier() {
5618 let prefs = DisplayPrefs::balance(Some(1.0), crate::balance::Headline::Percent);
5621 let sections = sections_with_metadata_for(&ready_with(deepseek(0.50), prefs), now(), 5);
5622 match &balance_row(§ions).section {
5623 Section::Metric { severity, .. } => assert_eq!(*severity, PaceSeverity::Critical),
5624 _ => panic!("expected a metric"),
5625 }
5626
5627 let prefs = DisplayPrefs::balance(Some(200.0), crate::balance::Headline::Percent);
5630 let sections = sections_with_metadata_for(&ready_with(deepseek(30.0), prefs), now(), 5);
5631 match &balance_row(§ions).section {
5632 Section::Metric { pct, severity, .. } => {
5633 assert_eq!(*pct, 85);
5634 assert_eq!(*severity, PaceSeverity::High);
5635 }
5636 _ => panic!("expected a metric"),
5637 }
5638 }
5639
5640 #[test]
5643 fn every_balance_only_vendor_meters_the_same_way() {
5644 let snapshots: Vec<VendorSnapshot> = vec![
5645 deepseek(50.0),
5646 VendorSnapshot::Kilo(crate::usage::KiloSnapshot {
5647 label: "Kilo".into(),
5648 balance: 50.0,
5649 }),
5650 VendorSnapshot::Novita(crate::usage::NovitaSnapshot {
5651 available: 50.0,
5652 cash: 50.0,
5653 credit_limit: 0.0,
5654 outstanding: 0.0,
5655 }),
5656 VendorSnapshot::Moonshot(crate::usage::MoonshotSnapshot {
5657 available: 50.0,
5658 cash: 50.0,
5659 voucher: 0.0,
5660 currency: "USD".into(),
5661 }),
5662 VendorSnapshot::Grok(crate::usage::GrokSnapshot { balance: 50.0 }),
5663 ];
5664 for snapshot in snapshots {
5665 let bare = sections_with_metadata_for(&ready(snapshot.clone()), now(), 5);
5666 assert!(
5667 matches!(&balance_row(&bare).section, Section::Text { .. }),
5668 "{snapshot:?} should keep a text row without a tank"
5669 );
5670
5671 let prefs = DisplayPrefs::balance(Some(200.0), crate::balance::Headline::Percent);
5672 let metered =
5673 sections_with_metadata_for(&ready_with(snapshot.clone(), prefs), now(), 5);
5674 let row = balance_row(&metered);
5675 assert_eq!(row.headline, MetricHeadline::Percent, "{snapshot:?}");
5676 match &row.section {
5677 Section::Metric {
5678 pct, value_label, ..
5679 } => {
5680 assert_eq!(*pct, 75, "{snapshot:?}");
5681 assert_eq!(value_label, "75%", "{snapshot:?}");
5682 }
5683 _ => panic!("{snapshot:?} should meter with a tank"),
5684 }
5685 }
5686 }
5687
5688 #[test]
5691 fn openrouters_own_credits_win_over_a_configured_display_limit() {
5692 let snapshot = VendorSnapshot::Openrouter(crate::usage::OpenRouterSnapshot {
5693 label: "OpenRouter".into(),
5694 total_credits: 100.0,
5695 total_usage: 25.0,
5696 usage_daily: 0.0,
5697 usage_weekly: 0.0,
5698 usage_monthly: 0.0,
5699 is_free_tier: false,
5700 limit: None,
5701 limit_remaining: None,
5702 recent_models: Vec::new(),
5703 });
5704 let prefs = DisplayPrefs::balance(Some(10_000.0), crate::balance::Headline::Percent);
5708 let sections = sections_with_metadata_for(&ready_with(snapshot, prefs), now(), 5);
5709 let metric = only_metric(§ions);
5710 assert_eq!(metric.headline, MetricHeadline::Percent);
5711 match &metric.section {
5712 Section::Metric { pct, .. } => assert_eq!(*pct, 25),
5713 _ => unreachable!(),
5714 }
5715 }
5716
5717 #[test]
5720 fn openrouter_defaults_to_percent_and_keeps_the_money_in_the_value() {
5721 let snapshot = VendorSnapshot::Openrouter(crate::usage::OpenRouterSnapshot {
5722 label: "OpenRouter".into(),
5723 total_credits: 100.0,
5724 total_usage: 25.0,
5725 usage_daily: 0.0,
5726 usage_weekly: 0.0,
5727 usage_monthly: 0.0,
5728 is_free_tier: false,
5729 limit: None,
5730 limit_remaining: None,
5731 recent_models: Vec::new(),
5732 });
5733 let sections = sections_with_metadata_for(
5734 &ready_with(
5735 snapshot,
5736 crate::config::Config::default().display_prefs(crate::vendor::VendorId::Openrouter),
5737 ),
5738 now(),
5739 5,
5740 );
5741 let metric = only_metric(§ions);
5742 assert_eq!(metric.headline, MetricHeadline::Percent);
5743 match &metric.section {
5744 Section::Metric { value_label, .. } => assert_eq!(value_label, "$75.00"),
5745 _ => unreachable!(),
5746 }
5747 }
5748
5749 #[test]
5760 fn a_free_tier_openrouter_account_has_no_denominator_and_shows_the_money() {
5761 let snapshot = VendorSnapshot::Openrouter(crate::usage::OpenRouterSnapshot {
5762 label: "OpenRouter".into(),
5763 total_credits: 0.0,
5764 total_usage: 0.0,
5765 usage_daily: 0.0,
5766 usage_weekly: 0.0,
5767 usage_monthly: 0.0,
5768 is_free_tier: true,
5769 limit: None,
5770 limit_remaining: None,
5771 recent_models: Vec::new(),
5772 });
5773 for prefs in [
5774 DisplayPrefs::default(),
5775 DisplayPrefs::balance(Some(200.0), crate::balance::Headline::Percent),
5776 DisplayPrefs::balance(Some(200.0), crate::balance::Headline::Amount),
5777 ] {
5778 let sections =
5779 sections_with_metadata_for(&ready_with(snapshot.clone(), prefs), now(), 5);
5780 let metric = only_metric(§ions);
5781 assert_eq!(metric.headline, MetricHeadline::Value, "{prefs:?}");
5782 match &metric.section {
5783 Section::Metric { value_label, .. } => {
5784 assert_eq!(value_label, "$0.00", "{prefs:?}")
5785 }
5786 _ => unreachable!(),
5787 }
5788 }
5789 }
5790
5791 #[test]
5795 fn a_credit_less_openrouter_snapshot_is_never_metered_against_a_tank() {
5796 let snapshot = VendorSnapshot::Openrouter(crate::usage::OpenRouterSnapshot {
5797 label: "OpenRouter".into(),
5798 total_credits: 0.0,
5799 total_usage: 0.0,
5800 usage_daily: 0.0,
5801 usage_weekly: 0.0,
5802 usage_monthly: 0.0,
5803 is_free_tier: false,
5804 limit: Some(50.0),
5805 limit_remaining: Some(50.0),
5806 recent_models: Vec::new(),
5807 });
5808 let prefs = DisplayPrefs::balance(Some(200.0), crate::balance::Headline::Percent);
5809 let sections = sections_with_metadata_for(&ready_with(snapshot, prefs), now(), 5);
5810 let metric = only_metric(§ions);
5811 assert_eq!(metric.headline, MetricHeadline::Value);
5812 match &metric.section {
5813 Section::Metric {
5816 pct, value_label, ..
5817 } => {
5818 assert_eq!(*pct, 0);
5819 assert_eq!(value_label, "$0.00");
5820 }
5821 _ => unreachable!(),
5822 }
5823 }
5824
5825 #[test]
5827 fn muse_windows_keep_their_reset_and_duration_in_the_report_projection() {
5828 let at = now();
5829 let snapshot = crate::usage::MuseSnapshot {
5830 plan: "Muse Code Power Usage".into(),
5831 session: Some(crate::usage::UsageWindow {
5832 utilization_pct: 38,
5833 resets_at: Some(at + chrono::Duration::hours(3)),
5834 window_duration: chrono::Duration::hours(5),
5835 }),
5836 weekly: Some(crate::usage::UsageWindow {
5837 utilization_pct: 34,
5838 resets_at: Some(at + chrono::Duration::days(4)),
5839 window_duration: chrono::Duration::days(7),
5840 }),
5841 };
5842 let vendor = VendorSnapshot::MuseCode(snapshot.clone());
5843 assert_eq!(headline_pct(&vendor), Some(38));
5844 let sections = sections_with_metadata_for(&ready(vendor), at, 5);
5845 let metrics = sections
5846 .iter()
5847 .filter(|section| matches!(section.section, Section::Metric { .. }))
5848 .collect::<Vec<_>>();
5849 assert_eq!(metrics.len(), 2);
5850 assert_eq!(
5851 metrics[0].reset_at,
5852 snapshot.session.as_ref().unwrap().resets_at
5853 );
5854 assert_eq!(metrics[0].window, Some(chrono::Duration::hours(5)));
5855 assert_eq!(
5856 metrics[1].reset_at,
5857 snapshot.weekly.as_ref().unwrap().resets_at
5858 );
5859 assert_eq!(metrics[1].window, Some(chrono::Duration::days(7)));
5860 }
5861
5862 #[test]
5864 fn a_quota_vendors_metrics_stay_percent_headlines() {
5865 let sections = sections_with_metadata_for(
5866 &ready(supergrok(crate::usage::SuperGrokPeriod::Weekly)),
5867 now(),
5868 5,
5869 );
5870 for projection in §ions {
5871 if matches!(projection.section, Section::Metric { .. }) {
5872 assert_eq!(projection.headline, MetricHeadline::Percent);
5873 }
5874 }
5875 }
5876}