Skip to main content

ai_usagebar/tui/
panels.rs

1//! Native ratatui panels.
2//!
3//! Each vendor projects its snapshot into a sequence of [`Section`]s — either
4//! a metric (gauge + footnote) or a free-form text block. The renderer lays
5//! them out vertically with consistent spacing so every panel has the same
6//! visual rhythm regardless of vendor.
7//!
8//! Progress bars use Bubble Tea-style block glyphs that scale to the available
9//! width, so on a wide monitor you get long, readable bars instead of the
10//! 20-char Pango ones the Waybar tooltip is stuck with.
11
12use 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
34/// One row of the panel body. Vendors emit a `Vec<Section>`; the renderer
35/// turns them into ratatui widgets.
36pub enum Section {
37    /// Title row at the top. `left` is the plan/vendor label (accent-colored,
38    /// bold); `right` is an optional right-aligned annotation, used for the
39    /// "Updated HH:MM:SS" timestamp so it shares the title row instead of
40    /// taking a separate body row + duplicating the global footer's clock.
41    Title { left: String, right: Option<String> },
42    /// A metric: label + gauge + value annotation + dim footnote.
43    Metric {
44        label: String,
45        pct: u16,
46        severity: PaceSeverity,
47        value_label: String,
48        footnote: String,
49    },
50    /// Free-form key/value text line.
51    Text { label: String, value: String },
52    /// A label followed by a multi-line dim block (no gauge).
53    Block { label: String, body: Vec<String> },
54    /// Visual spacer (one blank row).
55    Spacer,
56}
57
58/// Internal metadata carried alongside a public [`Section`]. Keeping this
59/// wrapper private to the crate lets machine-readable frontends receive
60/// absolute reset timestamps without adding a source-breaking field to the
61/// public `Section::Metric` variant.
62pub(crate) struct SectionProjection {
63    pub section: Section,
64    pub reset_at: Option<DateTime<Utc>>,
65    /// Full length of the metric's reset window, when it is known exactly
66    /// (a rolling 5h/7d window). `None` for calendar periods and for quotas
67    /// whose window length the vendor never states — a frontend can pace a
68    /// metric only when this is `Some`.
69    pub window: Option<chrono::Duration>,
70    /// Named sub-group the metric belongs under (e.g. SuperGrok's product
71    /// slices under `"Breakdown"`), so a frontend can draw it as a compact
72    /// row beneath a heading instead of a peer of the overall meter. The TUI
73    /// renders grouped metrics like any other; only the report carries this.
74    pub group: Option<&'static str>,
75    /// A per-model quota pool (Antigravity's "Gemini 4 Argon"): the limit of
76    /// one model, not one of the windows every model of the provider shares.
77    /// A frontend that sums the provider up in one figure ranks it below
78    /// those, so an exhausted model does not read as an exhausted provider.
79    pub model_pool: bool,
80    /// Which of the metric's two numbers a report-driven frontend puts on the
81    /// bar. Every metric is a percentage unless its vendor says otherwise, so
82    /// this is [`MetricHeadline::Percent`] by default — the bar draws it and
83    /// leaves the money figure in the detail. A prepaid-balance vendor flips
84    /// it, and that declaration is what the frontend reads instead of guessing
85    /// from the label.
86    pub headline: MetricHeadline,
87    /// USD cents of spend behind a text row's formatted value. Set only for
88    /// Cursor's On-Demand line. The report copies the cents onto that text
89    /// section so a frontend can meter the prepaid cap without parsing the
90    /// `$spent / $cap` string `fmt_minor` produced.
91    pub used_cents: Option<i64>,
92    /// Positive prepaid cap in USD cents. `None` when Cursor reported spend
93    /// without a cap — the row stays a plain amount.
94    pub limit_cents: Option<i64>,
95    /// Amount the metric's window refills by when it reopens (Synthetic's
96    /// weekly credit tick, e.g. `"2%"`). Present only for refill-style
97    /// windows, so a frontend's countdown reads "Regenerates X in …"
98    /// instead of "Resets in …".
99    pub regen: Option<String>,
100    /// The metric's percentage with its decimal precision preserved, for
101    /// frontends that print a figure ("70.5%"). Only vendors whose API
102    /// reports a fraction set this; the integer `Section::Metric::pct`
103    /// remains what meters and severity colours use.
104    pub percent_precise: Option<f64>,
105    /// How a frontend should read the percentage. Refill-style windows
106    /// (Synthetic's weekly credits) read as a fuel gauge — `"left"`, what
107    /// remains — while fixed windows keep the default "used" reading and
108    /// omit the field.
109    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    /// A metric whose window length is not known exactly (a calendar month,
155    /// a vendor-defined billing period, or no stated window at all).
156    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    /// Mark the most recently pushed metric as a refilling window: `amount`
164    /// is what the vendor restores when it reopens (Synthetic's weekly
165    /// credit tick), so the footnote and every report-driven countdown read
166    /// "Regenerates {amount} in …" instead of "Resets in …". The report
167    /// also tags the metric `reading: "left"`: a budget that regenerates
168    /// reads as a fuel gauge — what remains — whatever the global reading.
169    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    /// Give the most recently pushed metric a decimal-precise percentage
183    /// (Synthetic's weekly budget), so report-driven frontends can print
184    /// "70.5%" instead of the meter's rounded integer. The printed value
185    /// label gains the same precision; the meter keeps the integer.
186    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    /// A metric whose vendor chose which of its two numbers goes on the bar.
197    /// Only a prepaid-balance vendor (and Nous Research, whose credits are a
198    /// balance too) needs this; everything else is a percentage and uses
199    /// [`SectionBuilder::push_metric`].
200    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    /// A metric on a window of exactly `window` length, so a frontend can
214    /// compute how far through the window `reset_at` sits.
215    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    /// A metric that belongs to a named sub-group of the panel (SuperGrok's
229    /// product slices under `"Breakdown"`). Grouped slices share the overall
230    /// pool's window, so they carry no reset of their own; the group label is
231    /// the only extra thing they assert.
232    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    /// Mark the metric just pushed as a per-model quota pool; see
239    /// [`SectionProjection::model_pool`].
240    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    /// Cursor's prepaid on-demand row. `value` stays the formatted pair the
247    /// TUI and every other frontend print. The cents travel beside it so a
248    /// frontend can meter the cap without parsing that string. A non-positive
249    /// cap is spend with nothing to meter against, so it is not recorded.
250    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
269/// The headline row of a prepaid-balance vendor: money remaining, drawn as a
270/// consumed meter when something supplies a tank size and as a plain text row
271/// when nothing does.
272///
273/// Every balance-only vendor gets the same shape from here, so the tank, the
274/// severity floor and the detail line cannot drift apart between them.
275///
276/// `api_limit` is the denominator the vendor's own API states, if any; it wins
277/// over the user's `display_limit`. `money_severity` is the vendor's existing
278/// money-based tier, which stays the floor: a nearly empty wallet is still
279/// critical however large the tank, and a mostly-spent tank is still high
280/// however much money is nominally left.
281fn 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        // No denominator from either source: nothing to meter against, so the
293        // row stays free text rather than becoming a fabricated 0% gauge.
294        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
327/// Compact one-line projection of a vendor snapshot for the Overview: a short
328/// plan/tier sub-label (may be empty) plus a few key metric cells — a percent
329/// or a balance — each carrying a severity for coloring. Same numbers as
330/// [`sections_for`], flattened for a dense multi-vendor list. The vendor's name
331/// is supplied by the caller, so it is not repeated here.
332pub fn compact_cells(snapshot: &VendorSnapshot) -> (String, Vec<(String, PaceSeverity)>) {
333    let pct = |label: &str, p: i32| (format!("{label} {p}%"), severity_for(p));
334    // Named `*_cell` so neither shadows `format::{usd, money}`, which they
335    // wrap — the cell is the string plus the severity the Overview colours it with.
336    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            // No included allowance is a state, not a 0% — no meter cell.
435            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            // Absent windows drop their cell — no-data is not 0%.
444            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            // The caller prints the vendor name; an empty plan label keeps it
471            // from appearing twice (Lyceum does the same).
472            (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            // The caller prints the vendor name; an empty plan label keeps it
490            // from appearing twice (Devin and Lyceum do the same).
491            (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
583/// The single most-relevant percentage for a vendor in the Overview — what its
584/// per-row mini bar shows. Mirrors the macOS menu bar's headline: Cursor is the
585/// combined included-total, quota vendors the most-exhausted window; balance
586/// vendors have no meaningful percentage (`None` → no bar).
587pub 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
693/// Build the section list for the currently-active vendor's snapshot.
694pub 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
701/// Rich projection used by machine-readable frontends. The TUI continues to
702/// expose the source-compatible [`sections_for`] result above.
703pub(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            // Inject the (already-absolute) fetched-at instant into the title
777            // row, right-aligned. Pre-snapshotted in app::refresh_one so it
778            // doesn't drift between redraws.
779            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            // Error footer (when present) still lives in the body.
791            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
804/// Sanitize at the final projection boundary so every vendor field, cached
805/// diagnostic, and fetch error is inert before ratatui writes it to a terminal.
806fn 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
841/// Translate cache diagnostics at the presentation boundary. Cache files keep
842/// their established `(u16, String)` form: only non-zero codes are HTTP, while
843/// Kimi's stable schema marker identifies its code-zero schema warning.
844fn 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    // The stable marker is already the schema-warning label. Keep the label
865    // visible but do not repeat that sentinel as a redundant body value.
866    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    // The label is the row's name to everything that reads it: the
880    // notification dedupe key, the popover's stars and hidden rows, Omarchy's
881    // pick of a balance row. The covered day changes daily, so it gets a row of
882    // its own instead (#470).
883    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        // An uncapped plan (`monthly_limit: null`) has spend but no
955        // denominator: show the amount alone rather than "of $0.00" or a
956        // percentage nobody can vouch for (#30).
957        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        // Same shape as Cursor's included credits: the remaining dollars are
982        // the headline the tray, GNOME and KDE draw (a prepaid balance, not
983        // a quota), the bar meters the used share, and the caption carries
984        // the spend and the expiry. The parse gate guarantees a positive
985        // limit, so `cursor_credit_meter` has no zero denominator to guard.
986        // The meter clamps the remaining to the limit for its bar and its
987        // spend; the headline is the balance the response reported, so a
988        // remaining above the limit is shown as it is, not cut down to it.
989        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            // Appended to the caption the meter already built — it never
994            // comes back empty, so the separator never dangles.
995            footnote.push_str(" · locked");
996        }
997        // The second argument of push_metric stays None: this is an expiry,
998        // not a reset instant the account can bank.
999        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        // `None` means "not reported": the row is absent, never an invented
1018        // "off".
1019        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    // A named limit can be the binding one while the headline window reads
1065    // low, so it gets a real row rather than a footnote.
1066    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    // No percentage reflects a model the account cannot dispatch to, so say it
1075    // outright rather than leaving the user to infer it from healthy bars.
1076    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    // OpenRouter states its own denominator — credits purchased — so there is
1185    // no user tank to fall back to and `None` is passed literally rather than
1186    // `prefs.display_limit`. A free-tier-only account buys nothing, so
1187    // `total_credits` is 0, `denominator` is `None`, and the money stays on the
1188    // bar. Routing `prefs.display_limit` in here instead would let a tank take
1189    // over in exactly that case and name the headline `percent` — while `pct`
1190    // below still came from `consumed_pct()`, which is 0 without credits. The
1191    // bar would read "0%" for an account with money in it.
1192    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            // One severity policy for every frontend: this value is what the
1199            // Omarchy, GNOME and KDE panels colour their row with, so it has to
1200            // agree with the Waybar tooltip about what "in debt" looks like.
1201            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
1246/// OrcaRouter is a balance card: spend of a total credit limit, remaining,
1247/// and an optional key expiry. An unlimited key (the `100000000` sentinel)
1248/// has no denominator, so its panel is spend-only — the same treatment a
1249/// missing monthly limit gets, never a $100M wallet.
1250fn 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                // The key's own expiry is the one absolute timestamp this
1274                // vendor reports; it travels with the row for frontends.
1275                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
1304/// Antigravity holds two independent pools (Gemini, Claude & GPT OSS), each
1305/// with a 5-hour and a weekly window. Grouped by window type so the two pools
1306/// sit side by side, matching the GNOME dropdown. A per-model pool (Pro's
1307/// "Gemini 4 Argon") joins them under the heading of its cadence, in the order
1308/// `antigravity::vendor::rows` sets.
1309fn 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        // A cadence no bucket reported gets no heading either — an empty
1320        // "Session" with nothing under it reads as a failed fetch.
1321        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    // Figures read from the Cloud Code API fallback can lag what the local
1337    // product would show; say where they came from.
1338    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
1348/// A Cursor pool row. The billing cycle carries an exact window only when the
1349/// API stated both ends; when it did not, the row goes out with its reset time
1350/// and no window rather than a guessed month a frontend would pace as exact.
1351fn 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
1358/// A pool's footnote, with how far through the billing cycle we are and the
1359/// point delta when the cycle's length is exact. Both pools share the cycle,
1360/// so only the delta differs between them. An unstated cycle adds nothing: no
1361/// estimate is better than one paced against a guessed month.
1362fn 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        // Two included-usage pools, mirroring the dashboard's two bars.
1396        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        // Same shape as On-Demand: remaining dollars on the row, a bar for
1446        // how much of the grant is already spent, expiry in the caption.
1447        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        // `[nous] headline = "amount"` puts the credits still usable on the
1479        // bar, the way the prepaid-balance vendors do. The consumed percentage
1480        // keeps the meter, the severity and the detail line, so a spent
1481        // allocation still reads critical while the balance stays visible.
1482        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
1583/// Cline shows one bar per ClinePass limit. The reset row counts down to the
1584/// reset time Cline reports; a limit with no open window has none ("—").
1585fn 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    // Monthly keeps its reset countdown but no pacing and no `window_secs`:
1664    // the cycle follows the subscription date (28/29/31-day months), so no
1665    // fixed denominator is exact. `push_metric` (not `push_metric_in_window`)
1666    // is what withholds the window from machine-readable frontends.
1667    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
1694/// Kiro has a single credit pool, so the panel is a single metric bar plus
1695/// the reset row — the same shape as `anthropic_api_sections` but with a
1696/// real percentage (Kiro always reports both used and limit) instead of an
1697/// optional configured one.
1698fn 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
1725/// MiniMax groups quota by model bucket, so the panel is laid out by window
1726/// (Session, Weekly) with one row per pool — the same shape as Antigravity's
1727/// two-group panel. Pacing is shown: both windows report a real duration, so
1728/// the marker is meaningful.
1729fn 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    // `credit_limit` is a credit line Novita extends, not a cap on spend, so it
1786    // is not a denominator — it stays in the breakdown.
1787    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    // The weekly budget refills by this much at the countdown moment rather
1831    // than resetting wholesale, so the countdown line carries the amount.
1832    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    // Keep the API's decimal precision for printed figures ("70.5%"); the
1845    // meter itself stays on the rounded integer.
1846    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
1924/// A user-declared `[[custom]]` provider: the plan (if the response carried
1925/// one), one gauge per configured metric, then the free-form text rows. The
1926/// vendor never states a reset countdown of its own; a metric's `resets_at`
1927/// and optional exact `window_secs` ride along as reset metadata so every
1928/// frontend paces it exactly like a built-in.
1929fn 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    // The reset line `push_window` and Grok Bot print under their meters. Pace
1981    // needs the exact window length, which only the weekly period has (see
1982    // below).
1983    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    // Only the weekly period has an exact length; a month varies and an
2004    // unknown period says nothing, so neither can be paced.
2005    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        // Product slices share the overall pool. They must not carry the
2012        // window reset or a severity colour — only the overall usage meter
2013        // is the binding constraint. The "Breakdown" group lets frontends
2014        // draw them compactly under a heading instead of as peers of it.
2015        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    // A $0.00 prepaid row reads as "you have no money" when the field merely
2027    // says no credit was purchased on top of the subscription — and a unified
2028    // billing account keeps its real dollars in the Management API wallet
2029    // (`[grok]`), not here. Show the row only when there is credit to show.
2030    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
2124/// Whole numbers print bare; fractions keep two decimals (Synthetic credits).
2125fn 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
2144/// The "Reset credits" block itself. Codex calls it directly whenever its
2145/// usage response states a count, so a zero reads "None available" instead
2146/// of dropping the block.
2147fn 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    // `/user/balance` reports money remaining and no denominator at all, so
2173    // `api_limit` is unconditionally `None` here.
2174    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            // A vendor-defined billing period with no reset instant — exactly
2222            // the case push_metric exists for (a bare push would trip the
2223            // reset-metadata assert the moment a limit is set).
2224            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
2251/// Kimi reports each quota as used/limit against a limit of 100, so the pair
2252/// is the percentage in longhand. Projecting both onto a `UsageWindow` lets
2253/// the shared `push_window` draw them, which is what keeps the row identical
2254/// to every other vendor's instead of a hand-rolled near-copy.
2255fn 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        // The monthly pool resets from the order date, so there is no fixed
2279        // window length: the metric carries its reset but no window metadata,
2280        // and nothing paces it.
2281        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
2297/// Grok Bot: one weekly meter for the included pool — plus the honest
2298/// window length when both period instants were reported, so the report
2299/// carries exact `window_secs` — or the no-included-allowance state, which is
2300/// a text row, never a 0% meter.
2301fn 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    // At 100% with the account still serving, on-demand may be picking up the
2342    // rest — a footnote row, not its own meter.
2343    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
2353/// Model Studio Token Plan: a 5h, a weekly and a monthly window, each of
2354/// which the console account may not report. Present windows ride the shared
2355/// `push_window`; an absent one is no-data (possibly unlimited), drawn as a
2356/// text row — never a 0% meter. The monthly window rides `push_metric`,
2357/// without a declared window length: the cycle is plan-dependent (28–31
2358/// days), so a frontend must not pace against a guessed denominator.
2359fn 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        // Three empty bars say nothing a single line does not: an account
2493        // that never touched Droid Core reads as one row.
2494        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
2530/// Droid reports `windowEnd: null` for a window the account has not started:
2531/// there is no reset and no elapsed time to pace against, so the footnote
2532/// says so instead of "Resets in — · 0% elapsed · on track".
2533fn 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
2558/// One decimal when it matters ("70.5%"), none when it does not ("70%").
2559fn 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
2601/// Render the given sections into `area`. Lays them out vertically; metric
2602/// rows take 2 lines (label+gauge / footnote), text and spacer rows take 1.
2603///
2604/// The trailing "Updated …" footer is detected (the last `Text` section)
2605/// and pinned to the bottom of the area, with the slack absorbed *between*
2606/// content and footer. This way shorter vendor panels (OpenRouter, Z.AI)
2607/// don't leave a giant gap below the footer.
2608pub 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    // Heuristic: if the last section is a Text starting with "  Updated",
2614    // pin it to the bottom. Otherwise just lay everything out top-down.
2615    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)); // slack between body and footer
2628        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            // Left: bold accent-colored plan/vendor label. Right: dim-styled
2666            // "Updated HH:MM:SS" pinned to the right edge of the title row.
2667            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    // Row 1: label
2762    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    // Row 2: gauge spanning most of the width + value annotation on the right
2769    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    // Row 3: footnote (dim)
2795    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    /// The spend row's label is its name to the notification dedupe key, the
2847    /// popover's stars and hidden rows and Omarchy's pick of a balance row, so
2848    /// it stays put while the covered day moves on a row of its own (#470).
2849    #[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        // A report with no dated bucket names no day.
2879        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    /// `[nous] headline = "amount"` moves the credits still usable onto the
2934    /// bar; the plan-usage percentage keeps the meter and the detail line.
2935    #[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    /// Only a rolling window has a length a frontend can pace against. The
2990    /// shared `UsageWindow` helper always knows it; SuperGrok knows it for a
2991    /// week and not for a month, whose length varies.
2992    #[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        // Without `billingCycleStart` the cycle length is unknown. Reporting a
3052        // guessed month here would reach a frontend as an exact window and be
3053        // paced as one; every pool goes out with no window instead. The reset
3054        // time is unaffected.
3055        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        // No `billingCycleStart`: the footnotes stay what they were, as the
3101        // window stays absent from the report.
3102        let sections = sections_for(&ready(VendorSnapshot::Cursor(cursor_snap())), now(), 5);
3103        for section in &sections {
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        // A vendor-defined billing period has no reset instant; the row must
3113        // still travel through push_metric (reset_at None) — a bare push
3114        // trips the reset-metadata assert the moment a limit is set.
3115        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            &sections[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        // Title (carries "Updated …" inline now) + 4 metrics (3 windows +
3201        // extra) each preceded by a Spacer. 1 + 4*2 = 9 sections.
3202        assert_eq!(sections.len(), 9);
3203        assert!(matches!(sections[0], Section::Title { .. }));
3204        // Title's right-aligned slot should carry the timestamp.
3205        if let Section::Title { right, .. } = &sections[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        // The #30 shape: `monthly_limit: null` (Pro). The panel must show the
3220        // spend alone — not "of $0.00", not an invented percentage.
3221        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        // Non-vacuous currency pin: a bare `$` formatter would say "$141.57" here.
3262        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        // The real 2026-10-26 payload shape: $250 of $250 left — 0% used.
3328        // Built exactly like Cursor's included credits: the remaining dollars
3329        // are the headline the tray, GNOME and KDE draw, the bar meters the
3330        // used share, and the caption carries the spend and the expiry.
3331        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        // The value is the money figure itself, not a percentage readout.
3361        assert_eq!(metric.2, "$250.00");
3362        // The caption reads like the included credits: spend first, then the
3363        // expiry. The date is local-timezone output, so the shape is pinned,
3364        // not the zone.
3365        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        // The report projection names the value as the headline — that is
3374        // what the popover model keys its headlineLabel on — and there is
3375        // no reset instant behind the expiry.
3376        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        // The bar clamps to nothing used; the money stays what was reported.
3390        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        // 100 of 250 spent: the meter shows the used share, 40%.
3419        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        // Remaining dollars on the row; the used share and spend in the bar
3447        // and caption, exactly as the included credits read.
3448        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        // No expiry was reported, so the caption stops at the spend.
3459        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        // No expires_at → the lock stands alone, with no leading separator.
3465        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        // No expiry clause, so the lock appends straight onto the spend
3485        // caption with its own separator — never an empty or dangling one.
3486        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        // A reported zero balance renders at the currency's own scale —
3575        // not as a $0.00 claim for a zero-exponent currency.
3576        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    /// #118 reached every frontend, not just Waybar: the panel row is what the
3678    /// Omarchy, GNOME and KDE plugins colour and label from, so the debt has to
3679    /// survive the projection with its sign and its severity intact.
3680    #[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        // ...and the same number in the dense Overview list.
3713        let (_, cells) = compact_cells(&VendorSnapshot::Openrouter(snap));
3714        assert_eq!(cells[0].0, "-$5.71");
3715    }
3716
3717    /// The panels express pace as a footnote on the row; the arrow is the
3718    /// widget's idiom and the bar tick the menu bar's. All three Z.AI windows
3719    /// report a duration and a reset, so all three carry one.
3720    #[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        // 40% used with 60% of a 5h window gone: behind pace, not ahead.
3747        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            &sections[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        // The balance is a count of Codex credits, not dollars: a bare number
3861        // followed by `credits`, the same line the Waybar tooltip renders. The
3862        // block holds only that line — the estimated message counts (reported
3863        // by both ranges here) are no longer listed under it.
3864        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        // With both ranges reported, no line of the block may carry a message
3895        // estimate; the balance line still does.
3896        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        // Vendor text that is not a number keeps its own text: no `credits`.
3929        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    /// Every vendor that banks resets reaches every frontend through these
3998    /// sections — the TUI panel, `usage --json`, and from there the Omarchy,
3999    /// GNOME and KDE surfaces. One row, one wording, whichever provider
4000    /// banked the reset.
4001    #[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    /// The row is absent, not zeroed: an account that has never earned a reset
4081    /// should not carry a permanent "0 resets available" line.
4082    #[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        // Zero prepaid is noise (see `supergrok_hides_a_zero_prepaid_balance`),
4144        // so even a present-but-zero field draws no row.
4145        assert!(!sections.iter().any(|section| matches!(
4146            section,
4147            Section::Text { label, .. } if label == "Prepaid API"
4148        )));
4149    }
4150
4151    /// A $0.00 prepaid line reads as "no money" when the billing document
4152    /// merely reports that nothing was purchased on top of the subscription.
4153    /// The row appears only when there is credit to show.
4154    #[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    /// Additional usage is the on-demand pair from the same billing document.
4190    /// A zero pair draws no row; a cap is shown as dollars used of dollars allowed.
4191    #[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    /// Product slices report the "Breakdown" group so frontends can draw them
4266    /// under a heading; the overall meter stays ungrouped, and neither gains
4267    /// reset metadata it must not have.
4268    #[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    /// The overall meter names its reset the way Grok Bot's does, so the TUI,
4301    /// `usage --json` and the Omarchy panel, which all read this footnote, do
4302    /// not show a bare percentage.
4303    #[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(&sections).section else {
4310            unreachable!()
4311        };
4312        // Two of seven days left is 71% elapsed; 40% used is 31 points under.
4313        assert_eq!(footnote, "Resets in 2d 0h · 71% elapsed · 31pts under");
4314    }
4315
4316    /// A month varies and an unknown period says nothing, so neither can be
4317    /// paced: the countdown stands alone.
4318    #[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(&sections).section else {
4325                unreachable!()
4326            };
4327            assert_eq!(footnote, "Resets in 2d 0h", "{period:?}");
4328        }
4329    }
4330
4331    /// With no reset reported there is nothing to count down to, so the row
4332    /// carries no footnote rather than a "Resets in" line with a placeholder.
4333    #[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    /// Product slices share the overall pool's window, so only the overall
4356    /// meter carries the reset line.
4357    #[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        // The bar carries the percentage, so the footnote is the plain
4437        // `Resets in …` every other window row shows — not the counters, which
4438        // against Kimi's limit of 100 only restate the percentage.
4439        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    /// Every vendor holding both a short and a long window opens on the short
4449    /// one — Claude's `Session (5h)`, Codex's `Codex 5h`, GLM's `Session (5h)`,
4450    /// OpenCode Go's `Rolling`. Kimi's rolling bucket is that window, so it
4451    /// leads both projections this module feeds: the section list the Quattro
4452    /// panel and the KDE plasmoid render in order, and the Overview's compact
4453    /// cells.
4454    #[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    /// The newer `usages`-map shape has no weekly bucket: the panel drops the
4511    /// weekly row and lists the monthly pool — with its reset, but with no
4512    /// window metadata, because the reset hangs on the order date and there
4513    /// is no fixed length to pace against.
4514    #[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        // The Overview cells follow the same presence rules.
4554        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        // Percent vendor (Cursor): plan + two colored pool cells.
4578        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); // 100% is critical
4583
4584        // Balance vendor (Kilo): no plan, a single money cell, calm severity.
4585        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            &sections[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        // Cursor: the combined total, not the worse pool (mirrors the menu bar).
4614        assert_eq!(
4615            headline_pct(&VendorSnapshot::Cursor(cursor_snap())),
4616            Some(99)
4617        );
4618
4619        // Balance-only vendors have no meaningful percentage → no bar.
4620        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        // Core at 100% never feeds the headline; the started Standard does.
4649        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        // Every Standard window unstarted → no headline, whatever Core says.
4660        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        // Any Core use brings its windows back, unstarted ones labeled.
4714        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        // Spend with no positive cap is still a row, and still not a meter.
4808        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(&sections);
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        // The honest derived window, so a frontend paces against 7d exactly.
4915        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(&sections).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        // "Grok Bot Plan" is the app's label on every account; the billing
4981        // product says where the pool comes from.
4982        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        // The report's plan is the title section, which the popover shows.
4991        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(&sections).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    /// Present windows ride the shared window metric, so the absolute reset
5049    /// travels with each row and a frontend can pace against the real length.
5050    #[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    /// An absent percentage is no-data (possibly unlimited): the window drops
5095    /// its meter and its compact cell — never a 0% anything.
5096    #[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        // One window present: one meter, one cell, worst-of headline.
5122        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    /// The monthly window — the individual Token Plan's only reported window —
5134    /// gets its own meter with the exact reset, but no declared window length
5135    /// (the cycle is plan-dependent), so no frontend paces against a guessed
5136    /// denominator.
5137    #[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        // The absent 5h/weekly windows stay text rows, never zero meters.
5174        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    /// A per-model pool is a row of its own under the heading of its cadence,
5351    /// and that heading appears even when no shared window of the cadence did.
5352    #[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    /// Figures read off the API while nothing runs say so; a running product's
5382    /// do not, since that is the normal case.
5383    #[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    /// A custom provider's rows come out in declaration order: title, gauges,
5419    /// then texts. Only a metric that states its window length carries one;
5420    /// every metric's own `resets_at` rides along as reset metadata.
5421    #[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            &sections[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            &sections[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        // An over-100 percentage is clamped for the gauge; no window is invented.
5465        assert!(matches!(
5466            &sections[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            &sections[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        // No plan and no texts: an empty title, no trailing spacer, no bar.
5486        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!(&sections[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    // --- prepaid-balance tank + headline (display_limit) ---
5499
5500    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    /// No `display_limit`: nothing supplies a denominator, so the row stays
5522    /// free text rather than becoming a fabricated 0% gauge.
5523    #[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(&sections).section,
5528            Section::Text { value, .. } if value == "$5.50"
5529        ));
5530    }
5531
5532    /// A tank turns the same row into a consumed meter. The default headline
5533    /// for a balance vendor is the money figure, and setting `display_limit`
5534    /// alone does not change that — the percentage rides in the detail.
5535    #[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(&sections);
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    /// Choosing `percent` moves the percentage onto the bar and the dollar
5557    /// figure into the detail line — the two numbers swap, neither is lost.
5558    #[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(&sections);
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    /// `percent` with nothing to divide by keeps the amount on the bar rather
5580    /// than inventing a denominator.
5581    #[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(&sections).section,
5587            Section::Text { value, .. } if value == "$5.50"
5588        ));
5589    }
5590
5591    /// A balance above the cap is 0% used; the money figure is what says how
5592    /// far above it sits, and it stays in the detail line.
5593    #[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(&sections).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    /// The money tier stays the floor: a nearly-empty wallet is critical even
5615    /// when the tank it is measured against is tiny enough to look healthy.
5616    #[test]
5617    fn severity_is_the_worse_of_the_money_tier_and_the_percentage_tier() {
5618        // $0.50 left of a $1 tank: 50% used is only "mid", but under the $1
5619        // USD floor the wallet itself is critical.
5620        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(&sections).section {
5623            Section::Metric { severity, .. } => assert_eq!(*severity, PaceSeverity::Critical),
5624            _ => panic!("expected a metric"),
5625        }
5626
5627        // $30 left of a $200 tank: the money tier is comfortable, but 85% of
5628        // the tank is gone and the meter has to say so.
5629        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(&sections).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    /// Every balance-only vendor gets the same shape from one helper, so a
5641    /// tank and a headline behave identically across all of them.
5642    #[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    /// OpenRouter states its own denominator, so a `display_limit` on it never
5689    /// applies — the fallback order puts the API's number first.
5690    #[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        // A wildly different tank size changes nothing: 25 of 100 is 25%.
5705        // `[openrouter]` carries no `display_limit`, so this can only arrive
5706        // through a hand-built `DisplayPrefs` — and the call site drops it.
5707        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(&sections);
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    /// The default for OpenRouter is the percent it always has a denominator
5718    /// for; the dollar figure stays available in `value` for the detail line.
5719    #[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(&sections);
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    /// A free-tier-only OpenRouter account bought no credits, so there is no
5750    /// denominator and the money stays on the bar even at the percent default.
5751    ///
5752    /// The `prefs` loop is the regression: `openrouter_sections` used to route
5753    /// `prefs.display_limit` into `balance::denominator`, and with credits at 0
5754    /// the API value was dropped as unusable, so a tank took over and named the
5755    /// headline `percent` — while `pct` still came from `consumed_pct()`, which
5756    /// is 0 without credits. The bar read "0%" for an account holding money. No
5757    /// `[openrouter] display_limit` exists any more, and the call site passes
5758    /// `None` literally, so neither half of that can come back.
5759    #[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(&sections);
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    /// The same corner with money actually in the account: an overdrawn or
5792    /// zero-credit snapshot must never be metered against a tank, because the
5793    /// percentage on that row does not come from one.
5794    #[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(&sections);
5811        assert_eq!(metric.headline, MetricHeadline::Value);
5812        match &metric.section {
5813            // Not "0%": the row reports the money, which is what this snapshot
5814            // actually knows.
5815            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    /// Muse's generic report carries both windows' absolute reset metadata.
5826    #[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    /// Nothing else moved: a quota vendor's metrics stay percentages.
5863    #[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 &sections {
5871            if matches!(projection.section, Section::Metric { .. }) {
5872                assert_eq!(projection.headline, MetricHeadline::Percent);
5873            }
5874        }
5875    }
5876}