diff --git a/codex-rs/tui/src/analytics.rs b/codex-rs/tui/src/analytics.rs index 5c29a6fad8..f9c487fc2b 100644 --- a/codex-rs/tui/src/analytics.rs +++ b/codex-rs/tui/src/analytics.rs @@ -1,15 +1,413 @@ -//! Account analytics data and chart primitives, staged for the dashboard integration. +//! Authenticated account analytics dashboard. +//! A maximized section and an overview share bounded report loads, selection, and inline details. +mod chart; mod client; +mod dashboard; mod data; +#[cfg(test)] +#[path = "analytics/test_fixtures.rs"] +mod fixture; mod models; mod normalize; +mod panels; mod plot; mod render; mod report_data; +mod sections; mod styles; +#[cfg(test)] +#[path = "analytics/test_support.rs"] +mod test_support; mod tokens; +mod tool_panel; #[cfg(test)] #[path = "analytics_tests.rs"] mod tests; + +use crate::key_hint; +use crate::keymap::ListAction; +use crate::keymap::ListKeymap; +use crate::tui; +use crate::tui::FrameRequester; +use crate::tui::TuiEvent; +use codex_app_server_client::AppServerRequestHandle; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; +use data::Load; +use sections::Section; +use sections::SectionState; +use sections::SectionStates; + +pub(crate) struct AnalyticsView { + model_names: std::collections::HashMap, + sections: SectionStates, + account: Load, + reports_started: bool, + connection: Option<( + std::sync::Arc, + AppServerRequestHandle, + FrameRequester, + )>, + live: Option>, + pub(crate) keymap: ListKeymap, + section: Section, + zoomed: bool, + scroll_offset: usize, + follow_selection: bool, + viewport_height: usize, + ranges: [usize; 3], + end_date: chrono::NaiveDate, + token_model: Option, + group_picker: Option, + pub(crate) is_done: bool, +} + +impl AnalyticsView { + pub(crate) fn new(keymap: ListKeymap) -> Self { + let mut view = Self { + model_names: std::collections::HashMap::new(), + sections: SectionStates(std::array::from_fn(|_| SectionState::default())), + account: Load::Unavailable, + reports_started: false, + connection: None, + live: None, + keymap, + section: Section::Usage, + zoomed: true, + scroll_offset: 0, + follow_selection: true, + viewport_height: 1, + ranges: [0; 3], + end_date: chrono::Utc::now().date_naive(), + token_model: None, + group_picker: None, + is_done: false, + }; + view.sections[Section::Usage].group = 1; + view.sections[Section::Activity].group = 2; + view.refresh(); + view + } + + pub(crate) fn open( + &mut self, + handle: AppServerRequestHandle, + frame: FrameRequester, + models: Vec, + config: std::sync::Arc, + ) { + self.model_names = models + .into_iter() + .map(|model| (model.model, model.display_name)) + .collect(); + self.connection = Some((config, handle, frame)); + self.is_done = false; + self.refresh(); + } + + pub(crate) fn refresh(&mut self) { + self.end_date = chrono::Utc::now().date_naive(); + self.group_picker = None; + self.live = self.connection.as_ref().map(|(config, _, _)| { + std::sync::Arc::new(client::Live::new( + std::sync::Arc::clone(config), + self.end_date, + )) + }); + for section in &mut self.sections.0 { + section.history = Load::Unavailable; + } + self.reports_started = false; + self.token_model = None; + self.account = if let (Some((_, _, frame)), Some(live)) = (&self.connection, &self.live) { + let live = std::sync::Arc::clone(live); + Load::start( + async move { live.session().await.map(|session| Some(session.kind)) }, + frame.clone(), + ) + } else { + Load::Unavailable + }; + self.start_reports(); + } + + /// Abort work when closing or invalidating a retained view; reopening starts fresh requests. + pub(crate) fn cancel_loads(&mut self) { + for section in &mut self.sections.0 { + section.history = Load::Unavailable; + } + self.account = Load::Unavailable; + self.live = None; + self.connection = None; + } + + fn load_report(&mut self, section: Section) { + let Some(report) = section.report() else { + return; + }; + let group = self.sections[section].group; + let range = self.ranges[self.range_group(section) as usize]; + self.sections[section].history = + if let (Some((_, _, frame)), Some(live)) = (&self.connection, &self.live) { + let live = std::sync::Arc::clone(live); + let days = if range == 0 { 7 } else { 30 }; + let model = self.token_model.clone(); + Load::start( + async move { + if let Some(model) = model + .as_deref() + .filter(|_| report == models::AccountAnalyticsReport::Usage) + { + live.filtered_history(report, days, data::GROUPINGS[group], Some(model)) + .await + } else { + live.history(report, days, data::GROUPINGS[group]).await + } + }, + frame.clone(), + ) + } else { + Load::Unavailable + }; + } + + fn group_options(&self) -> &[usize] { + if self.visible_sections().is_empty() { + return &[]; + } + match self.section { + Section::Usage if self.business() => &[6, 2], + Section::Usage if self.consumer_attribution() => &[1, 2, 0, 3], + Section::Usage => &[0, 2], + Section::Credits => self.live.as_ref().map_or(&[0], |live| live.credit_groups()), + Section::Activity => &[2, 0], + Section::Plugins | Section::Skills => &[], + } + } + + fn consumer_attribution(&self) -> bool { + !self.business() + && self + .live + .as_ref() + .is_none_or(|live| live.attributed_usage()) + } + + fn section_title(&self, section: Section) -> &str { + match section { + Section::Usage if self.business() => "Token usage history", + Section::Usage if self.consumer_attribution() => "Total usage history", + Section::Usage => "Usage history", + Section::Plugins => "Plugins called", + Section::Credits => "Credits usage history", + Section::Activity => "Messages", + Section::Skills => "Skills used", + } + } + + fn date_range(&self) -> std::ops::RangeInclusive { + self.section_date_range(self.section) + } + + fn model_name<'a>(&'a self, model: &'a str) -> &'a str { + self.model_names + .get(model) + .filter(|name| !name.is_empty()) + .map(String::as_str) + .unwrap_or(model) + } + + fn row_count(&self) -> usize { + if self.ranges[self.range_group(self.section) as usize] == 0 { + 7 + } else { + 30 + } + } + + fn handle_key(&mut self, key: KeyEvent) { + if key.kind == KeyEventKind::Release { + return; + } + if key_hint::plain(KeyCode::Char('q')).is_press(key) + || key_hint::ctrl(KeyCode::Char('c')).is_press(key) + { + self.group_picker = None; + self.is_done = true; + return; + } + let action = if key_hint::plain(KeyCode::Char(' ')).is_press(key) { + Some(ListAction::Accept) + } else { + self.keymap.action_for(key) + }; + if let Some(choice) = self.group_picker { + let options = self.group_options(); + let Some(&group) = options.get(choice) else { + self.group_picker = None; + return; + }; + match action { + Some(ListAction::MoveUp | ListAction::MoveLeft) => { + self.group_picker = Some((choice + options.len() - 1) % options.len()); + } + Some(ListAction::MoveDown | ListAction::MoveRight) => { + self.group_picker = Some((choice + 1) % options.len()); + } + Some(ListAction::Accept) => { + self.sections[self.section].group = group; + self.group_picker = None; + self.load_report(self.section); + } + Some(ListAction::Cancel) => self.group_picker = None, + _ => {} + } + self.follow_selection = true; + return; + } + self.follow_selection = true; + if key_hint::plain(KeyCode::Char('z')).is_press(key) { + self.zoomed = !self.zoomed; + self.scroll_offset = 0; + return; + } + if !self.zoomed && action == Some(ListAction::Accept) { + self.zoomed = true; + self.scroll_offset = 0; + return; + } + if key_hint::plain(KeyCode::Char('R')).is_press(key) { + self.refresh(); + return; + } + if self.section == Section::Usage + && self.business() + && key_hint::plain(KeyCode::Char('m')).is_press(key) + { + let models = self + .live + .as_ref() + .map_or_else(Vec::new, |live| live.token_models()); + self.token_model = match self + .token_model + .as_ref() + .and_then(|model| models.iter().position(|candidate| candidate == model)) + { + Some(index) => models.get(index + 1).cloned(), + None => models.first().cloned(), + }; + self.load_report(Section::Usage); + return; + } + if key_hint::plain(KeyCode::Char('r')).is_press(key) && !self.visible_sections().is_empty() + { + self.change_range(); + return; + } + if key_hint::plain(KeyCode::Char('g')).is_press(key) && self.group_options().len() > 1 { + self.group_picker = self + .group_options() + .iter() + .position(|group| *group == self.sections[self.section].group); + return; + } + let visible = self.visible_sections(); + if visible.is_empty() { + self.is_done |= action == Some(ListAction::Cancel); + return; + } + let current = visible + .iter() + .position(|section| *section == self.section) + .unwrap_or_default(); + let target = match key.code { + KeyCode::Char(n @ '1'..='6') if key.modifiers.is_empty() => { + visible.get(n as usize - '1' as usize).copied() + } + KeyCode::Tab => Some(visible[(current + 1) % visible.len()]), + KeyCode::BackTab => Some(visible[(current + visible.len() - 1) % visible.len()]), + _ => None, + }; + if let Some(section) = target { + if self.zoomed && section != self.section { + self.scroll_offset = 0; + } + self.section = section; + self.group_picker = None; + return; + } + if matches!( + self.section, + Section::Plugins | Section::Activity | Section::Skills + ) && action == Some(ListAction::Accept) + && !self.day_has_details(self.section, self.sections[self.section].cursor) + { + return; + } + let count = self.row_count().max(/*other*/ 1); + let SectionState { cursor, detail, .. } = &mut self.sections[self.section]; + match action { + Some(ListAction::MoveUp) => *cursor = cursor.saturating_sub(/*rhs*/ 1), + Some(ListAction::MoveDown) => *cursor = (*cursor + 1).min(count - 1), + Some(ListAction::MoveLeft) => *cursor = cursor.saturating_sub(/*rhs*/ 1), + Some(ListAction::MoveRight) => *cursor = (*cursor + 1).min(count - 1), + Some(ListAction::JumpTop) => *cursor = 0, + Some(ListAction::JumpBottom) => *cursor = count - 1, + Some(ListAction::Accept) => { + *detail = if *detail == Some(*cursor) { + None + } else { + Some(*cursor) + }; + } + Some(ListAction::Cancel) => { + self.is_done |= !self.zoomed || detail.take().is_none(); + } + Some(ListAction::PageDown) => { + self.scroll_offset += self.viewport_height; + self.follow_selection = false; + } + Some(ListAction::PageUp) => { + self.scroll_offset = self.scroll_offset.saturating_sub(self.viewport_height); + self.follow_selection = false; + } + None => {} + } + if detail.is_some() { + *detail = Some(*cursor); + } + } + + pub(crate) fn handle_event( + &mut self, + tui: &mut tui::Tui, + event: TuiEvent, + ) -> std::io::Result<()> { + match event { + TuiEvent::Key(key) => { + self.handle_key(key); + if self.is_done { + for section in &mut self.sections.0 { + if matches!(section.history, Load::Loading(_)) { + section.history = Load::Unavailable; + } + } + self.account = Load::Unavailable; + self.connection = None; + } + tui.frame_requester().schedule_frame(); + } + TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) | TuiEvent::FocusGained => { + if matches!(event, TuiEvent::Resize(_)) { + self.follow_selection = true; + } + tui.draw(u16::MAX, |frame| self.render(frame.area(), frame.buffer))?; + } + _ => {} + } + Ok(()) + } +} diff --git a/codex-rs/tui/src/analytics/account_layout_tests.rs b/codex-rs/tui/src/analytics/account_layout_tests.rs new file mode 100644 index 0000000000..d8e93bcd08 --- /dev/null +++ b/codex-rs/tui/src/analytics/account_layout_tests.rs @@ -0,0 +1,292 @@ +//! Account layouts, authenticated report loading, independent ranges, and model filters. +use super::*; +use pretty_assertions::assert_eq; + +use crate::analytics::sections::Section; +#[test] +fn account_layouts_show_reports_and_wrap_navigation() { + let mut screens = Vec::new(); + for (kind, expected) in [ + ( + models::AccountKind::Consumer, + vec![ + Section::Usage, + Section::Activity, + Section::Plugins, + Section::Skills, + ], + ), + ( + models::AccountKind::Enterprise, + vec![ + Section::Credits, + Section::Usage, + Section::Plugins, + Section::Skills, + ], + ), + ] { + let mut view = fixture::view(kind); + for (index, section) in expected.iter().enumerate() { + press( + &mut view, + KeyCode::Char(char::from_digit(index as u32 + 1, /*radix*/ 10).unwrap()), + ); + assert_eq!(view.section, *section); + screens.push(screen(&mut view, /*width*/ 100, /*height*/ 30)); + } + press(&mut view, KeyCode::Tab); + assert_eq!(view.section, expected[0]); + press(&mut view, KeyCode::BackTab); + assert_eq!(view.section, *expected.last().unwrap()); + view.zoomed = false; + screens.push(screen(&mut view, /*width*/ 140, /*height*/ 64)); + } + insta::assert_snapshot!(screens.join("\n")); +} + +#[tokio::test] +async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() { + let server = test_support::server().await; + let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await; + test_support::settle(&mut view).await; + press(&mut view, KeyCode::Left); + press(&mut view, KeyCode::Enter); + press(&mut view, KeyCode::Char('r')); + test_support::settle(&mut view).await; + press(&mut view, KeyCode::Char('g')); + press(&mut view, KeyCode::Down); + press(&mut view, KeyCode::Enter); + test_support::settle(&mut view).await; + assert_eq!( + ( + view.ranges, + view.sections.0.each_ref().map(|state| state.cursor), + view.sections[Section::Usage].detail, + view.sections[Section::Usage].group + ), + ([1, 0, 0], [28, 6, 6, 6, 6], Some(28), 2) + ); + let context = ( + view.ranges, + view.sections.0.each_ref().map(|state| state.cursor), + view.sections.0.each_ref().map(|state| state.detail), + view.sections.0.each_ref().map(|state| state.group), + ); + press(&mut view, KeyCode::Char('z')); + screen(&mut view, /*width*/ 58, /*height*/ 25); + press(&mut view, KeyCode::Char('z')); + assert_eq!( + ( + view.ranges, + view.sections.0.each_ref().map(|state| state.cursor), + view.sections.0.each_ref().map(|state| state.detail), + view.sections.0.each_ref().map(|state| state.group) + ), + context + ); + press(&mut view, KeyCode::Char('2')); + press(&mut view, KeyCode::Char('r')); + test_support::settle(&mut view).await; + assert_eq!(view.ranges, [1, 1, 0]); + assert_eq!( + view.sections + .0 + .each_ref() + .map(|state| state.history.ready().map(|history| history.data.len())), + [Some(30), Some(7), None, Some(30), Some(7)] + ); + let server = test_support::server().await; + let (_business_home, _business_server, mut view) = + client::tests::connected_view(&server, "business").await; + test_support::settle(&mut view).await; + view.ranges = [1, 1, 0]; + view.load_report(Section::Usage); + press(&mut view, KeyCode::Char('1')); + press(&mut view, KeyCode::Char('r')); + test_support::settle(&mut view).await; + assert_eq!(view.ranges, [0, 1, 0]); + assert_eq!( + view.sections + .0 + .each_ref() + .map(|state| state.history.ready().map(|history| history.data.len())), + [Some(30), Some(7), Some(7), None, Some(7)] + ); +} + +#[tokio::test] +async fn business_tokens_filter_models() { + let server = test_support::server().await; + let (_home, _app_server, mut view) = client::tests::connected_view(&server, "business").await; + test_support::settle(&mut view).await; + view.end_date = fixture::END_DATE; + fixture::seed_reports(&mut view); + press(&mut view, KeyCode::Char('2')); + let all = screen(&mut view, /*width*/ 100, /*height*/ 30); + assert!(all.contains("All models") && all.contains("Cached input")); + press(&mut view, KeyCode::Char('m')); + test_support::settle(&mut view).await; + assert_eq!( + view.sections[Section::Usage] + .history + .ready() + .unwrap() + .data + .iter() + .map(|day| day.total) + .sum::(), + 420.0 + ); + fixture::seed_reports(&mut view); + assert_eq!(view.token_model.as_deref(), Some("GPT-5.5")); + let filtered = screen(&mut view, /*width*/ 100, /*height*/ 30); + press(&mut view, KeyCode::Char('m')); + test_support::settle(&mut view).await; + fixture::seed_reports(&mut view); + assert_eq!(view.token_model, None); + insta::assert_snapshot!(format!("{all}\n{filtered}")); +} + +#[tokio::test] +async fn account_pending_and_error_do_not_start_reports() { + let mut view = AnalyticsView::new(RuntimeKeymap::defaults().list); + let (send, receive) = tokio::sync::oneshot::channel(); + view.account = Load::start( + async move { receive.await.unwrap() }, + FrameRequester::test_dummy(), + ); + screen(&mut view, /*width*/ 80, /*height*/ 24); + assert!( + view.sections + .0 + .iter() + .all(|state| matches!(state.history, Load::Unavailable)) + ); + send.send(Err("Sign in again to view Analytics.".to_string())) + .unwrap(); + tokio::task::yield_now().await; + let error = screen(&mut view, /*width*/ 80, /*height*/ 24); + assert!(error.contains("Sign in again")); + assert!( + view.sections + .0 + .iter() + .all(|state| matches!(state.history, Load::Unavailable)) + ); + insta::assert_snapshot!(error); +} + +#[tokio::test] +async fn account_reports_request_only_eligible_endpoints_and_refresh() { + for (plan, usage, activity) in [ + ( + "plus", + "usage/daily-token-usage-breakdown", + "analytics/daily-workspace-usage-counts", + ), + ( + "business", + "usage/daily-workspace-user-token-usage-breakdown", + "usage/daily-workspace-user-credit-usage", + ), + ] { + let server = test_support::server().await; + let (_home, _app_server, mut view) = client::tests::connected_view(&server, plan).await; + test_support::settle(&mut view).await; + let mut expected = [ + "accounts/check", + usage, + activity, + "analytics/daily-plugin-usage-metrics", + "analytics/daily-skill-usage-metrics", + ] + .map(|endpoint| format!("/backend-api/wham/{endpoint}")); + expected.sort(); + let mut paths = server + .received_requests() + .await + .unwrap() + .into_iter() + .filter(|request| request.url.path().starts_with("/backend-api/wham/")) + .map(|request| request.url.path().to_string()) + .collect::>(); + paths.sort(); + assert_eq!(paths, expected); + let ready = view + .sections + .0 + .each_ref() + .map(|state| state.history.ready().is_some()); + assert_eq!( + ready, + if plan == "plus" { + [true, true, false, true, true] + } else { + [true, true, true, false, true] + } + ); + press(&mut view, KeyCode::Char('R')); + test_support::settle(&mut view).await; + assert_eq!( + view.sections + .0 + .each_ref() + .map(|state| state.history.ready().is_some()), + ready + ); + assert_eq!( + server + .received_requests() + .await + .unwrap() + .iter() + .filter(|request| request.url.path().starts_with("/backend-api/wham/")) + .count(), + 10 + ); + view.cancel_loads(); + } +} + +#[tokio::test] +async fn legacy_response_closes_an_attribution_picker_that_is_no_longer_valid() { + let server = test_support::server().await; + let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await; + view.account = Load::Ready(models::AccountKind::Consumer); + view.start_reports(); + view.group_picker = Some(3); + test_support::settle(&mut view).await; + assert_eq!( + ( + view.group_picker, + view.group_options(), + view.sections[Section::Usage].group + ), + (None, &[0, 2][..], 0) + ); + press(&mut view, KeyCode::Enter); +} + +#[tokio::test] +async fn legacy_response_invalidates_picker_before_the_next_draw() { + let server = test_support::server().await; + for choice in [2, 3] { + let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await; + view.account = Load::Ready(models::AccountKind::Consumer); + view.start_reports(); + view.group_picker = Some(choice); + // Complete the request without polling the view, as can happen between key events. + tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 10), async { + while view.consumer_attribution() { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + press(&mut view, KeyCode::Enter); + assert_eq!((view.group_picker, view.is_done), (None, false)); + test_support::settle(&mut view).await; + assert_eq!(view.sections[Section::Usage].group, 0); + } +} diff --git a/codex-rs/tui/src/analytics/chart.rs b/codex-rs/tui/src/analytics/chart.rs new file mode 100644 index 0000000000..9d478c55c4 --- /dev/null +++ b/codex-rs/tui/src/analytics/chart.rs @@ -0,0 +1,322 @@ +//! Daily history with a stable chart and a compact, separated breakdown table. +//! Chart series rank by the full range; table rows rank by the selected day and retain series colors. +//! Missing and zero days retain the same row count; only explicit expansion changes height. + +use super::AnalyticsView; +use super::data; +use super::plot; +use super::render::categories; +use super::render::columns; +use super::styles::number; +use super::styles::secondary_style; +use super::styles::series_colors; +use crate::analytics::models::AccountAnalyticsUnit; +use crate::analytics::models::AccountAnalyticsValue; +use crate::analytics::sections::Section; +use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; +use ratatui::style::Styled; +use ratatui::style::Stylize; +use ratatui::text::Line; + +impl AnalyticsView { + pub(super) fn day_has_details(&self, section: Section, day_index: usize) -> bool { + let Some(history) = self.sections[section].history.ready() else { + return false; + }; + let date = self + .section_date_range(section) + .start() + .iter_days() + .nth(day_index); + history.data.iter().any(|day| { + date.is_some_and(|date| day.date == date) + && day.values.iter().any(|value| value.value != 0.0) + }) + } + + pub(super) fn history_lines( + &self, + section: Section, + width: usize, + height: usize, + ) -> plot::Chart { + let state = &self.sections[section].history; + let Some(history) = state.ready() else { + return plot::Chart { + lines: state + .message() + .into_iter() + .map(|message| message.to_string().set_style(secondary_style()).into()) + .collect(), + bands: 0, + }; + }; + if history.data.is_empty() { + return plot::Chart { + lines: vec![ + "No data reported for this range." + .set_style(secondary_style()) + .into(), + ], + bands: 0, + }; + } + let range = self.section_date_range(section); + // At most 30 daily slots; index by date if report ranges grow. + let days = range + .start() + .iter_days() + .take_while(|date| date <= range.end()) + .map(|date| { + let day = history.data.iter().find(|day| day.date == date); + (date, day) + }) + .collect::>(); + let cursor = self.sections[section].cursor.min(days.len() - 1); + let selected_date = days[cursor].0.format("%b %-d").to_string(); + let day = days[cursor].1; + let expanded = self.zoomed && self.sections[section].detail.is_some(); + let colors = series_colors(); + let mut categories = categories(history); + if matches!(section, Section::Plugins | Section::Skills) { + for category in &mut categories { + category.label = super::tool_panel::tool_name(&category.label); + } + } + if self.sections[section].group == 2 { + for category in &mut categories { + category.label = self.model_name(&category.label).to_string(); + } + } + let unit = match section { + Section::Usage if history.unit == AccountAnalyticsUnit::Tokens => "tokens", + Section::Activity => "messages", + Section::Plugins => "calls", + Section::Skills => "uses", + Section::Usage | Section::Credits => "credits", + }; + let empty_breakdown = matches!( + section, + Section::Plugins | Section::Activity | Section::Skills + ) && day.is_none_or(|day| { + day.total == 0.0 && day.values.iter().all(|value| value.value == 0.0) + }); + let relative = history.unit == AccountAnalyticsUnit::RelativeUsage; + let format_amount = if history.unit == AccountAnalyticsUnit::Credits { + data::credit_amount + } else { + data::amount + }; + let mut lines = vec![if relative { + "Total usage · relative units" + .set_style(secondary_style()) + .into() + } else { + number(format!( + "{} {}{unit}", + format_amount(history.data.iter().map(|day| day.total).sum()), + if section == Section::Activity { + "reported " + } else { + "" + } + )) + .into() + }]; + if section == Section::Usage && !self.business() && self.zoomed { + lines.push( + "Approximate · may be delayed up to 6 hours" + .set_style(secondary_style()) + .into(), + ); + } + if self.sections[section].group == 3 { + lines.push( + "All usage by how each turn started" + .set_style(secondary_style()) + .into(), + ); + } + let has_activity = history + .data + .iter() + .any(|day| day.total != 0.0 || day.values.iter().any(|value| value.value != 0.0)); + let mut bands = 0; + if has_activity { + let chart = plot::chart(&days, &categories, cursor, width, height, history.unit); + bands = chart.bands; + lines.extend(chart.lines); + } else { + lines.push( + if matches!( + section, + Section::Plugins | Section::Activity | Section::Skills + ) { + format!("No {unit} reported in this range.") + .set_style(secondary_style()) + .into() + } else { + "No activity reported in this range." + .set_style(secondary_style()) + .into() + }, + ); + } + + let table_width = width.min(/*other*/ 56); + lines.push(Line::default()); + if self.zoomed { + lines.push("─".repeat(table_width).dim().into()); + } + let table_start = lines.len(); + let summary = match day { + None => " · Not reported".set_style(secondary_style()), + Some(day) if day.total == 0.0 && day.values.iter().all(|value| value.value == 0.0) => { + " · No activity (0)".set_style(secondary_style()) + } + Some(day) if relative => number(format!(" · {} usage units", data::amount(day.total))), + Some(day) => number(format!( + " · {} {unit}", + if expanded { + day.total.to_string() + } else { + format_amount(day.total) + } + )), + }; + lines.push(truncate_line_with_ellipsis_if_overflow( + vec![selected_date.clone().bold(), summary].into(), + width, + )); + if self.zoomed { + lines.push(columns( + match section { + Section::Plugins => "Plugin", + Section::Skills => "Skill", + Section::Usage | Section::Credits | Section::Activity => { + self.group_label(section, self.sections[section].group) + } + } + .bold() + .into(), + if history.unit == AccountAnalyticsUnit::Tokens { + "Tokens" + } else if section == Section::Usage { + "Share" + } else if section == Section::Activity { + "Messages" + } else if section == Section::Plugins { + "Calls" + } else if section == Section::Skills { + "Uses" + } else { + "Credits" + } + .bold() + .into(), + table_width, + )); + } + let mut values = categories + .iter() + .enumerate() + .map(|(index, category)| { + ( + index, + AccountAnalyticsValue { + value: day + .and_then(|day| { + day.values.iter().find(|value| value.key == category.key) + }) + .map_or(/*default*/ 0.0, |value| value.value), + ..category.clone() + }, + ) + }) + .collect::>(); + values.sort_by(|(_, a), (_, b)| b.value.abs().total_cmp(&a.value.abs())); + let remainder = if !expanded && values.len() > 4 { + let remaining = values.split_off(/*at*/ 3); + Some(( + format!("{} more", remaining.len()), + remaining.iter().map(|(_, value)| value.value).sum::(), + )) + } else { + None + }; + let rows = values + .iter() + .map(|(index, value)| { + ( + Line::from(vec![ + "● ".fg(colors[(*index).min(/*other*/ 3)]), + value.label.clone().into(), + ]), + value.value, + ) + }) + .chain( + remainder.map(|(label, value)| (Line::from(vec!["… ".dim(), label.dim()]), value)), + ); + for (label, value) in rows.take(if self.zoomed { usize::MAX } else { 2 }) { + let amount = match day { + None => "—".into(), + Some(day) + if section == Section::Usage + && history.unit != AccountAnalyticsUnit::Tokens => + { + format!( + "{:.1}%", + if day.total == 0.0 { + 0.0 + } else { + value / day.total * 100.0 + } + ) + } + Some(_) if expanded => value.to_string(), + Some(_) => format_amount(value), + }; + lines.push(columns( + truncate_line_with_ellipsis_if_overflow( + label, + table_width.saturating_sub(amount.len() + 2), + ), + if day.is_none() || value == 0.0 { + amount.dim() + } else { + amount.into() + } + .into(), + table_width, + )); + } + if empty_breakdown { + // Preserve the measured table allocation when the selected day has no breakdown. + let table_end = lines.len(); + lines.truncate(table_start); + lines.push(truncate_line_with_ellipsis_if_overflow( + if day.is_none() { + format!("Data not reported for {selected_date}.") + } else { + format!("No {unit} reported for {selected_date}.") + } + .set_style(secondary_style()) + .into(), + width, + )); + lines.resize(table_end, Line::default()); + } + if let Some(updated) = history + .updated_at + .and_then(|value| chrono::DateTime::from_timestamp(value, /*nsecs*/ 0)) + { + lines.push( + format!("Updated {} UTC", updated.format("%b %-d %H:%M")) + .set_style(secondary_style()) + .into(), + ); + } + plot::Chart { lines, bands } + } +} diff --git a/codex-rs/tui/src/analytics/client_tests.rs b/codex-rs/tui/src/analytics/client_tests.rs index a85f510713..8ecca93778 100644 --- a/codex-rs/tui/src/analytics/client_tests.rs +++ b/codex-rs/tui/src/analytics/client_tests.rs @@ -1,6 +1,7 @@ //! Account-scoped authentication and request identity regression coverage. use super::*; +use crate::analytics::sections::Section; use crate::legacy_core::config::ConfigBuilder; use base64::Engine; use codex_config::LoaderOverrides; @@ -763,3 +764,58 @@ async fn invalid_cached_breakdowns_are_evicted_before_retry() { assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 2); server.verify().await; } + +pub(in crate::analytics) async fn connected_view( + server: &MockServer, + plan: &str, +) -> ( + tempfile::TempDir, + crate::AppServerSession, + crate::analytics::AnalyticsView, +) { + let (home, live) = live(server, plan).await; + let config = Arc::clone(&live.config); + let app_server = crate::start_embedded_app_server_for_picker(&config) + .await + .unwrap(); + let mut view = + crate::analytics::AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list); + view.open( + app_server.request_handle(), + crate::tui::FrameRequester::test_dummy(), + Vec::new(), + config, + ); + (home, app_server, view) +} + +#[tokio::test] +async fn live_account_identity_is_visible_and_closing_aborts_pending_loads() { + let server = MockServer::start().await; + let (_home, live) = live(&server, "plus").await; + live.session().await.unwrap(); + let mut view = + crate::analytics::AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list); + view.live = Some(Arc::new(live)); + view.sections[Section::Usage].history = + super::super::data::Load::Error("Couldn't load usage. Press R to retry.".into()); + let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new( + /*width*/ 100, /*height*/ 24, + )) + .unwrap(); + terminal + .draw(|frame| view.render(frame.area(), frame.buffer_mut())) + .unwrap(); + insta::assert_snapshot!(terminal.backend().to_string()); + let (draw_tx, _) = tokio::sync::broadcast::channel(/*capacity*/ 1); + let (cancelled_tx, cancelled_rx) = tokio::sync::oneshot::channel::<()>(); + view.sections[Section::Usage].history = super::super::data::Load::start( + async move { + let _cancelled = cancelled_tx; + std::future::pending().await + }, + crate::tui::FrameRequester::new(draw_tx), + ); + view.cancel_loads(); + assert!(cancelled_rx.await.is_err()); +} diff --git a/codex-rs/tui/src/analytics/dashboard.rs b/codex-rs/tui/src/analytics/dashboard.rs new file mode 100644 index 0000000000..db1b51ab8a --- /dev/null +++ b/codex-rs/tui/src/analytics/dashboard.rs @@ -0,0 +1,181 @@ +//! Bordered, aligned overview cards. Detailed reports remain in maximized mode. +//! Card heights depend on the viewport, never on the selected day or load state. + +use super::AnalyticsView; +use super::render::columns; +use super::render::join_columns; +use super::styles::secondary_style; +use crate::analytics::sections::Section; +use crate::line_truncation::truncate_line_with_ellipsis_if_overflow as truncate; +use crate::style::accent_style; +use crate::wrapping::RtOptions; +use crate::wrapping::word_wrap_lines; +use ratatui::style::Styled; +use ratatui::style::Stylize; +use ratatui::text::Line; +use std::ops::Range; + +impl AnalyticsView { + pub(super) fn dashboard_lines( + &self, + width: usize, + height: usize, + ) -> (Vec>, Range) { + let count = if width >= 104 { 2 } else { 1 }; + let card_width = if count == 2 { (width - 4) / 2 } else { width }; + let visible = self.visible_sections(); + let rows = visible.len().div_ceil(count).max(/*other*/ 1); + let card_height = + (height.saturating_sub(/*rhs*/ 2) / rows).clamp(/*min*/ 16, /*max*/ 24); + let inner_width = card_width.saturating_sub(/*rhs*/ 4).max(/*other*/ 1); + let inner_height = card_height - 4; + let mut lines = Vec::new(); + let mut selected = 0..0; + for (row, chunk) in visible.chunks(count).enumerate() { + let mut cards = Vec::new(); + for (column, section) in chunk.iter().copied().enumerate() { + let focused = self.section == section; + let border = if focused { + accent_style() + } else { + secondary_style().dim() + }; + let mut content = Vec::new(); + content.push( + format!( + "{}d{}", + if self.ranges[self.range_group(section) as usize] == 0 { + 7 + } else { + 30 + }, + if section == Section::Usage && self.business() { + format!(" · {}", self.token_model.as_deref().unwrap_or("All models")) + } else { + String::new() + } + ) + .set_style(secondary_style()) + .into(), + ); + if matches!( + section, + Section::Usage | Section::Credits | Section::Activity + ) { + content.push( + format!( + "By {}", + self.group_label(section, self.sections[section].group) + .to_lowercase() + ) + .set_style(secondary_style()) + .into(), + ); + } + if focused && self.group_picker.is_some() { + content.extend(self.group_options().iter().enumerate().map( + |(index, group)| { + let label = format!( + "{} {}", + if self.group_picker == Some(index) { + "›" + } else { + " " + }, + self.group_label(section, *group) + ); + if self.group_picker == Some(index) { + label.set_style(accent_style()).into() + } else { + label.into() + } + }, + )); + } else { + content.extend( + self.history_lines( + section, + inner_width, + inner_height.saturating_sub(/*rhs*/ 13).max(/*other*/ 1), + ) + .lines, + ); + } + let mut content = word_wrap_lines(content, RtOptions::new(inner_width)); + if content.len() > inner_height { + content.truncate(inner_height); + content[inner_height - 1] = + "… z to maximize".set_style(secondary_style()).into(); + } + content.resize(inner_height, Line::default()); + let label = format!( + " {} {} ", + row * count + column + 1, + self.section_title(section) + ); + let title = truncate( + Line::from(vec![ + " ".into(), + if focused { + "▸".set_style(accent_style()) + } else { + " ".into() + }, + if focused { + label.bold() + } else { + label.set_style(secondary_style()) + }, + ]), + card_width.saturating_sub(/*rhs*/ 3), + ); + let mut top = vec!["╭─".set_style(border)]; + top.extend(title.spans.iter().cloned()); + top.push( + "─" + .repeat(card_width.saturating_sub(title.width() + 3)) + .set_style(border), + ); + top.push("╮".set_style(border)); + let blank = columns( + "│".set_style(border).into(), + "│".set_style(border).into(), + card_width, + ); + let mut card = vec![Line::from(top), blank.clone()]; + card.extend(content.into_iter().map(|line| { + let line = truncate(line, inner_width); + let padding = card_width.saturating_sub(line.width() + 3); + let mut padded = vec!["│".set_style(border), " ".into()]; + padded.extend(line.spans); + padded.push(" ".repeat(padding).into()); + padded.push("│".set_style(border)); + Line::from(padded) + })); + card.push(blank); + card.push( + format!("╰{}╯", "─".repeat(card_width.saturating_sub(/*rhs*/ 2))) + .set_style(border) + .into(), + ); + cards.push( + card.into_iter() + .map(|line| truncate(line, card_width)) + .collect::>(), + ); + } + if chunk.contains(&self.section) { + selected = lines.len()..lines.len() + card_height; + } + lines.extend(if cards.len() == 2 { + join_columns(&cards[0], &cards[1], card_width) + } else { + cards.remove(/*index*/ 0) + }); + if (row + 1) * count < visible.len() { + lines.push(Line::default()); + } + } + (lines, selected) + } +} diff --git a/codex-rs/tui/src/analytics/dashboard_tests.rs b/codex-rs/tui/src/analytics/dashboard_tests.rs new file mode 100644 index 0000000000..b3bd2a3cc1 --- /dev/null +++ b/codex-rs/tui/src/analytics/dashboard_tests.rs @@ -0,0 +1,95 @@ +//! Overview cards retain alignment and geometry while selection and report states change. +use super::*; +use crate::style::accent_style; +use pretty_assertions::assert_eq; +use ratatui::style::Modifier; + +use crate::analytics::sections::Section; +#[test] +fn dashboard_focus_uses_marker_and_default_foreground_title() { + let mut view = fixture::view(models::AccountKind::Consumer); + view.zoomed = false; + for section in [Section::Usage, Section::Activity] { + view.section = section; + let (lines, _) = view.dashboard_lines(/*width*/ 140, /*height*/ 56); + let title = &lines[0]; + let marker = title.spans.iter().find(|span| span.content == "▸").unwrap(); + assert_eq!(marker.style, accent_style()); + let focused_title = title + .spans + .iter() + .find(|span| span.content.contains(view.section_title(section))) + .unwrap(); + assert_eq!(focused_title.style.fg, None); + assert!(focused_title.style.add_modifier.contains(Modifier::BOLD)); + assert_eq!(title.to_string().matches('▸').count(), 1); + } +} + +#[test] +fn dashboard_cards_align_and_keep_stable_summary_heights() { + let mut view = fixture::view(models::AccountKind::Consumer); + press(&mut view, KeyCode::Char('z')); + let wide = screen(&mut view, /*width*/ 144, /*height*/ 64); + for (left, right) in [ + ("1 Total usage history", "2 Messages"), + ("3 Plugins called", "4 Skills used"), + ] { + assert!( + wide.lines() + .any(|line| line.contains(left) && line.contains(right)) + ); + } + assert!(!wide.contains("Coverage from")); + let before = view.dashboard_lines(/*width*/ 140, /*height*/ 56); + press(&mut view, KeyCode::Left); + view.sections[Section::Plugins].history = Load::Error("Access denied for this report.".into()); + let after = view.dashboard_lines(/*width*/ 140, /*height*/ 56); + assert_eq!((before.0.len(), before.1), (after.0.len(), after.1)); + let edges = |lines: &[ratatui::text::Line<'_>]| { + lines + .iter() + .enumerate() + .filter(|(_, line)| line.to_string().starts_with(['╭', '╰'])) + .map(|(index, _)| index) + .collect::>() + }; + assert_eq!(edges(&before.0), edges(&after.0)); + for width in [1, 4, 40, 100, 140] { + assert!( + view.dashboard_lines(width, /*height*/ 56) + .0 + .iter() + .all(|line| line.width() <= width) + ); + } + let narrow = screen(&mut view, /*width*/ 72, /*height*/ 48); + assert!(narrow.contains("1 Total usage history")); + let context = ( + view.section, + view.sections.0.each_ref().map(|state| state.cursor), + view.sections.0.each_ref().map(|state| state.group), + ); + press(&mut view, KeyCode::Enter); + assert!(view.zoomed); + assert_eq!( + context, + ( + view.section, + view.sections.0.each_ref().map(|state| state.cursor), + view.sections.0.each_ref().map(|state| state.group) + ) + ); + insta::assert_snapshot!(format!("{wide}\n{narrow}")); +} + +#[test] +fn overview_closes_on_escape_with_retained_day_details() { + let mut view = fixture::view(models::AccountKind::Consumer); + press(&mut view, KeyCode::Enter); + assert_eq!(view.sections[Section::Usage].detail, Some(6)); + press(&mut view, KeyCode::Char('z')); + assert!(!view.zoomed); + press(&mut view, KeyCode::Esc); + assert!(view.is_done); +} diff --git a/codex-rs/tui/src/analytics/data.rs b/codex-rs/tui/src/analytics/data.rs index d1dfd70551..e0479d0e3a 100644 --- a/codex-rs/tui/src/analytics/data.rs +++ b/codex-rs/tui/src/analytics/data.rs @@ -101,13 +101,6 @@ impl Load { } } -pub(super) fn error(error: codex_app_server_client::TypedRequestError) -> String { - match error { - codex_app_server_client::TypedRequestError::Server { source, .. } => source.message, - _ => "Couldn't load analytics. Press R to retry.".into(), - } -} - /// Keep tiny refunds visible while avoiding noise on ordinary credit amounts. pub(super) fn amount(value: f64) -> String { if value == 0.0 { @@ -154,32 +147,6 @@ pub(super) fn credit_amount(value: f64) -> String { } } -/// Format integer millionths without floating-point rounding or hiding tiny adjustments. -pub(super) fn credits(micros: i64) -> String { - let magnitude = micros.unsigned_abs(); - if magnitude > 0 && magnitude < 10_000 { - let fractional = format!("{magnitude:06}"); - return format!( - "{}0.{}", - if micros < 0 { "-" } else { "" }, - fractional.trim_end_matches('0') - ); - } - let cents = (magnitude + 5_000) / 10_000; - let mut whole = (cents / 100).to_string(); - let digits = whole.len(); - for index in (1..digits).rev() { - if (digits - index).is_multiple_of(/*rhs*/ 3) { - whole.insert(index, ','); - } - } - format!( - "{}{whole}.{:02}", - if micros < 0 { "-" } else { "" }, - cents % 100 - ) -} - pub(super) fn date(date: &str) -> String { chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d") .map(|date| date.format("%b %-d").to_string()) diff --git a/codex-rs/tui/src/analytics/normalize.rs b/codex-rs/tui/src/analytics/normalize.rs index 26ae206ce6..0dad8d7eb1 100644 --- a/codex-rs/tui/src/analytics/normalize.rs +++ b/codex-rs/tui/src/analytics/normalize.rs @@ -317,7 +317,10 @@ pub(super) fn label(key: &str) -> &str { "subagent" => "Subagents", "image_generation" => "Image generation", "automation" => "Automations", - "guardian_review" => "Guardian review", + "guardian_review" => "Auto review", + "guardian_classifier" => "Auto review classifier", + "thread_title" => "Thread title", + "system" => "System", "automated_review" => "Auto review", "agent_identity" => "Workspace agents", "memory_consolidation" => "Memory consolidation", diff --git a/codex-rs/tui/src/analytics/panels.rs b/codex-rs/tui/src/analytics/panels.rs new file mode 100644 index 0000000000..1e1bab90e3 --- /dev/null +++ b/codex-rs/tui/src/analytics/panels.rs @@ -0,0 +1,108 @@ +//! Responsive panels with consistent category colors and signed daily amounts. + +use super::AnalyticsView; +use super::render::columns; +use super::styles::secondary_style; +use crate::analytics::sections::Section; +use crate::style::accent_style; +use crate::wrapping::RtOptions; +use crate::wrapping::word_wrap_lines; +use ratatui::style::Styled; +use ratatui::style::Stylize; +use ratatui::text::Line; +use std::ops::Range; + +/// Panel content and measured geometry shared by layout and scrolling. +pub(super) struct Panel { + pub(super) lines: Vec>, + pub(super) selection: Range, + pub(super) chart_bands: usize, +} + +impl AnalyticsView { + pub(super) fn panel(&self, section: Section, width: usize, chart_height: usize) -> Panel { + let focused = section == self.section; + let title = format!( + "{} {}", + if focused { "▎" } else { " " }, + self.section_title(section) + ); + let heading = if focused { + title.set_style(accent_style()).into() + } else { + title.bold().into() + }; + let group = self.sections[section].group; + let heading = if matches!( + section, + Section::Usage | Section::Credits | Section::Activity + ) { + columns( + heading, + format!("By {}", self.group_label(section, group).to_lowercase()) + .set_style(secondary_style()) + .into(), + width, + ) + } else { + heading + }; + let mut lines = vec![heading, "─".repeat(width).dim().into()]; + if section == Section::Usage && self.business() { + lines.push( + format!( + "{} · m change model", + self.token_model.as_deref().unwrap_or("All models") + ) + .set_style(secondary_style()) + .into(), + ); + } + let mut selection; + if focused && let Some(choice) = self.group_picker { + lines.extend( + self.group_options() + .iter() + .enumerate() + .map(|(index, group)| { + let label = format!( + "{} {}", + if index == choice { "›" } else { " " }, + self.group_label(section, *group) + ); + if index == choice { + label.set_style(accent_style()).into() + } else { + label.into() + } + }), + ); + } + let history = self.history_lines(section, width, chart_height); + let chart_bands = history.bands; + lines.extend(history.lines); + selection = 0..lines.len(); + if focused && self.group_picker.is_some() { + selection = 0..2 + self.group_options().len(); + } + let mut wrapped = Vec::new(); + let mut wrapped_selection = 0..0; + for (index, line) in lines.into_iter().enumerate() { + if index == selection.start { + wrapped_selection.start = wrapped.len(); + } + wrapped.extend(word_wrap_lines( + [line], + RtOptions::new(width.max(/*other*/ 1)), + )); + if index + 1 == selection.end { + wrapped_selection.end = wrapped.len(); + } + } + Panel { + lines: wrapped, + selection: wrapped_selection, + chart_bands, + } + } +} diff --git a/codex-rs/tui/src/analytics/render.rs b/codex-rs/tui/src/analytics/render.rs index 0229a604fd..02e6d338e6 100644 --- a/codex-rs/tui/src/analytics/render.rs +++ b/codex-rs/tui/src/analytics/render.rs @@ -1,9 +1,55 @@ -//! Shared report category aggregation and stacked-chart partitioning. +//! Maximized sections and a responsive overview share the same panels and scrolling behavior. +//! Zoom, section changes, and reflow preserve report selections and inline details. -use super::models::AccountAnalyticsHistory; -use super::models::AccountAnalyticsValue; +use super::AnalyticsView; +use super::data; +use super::styles::secondary_style; +use crate::analytics::models::AccountAnalyticsHistory; +use crate::analytics::models::AccountAnalyticsValue; +use crate::analytics::sections::Section; +use crate::keymap::ListAction; +use crate::style::accent_style; +use crate::wrapping::RtOptions; +use crate::wrapping::word_wrap_lines; +use ratatui::buffer::Buffer; +use ratatui::layout::Constraint; +use ratatui::layout::Layout; +use ratatui::layout::Rect; +use ratatui::style::Styled; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::widgets::Widget; use std::collections::BTreeMap; +pub(super) fn columns(left: Line<'static>, right: Line<'static>, width: usize) -> Line<'static> { + let padding = width + .saturating_sub(left.width() + right.width()) + .max(/*other*/ 1); + let mut spans = left.spans; + spans.push(" ".repeat(padding).into()); + spans.extend(right.spans); + Line::from(spans) +} + +/// Join independently stacked panels without leaving holes below shorter sections. +pub(super) fn join_columns( + left: &[Line<'static>], + right: &[Line<'static>], + column_width: usize, +) -> Vec> { + (0..left.len().max(right.len())) + .map(|row| { + let left = left.get(row).cloned().unwrap_or_default(); + let mut line = columns(left, Line::default(), column_width + 4); + if let Some(right) = right.get(row) { + line.spans.extend(right.spans.iter().cloned()); + } + line + }) + .collect() +} + pub(super) fn categories(history: &AccountAnalyticsHistory) -> Vec { let mut totals = BTreeMap::::new(); for value in history.data.iter().flat_map(|day| &day.values) { @@ -41,3 +87,212 @@ pub(super) fn parts( } parts } + +impl AnalyticsView { + pub(super) fn render(&mut self, area: Rect, buf: &mut Buffer) { + self.poll_reports(); + let width = usize::from(area.width.saturating_sub(/*rhs*/ 4)).max(/*other*/ 1); + let show_range = !self.visible_sections().is_empty(); + let hint = |action| { + self.keymap + .primary_hint(action) + .map(crate::key_hint::ShortcutHint::display_label) + .unwrap_or_default() + }; + let controls = if self.visible_sections().is_empty() { + format!("R refresh · {} back · q close", hint(ListAction::Cancel)) + } else if self.group_picker.is_some() { + format!( + "{}/{} group · {} select · {} cancel", + hint(ListAction::MoveUp), + hint(ListAction::MoveDown), + hint(ListAction::Accept), + hint(ListAction::Cancel) + ) + } else { + let navigation = if !self.zoomed { + format!("{}/z maximize", hint(ListAction::Accept)) + } else { + format!( + "{}/{} day · {} details", + hint(ListAction::MoveLeft), + hint(ListAction::MoveRight), + hint(ListAction::Accept) + ) + }; + format!( + "tab/1–{} section · {}{navigation}\n{}{}R refresh · {}/{} scroll · {} back · q close", + self.visible_sections().len(), + if self.zoomed { "z dashboard · " } else { "" }, + if show_range { "r 7/30d · " } else { "" }, + if self.group_options().len() < 2 { + "" + } else { + "g group · " + }, + hint(ListAction::PageUp), + hint(ListAction::PageDown), + hint(ListAction::Cancel) + ) + }; + let control_line = |line: &str| { + let mut spans = Vec::new(); + for (index, control) in line.split(" · ").enumerate() { + if index > 0 { + spans.push(" · ".into()); + } + let (keys, description) = control.rsplit_once(' ').unwrap_or((control, "")); + #[expect( + clippy::disallowed_methods, + reason = "Analytics shortcut keys intentionally use white to distinguish them from descriptions." + )] + spans.push(keys.to_owned().white().bold().not_dim()); + if !description.is_empty() { + spans.push(format!(" {description}").into()); + } + } + Line::from(spans) + }; + let mut footer = word_wrap_lines(controls.lines().map(control_line), RtOptions::new(width)); + if self.group_picker.is_none() + && matches!( + self.section, + Section::Plugins | Section::Activity | Section::Skills + ) + && !self.day_has_details(self.section, self.sections[self.section].cursor) + { + let height = footer.len(); + let controls = + controls.replace(&format!(" · {} details", hint(ListAction::Accept)), ""); + footer = word_wrap_lines(controls.lines().map(control_line), RtOptions::new(width)); + // Removing a contextual hint must not resize the chart on narrow terminals. + footer.resize(height, Line::default()); + } + let sections = if self.zoomed { + let line = Line::from( + self.visible_sections() + .iter() + .copied() + .enumerate() + .map(|(index, section)| { + let label = self.section_title(section); + let label = if width >= 64 { + format!("{} {label}", index + 1) + } else { + label.to_string() + }; + if section == self.section { + format!("[{label}] ").set_style(accent_style()) + } else { + format!("{label} ").set_style(secondary_style()) + } + }) + .collect::>(), + ); + word_wrap_lines([line], RtOptions::new(width)) + } else { + Vec::new() + }; + let [title, range, tabs, body, hints] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(if show_range { 2 } else { 1 }), + Constraint::Length(if sections.is_empty() { + 0 + } else { + sections.len() as u16 + 1 + }), + Constraint::Min(0), + Constraint::Length(footer.len() as u16), + ]) + .areas(Rect { + x: area.x + area.width.min(/*other*/ 2), + width: area.width.saturating_sub(/*rhs*/ 4), + ..area + }); + Line::from(vec![ + "Analytics".bold(), + self.live + .as_ref() + .and_then(|live| live.account_label()) + .map(|account| format!(" · Live account · {account}")) + .unwrap_or_else(|| " · Live account".into()) + .set_style(secondary_style()), + ]) + .render(title, buf); + if show_range { + let dates = self.date_range(); + let (start, end) = (dates.start(), dates.end()); + Line::from(vec![ + if self.ranges[self.range_group(self.section) as usize] == 0 { + "[7d]".set_style(accent_style()) + } else { + "7d".into() + }, + " ".into(), + if self.ranges[self.range_group(self.section) as usize] == 1 { + "[30d]".set_style(accent_style()) + } else { + "30d".into() + }, + format!( + " · {}–{}", + data::date(&start.to_string()), + end.format("%b %-d, %Y") + ) + .set_style(secondary_style()), + ]) + .render(range, buf); + } + Paragraph::new(sections).render(tabs, buf); + self.viewport_height = usize::from(body.height).max(/*other*/ 1); + let (lines, selection) = if self.visible_sections().is_empty() { + let message = self + .account + .message() + .unwrap_or("Analytics is not available for this account type."); + (vec![Line::from(message.to_string())], 0..1) + } else if self.zoomed { + let chart_height = + (usize::from(body.height) / 5).clamp(/*min*/ 4, /*max*/ 10); + let mut panel = self.panel(self.section, width, chart_height); + if panel.chart_bands > 0 { + let overhead = panel.lines.len() - chart_height * panel.chart_bands; + let height = + usize::from(body.height).saturating_sub(overhead + 1) / panel.chart_bands; + let height = height.max(/*other*/ 2); + if height != chart_height { + panel = self.panel(self.section, width, height); + } + } + let selection = if panel.lines.len() <= usize::from(body.height) { + 0..panel.lines.len() + } else { + panel.selection + }; + panel.lines.push(Line::default()); + (panel.lines, selection) + } else { + self.dashboard_lines(width, usize::from(body.height)) + }; + if self.follow_selection { + if selection.start < self.scroll_offset { + self.scroll_offset = selection.start; + } else { + let end = selection.start + selection.len().min(self.viewport_height); + if end > self.scroll_offset + self.viewport_height { + self.scroll_offset = end - self.viewport_height; + } + } + } + self.scroll_offset = self + .scroll_offset + .min(lines.len().saturating_sub(self.viewport_height)); + self.follow_selection = false; + Paragraph::new(lines) + .scroll(/*offset*/ (self.scroll_offset as u16, 0)) + .render(body, buf); + Paragraph::new(footer) + .set_style(secondary_style()) + .render(hints, buf); + } +} diff --git a/codex-rs/tui/src/analytics/sections.rs b/codex-rs/tui/src/analytics/sections.rs new file mode 100644 index 0000000000..e0630ca430 --- /dev/null +++ b/codex-rs/tui/src/analytics/sections.rs @@ -0,0 +1,212 @@ +//! Account-specific section order, request eligibility, and independent report ranges. +//! Resolving the account precedes report loading; refreshing drops all account-owned data. + +use super::AnalyticsView; +use super::data::Load; +use super::models::AccountKind; + +/// Stable identities preserve selections while account layouts change their order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum Section { + Usage, + Plugins, + Credits, + Activity, + Skills, +} + +impl Section { + pub(super) fn report(self) -> Option { + use super::models::AccountAnalyticsReport as Report; + match self { + Self::Usage => Some(Report::Usage), + Self::Plugins => Some(Report::Plugins), + Self::Credits => Some(Report::Credits), + Self::Activity => Some(Report::Messages), + Self::Skills => Some(Report::Skills), + } + } +} + +/// Section-only indexing keeps report identity separate from array positions. +pub(super) struct SectionStates(pub(super) [SectionState; 5]); + +impl std::ops::Index
for SectionStates { + type Output = SectionState; + fn index(&self, section: Section) -> &Self::Output { + &self.0[section as usize] + } +} + +impl std::ops::IndexMut
for SectionStates { + fn index_mut(&mut self, section: Section) -> &mut Self::Output { + &mut self.0[section as usize] + } +} + +/// A report and its selection are replaced independently of the other sections. +pub(super) struct SectionState { + pub(super) history: Load, + pub(super) cursor: usize, + pub(super) detail: Option, + pub(super) group: usize, +} + +impl Default for SectionState { + fn default() -> Self { + Self { + history: Load::Unavailable, + cursor: 6, + detail: None, + group: 0, + } + } +} + +/// Usage, product activity, and tools each share one range across their related sections. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum RangeGroup { + Usage, + Activity, + Tools, +} + +impl AnalyticsView { + pub(super) fn poll_reports(&mut self) { + for section in &mut self.sections.0 { + section.history.poll(); + } + self.account.poll(); + self.start_reports(); + if !self.business() + && !self.consumer_attribution() + && ![0, 2].contains(&self.sections[Section::Usage].group) + { + self.sections[Section::Usage].group = 0; + } + if self + .group_picker + .is_some_and(|choice| choice >= self.group_options().len()) + { + self.group_picker = None; + } + } + + pub(super) fn range_group(&self, section: Section) -> RangeGroup { + match section { + Section::Usage if self.business() => RangeGroup::Activity, + Section::Usage | Section::Credits => RangeGroup::Usage, + Section::Activity => RangeGroup::Activity, + Section::Plugins | Section::Skills => RangeGroup::Tools, + } + } + + pub(super) fn group_label(&self, section: Section, group: usize) -> &'static str { + if group == 0 + && (section == Section::Credits + || (section == Section::Usage && !self.consumer_attribution())) + { + "Product" + } else { + super::data::GROUP_LABELS[group] + } + } + + pub(super) fn business(&self) -> bool { + matches!( + self.account.ready(), + Some(AccountKind::Business | AccountKind::Enterprise) + ) + } + + pub(super) fn visible_sections(&self) -> &'static [Section] { + match self.account.ready() { + Some(AccountKind::Consumer) => &[ + Section::Usage, + Section::Activity, + Section::Plugins, + Section::Skills, + ], + Some(AccountKind::Business | AccountKind::Enterprise) => &[ + Section::Credits, + Section::Usage, + Section::Plugins, + Section::Skills, + ], + Some(AccountKind::Unknown) | None => &[], + } + } + + pub(super) fn start_reports(&mut self) { + if self.reports_started || self.account.ready().is_none() { + return; + } + self.reports_started = true; + let visible = self.visible_sections(); + let Some(first) = visible.first() else { + return; + }; + if !visible.contains(&self.section) { + self.section = *first; + } + let usage_groups: &[usize] = if self.business() { + &[6, 2] + } else { + &[1, 2, 0, 3] + }; + if !usage_groups.contains(&self.sections[Section::Usage].group) { + self.sections[Section::Usage].group = + if matches!(self.account.ready(), Some(AccountKind::Business)) { + 2 + } else { + usage_groups[0] + }; + } + if let Some(live) = &self.live + && !live + .credit_groups() + .contains(&self.sections[Section::Credits].group) + { + self.sections[Section::Credits].group = live.credit_groups()[0]; + } + for section in visible { + if section.report().is_some() { + self.load_report(*section); + } + } + } + + pub(super) fn section_date_range( + &self, + section: Section, + ) -> std::ops::RangeInclusive { + let end = self.end_date; + let range = self.ranges[self.range_group(section) as usize]; + (end - chrono::Days::new(if range == 0 { 6 } else { 29 }))..=end + } + + pub(super) fn change_range(&mut self) { + let index = self.range_group(self.section); + self.ranges[index as usize] ^= 1; + for section in self.visible_sections() { + if self.range_group(*section) != index { + continue; + } + self.sections[*section].cursor = if self.ranges[index as usize] == 1 { + self.sections[*section].cursor + 23 + } else { + self.sections[*section].cursor.saturating_sub(/*rhs*/ 23) + }; + self.sections[*section].detail = self.sections[*section].detail.and_then(|day| { + if self.ranges[index as usize] == 1 { + Some(day + 23) + } else { + day.checked_sub(/*rhs*/ 23) + } + }); + if section.report().is_some() { + self.load_report(*section); + } + } + } +} diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__tests__live_account_identity_is_visible_and_closing_aborts_pending_loads.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__tests__live_account_identity_is_visible_and_closing_aborts_pending_loads.snap new file mode 100644 index 0000000000..42810b143e --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__tests__live_account_identity_is_visible_and_closing_aborts_pending_loads.snap @@ -0,0 +1,28 @@ +--- +source: tui/src/analytics/client_tests.rs +expression: terminal.backend().to_string() +--- +" Analytics · Live account · analytics@example.test · account-a " +" " +" " +" " +" No history has been reported. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" R refresh · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__account_layouts_show_reports_and_wrap_navigation.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__account_layouts_show_reports_and_wrap_navigation.snap new file mode 100644 index 0000000000..82a983d900 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__account_layouts_show_reports_and_wrap_navigation.snap @@ -0,0 +1,381 @@ +--- +source: tui/src/analytics/account_layout_tests.rs +expression: "screens.join(\"\\n\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used " +" " +" ▎ Total usage history By feature " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" Total usage · relative units " +" Approximate · may be delayed up to 6 hours " +" 86 " +" 100 ▃▃▃▃▃▃▃▃ ████████ ▁▁▁▁▁▁▁▁ " +" ▃▃▃▃▃▃▃▃ ████████ ▆▆▆▆▆▆▆▆ ████████ ████████ " +" ████████ ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ " +" 50 ████████ ████████ ████████ ████████ ████████ ████████ " +" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 86 usage units " +" Feature Share " +" ● Tasks 64.0% " +" ● Code review 14.0% " +" ● Images 11.6% " +" ● Other 10.5% " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used " +" " +" ▎ Messages By model " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 574 reported messages " +" " +" 150 " +" 103 " +" ▇▇▇▇▇▇▇▇ ▆▆▆▆▆▆▆▆ " +" 75 ▆▆▆▆▆▆▆▆ ████████ ▄▄▄▄▄▄▄▄ ▇▇▇▇▇▇▇▇ ████████ " +" ▇▇▇▇▇▇▇▇ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 103 messages " +" Model Messages " +" ● GPT-5.6-Sol 61 " +" ● GPT-5.6-Terra 25 " +" ● GPT-5.5 10 " +" ● Other 7 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 186 calls " +" " +" 40 29 " +" ▇▇▇▇▇▇▇▇ " +" ████████ ████████ ▄▄▄▄▄▄▄▄ █████████ " +" 20 ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ ████████ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 29 calls " +" Plugin Calls " +" ● Slack 10 " +" ● Figma 10 " +" ● GitHub 5 " +" ● Notion 4 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" 11 " +" 10 ▁▁▁▁▁▁▁▁ ▆▆▆▆▆▆▆▆▆ " +" ▆▆▆▆▆▆▆▆ ████████ ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ █████████ " +" ████████ ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 11 uses " +" Skill Uses " +" ● Testing 4 " +" ● Code review 3 " +" ● Planning 3 " +" ● Debugging 1 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" ╭─ 1 Total usage history ──────────────────────────────────────╮ ╭─ 2 Messages ─────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ By feature │ │ By model │ " +" │ Total usage · relative units │ │ 574 reported messages │ " +" │ 86 │ │ │ " +" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ " +" │ █████ ▅▅▅▅▅ █████ ██████ │ │ 103 │ " +" │ █████ ▃▃▃▃▃ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ ▆▆▆▆▆▆ │ " +" │ 50 █████ █████ █████ █████ █████ ██████ │ │ 75 ▆▆▆▆▆ █████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ " +" │ █████ █████ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ " +" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ " +" │ █████ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ " +" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ───────────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ " +" │ ● Tasks 64.0% │ │ ● GPT-5.6-Sol 61 │ " +" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ ▸ 4 Skills used ──────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ 186 calls │ │ 54 uses │ " +" │ │ │ │ " +" │ 40 29 │ │ 20 │ " +" │ ▇▇▇▇▇ │ │ │ " +" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ " +" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ " +" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " +" │ ● Slack 10 │ │ ● Testing 4 │ " +" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · enter/z maximize " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " +" " +" ▎ Credits usage history By product " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 431.00 credits " +" " +" 150 " +" " +" ████████ " +" 75 ▇▇▇▇▇▇▇▇ ▄▄▄▄▄▄▄▄ ████████ 64.00 " +" ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ▇▇▇▇▇▇▇▇ " +" ▇▇▇▇▇▇▇▇ ████████ ████████ ████████ ████████ ████████ " +" 0 ████████ ████████ ████████ ████████ ████████ ████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 64.00 credits " +" Product Credits " +" ● CLI 23.00 " +" ● Desktop app 22.00 " +" ● IDE extension 10.00 " +" ● Web 9.00 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used " +" " +" ▎ Token usage history By token type " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" All models · m change model " +" 4,377,500 tokens " +" " +" 1,000,000 731,000 " +" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ " +" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ " +" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" 0 ───────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 731,000 tokens " +" Token type Tokens " +" ● Cached input 602,000 " +" ● Uncached input 86,000 " +" ● Output 43,000 " +" Updated Sep 2 16:00 UTC " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 186 calls " +" " +" 40 29 " +" ▇▇▇▇▇▇▇▇ " +" ████████ ████████ ▄▄▄▄▄▄▄▄ █████████ " +" 20 ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ ████████ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 29 calls " +" Plugin Calls " +" ● Slack 10 " +" ● Figma 10 " +" ● GitHub 5 " +" ● Notion 4 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" 11 " +" 10 ▁▁▁▁▁▁▁▁ ▆▆▆▆▆▆▆▆▆ " +" ▆▆▆▆▆▆▆▆ ████████ ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ █████████ " +" ████████ ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 11 uses " +" Skill Uses " +" ● Testing 4 " +" ● Code review 3 " +" ● Planning 3 " +" ● Debugging 1 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" ╭─ 1 Credits usage history ────────────────────────────────────╮ ╭─ 2 Token usage history ──────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d · All models │ " +" │ By product │ │ By token type │ " +" │ 431.00 credits │ │ 4,377,500 tokens │ " +" │ │ │ │ " +" │ 150 │ │ 1,000,000 731,000 │ " +" │ │ │ ▃▃▃▃▃ ▇▇▇▇ │ " +" │ █████ │ │ ▂▂▂▂ █████ ▆▆▆▆ ████ █████ │ " +" │ 75 ▇▇▇▇▇ ▄▄▄▄▄ █████ 64.00 │ │ 500,000 ████ ▆▆▆▆ █████ ████ ████ █████ │ " +" │ ▅▅▅▅▅ █████ █████ █████ ▇▇▇▇▇▇ │ │ ████ ████ █████ ████ ████ █████ │ " +" │ ▇▇▇▇▇ █████ █████ █████ █████ ██████ │ │ ▁▁▁▁ ████ ████ █████ ████ ████ █████ │ " +" │ 0 █████ █████ █████ █████ █████ ██████ │ │ ████ ████ ████ █████ ████ ████ █████ │ " +" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 64.00 credits │ │ Sep 2 · 731,000 tokens │ " +" │ ● CLI 23.00 │ │ ● Cached input 602,000 │ " +" │ ● Desktop app 22.00 │ │ ● Uncached input 86,000 │ " +" │ │ │ Updated Sep 2 16:00 UTC │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ ▸ 4 Skills used ──────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ 186 calls │ │ 54 uses │ " +" │ │ │ │ " +" │ 40 29 │ │ 20 │ " +" │ ▇▇▇▇▇ │ │ │ " +" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ " +" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ " +" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " +" │ ● Slack 10 │ │ ● Testing 4 │ " +" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · enter/z maximize " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__account_pending_and_error_do_not_start_reports.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__account_pending_and_error_do_not_start_reports.snap new file mode 100644 index 0000000000..1b32eda5ec --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__account_pending_and_error_do_not_start_reports.snap @@ -0,0 +1,28 @@ +--- +source: tui/src/analytics/account_layout_tests.rs +expression: error +--- +" Analytics · Live account " +" " +" " +" " +" Sign in again to view Analytics. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" R refresh · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__business_tokens_filter_models.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__business_tokens_filter_models.snap new file mode 100644 index 0000000000..401295b6d9 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__business_tokens_filter_models.snap @@ -0,0 +1,65 @@ +--- +source: tui/src/analytics/account_layout_tests.rs +expression: "format!(\"{all}\\n{filtered}\")" +--- +" Analytics · Live account · analytics@example.test · account-a " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used " +" " +" ▎ Token usage history By token type " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" All models · m change model " +" 4,377,500 tokens " +" " +" 1,000,000 731,000 " +" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ " +" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ " +" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" 0 ───────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 731,000 tokens " +" Token type Tokens " +" ● Cached input 602,000 " +" ● Uncached input 86,000 " +" ● Output 43,000 " +" Updated Sep 2 16:00 UTC " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account · analytics@example.test · account-a " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used " +" " +" ▎ Token usage history By token type " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" GPT-5.5 · m change model " +" 4,377,500 tokens " +" " +" 1,000,000 731,000 " +" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ " +" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ " +" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" 0 ───────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 731,000 tokens " +" Token type Tokens " +" ● Cached input 602,000 " +" ● Uncached input 86,000 " +" ● Output 43,000 " +" Updated Sep 2 16:00 UTC " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__dashboard__dashboard_cards_align_and_keep_stable_summary_heights.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__dashboard__dashboard_cards_align_and_keep_stable_summary_heights.snap new file mode 100644 index 0000000000..462c62a86a --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__dashboard__dashboard_cards_align_and_keep_stable_summary_heights.snap @@ -0,0 +1,117 @@ +--- +source: tui/src/analytics/dashboard_tests.rs +expression: "format!(\"{wide}\\n{narrow}\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" ╭─ ▸ 1 Total usage history ────────────────────────────────────────╮ ╭─ 2 Messages ───────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ By feature │ │ By model │ " +" │ Total usage · relative units │ │ 574 reported messages │ " +" │ 86 │ │ │ " +" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ " +" │ █████ ▅▅▅▅▅▅ █████ ██████ │ │ 103 │ " +" │ █████ ▃▃▃▃▃▃ █████ ██████ █████ ██████ │ │ ▇▇▇▇▇▇ ▆▆▆▆▆▆ │ " +" │ 50 █████ ██████ █████ ██████ █████ ██████ │ │ 75 ▆▆▆▆▆ ██████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ " +" │ █████ ██████ █████ ██████ █████ ██████ │ │ ▇▇▇▇▇ █████ ██████ █████ ▆▆▆▆▆▆ █████ ██████ │ " +" │ ▃▃▃▃▃ █████ ██████ █████ ██████ █████ ██████ │ │ █████ █████ ██████ █████ ██████ █████ ██████ │ " +" │ █████ █████ ██████ █████ ██████ █████ ██████ │ │ █████ █████ ██████ █████ ██████ █████ ██████ │ " +" │ 0 ─────────────────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ " +" │ ● Tasks 64.0% │ │ ● GPT-5.6-Sol 61 │ " +" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 3 Plugins called ─────────────────────────────────────────────╮ ╭─ 4 Skills used ────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ 186 calls │ │ 54 uses │ " +" │ │ │ │ " +" │ 40 29 │ │ 20 │ " +" │ ▇▇▇▇▇ │ │ │ " +" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ " +" │ 20 █████ ██████ █████ ██████ ▆▆▆▆▆ ██████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ " +" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ " +" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ █████ ██████ █████ ██████ █████ ▆▆▆▆▆▆ ██████ │ " +" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ █████ ██████ █████ ██████ █████ ██████ ██████ │ " +" │ 0 ──────────────────────────────────────────────────────────── │ │ 0 ──────────────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " +" │ ● Slack 10 │ │ ● Testing 4 │ " +" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · enter/z maximize " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" ╭─ ▸ 1 Total usage history ────────────────────────────────────────╮ " +" │ │ " +" │ 7d │ " +" │ By feature │ " +" │ Total usage · relative units │ " +" │ 100 │ " +" │ 100 ▁▁▁▁▁ ▅▅▅▅▅ ▅▅▅▅▅▅ ▇▇▇▇▇ ▆▆▆▆▆▆ █████ ▆▆▆▆▆▆ │ " +" │ 0 ─────────────────────────────────────────────────────────── │ " +" │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ " +" │ Sep 1 · 100 usage units │ " +" │ ● Tasks 65.0% │ " +" │ ● Code review 15.0% │ " +" │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 2 Messages ───────────────────────────────────────────────────╮ " +" │ │ " +" │ 7d │ " +" │ By model │ " +" │ 574 reported messages │ " +" │ 103 │ " +" │ 150 ▃▃▃▃▃ ▄▄▄▄▄ ▅▅▅▅▅▅ ▄▄▄▄▄ ▃▃▃▃▃▃ ▄▄▄▄▄ ▅▅▅▅▅▅ │ " +" │ 0 ─────────────────────────────────────────────────────────── │ " +" │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ " +" │ Sep 2 · 103 messages │ " +" │ ● GPT-5.6-Sol 61 │ " +" │ ● GPT-5.6-Terra 25 │ " +" │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 3 Plugins called ─────────────────────────────────────────────╮ " +" │ │ " +" │ 7d │ " +" │ Access denied for this report. │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" tab/1–4 section · enter/z maximize " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q " +" close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_1.snap new file mode 100644 index 0000000000..ff0e167125 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_1.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used " +" " +" ▎ Token usage history By token type " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" All models · m change model " +" 4,377,500 tokens " +" " +" 1,000,000 " +" " +" ▂▂▂▂▂▂▂▂▂▂ " +" ██████████ 731,000 " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ " +" 500,000 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 731,000 tokens " +" Token type Tokens " +" ● Cached input 602,000 " +" ● Uncached input 86,000 " +" ● Output 43,000 " +" Updated Sep 2 16:00 UTC " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 27:3 52:2 83:0 +4: 0:0 +5: 0:0 2:3 23:0 105:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:2 29:0 +8: 0:0 2:3 18:0 +9: 0:0 +10: 0:0 3:2 12:0 +11: 0:0 +12: 0:0 90:5 100:0 +13: 0:0 90:5 100:0 107:3 114:0 +14: 0:0 60:5 70:0 90:6 100:0 105:5 115:0 +15: 0:0 60:6 70:0 75:5 85:0 90:7 100:0 105:5 115:0 +16: 0:0 30:5 40:0 60:6 70:0 75:6 85:0 90:7 100:0 105:6 115:0 +17: 0:0 30:6 40:0 60:7 70:0 75:6 85:0 90:7 100:0 105:7 115:0 +18: 0:0 30:6 40:0 45:5 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +19: 0:0 5:2 12:0 30:7 40:0 45:6 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +20: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +21: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +22: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +23: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +24: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +25: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +26: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +27: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +28: 0:0 11:2 12:0 13:4 118:0 +29: 0:0 110:3 111:0 +30: 0:0 17:2 23:0 32:2 38:0 47:2 53:0 62:2 68:0 77:2 83:0 93:2 98:0 108:8 113:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 24:0 +34: 0:0 2:1 12:0 52:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 2:2 25:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_2.snap new file mode 100644 index 0000000000..63577d2073 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_2.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 186 calls " +" " +" 40 " +" " +" ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ " +" ██████████ 29 " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ " +" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ " +" ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 29 calls " +" Plugin Calls " +" ● Slack 10 " +" ● Figma 10 " +" ● GitHub 5 " +" ● Notion 4 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 50:3 70:2 83:0 +4: 0:0 +5: 0:0 2:3 18:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 11:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 108:3 110:0 +14: 0:0 9:5 19:0 +15: 0:0 9:5 19:0 +16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0 +17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0 +18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0 +19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0 +21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 18:0 +34: 0:0 2:1 8:0 53:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_3.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_3.snap new file mode 100644 index 0000000000..0640edc77d --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_3.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " +" " +" ▎ Credits usage history By product " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 17,240.00 credits " +" " +" 5,000 " +" " +" ▄▄▄▄▄▄▄▄▄▄ " +" ██████████ " +" ██████████ " +" ██████████ " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ " +" ██████████ ██████████ ██████████ 2,560.00 " +" 2,500 ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ " +" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 2,560.00 credits " +" Product Credits " +" ● CLI 920.00 " +" ● Desktop app 880.00 " +" ● IDE extension 400.00 " +" ● Web 360.00 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:3 29:2 83:0 +4: 0:0 +5: 0:0 2:3 25:0 108:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 19:0 +8: 0:0 +9: 0:0 3:2 8:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 89:5 99:0 +15: 0:0 89:5 99:0 +16: 0:0 89:6 99:0 +17: 0:0 58:5 68:0 89:6 99:0 105:3 113:0 +18: 0:0 3:2 8:0 58:5 68:0 73:5 83:0 89:6 99:0 +19: 0:0 58:6 68:0 73:5 83:0 89:6 99:0 +20: 0:0 58:6 68:0 73:6 83:0 89:6 99:0 105:5 115:0 +21: 0:0 42:5 52:0 58:6 68:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 42:6 52:0 58:6 68:0 73:6 83:0 89:7 99:0 105:6 115:0 +23: 0:0 27:5 37:0 42:6 52:0 58:7 68:0 73:6 83:0 89:7 99:0 105:6 115:0 +24: 0:0 27:6 37:0 42:6 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:6 115:0 +25: 0:0 27:6 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 16:4 17:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 7:2 8:0 9:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 13:2 19:0 28:2 34:0 44:2 50:0 59:2 65:0 75:2 81:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 26:0 +34: 0:0 2:1 9:0 51:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_5.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_5.snap new file mode 100644 index 0000000000..7d1eea4602 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_5.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" " +" " +" " +" " +" " +" 11 " +" ▃▃▃▃▃▃▃▃▃▃ " +" 10 ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ " +" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 11 uses " +" Skill Uses " +" ● Testing 4 " +" ● Code review 3 " +" ● Planning 3 " +" ● Debugging 1 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 68:3 83:0 +4: 0:0 +5: 0:0 2:3 15:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 9:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 108:3 110:0 +17: 0:0 +18: 0:0 3:2 5:0 +19: 0:0 105:5 115:0 +20: 0:0 41:5 51:0 105:5 115:0 +21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0 +22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0 +23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 17:0 +34: 0:0 2:1 7:0 54:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_1.snap new file mode 100644 index 0000000000..9e229152b2 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_1.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used " +" " +" ▎ Total usage history By feature " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" Total usage · relative units " +" Approximate · may be delayed up to 6 hours " +" " +" 100 ██████████ " +" ▃▃▃▃▃▃▃▃▃▃ ██████████ 86 " +" ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ " +" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 50 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 86 usage units " +" Feature Share " +" ● Tasks 64.0% " +" ● Code review 14.0% " +" ● Images 11.6% " +" ● Other 10.5% " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:3 27:2 70:0 +4: 0:0 +5: 0:0 2:3 23:0 108:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:2 30:0 +8: 0:0 2:2 44:0 +9: 0:0 +10: 0:0 3:2 6:0 +11: 0:0 89:5 99:0 108:3 110:0 +12: 0:0 89:5 99:0 +13: 0:0 57:5 67:0 89:5 99:0 +14: 0:0 57:5 67:0 89:6 99:0 105:5 115:0 +15: 0:0 57:6 67:0 73:5 83:0 89:6 99:0 105:5 115:0 +16: 0:0 25:5 35:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +17: 0:0 25:5 35:0 57:7 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +18: 0:0 25:6 35:0 41:5 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +19: 0:0 4:2 6:0 25:6 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +20: 0:0 25:7 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +21: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +22: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +23: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +24: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 5:2 6:0 7:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 24:0 +34: 0:0 2:1 9:0 53:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_2.snap new file mode 100644 index 0000000000..1e87635832 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_2.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 186 calls " +" " +" 40 " +" " +" ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ " +" ██████████ 29 " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ " +" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ " +" ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 29 calls " +" Plugin Calls " +" ● Slack 10 " +" ● Figma 10 " +" ● GitHub 5 " +" ● Notion 4 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 37:3 57:2 70:0 +4: 0:0 +5: 0:0 2:3 18:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 11:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 108:3 110:0 +14: 0:0 9:5 19:0 +15: 0:0 9:5 19:0 +16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0 +17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0 +18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0 +19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0 +21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 18:0 +34: 0:0 2:1 8:0 53:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_4.snap new file mode 100644 index 0000000000..874894434b --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_4.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used " +" " +" ▎ Messages By model " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 574 reported messages " +" " +" 150 " +" " +" " +" " +" 103 " +" ▄▄▄▄▄▄▄▄▄▄ " +" ██████████ ██████████ " +" ██████████ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ " +" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 103 messages " +" Model Messages " +" ● GPT-5.6-Sol 61 " +" ● GPT-5.6-Terra 25 " +" ● GPT-5.5 10 " +" ● Other 7 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 25:3 39:2 70:0 +4: 0:0 +5: 0:0 2:3 12:0 110:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 23:0 +8: 0:0 +9: 0:0 3:2 6:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 108:3 111:0 +14: 0:0 +15: 0:0 41:5 51:0 +16: 0:0 41:5 51:0 105:5 115:0 +17: 0:0 41:6 51:0 105:6 115:0 +18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0 +19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0 +20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0 +21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0 +22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 5:2 6:0 7:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 22:0 +34: 0:0 2:1 7:0 50:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_5.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_5.snap new file mode 100644 index 0000000000..c6d14747fd --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_5.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" " +" " +" " +" " +" " +" 11 " +" ▃▃▃▃▃▃▃▃▃▃ " +" 10 ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ " +" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 11 uses " +" Skill Uses " +" ● Testing 4 " +" ● Code review 3 " +" ● Planning 3 " +" ● Debugging 1 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(165, 165, 165)|Reset|Reset|NONE +3: Cyan|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Cyan|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 55:3 70:0 +4: 0:0 +5: 0:0 2:3 15:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 9:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 108:3 110:0 +17: 0:0 +18: 0:0 3:2 5:0 +19: 0:0 105:5 115:0 +20: 0:0 41:5 51:0 105:5 115:0 +21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0 +22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0 +23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 17:0 +34: 0:0 2:1 7:0 54:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_1.snap new file mode 100644 index 0000000000..53c8cf4a2f --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_1.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used " +" " +" ▎ Token usage history By token type " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" All models · m change model " +" 4,377,500 tokens " +" " +" 1,000,000 " +" " +" ▂▂▂▂▂▂▂▂▂▂ " +" ██████████ 731,000 " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ " +" 500,000 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 731,000 tokens " +" Token type Tokens " +" ● Cached input 602,000 " +" ● Uncached input 86,000 " +" ● Output 43,000 " +" Updated Sep 2 16:00 UTC " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 27:3 52:2 83:0 +4: 0:0 +5: 0:0 2:3 23:0 105:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:2 29:0 +8: 0:0 2:3 18:0 +9: 0:0 +10: 0:0 3:2 12:0 +11: 0:0 +12: 0:0 90:5 100:0 +13: 0:0 90:5 100:0 107:3 114:0 +14: 0:0 60:5 70:0 90:6 100:0 105:5 115:0 +15: 0:0 60:6 70:0 75:5 85:0 90:7 100:0 105:5 115:0 +16: 0:0 30:5 40:0 60:6 70:0 75:6 85:0 90:7 100:0 105:6 115:0 +17: 0:0 30:6 40:0 60:7 70:0 75:6 85:0 90:7 100:0 105:7 115:0 +18: 0:0 30:6 40:0 45:5 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +19: 0:0 5:2 12:0 30:7 40:0 45:6 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +20: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +21: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +22: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +23: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +24: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +25: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +26: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +27: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0 +28: 0:0 11:2 12:0 13:4 118:0 +29: 0:0 110:3 111:0 +30: 0:0 17:2 23:0 32:2 38:0 47:2 53:0 62:2 68:0 77:2 83:0 93:2 98:0 108:8 113:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 24:0 +34: 0:0 2:1 12:0 52:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 2:2 25:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_2.snap new file mode 100644 index 0000000000..0173d02027 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_2.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 186 calls " +" " +" 40 " +" " +" ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ " +" ██████████ 29 " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ " +" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ " +" ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 29 calls " +" Plugin Calls " +" ● Slack 10 " +" ● Figma 10 " +" ● GitHub 5 " +" ● Notion 4 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 50:3 70:2 83:0 +4: 0:0 +5: 0:0 2:3 18:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 11:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 108:3 110:0 +14: 0:0 9:5 19:0 +15: 0:0 9:5 19:0 +16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0 +17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0 +18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0 +19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0 +21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 18:0 +34: 0:0 2:1 8:0 53:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_3.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_3.snap new file mode 100644 index 0000000000..706215098a --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_3.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " +" " +" ▎ Credits usage history By product " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 17,240.00 credits " +" " +" 5,000 " +" " +" ▄▄▄▄▄▄▄▄▄▄ " +" ██████████ " +" ██████████ " +" ██████████ " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ " +" ██████████ ██████████ ██████████ 2,560.00 " +" 2,500 ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ " +" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 2,560.00 credits " +" Product Credits " +" ● CLI 920.00 " +" ● Desktop app 880.00 " +" ● IDE extension 400.00 " +" ● Web 360.00 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:3 29:2 83:0 +4: 0:0 +5: 0:0 2:3 25:0 108:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 19:0 +8: 0:0 +9: 0:0 3:2 8:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 89:5 99:0 +15: 0:0 89:5 99:0 +16: 0:0 89:6 99:0 +17: 0:0 58:5 68:0 89:6 99:0 105:3 113:0 +18: 0:0 3:2 8:0 58:5 68:0 73:5 83:0 89:6 99:0 +19: 0:0 58:6 68:0 73:5 83:0 89:6 99:0 +20: 0:0 58:6 68:0 73:6 83:0 89:6 99:0 105:5 115:0 +21: 0:0 42:5 52:0 58:6 68:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 42:6 52:0 58:6 68:0 73:6 83:0 89:7 99:0 105:6 115:0 +23: 0:0 27:5 37:0 42:6 52:0 58:7 68:0 73:6 83:0 89:7 99:0 105:6 115:0 +24: 0:0 27:6 37:0 42:6 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:6 115:0 +25: 0:0 27:6 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 16:4 17:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 7:2 8:0 9:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 13:2 19:0 28:2 34:0 44:2 50:0 59:2 65:0 75:2 81:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 26:0 +34: 0:0 2:1 9:0 51:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_5.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_5.snap new file mode 100644 index 0000000000..1965dfd35d --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_5.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" " +" " +" " +" " +" " +" 11 " +" ▃▃▃▃▃▃▃▃▃▃ " +" 10 ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ " +" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 11 uses " +" Skill Uses " +" ● Testing 4 " +" ● Code review 3 " +" ● Planning 3 " +" ● Debugging 1 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 68:3 83:0 +4: 0:0 +5: 0:0 2:3 15:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 9:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 108:3 110:0 +17: 0:0 +18: 0:0 3:2 5:0 +19: 0:0 105:5 115:0 +20: 0:0 41:5 51:0 105:5 115:0 +21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0 +22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0 +23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 17:0 +34: 0:0 2:1 7:0 54:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_1.snap new file mode 100644 index 0000000000..d32cd8e323 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_1.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used " +" " +" ▎ Total usage history By feature " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" Total usage · relative units " +" Approximate · may be delayed up to 6 hours " +" " +" 100 ██████████ " +" ▃▃▃▃▃▃▃▃▃▃ ██████████ 86 " +" ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ " +" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 50 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 86 usage units " +" Feature Share " +" ● Tasks 64.0% " +" ● Code review 14.0% " +" ● Images 11.6% " +" ● Other 10.5% " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:3 27:2 70:0 +4: 0:0 +5: 0:0 2:3 23:0 108:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:2 30:0 +8: 0:0 2:2 44:0 +9: 0:0 +10: 0:0 3:2 6:0 +11: 0:0 89:5 99:0 108:3 110:0 +12: 0:0 89:5 99:0 +13: 0:0 57:5 67:0 89:5 99:0 +14: 0:0 57:5 67:0 89:6 99:0 105:5 115:0 +15: 0:0 57:6 67:0 73:5 83:0 89:6 99:0 105:5 115:0 +16: 0:0 25:5 35:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +17: 0:0 25:5 35:0 57:7 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +18: 0:0 25:6 35:0 41:5 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +19: 0:0 4:2 6:0 25:6 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +20: 0:0 25:7 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +21: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +22: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +23: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +24: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 5:2 6:0 7:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 24:0 +34: 0:0 2:1 9:0 53:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_2.snap new file mode 100644 index 0000000000..cc9f2f9e1b --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_2.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 186 calls " +" " +" 40 " +" " +" ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ " +" ██████████ 29 " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ " +" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ " +" ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 29 calls " +" Plugin Calls " +" ● Slack 10 " +" ● Figma 10 " +" ● GitHub 5 " +" ● Notion 4 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 37:3 57:2 70:0 +4: 0:0 +5: 0:0 2:3 18:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 11:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 108:3 110:0 +14: 0:0 9:5 19:0 +15: 0:0 9:5 19:0 +16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0 +17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0 +18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0 +19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0 +21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 18:0 +34: 0:0 2:1 8:0 53:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_4.snap new file mode 100644 index 0000000000..52460f0d5f --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_4.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used " +" " +" ▎ Messages By model " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 574 reported messages " +" " +" 150 " +" " +" " +" " +" 103 " +" ▄▄▄▄▄▄▄▄▄▄ " +" ██████████ ██████████ " +" ██████████ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ " +" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 103 messages " +" Model Messages " +" ● GPT-5.6-Sol 61 " +" ● GPT-5.6-Terra 25 " +" ● GPT-5.5 10 " +" ● Other 7 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 25:3 39:2 70:0 +4: 0:0 +5: 0:0 2:3 12:0 110:2 118:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 23:0 +8: 0:0 +9: 0:0 3:2 6:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 108:3 111:0 +14: 0:0 +15: 0:0 41:5 51:0 +16: 0:0 41:5 51:0 105:5 115:0 +17: 0:0 41:6 51:0 105:6 115:0 +18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0 +19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0 +20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0 +21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0 +22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 5:2 6:0 7:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 22:0 +34: 0:0 2:1 7:0 50:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_5.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_5.snap new file mode 100644 index 0000000000..3e0f8610ad --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_5.snap @@ -0,0 +1,102 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" " +" " +" " +" " +" " +" 11 " +" ▃▃▃▃▃▃▃▃▃▃ " +" 10 ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ " +" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ " +" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 11 uses " +" Skill Uses " +" ● Testing 4 " +" ● Code review 3 " +" ● Planning 3 " +" ● Debugging 1 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " +Palette (foreground|background|underline|modifiers): +0: Reset|Reset|Reset|NONE +1: Reset|Reset|Reset|BOLD +2: Rgb(76, 76, 76)|Reset|Reset|NONE +3: Rgb(0, 95, 135)|Reset|Reset|BOLD +4: Reset|Reset|Reset|DIM +5: Green|Reset|Reset|NONE +6: Magenta|Reset|Reset|NONE +7: Cyan|Reset|Reset|NONE +8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED +9: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 2:3 6:0 11:2 34:0 +2: 0:0 +3: 0:0 2:2 55:3 70:0 +4: 0:0 +5: 0:0 2:3 15:0 +6: 0:0 2:4 118:0 +7: 0:0 2:3 9:0 +8: 0:0 +9: 0:0 3:2 5:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 108:3 110:0 +17: 0:0 +18: 0:0 3:2 5:0 +19: 0:0 105:5 115:0 +20: 0:0 41:5 51:0 105:5 115:0 +21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0 +22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0 +23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0 +24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0 +25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 4:2 5:0 6:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0 +31: 0:0 +32: 0:0 2:4 58:0 +33: 0:0 2:1 7:3 17:0 +34: 0:0 2:1 7:0 54:1 58:0 +35: 0:0 2:7 4:0 +36: 0:0 2:6 4:0 +37: 0:0 2:5 4:0 +38: 0:0 +39: 0:0 +40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0 +41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__tools_panel__analytics_tools_handle_empty_days_and_independent_failures.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__tools_panel__analytics_tools_handle_empty_days_and_independent_failures.snap new file mode 100644 index 0000000000..e98dfe9abc --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__tools_panel__analytics_tools_handle_empty_days_and_independent_failures.snap @@ -0,0 +1,138 @@ +--- +source: tui/src/analytics/tool_panel_tests.rs +expression: "format!(\"{empty}\\n{missing}\\n{partial}\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 38 uses " +" " +" 10 " +" " +" ▇▇▇▇▇▇▇▇▇▇▇▇ " +" ████████████ " +" ▆▆▆▆▆▆▆▆▆▆▆▆ ████████████ ▆▆▆▆▆▆▆▆▆▆▆▆ " +" ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ▅▅▅▅▅▅▅▅▅▅▅▅ " +" ████████████ ████████████ ████████████ ████████████ " +" ████████████ ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" 5 ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ — 0 " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" No uses reported for Sep 2. " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · z dashboard · ←/→ day " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 38 uses " +" " +" 10 " +" " +" ▇▇▇▇▇▇▇▇▇▇▇▇ " +" ████████████ " +" ▆▆▆▆▆▆▆▆▆▆▆▆ ████████████ ▆▆▆▆▆▆▆▆▆▆▆▆ " +" ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ▅▅▅▅▅▅▅▅▅▅▅▅ " +" ████████████ ████████████ ████████████ ████████████ " +" ████████████ ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" 5 ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ — 0 " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Data not reported for Sep 1. " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · z dashboard · ←/→ day " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" Skills could not be loaded. Press R to retry. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · z dashboard · ←/→ day " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__tools_panel__analytics_tools_reflow_and_keep_independent_days.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__tools_panel__analytics_tools_reflow_and_keep_independent_days.snap new file mode 100644 index 0000000000..9679c35efc --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__tools_panel__analytics_tools_reflow_and_keep_independent_days.snap @@ -0,0 +1,85 @@ +--- +source: tui/src/analytics/tool_panel_tests.rs +expression: "format!(\"{wide}\\n{narrow}\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 186 calls " +" " +" 40 " +" " +" " +" ▂▂▂▂▂▂▂▂▂▂▂▂ " +" ████████████ " +" ████████████ 29 " +" ████████████ ▁▁▁▁▁▁▁▁▁▁▁▁ ▁▁▁▁▁▁▁▁▁▁▁▁ " +" ████████████ ████████████ ████████████ " +" ████████████ ████████████ ▂▂▂▂▂▂▂▂▂▂▂▂ ████████████ " +" ████████████ ████████████ ████████████ ████████████ " +" ████████████ ▃▃▃▃▃▃▃▃▃▃▃▃ ████████████ ████████████ ▃▃▃▃▃▃▃▃▃▃▃▃ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ▆▆▆▆▆▆▆▆▆▆▆▆ ████████████ ████████████ " +" 20 ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 29 calls " +" Plugin Calls " +" ● Slack 10 " +" ● Figma 10 " +" ● GitHub 5 " +" ● Notion 4 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" Total usage history Messages Plugins called [Skills " +" used] " +" " +" ▎ Skills used " +" ────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" ▂▂▂▂▂ " +" 10 ▃▃▃▃ ▅▅▅▅ ▃▃▃▃ 5 █████ " +" ████ ▆▆▆▆ ████ ████ ████ ▄▄▄▄ █████ " +" ████ ████ ████ ████ ████ ████ █████ " +" 0 ────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ────────────────────────────────────────────────────── " +" Sep 1 · 5 uses " +" Skill Uses " +" ● Testing 3 " +" ● Code review 1 " +" ● Planning 1 " +" ● Debugging 0 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter " +" details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q " +" close " diff --git a/codex-rs/tui/src/analytics/styles_tests.rs b/codex-rs/tui/src/analytics/styles_tests.rs new file mode 100644 index 0000000000..110a32b44f --- /dev/null +++ b/codex-rs/tui/src/analytics/styles_tests.rs @@ -0,0 +1,153 @@ +//! Render both account layouts with explicit terminal palettes, including color and selection styles. + +use super::*; +use crate::analytics::sections::Section; +use crate::style::accent_style; +use crate::terminal_palette::with_test_default_colors; +use crate::terminal_probe::DefaultColors; +use pretty_assertions::assert_eq; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Color; +use ratatui::style::Modifier; + +fn luminance(rgb: (u8, u8, u8)) -> f64 { + let [r, g, b] = [rgb.0, rgb.1, rgb.2].map(|channel| { + let value = f64::from(channel) / 255.0; + if value <= 0.04045 { + value / 12.92 + } else { + ((value + 0.055) / 1.055).powf(/*n*/ 2.4) + } + }); + 0.2126 * r + 0.7152 * g + 0.0722 * b +} + +#[test] +fn analytics_sections_adapt_to_terminal_colors() { + for (theme, colors) in [ + ( + "light", + DefaultColors { + fg: (0, 0, 0), + bg: (255, 255, 255), + }, + ), + ( + "dark", + DefaultColors { + fg: (230, 230, 230), + bg: (16, 16, 16), + }, + ), + ] { + with_test_default_colors(colors, || { + for business in [false, true] { + let mut view = fixture::view(models::AccountKind::Consumer); + if business { + view = fixture::view(models::AccountKind::Enterprise); + } + // Exercise the reported credit range and preserve exact amounts below the plot. + let mut credits = + fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0); + for day in &mut credits.data { + day.total *= 40.0; + for value in &mut day.values { + value.value *= 40.0; + } + } + view.sections[Section::Credits].history = Load::Ready(credits); + for section in view.visible_sections() { + let section = *section; + view.section = section; + let area = Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 120, /*height*/ 42, + ); + let mut buffer = Buffer::empty(area); + view.render(area, &mut buffer); + let heading = buffer + .content + .iter() + .find(|cell| cell.symbol() == "▎") + .unwrap(); + assert_eq!(heading.fg, accent_style().fg.unwrap()); + assert!(heading.modifier.contains(Modifier::BOLD)); + for marker in buffer.content.iter().filter(|cell| cell.symbol() == "▲") { + assert_eq!(marker.fg, accent_style().fg.unwrap()); + assert_eq!(marker.bg, Color::Reset); + assert!(marker.modifier.contains(Modifier::BOLD)); + } + if theme == "light" { + for cell in &buffer.content { + if !cell.symbol().trim().is_empty() + && let Color::Rgb(r, g, b) = cell.fg + { + let contrast = + (luminance(colors.bg) + 0.05) / (luminance((r, g, b)) + 0.05); + assert!( + contrast >= 4.5, + "{}: {:?} has contrast {contrast}", + cell.symbol(), + cell.fg + ); + } + } + } + // Preserve every symbol and style while sharing repeated styles in a palette. + let mut palette = Vec::new(); + let mut text = Vec::new(); + let mut runs = Vec::new(); + for (y, row) in buffer.content.chunks(usize::from(area.width)).enumerate() { + text.push(format!( + "{:?}", + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + )); + let mut previous = None; + let mut changes = Vec::new(); + for (x, cell) in row.iter().enumerate() { + let style = format!( + "{:?}|{:?}|{:?}|{:?}", + cell.fg, cell.bg, cell.underline_color, cell.modifier + ); + let index = palette + .iter() + .position(|candidate| *candidate == style) + .unwrap_or_else(|| { + palette.push(style); + palette.len() - 1 + }); + if previous != Some(index) { + changes.push(format!("{x}:{index}")); + previous = Some(index); + } + } + runs.push(format!("{y}: {}", changes.join(" "))); + } + let palette = palette + .iter() + .enumerate() + .map(|(index, style)| format!("{index}: {style}")) + .collect::>() + .join("\n"); + let snapshot = format!( + "{}x{}\nText:\n{}\nPalette (foreground|background|underline|modifiers):\n{palette}\nRows (column:palette):\n{}", + area.width, + area.height, + text.join("\n"), + runs.join("\n") + ); + insta::assert_snapshot!( + format!( + "analytics_{theme}_{}_{}", + if business { "business" } else { "consumer" }, + section as usize + 1 + ), + snapshot + ); + } + } + }); + } +} diff --git a/codex-rs/tui/src/analytics/test_fixtures.rs b/codex-rs/tui/src/analytics/test_fixtures.rs new file mode 100644 index 0000000000..9cab9854fe --- /dev/null +++ b/codex-rs/tui/src/analytics/test_fixtures.rs @@ -0,0 +1,188 @@ +//! Bounded fictional dashboard data with raw daily units. +//! Charts and their totals share the same daily series; no account activity is read. + +use crate::analytics::sections::Section; +pub(super) const END_DATE: chrono::NaiveDate = + match chrono::NaiveDate::from_ymd_opt(/*year*/ 2026, /*month*/ 9, /*day*/ 2) { + Some(date) => date, + None => panic!("invalid test date"), + }; + +pub(super) const TOOL_DAILY: [[u32; 7]; 2] = [[34, 23, 29, 26, 22, 23, 29], [8, 6, 9, 8, 7, 5, 11]]; + +pub(super) const GROUPS: &[(&str, [&str; 4], [u32; 4])] = &[ + ( + "Surface", + ["Desktop app", "CLI", "IDE extension", "Web"], + [35, 36, 17, 12], + ), + ( + "Feature", + ["Tasks", "Code review", "Images", "Other"], + [65, 15, 12, 8], + ), + ( + "Model", + ["GPT-5.6-Sol", "GPT-5.6-Terra", "GPT-5.5", "Other"], + [60, 25, 10, 5], + ), + ( + "Task start", + ["User messages", "Goals", "Automations", "Other"], + [29, 10, 5, 5], + ), + ( + "Speed", + ["Standard", "Fast", "Priority", "Unknown"], + [65, 30, 5, 0], + ), + ( + "Reasoning", + ["Low", "Medium", "High", "Other"], + [10, 30, 50, 10], + ), + ( + "Token type", + ["Uncached input", "Cached input", "Output", "Other"], + [20, 70, 10, 0], + ), +]; + +pub(super) fn daily(section: Section, range: usize) -> Vec { + let (earlier, recent) = match section { + Section::Usage => (40, [20, 73, 64, 91, 81, 100, 86]), + Section::Credits => (35, [0, 41, 57, 85, 75, 109, 64]), + Section::Activity => (34, [62, 83, 107, 75, 59, 85, 103]), + _ => return Vec::new(), + }; + let mut values = vec![earlier; if range == 0 { 0 } else { 23 }]; + values.extend(recent); + values +} + +pub(super) fn breakdown(total: u32, group: usize) -> [u32; 4] { + let mut values = GROUPS[group].2.map(|weight| total * weight / 100); + if group != 3 { + values[3] = total - values[..3].iter().sum::(); + } + values +} + +/// Fixtures use the private report types and the same presentation path as live reports. +pub(super) fn history( + report: usize, + range: usize, + group: usize, +) -> crate::analytics::models::AccountAnalyticsHistory { + use crate::analytics::models::AccountAnalyticsDay; + use crate::analytics::models::AccountAnalyticsHistory; + use crate::analytics::models::AccountAnalyticsUnit; + use crate::analytics::models::AccountAnalyticsValue; + let section = [ + Section::Usage, + Section::Credits, + Section::Activity, + Section::Plugins, + Section::Skills, + ][report]; + let series = if report < 3 { + daily(section, range) + } else { + let mut series = vec![if report == 3 { 20 } else { 5 }; if range == 0 { 0 } else { 23 }]; + series.extend(TOOL_DAILY[report - 3]); + series + }; + let start = END_DATE - chrono::Days::new(if range == 0 { 6 } else { 29 }); + AccountAnalyticsHistory { + unit: if report == 0 { + AccountAnalyticsUnit::RelativeUsage + } else if report == 1 { + AccountAnalyticsUnit::Credits + } else { + AccountAnalyticsUnit::Count + }, + updated_at: None, + data: series + .into_iter() + .enumerate() + .map(|(day, total)| { + let labels = if report < 3 { + GROUPS[group].1 + } else if report == 3 { + ["Figma", "Slack", "Notion", "GitHub"] + } else { + ["Code review", "Planning", "Debugging", "Testing"] + }; + let values = breakdown(total, if report < 3 { group } else { 0 }); + let pairs = if report == 0 && group == 0 { + vec![ + ("Codex", total * 65 / 100), + ("Work", total - total * 65 / 100), + ] + } else { + labels.into_iter().zip(values).collect() + }; + AccountAnalyticsDay { + date: start + chrono::Days::new(day as u64), + total: f64::from(total), + values: pairs + .into_iter() + .map(|(label, value)| AccountAnalyticsValue { + key: label.into(), + label: label.into(), + value: f64::from(value), + }) + .collect(), + } + }) + .collect(), + } +} + +pub(super) fn tokens(range: usize, grouping: usize) -> super::models::AccountAnalyticsHistory { + let response = serde_json::json!({"data_freshness_ts":"2026-09-02T16:00:00Z", "data": + daily(Section::Usage, range).into_iter().enumerate().map(|(index, total)| serde_json::json!({ + "date": (END_DATE - chrono::Days::new(if range == 0 { 6 } else { 29 }) + chrono::Days::new(index as u64)).to_string(), + "groups": [{"dimensions":{"model":"GPT-5.5"}, "uncached_text_input_tokens": total * 1000, "cached_text_input_tokens":total * 7000, "text_output_tokens":total * 500}] + })).collect::>() + }); + match super::tokens::history( + match serde_json::from_value(response) { + Ok(response) => response, + Err(_) => panic!("invalid test token response"), + }, + super::data::GROUPINGS[grouping], + END_DATE - chrono::Days::new(if range == 0 { 6 } else { 29 }), + END_DATE, + ) { + Ok(history) => history, + Err(_) => panic!("invalid test token history"), + } +} + +/// Build a fixed account state for rendering tests; never opens a connection. +pub(super) fn view(kind: super::models::AccountKind) -> super::AnalyticsView { + let mut view = super::AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list); + view.end_date = END_DATE; + view.account = super::data::Load::Ready(kind); + view.start_reports(); + seed_reports(&mut view); + view +} + +/// Populate explicit report state after a test chooses its account, period, and grouping. +pub(super) fn seed_reports(view: &mut super::AnalyticsView) { + use super::data::Load; + for section in view.visible_sections() { + if let Some(report) = section.report() { + let range = view.ranges[view.range_group(*section) as usize]; + let group = view.sections[*section].group; + view.sections[*section].history = + Load::Ready(if *section == Section::Usage && view.business() { + tokens(range, group) + } else { + history(report as usize, range, group) + }); + } + } +} diff --git a/codex-rs/tui/src/analytics/test_support.rs b/codex-rs/tui/src/analytics/test_support.rs new file mode 100644 index 0000000000..3cac090218 --- /dev/null +++ b/codex-rs/tui/src/analytics/test_support.rs @@ -0,0 +1,72 @@ +//! Mock HTTP reports for tests that exercise the authenticated dashboard loading path. +use super::data::Load; +use serde_json::json; + +pub(super) async fn server() -> wiremock::MockServer { + let server = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path_regex("^/backend-api/wham/(usage|analytics)/")) + .respond_with(|request: &wiremock::Request| { + let params = request.url.query_pairs().collect::>(); + let today = chrono::Utc::now().date_naive(); + let start = params.get("start_date").map_or(today, |date| date.parse().unwrap()); + let end = params.get("end_date").map_or(today, |date| date.parse().unwrap()); + let route = request.url.path().rsplit('/').next().unwrap(); + let data = start.iter_days().take_while(|date| *date <= end).map(|date| { + let date = date.to_string(); + match route { + "daily-token-usage-breakdown" | "daily-workspace-user-token-usage-breakdown" => json!({ + "date":date, "product_surface_usage_values":{"codex_cli":10.0}, + "premium_usage_values":{ + "credit_usage_credits":{"codex_cli":10.0}, "total_usage_credits":{}, + "uncached_text_input_tokens_by_surface":{}, "cached_text_input_tokens_by_surface":{}, + "text_output_tokens_by_surface":{}, "text_total_tokens_by_surface":{} + }, + "models":[{"model":"GPT-5.5","credits":10.0,"on_demand_credits":10.0,"turns":10,"speed":"standard"}], + "groups":[{"dimensions":{"model":"GPT-5.5"},"credits":10.0,"uncached_text_input_tokens":10,"cached_text_input_tokens":20,"text_output_tokens":30}] + }), + "credit-usage-events" => json!({"date":date,"product_surface":"cli","credit_amount":10.0}), + "daily-workspace-user-credit-usage" => json!({"date":date,"values":{"codex":10.0}}), + "daily-workspace-usage-counts" => json!({ + "date":date,"totals":{"users":1,"threads":1,"turns":10,"credits":10.0},"clients":[], + "models":[{"model":"GPT-5.5","credits":10.0,"turns":10}] + }), + "daily-plugin-usage-metrics" => json!({"date":date,"plugin_usage_overviews":[{ + "plugin_id":null,"plugin_name":"test","marketplace":null,"display_name":"Test plugin","invocation_counts":10 + }]}), + "daily-skill-usage-metrics" => json!({"date":date,"skill_usage_overviews":[{ + "skill_name":"test","skill_ids":[],"display_name":"Test skill","invocation_counts":10 + }]}), + _ => panic!("Unexpected mock analytics route: {route}"), + } + }).collect::>(); + let response = if route == "daily-workspace-user-credit-usage" { + json!({"breakdown":params["breakdown"],"series":[{"key":"codex","label":"Codex","total":data.len() as f64 * 10.0}],"data":data}) + } else { + json!({"data":data}) + }; + wiremock::ResponseTemplate::new(/*s*/ 200).set_body_json(response) + }) + .mount(&server).await; + server +} + +pub(super) async fn settle(view: &mut super::AnalyticsView) { + tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 10), async { + loop { + view.poll_reports(); + if !matches!(view.account, Load::Loading(_)) + && !view + .sections + .0 + .iter() + .any(|state| matches!(state.history, Load::Loading(_))) + { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(/*millis*/ 5)).await; + } + }) + .await + .unwrap(); +} diff --git a/codex-rs/tui/src/analytics/tool_panel.rs b/codex-rs/tui/src/analytics/tool_panel.rs new file mode 100644 index 0000000000..c0a4d4472d --- /dev/null +++ b/codex-rs/tui/src/analytics/tool_panel.rs @@ -0,0 +1,26 @@ +//! Human-readable plugin and skill labels. + +pub(super) fn tool_name(label: &str) -> String { + let label = match label.split_once(':') { + Some((namespace, name)) if namespace.trim().eq_ignore_ascii_case(name.trim()) => { + name.trim() + } + _ => label, + }; + label + .split_whitespace() + .map(|word| match word.to_ascii_lowercase().as_str() { + "pr" => "PR", + "cli" => "CLI", + "api" => "API", + "ide" => "IDE", + "mcp" => "MCP", + "sdk" => "SDK", + "ui" => "UI", + "tui" => "TUI", + "openai" => "OpenAI", + _ => word, + }) + .collect::>() + .join(" ") +} diff --git a/codex-rs/tui/src/analytics/tool_panel_tests.rs b/codex-rs/tui/src/analytics/tool_panel_tests.rs new file mode 100644 index 0000000000..b13ea6cd15 --- /dev/null +++ b/codex-rs/tui/src/analytics/tool_panel_tests.rs @@ -0,0 +1,101 @@ +//! Tool chart layout, independent selection, naming, and partial report states. + +use super::*; +use crate::analytics::sections::Section; +use pretty_assertions::assert_eq; + +#[test] +fn analytics_tools_reflow_and_keep_independent_days() { + let mut view = fixture::view(models::AccountKind::Consumer); + press(&mut view, KeyCode::Char('3')); + let wide = screen(&mut view, /*width*/ 140, /*height*/ 48); + assert!(wide.contains("186 calls")); + press(&mut view, KeyCode::Char('4')); + press(&mut view, KeyCode::Left); + press(&mut view, KeyCode::Enter); + assert_eq!( + ( + view.sections[Section::Plugins].cursor, + view.sections[Section::Skills].cursor, + view.sections[Section::Skills].detail + ), + (6, 5, Some(5)) + ); + let narrow = screen(&mut view, /*width*/ 58, /*height*/ 32); + assert!(narrow.contains("Sep 1 · 5 uses")); + press(&mut view, KeyCode::Char('r')); + assert_eq!( + ( + view.sections[Section::Plugins].cursor, + view.sections[Section::Skills].cursor, + view.ranges + ), + (29, 28, [0, 0, 1]) + ); + press(&mut view, KeyCode::Char('r')); + press(&mut view, KeyCode::Char('z')); + press(&mut view, KeyCode::Char('z')); + assert_eq!( + ( + view.sections[Section::Plugins].cursor, + view.sections[Section::Skills].cursor, + view.sections[Section::Skills].detail + ), + (6, 5, Some(5)) + ); + insta::assert_snapshot!(format!("{wide}\n{narrow}")); +} + +#[test] +fn analytics_tools_handle_empty_days_and_independent_failures() { + let mut view = fixture::view(models::AccountKind::Consumer); + let mut history = fixture::history(/*report*/ 4, /*range*/ 0, /*group*/ 0); + history.data[6].total = 0.0; + for value in &mut history.data[6].values { + value.value = 0.0; + } + history.data.remove(/*index*/ 5); + view.sections[Section::Skills].history = Load::Ready(history); + press(&mut view, KeyCode::Char('4')); + let empty = screen(&mut view, /*width*/ 140, /*height*/ 44); + assert!(empty.contains("No uses reported for Sep 2.")); + assert!(!empty.contains("details")); + press(&mut view, KeyCode::Enter); + assert_eq!(view.sections[Section::Skills].detail, None); + press(&mut view, KeyCode::Left); + let missing = screen(&mut view, /*width*/ 140, /*height*/ 44); + assert!(missing.contains("Data not reported for Sep 1.")); + assert_eq!( + empty.lines().position(|line| line.contains('▲')), + missing.lines().position(|line| line.contains('▲')), + ); + view.sections[Section::Skills].history = + Load::Error("Skills could not be loaded. Press R to retry.".into()); + let partial = screen(&mut view, /*width*/ 140, /*height*/ 44); + assert!(partial.contains("Skills could not be loaded")); + press(&mut view, KeyCode::Char('3')); + assert!(screen(&mut view, /*width*/ 140, /*height*/ 44).contains("186 calls")); + insta::assert_snapshot!(format!("{empty}\n{missing}\n{partial}")); +} + +#[test] +fn analytics_tool_names_preserve_acronyms_and_distinct_namespaces() { + let labels = [ + "Pr Reviewer", + "Slack Cli", + "Squatch: Squatch", + "Openai Docs", + "User Writing: Writing Style", + ]; + assert_eq!( + labels.map(tool_panel::tool_name), + [ + "PR Reviewer", + "Slack CLI", + "Squatch", + "OpenAI Docs", + "User Writing: Writing Style" + ] + .map(str::to_owned), + ); +} diff --git a/codex-rs/tui/src/analytics_tests.rs b/codex-rs/tui/src/analytics_tests.rs index f1e5b3a6f5..17b1611d5a 100644 --- a/codex-rs/tui/src/analytics_tests.rs +++ b/codex-rs/tui/src/analytics_tests.rs @@ -1,7 +1,215 @@ -//! Credit formatting preserves tiny adjustments and integer precision. +//! Dashboard layout, local detail, shared ranges, grouping, and scroll/focus retention. -use super::data; +use crate::analytics::sections::Section; +#[path = "analytics/dashboard_tests.rs"] +mod dashboard; + +#[path = "analytics/account_layout_tests.rs"] +mod account_layout; + +#[path = "analytics/tool_panel_tests.rs"] +mod tools_panel; + +#[path = "analytics/styles_tests.rs"] +mod terminal_styles; + +use super::*; +use crate::keymap::RuntimeKeymap; +use crossterm::event::KeyModifiers; use pretty_assertions::assert_eq; +use ratatui::Terminal; +use ratatui::backend::TestBackend; + +fn press(view: &mut AnalyticsView, code: KeyCode) { + view.handle_key(KeyEvent::new(code, KeyModifiers::NONE)); +} + +fn screen(view: &mut AnalyticsView, width: u16, height: u16) -> String { + let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); + terminal + .draw(|frame| view.render(frame.area(), frame.buffer_mut())) + .unwrap(); + terminal.backend().to_string() +} + +#[test] +fn analytics_overview() { + let mut view = fixture::view(models::AccountKind::Consumer); + press(&mut view, KeyCode::Char('z')); + let wide = screen(&mut view, /*width*/ 120, /*height*/ 60); + for section in view.visible_sections() { + assert!(wide.contains(view.section_title(*section))); + } + insta::assert_snapshot!(wide); +} + +#[test] +fn analytics_narrow_charts_and_small_terminals_keep_valid_cursors() { + let mut view = fixture::view(models::AccountKind::Consumer); + press(&mut view, KeyCode::Char('z')); + for section in ['1', '2', '3'] { + press(&mut view, KeyCode::Char(section)); + press(&mut view, KeyCode::Char('r')); + fixture::seed_reports(&mut view); + press(&mut view, KeyCode::Home); + for _ in 0..30 { + screen(&mut view, /*width*/ 28, /*height*/ 12); + press(&mut view, KeyCode::Right); + } + assert_eq!(view.sections[view.section].cursor, 29); + } + for _ in 0..2 { + press(&mut view, KeyCode::Char('z')); + for width in [1, 4, 32, 107, 108] { + screen(&mut view, width, /*height*/ 1); + } + } + let empty = ratatui::layout::Rect::default(); + view.render(empty, &mut ratatui::buffer::Buffer::empty(empty)); + press(&mut view, KeyCode::Char('4')); + let tools = screen(&mut view, /*width*/ 58, /*height*/ 20); + insta::assert_snapshot!(tools); +} + +#[tokio::test] +async fn analytics_credits_grouping_preserves_selected_day_and_total() { + let server = test_support::server().await; + let (_home, _app_server, mut view) = client::tests::connected_view(&server, "business").await; + test_support::settle(&mut view).await; + press(&mut view, KeyCode::Char('1')); + press(&mut view, KeyCode::Left); + let before = view.sections[Section::Credits] + .history + .ready() + .unwrap() + .data + .iter() + .map(|day| day.total) + .collect::>(); + press(&mut view, KeyCode::Char('g')); + view.end_date = fixture::END_DATE; + fixture::seed_reports(&mut view); + let picker = screen(&mut view, /*width*/ 85, /*height*/ 40); + press(&mut view, KeyCode::Esc); + for group in [2, 4, 5, 0] { + press(&mut view, KeyCode::Char('g')); + press(&mut view, KeyCode::Down); + press(&mut view, KeyCode::Enter); + test_support::settle(&mut view).await; + assert_eq!( + ( + view.sections[Section::Credits].group, + view.sections[Section::Credits].cursor + ), + (group, 5) + ); + assert_eq!( + view.sections[Section::Credits] + .history + .ready() + .unwrap() + .data + .iter() + .map(|day| day.total) + .collect::>(), + before + ); + } + fixture::seed_reports(&mut view); + insta::assert_snapshot!(format!( + "{picker}\n{}", + screen(&mut view, /*width*/ 85, /*height*/ 40) + )); +} + +#[tokio::test] +async fn analytics_loading_errors_and_empty_reports_stay_independent() { + let mut view = AnalyticsView::new(RuntimeKeymap::defaults().list); + let (send, receive) = tokio::sync::oneshot::channel(); + view.sections[Section::Usage].history = Load::start( + async move { receive.await.unwrap() }, + FrameRequester::test_dummy(), + ); + view.sections[Section::Credits].history = + Load::Error("Couldn't load credits. Press R to retry.".into()); + let mut empty = fixture::history(/*report*/ 2, /*range*/ 0, /*group*/ 2); + empty.data.clear(); + view.sections[Section::Activity].history = Load::Ready(empty); + let panels = [ + Section::Usage, + Section::Plugins, + Section::Credits, + Section::Activity, + ] + .into_iter() + .map(|section| { + view.panel(section, /*width*/ 54, /*chart_height*/ 2) + .lines + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n") + }) + .collect::>() + .join("\n\n"); + insta::assert_snapshot!(panels); + // Refresh drops pending work before a stale result can populate the retained view. + view.refresh(); + tokio::task::yield_now().await; + assert!(send.is_closed()); + assert!(matches!( + view.sections[Section::Usage].history, + Load::Unavailable + )); +} + +#[test] +fn analytics_preserves_full_denominators_and_signed_small_credits() { + use crate::analytics::models::AccountAnalyticsDay; + use crate::analytics::models::AccountAnalyticsValue; + let mut view = fixture::view(models::AccountKind::Consumer); + press(&mut view, KeyCode::Char('z')); + let mut history = fixture::history(/*report*/ 0, /*range*/ 0, /*group*/ 3); + history.data = vec![AccountAnalyticsDay { + date: "2026-09-02".parse().unwrap(), + total: 100.0, + values: vec![AccountAnalyticsValue { + key: "goal".into(), + label: "Goals".into(), + value: 30.0, + }], + }]; + view.sections[Section::Usage].history = Load::Ready(history.clone()); + view.sections[Section::Usage].group = 3; + view.sections[Section::Usage].cursor = 6; + let task = view + .panel(Section::Usage, /*width*/ 54, /*chart_height*/ 2) + .lines; + assert!( + task.iter() + .any(|line| line.to_string().contains("Goals") && line.to_string().contains("30.0%")) + ); + assert!( + task.iter() + .any(|line| line.to_string() == "Sep 2 · 100 usage units") + ); + history.unit = crate::analytics::models::AccountAnalyticsUnit::Credits; + history.data[0].total = -0.000004; + history.data[0].values[0].value = -0.000004; + history.data[0].values[0].label = "CLI".into(); + view.sections[Section::Credits].history = Load::Ready(history); + view.sections[Section::Credits].cursor = 6; + let credits = view + .panel(Section::Credits, /*width*/ 54, /*chart_height*/ 2) + .lines; + insta::assert_snapshot!( + task.into_iter() + .chain(credits) + .map(|line| line.to_string()) + .collect::>() + .join("\n") + ); +} #[test] fn analytics_credit_display_retains_integer_precision() { @@ -18,18 +226,6 @@ fn analytics_credit_display_retains_integer_precision() { ] .map(str::to_string) ); - assert_eq!( - [0, -4, 999_999_999, 19_350_041_555, i64::MIN, i64::MAX].map(data::credits), - [ - "0.00", - "-0.000004", - "1,000.00", - "19,350.04", - "-9,223,372,036,854.78", - "9,223,372,036,854.78" - ] - .map(str::to_string) - ); assert_eq!( [ 12_746.441206, @@ -53,3 +249,361 @@ fn analytics_credit_display_retains_integer_precision() { .map(str::to_string) ); } + +#[test] +fn analytics_dates_distinguish_zero_missing_and_exact_details() { + let mut view = fixture::view(models::AccountKind::Enterprise); + let mut history = fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0); + history.data[1].total = 12_746.441206; + history.data[1].values[0].value = 12_746.441206; + history.data[1].values.truncate(/*len*/ 1); + history.data.remove(/*index*/ 2); + view.sections[Section::Credits].history = Load::Ready(history.clone()); + press(&mut view, KeyCode::Char('1')); + press(&mut view, KeyCode::Home); + let mut screens = Vec::new(); + for keys in [ + vec![], + vec![KeyCode::Right, KeyCode::Enter], + vec![KeyCode::Right], + ] { + for key in keys { + press(&mut view, key); + } + screens.push( + view.panel(Section::Credits, /*width*/ 54, /*chart_height*/ 3) + .lines + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"), + ); + } + assert!(screens[0].contains("No activity (0)")); + assert!(screens[1].contains("12746.441206 credits")); + assert!(screens[2].contains("Aug 29 · Not reported")); + for day in &mut history.data { + day.total = 0.0; + day.values.clear(); + } + view.sections[Section::Credits].history = Load::Ready(history.clone()); + screens.push( + view.panel(Section::Credits, /*width*/ 54, /*chart_height*/ 3) + .lines + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"), + ); + history.data.clear(); + view.sections[Section::Credits].history = Load::Ready(history); + screens.push( + view.panel(Section::Credits, /*width*/ 54, /*chart_height*/ 3) + .lines + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"), + ); + insta::assert_snapshot!(screens.join("\n\n")); +} + +#[test] +fn analytics_model_labels_and_tool_remainders_are_unambiguous() { + let mut view = fixture::view(models::AccountKind::Consumer); + press(&mut view, KeyCode::Char('z')); + view.model_names + .insert("GPT-5.6-Sol".into(), "Friendly model".into()); + let mut tools = fixture::history(/*report*/ 4, /*range*/ 0, /*group*/ 0); + for day in &mut tools.data { + day.values[1].label = "Other".into(); + } + view.sections[Section::Skills].history = Load::Ready(tools); + press(&mut view, KeyCode::Char('2')); + let overview = screen(&mut view, /*width*/ 140, /*height*/ 64); + assert!(overview.contains("Friendly model")); + assert!(overview.contains("Skills used")); + press(&mut view, KeyCode::Char('4')); + press(&mut view, KeyCode::Enter); + let skills = screen(&mut view, /*width*/ 100, /*height*/ 32); + assert!(skills.contains("Other")); + insta::assert_snapshot!(format!("{overview}\n{skills}")); +} + +#[test] +fn analytics_maximized_refunds_leave_room_for_both_bands() { + let mut view = fixture::view(models::AccountKind::Enterprise); + let mut history = fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0); + history.data[6].total = -0.000004; + history.data[6].values = vec![crate::analytics::models::AccountAnalyticsValue { + key: "CLI".into(), + label: "CLI".into(), + value: -0.000004, + }]; + view.sections[Section::Credits].history = Load::Ready(history); + press(&mut view, KeyCode::Char('1')); + let output = screen(&mut view, /*width*/ 100, /*height*/ 32); + assert!(output.contains("Sep 2 · -0.000004 credits")); + assert!( + output + .lines() + .any(|line| line.contains("● CLI") && line.contains("-0.000004")) + ); + insta::assert_snapshot!(output); +} + +#[test] +fn analytics_day_navigation_keeps_geometry_and_legend() { + let mut view = fixture::view(models::AccountKind::Enterprise); + let mut history = fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0); + history.data[1].values.truncate(/*len*/ 1); + history.data[1].total = history.data[1].values[0].value; + history.data.remove(/*index*/ 2); + view.sections[Section::Credits].history = Load::Ready(history); + press(&mut view, KeyCode::Char('1')); + let mut snapshots = Vec::new(); + for width in [58, 160] { + let mut terminal = Terminal::new(TestBackend::new(width, /*height*/ 38)).unwrap(); + for expanded in [false, true] { + press(&mut view, KeyCode::Home); + if expanded { + press(&mut view, KeyCode::Enter); + } + let mut previous = None; + for _ in 0..7 { + terminal + .draw(|frame| view.render(frame.area(), frame.buffer_mut())) + .unwrap(); + let buffer = terminal.backend().buffer(); + let bars = buffer + .content + .iter() + .enumerate() + .filter(|(_, cell)| { + matches!(cell.symbol(), "█" | "▁" | "▂" | "▃" | "▄" | "▅" | "▆" | "▇") + }) + .map(|(index, cell)| (index, cell.clone())) + .collect::>(); + let output = terminal.backend().to_string(); + let marker_row = output.lines().position(|line| line.contains('▲')).unwrap(); + let table_row = output + .lines() + .position(|line| line.contains("Product")) + .unwrap(); + let geometry = (bars, marker_row, table_row); + if let Some(previous) = &previous { + assert_eq!(&geometry, previous); + } + previous = Some(geometry); + assert!(output.contains("● CLI") && output.contains("● Desktop app")); + if view.sections[Section::Credits].cursor == 2 && !expanded { + snapshots.push(output); + } + press(&mut view, KeyCode::Right); + } + if expanded { + press(&mut view, KeyCode::Enter); + } + } + } + insta::assert_snapshot!(snapshots.join("\n")); +} + +#[test] +fn analytics_series_rank_by_range_and_keep_calendar_ticks_stable() { + let mut view = fixture::view(models::AccountKind::Consumer); + screen(&mut view, /*width*/ 100, /*height*/ 38); + press(&mut view, KeyCode::Char('r')); + let mut history = fixture::history(/*report*/ 0, /*range*/ 1, /*group*/ 2); + for day in &mut history.data { + day.values[3].value += 500.0; + day.total += 500.0; + } + view.sections[Section::Usage].history = Load::Ready(history.clone()); + screen(&mut view, /*width*/ 100, /*height*/ 38); + let legend = view + .history_lines(Section::Usage, /*width*/ 96, /*height*/ 10) + .lines + .into_iter() + .filter(|line| line.spans.first().is_some_and(|span| span.content == "● ")) + .take(/*n*/ 3) + .map(|line| (line.spans[1].content.to_string(), line.spans[0].style.fg)) + .collect::>(); + assert_eq!( + legend, + ["Other", "GPT-5.6-Sol", "GPT-5.6-Terra"] + .into_iter() + .zip(styles::series_colors()) + .map(|(label, color)| (label.to_string(), Some(color))) + .collect::>() + ); + let series = render::categories(&history); + history.data[8].total = 0.4; + for value in &mut history.data[8].values { + value.value = 0.1; + } + let days = history + .data + .iter() + .map(|day| (day.date, Some(day))) + .collect::>(); + let mut ticks = None; + for cursor in [0, 7, 8, 25, 29] { + let chart = plot::chart( + &days, + &series, + cursor, + /*width*/ 96, + /*height*/ 10, + history.unit, + ); + let labels = chart.lines.last().unwrap().to_string(); + if let Some(ticks) = &ticks { + assert_eq!(&labels, ticks); + } + ticks = Some(labels); + assert_eq!( + chart + .lines + .iter() + .filter(|line| line.to_string().contains('▲')) + .count(), + 1 + ); + } + let crowded = plot::chart( + &days, + &series, + /*cursor*/ 8, + /*width*/ 54, + /*height*/ 10, + history.unit, + ) + .lines + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + assert!(crowded.contains('┊')); + assert!(crowded.contains("0.4")); + insta::assert_snapshot!(crowded); +} + +#[test] +fn analytics_usage_ranks_its_own_models_after_turns_load() { + let mut view = fixture::view(models::AccountKind::Consumer); + screen(&mut view, /*width*/ 100, /*height*/ 38); + press(&mut view, KeyCode::Char('g')); + press(&mut view, KeyCode::Down); + press(&mut view, KeyCode::Enter); + let mut history = fixture::history(/*report*/ 0, /*range*/ 0, /*group*/ 2); + for day in &mut history.data { + for label in ["Model E", "Model F"] { + day.values + .push(crate::analytics::models::AccountAnalyticsValue { + key: label.into(), + label: label.into(), + value: 0.0, + }); + } + for value in &mut day.values { + value.key = format!("remote-{}", value.key); + value.label = format!("Remote {}", value.label); + } + } + history.data[6].total = 100.0; + for (value, amount) in history.data[6] + .values + .iter_mut() + .zip([91.1, 0.0, 6.8, 2.1, 0.0, 0.0]) + { + value.value = amount; + } + view.sections[Section::Usage].history = Load::Ready(history); + let selected_day = screen(&mut view, /*width*/ 100, /*height*/ 38); + for _ in 0..7 { + screen(&mut view, /*width*/ 100, /*height*/ 38); + let expected = if view.sections[Section::Usage].cursor == 6 { + [ + ("Remote GPT-5.6-Sol", 0), + ("Remote GPT-5.5", 2), + ("Remote Other", 3), + ] + } else { + [ + ("Remote GPT-5.6-Sol", 0), + ("Remote GPT-5.6-Terra", 1), + ("Remote GPT-5.5", 2), + ] + } + .into_iter() + .map(|(label, color)| (label.to_string(), Some(styles::series_colors()[color]))) + .collect::>(); + let legend = view + .history_lines(Section::Usage, /*width*/ 96, /*height*/ 10) + .lines + .into_iter() + .filter(|line| line.spans.first().is_some_and(|span| span.content == "● ")) + .take(/*n*/ 3) + .map(|line| (line.spans[1].content.to_string(), line.spans[0].style.fg)) + .collect::>(); + assert_eq!(legend, expected); + press(&mut view, KeyCode::Left); + } + insta::assert_snapshot!(selected_day); +} + +#[test] +fn analytics_empty_turns_keep_geometry_and_disable_details() { + let mut view = fixture::view(models::AccountKind::Consumer); + let mut history = fixture::history(/*report*/ 2, /*range*/ 0, /*group*/ 2); + history.data[5].total = 0.0; + for value in &mut history.data[5].values { + value.value = 0.0; + } + history.data.remove(/*index*/ 6); + view.sections[Section::Activity].history = Load::Ready(history); + press(&mut view, KeyCode::Char('2')); + let mut snapshots = Vec::new(); + for width in [58, 100] { + view.sections[Section::Activity].cursor = 4; + let populated = screen(&mut view, width, /*height*/ 38); + assert!(populated.contains("reported messages")); + assert!(populated.contains("details")); + let marker_row = populated.lines().position(|line| line.contains('▲')); + for cursor in [5, 6] { + view.sections[Section::Activity].cursor = cursor; + let empty = screen(&mut view, width, /*height*/ 38); + assert!(empty.contains(if cursor == 5 { + "No messages reported for" + } else { + "Data not reported for" + })); + assert!(!empty.contains("details")); + assert!(!empty.contains('●')); + assert_eq!( + empty.lines().position(|line| line.contains('▲')), + marker_row + ); + press(&mut view, KeyCode::Enter); + assert_eq!(view.sections[Section::Activity].detail, None); + snapshots.push(empty); + } + view.sections[Section::Activity].cursor = 4; + press(&mut view, KeyCode::Enter); + assert_eq!(view.sections[Section::Activity].detail, Some(4)); + let expanded = screen(&mut view, width, /*height*/ 38); + let marker_row = expanded.lines().position(|line| line.contains('▲')); + press(&mut view, KeyCode::Right); + let empty = screen(&mut view, width, /*height*/ 38); + assert_eq!( + empty.lines().position(|line| line.contains('▲')), + marker_row + ); + let details = view.sections[Section::Activity].detail; + press(&mut view, KeyCode::Enter); + assert_eq!(view.sections[Section::Activity].detail, details); + view.sections[Section::Activity].detail = None; + } + insta::assert_snapshot!(snapshots.join("\n")); +} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 97303cb72a..5abbb01a0a 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -572,8 +572,9 @@ pub(crate) struct App { last_thread_usage_status_cell: Option, pub(crate) pending_thread_usage_history_refresh: bool, - // Pager overlay state (Transcript or Static like Diff) + // Alternate-screen overlays: transcript, diff, and analytics. pub(crate) overlay: Option, + pub(crate) retained_analytics: Option>, pub(crate) deferred_history_lines: Vec, has_emitted_history_lines: bool, transcript_reflow: TranscriptReflowState, diff --git a/codex-rs/tui/src/app/app_server_events.rs b/codex-rs/tui/src/app/app_server_events.rs index e3d54b8e75..357fca8f72 100644 --- a/codex-rs/tui/src/app/app_server_events.rs +++ b/codex-rs/tui/src/app/app_server_events.rs @@ -235,6 +235,12 @@ impl App { self.agents_overview.usage_disabled = false; self.repaint_agents_overview(); self.chat_widget.cyber_policy_notice = Default::default(); + if let Some(crate::pager_overlay::Overlay::Analytics(view)) = &mut self.overlay { + view.refresh(); + } + if let Some(view) = &mut self.retained_analytics { + view.cancel_loads(); + } self.rate_limit_hard_stop_generation = self.rate_limit_hard_stop_generation.wrapping_add(1); self.rate_limit_refresh_state.invalidate_recovery(); diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 27e9253821..ec26f4ccec 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1609,6 +1609,21 @@ impl App { self.chat_widget .add_token_activity_output(crate::chatwidget::TokenActivityView::Daily); } + AppEvent::OpenAnalytics => { + tui.enter_alt_screen()?; + let mut view = self.retained_analytics.take().unwrap_or_else(|| { + Box::new(crate::analytics::AnalyticsView::new(self.keymap.list.clone())) + }); + view.keymap = self.keymap.list.clone(); + view.open( + app_server.request_handle(), + tui.frame_requester(), + self.model_catalog.try_list_models()?, + std::sync::Arc::new(self.config.clone()), + ); + self.overlay = Some(Overlay::Analytics(view)); + tui.frame_requester().schedule_frame(); + } AppEvent::OpenRateLimitResetCredits => { let request_id = self.chat_widget.show_rate_limit_reset_loading_popup(); self.refresh_rate_limits( diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index 40fe3abc40..6bfa19b3a1 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -755,6 +755,7 @@ See the Codex keymap documentation for supported actions and examples." last_thread_usage_status_cell: None, pending_thread_usage_history_refresh: false, overlay: None, + retained_analytics: None, deferred_history_lines: Vec::new(), has_emitted_history_lines: false, transcript_reflow: TranscriptReflowState::default(), diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index 62fbdbe72b..ae6be36928 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -41,6 +41,7 @@ pub(super) async fn make_test_app() -> App { last_thread_usage_status_cell: None, pending_thread_usage_history_refresh: false, overlay: None, + retained_analytics: None, deferred_history_lines: Vec::new(), has_emitted_history_lines: false, transcript_reflow: TranscriptReflowState::default(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index afa25de17c..cfa991bd2b 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -5888,6 +5888,7 @@ async fn make_test_app() -> App { last_thread_usage_status_cell: None, pending_thread_usage_history_refresh: false, overlay: None, + retained_analytics: None, deferred_history_lines: Vec::new(), has_emitted_history_lines: false, transcript_reflow: TranscriptReflowState::default(), @@ -5987,6 +5988,7 @@ pub(super) async fn make_test_app_with_channels() -> ( last_thread_usage_status_cell: None, pending_thread_usage_history_refresh: false, overlay: None, + retained_analytics: None, deferred_history_lines: Vec::new(), has_emitted_history_lines: false, transcript_reflow: TranscriptReflowState::default(), diff --git a/codex-rs/tui/src/app/tests/transcript_composer.rs b/codex-rs/tui/src/app/tests/transcript_composer.rs index 7f71480da9..7e72da277e 100644 --- a/codex-rs/tui/src/app/tests/transcript_composer.rs +++ b/codex-rs/tui/src/app/tests/transcript_composer.rs @@ -1,6 +1,7 @@ //! Regression coverage for transcript viewer input, prompt selection, and restoration. //! //! The default-off feature must leave the existing viewer and its draft intact. +//! Analytics also preserves the composer and stays separate from transcript backtracking. use super::*; use crate::test_support::test_path_display; @@ -39,6 +40,40 @@ async fn press_key( Ok(()) } +#[tokio::test] +async fn analytics_preserves_the_draft_and_retains_the_view_on_reopen() -> Result<()> { + let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await; + let mut app_server = start_config_write_test_app_server(&app).await?; + let mut tui = crate::tui::test_support::make_test_tui()?; + app.chat_widget + .apply_external_edit("preserved draft".into()); + app.handle_event(&mut tui, &mut app_server, AppEvent::OpenAnalytics) + .await?; + for code in [ + KeyCode::Char('4'), + KeyCode::Enter, + KeyCode::Down, + KeyCode::Char('r'), + ] { + press_key(&mut app, &mut tui, &mut app_server, code).await?; + assert!(matches!(app.overlay, Some(Overlay::Analytics(_)))); + assert!(!app.backtrack.overlay_preview_active); + } + press_key(&mut app, &mut tui, &mut app_server, KeyCode::Char('q')).await?; + assert!(app.overlay.is_none()); + assert!(app.retained_analytics.is_some()); + app.handle_event(&mut tui, &mut app_server, AppEvent::OpenAnalytics) + .await?; + assert!(matches!(app.overlay, Some(Overlay::Analytics(_)))); + assert!(app.retained_analytics.is_none()); + press_key(&mut app, &mut tui, &mut app_server, KeyCode::Char('q')).await?; + assert_eq!( + app.chat_widget.composer_text_with_pending(), + "preserved draft" + ); + Ok(()) +} + #[tokio::test] async fn transcript_flag_off_preserves_viewer_and_backtracking() -> Result<()> { let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index bb6189bdd3..e71d75b9e9 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -84,7 +84,7 @@ pub(crate) struct BacktrackSelection { } impl App { - /// Route overlay events while the transcript overlay is active. + /// Route overlay events, reserving backtracking for the transcript overlay. /// /// If backtrack preview is active, Esc / Left steps selection, Right steps forward, Enter /// confirms. Otherwise, Esc begins preview mode and all other events are forwarded to the @@ -95,6 +95,10 @@ impl App { app_server: &mut AppServerSession, event: TuiEvent, ) -> Result { + if !matches!(self.overlay, Some(Overlay::Transcript(_))) { + self.overlay_forward_event(tui, event)?; + return Ok(true); + } self.handle_legacy_transcript_event(tui, app_server, event) } @@ -159,7 +163,7 @@ impl App { tui.frame_requester().schedule_frame(); } - /// Close transcript overlay and restore normal UI. + /// Close the current overlay and restore normal UI, retaining Analytics navigation state. pub(crate) fn close_transcript_overlay(&mut self, tui: &mut tui::Tui) { let _ = tui.leave_alt_screen(); let was_backtrack = self.backtrack.overlay_preview_active; @@ -170,7 +174,10 @@ impl App { self.history_line_wrap_policy(), ); } - self.overlay = None; + if let Some(Overlay::Analytics(mut view)) = self.overlay.take() { + view.cancel_loads(); + self.retained_analytics = Some(view); + } if self.pending_thread_usage_history_refresh && let Err(err) = self.refresh_thread_usage_history_tail(tui) { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index e35bec3d70..40c4462564 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -681,6 +681,9 @@ pub(crate) enum AppEvent { /// Open the default token-activity view selected from the `/usage` menu. OpenTokenActivity, + /// Open the authenticated account analytics dashboard. + OpenAnalytics, + /// Open the reset-credit flow selected from the `/usage` menu. OpenRateLimitResetCredits, diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu.snap index 70aa225aaf..cc2fb465bb 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu.snap @@ -6,6 +6,7 @@ expression: "render_bottom_popup(&chat, 80)" View account usage or redeem an earned reset. › 1. Show usage View recent account token usage. - 2. Redeem usage limit reset You have 2 usage limit resets available. + 2. Explore analytics Browse account usage and activity. + 3. Redeem usage limit reset You have 2 usage limit resets available. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_before_reset_refresh.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_before_reset_refresh.snap index d4dc5deff1..ef503e1b9f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_before_reset_refresh.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_before_reset_refresh.snap @@ -6,6 +6,7 @@ expression: "render_bottom_popup(&chat, 80)" View account usage or redeem an earned reset. › 1. Show usage View recent account token usage. - 2. Redeem usage limit reset Check reset availability. + 2. Explore analytics Browse account usage and activity. + 3. Redeem usage limit reset Check reset availability. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_without_resets.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_without_resets.snap index 127e37f7a3..f2f78e64a3 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_without_resets.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__usage_command_menu_without_resets.snap @@ -6,6 +6,7 @@ expression: "render_bottom_popup(&chat, 80)" View account usage or redeem an earned reset. › 1. Show usage View recent account token usage. + 2. Explore analytics Browse account usage and activity. Redeem usage limit reset No usage limit resets available. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests/usage.rs b/codex-rs/tui/src/chatwidget/tests/usage.rs index 82c201764e..5ea2f1910d 100644 --- a/codex-rs/tui/src/chatwidget/tests/usage.rs +++ b/codex-rs/tui/src/chatwidget/tests/usage.rs @@ -13,6 +13,16 @@ use uuid::Uuid; const TEST_OVERLAY_VIEW_ID: &str = "usage-test-overlay"; +#[tokio::test] +async fn usage_menu_opens_analytics() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + set_chatgpt_auth(&mut chat); + chat.dispatch_command(SlashCommand::Usage); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics)); +} + fn reset_credits(available_count: i64) -> RateLimitResetCreditsSummary { RateLimitResetCreditsSummary { available_count, @@ -148,7 +158,7 @@ async fn usage_command_disables_reset_after_cached_zero_snapshot() { ); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics)); } #[tokio::test] @@ -175,6 +185,7 @@ async fn usage_menu_refresh_enables_newly_available_reset() { Ok(reset_credits(/*available_count*/ 1)), ); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits)); @@ -207,7 +218,7 @@ async fn usage_menu_refresh_failure_preserves_disabled_known_zero() { assert!(render_bottom_popup(&chat, /*width*/ 80).contains("No usage limit resets available.")); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics)); } #[tokio::test] @@ -256,6 +267,7 @@ async fn usage_command_can_check_reset_availability_before_startup_refresh_finis render_bottom_popup(&chat, /*width*/ 80) ); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits)); } @@ -268,6 +280,7 @@ async fn usage_command_can_check_reset_availability_for_workspace_accounts() { chat.dispatch_command(SlashCommand::Usage); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits)); @@ -285,6 +298,7 @@ async fn usage_menu_rate_limit_reset_entry_opens_reset_flow() { )); chat.dispatch_command(SlashCommand::Usage); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -703,7 +717,7 @@ async fn no_credit_outcome_disables_reset_entry_in_usage_menu() { chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics)); chat.available_rate_limit_reset_credits = Some(2); let consume_request_id = chat.show_rate_limit_reset_consuming_popup(); diff --git a/codex-rs/tui/src/chatwidget/usage.rs b/codex-rs/tui/src/chatwidget/usage.rs index bd7a2f434e..88b9150b7c 100644 --- a/codex-rs/tui/src/chatwidget/usage.rs +++ b/codex-rs/tui/src/chatwidget/usage.rs @@ -45,7 +45,7 @@ impl ChatWidget { (false, "No usage limit resets available.".to_string()) } }; - SelectionViewParams { + let mut params = SelectionViewParams { view_id: Some(USAGE_MENU_VIEW_ID), title: Some("Usage".to_string()), subtitle: Some("View account usage or redeem an earned reset.".to_string()), @@ -72,7 +72,20 @@ impl ChatWidget { }, ], ..Default::default() + }; + if self.has_chatgpt_account { + params.items.insert( + /*index*/ 1, + SelectionItem { + name: "Explore analytics".to_string(), + description: Some("Browse account usage and activity.".to_string()), + actions: vec![Box::new(|tx| tx.send(AppEvent::OpenAnalytics))], + dismiss_on_select: true, + ..Default::default() + }, + ); } + params } pub(crate) fn finish_usage_menu_rate_limit_refresh( diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index f8e4158698..20989980ad 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -102,11 +102,6 @@ pub(crate) use codex_app_server_client::legacy_core; pub(crate) use worktree_startup::ManagedTuiWorktree; mod additional_dirs; -// Removed when the final stack layer connects Analytics to /usage. -#[expect( - dead_code, - reason = "analytics is being introduced in independently tested stack layers" -)] mod analytics; mod app; mod app_backtrack; diff --git a/codex-rs/tui/src/pager_overlay.rs b/codex-rs/tui/src/pager_overlay.rs index 09b9e6114b..107ea3da19 100644 --- a/codex-rs/tui/src/pager_overlay.rs +++ b/codex-rs/tui/src/pager_overlay.rs @@ -58,6 +58,7 @@ use scrolling::render_offset_content; pub(crate) enum Overlay { Transcript(TranscriptOverlay), Static(StaticOverlay), + Analytics(Box), } impl Overlay { @@ -85,6 +86,7 @@ impl Overlay { match self { Overlay::Transcript(o) => o.handle_event(tui, event), Overlay::Static(o) => o.handle_event(tui, event), + Overlay::Analytics(o) => o.handle_event(tui, event), } } @@ -92,6 +94,7 @@ impl Overlay { match self { Overlay::Transcript(o) => o.is_done(), Overlay::Static(o) => o.is_done(), + Overlay::Analytics(o) => o.is_done, } } } diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_credits_grouping_preserves_selected_day_and_total.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_credits_grouping_preserves_selected_day_and_total.snap new file mode 100644 index 0000000000..024edb44b6 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_credits_grouping_preserves_selected_day_and_total.snap @@ -0,0 +1,85 @@ +--- +source: tui/src/analytics_tests.rs +expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))" +--- +" Analytics · Live account · analytics@example.test · account-a " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " +" " +" ▎ Credits usage history By product " +" ───────────────────────────────────────────────────────────────────────────────── " +" › Product " +" Model " +" Speed " +" Reasoning " +" 431.00 credits " +" " +" 150 " +" " +" 109.00 " +" ▁▁▁▁▁▁▁ " +" ███████ " +" ███████ " +" ▇▇▇▇▇▇▇ ███████ " +" 75 ███████ ███████ ███████ " +" ▂▂▂▂▂▂▂ ███████ ███████ ███████ ▇▇▇▇▇▇▇ " +" ███████ ███████ ███████ ███████ ███████ " +" ▆▆▆▆▆▆▆ ███████ ███████ ███████ ███████ ███████ " +" ███████ ███████ ███████ ███████ ███████ ███████ " +" ███████ ███████ ███████ ███████ ███████ ███████ " +" 0 ███████ ███████ ███████ ███████ ███████ ███████ " +" 0 ──────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 1 · 109.00 credits " +" Product Credits " +" ● CLI 39.00 " +" ● Desktop app 38.00 " +" ● IDE extension 18.00 " +" ● Web 14.00 " +" " +" ↑/↓ group · enter select · esc cancel " + +" Analytics · Live account · analytics@example.test · account-a " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " +" " +" ▎ Credits usage history By product " +" ───────────────────────────────────────────────────────────────────────────────── " +" 431.00 credits " +" " +" 150 " +" " +" " +" 109.00 " +" ▂▂▂▂▂▂▂ " +" ███████ " +" ███████ " +" ▅▅▅▅▅▅▅ ███████ " +" 75 ███████ ▄▄▄▄▄▄▄ ███████ " +" ███████ ███████ ███████ ▂▂▂▂▂▂▂ " +" ▃▃▃▃▃▃▃ ███████ ███████ ███████ ███████ " +" ███████ ███████ ███████ ███████ ███████ " +" ▅▅▅▅▅▅▅ ███████ ███████ ███████ ███████ ███████ " +" ███████ ███████ ███████ ███████ ███████ ███████ " +" ███████ ███████ ███████ ███████ ███████ ███████ " +" ███████ ███████ ███████ ███████ ███████ ███████ " +" 0 ███████ ███████ ███████ ███████ ███████ ███████ " +" 0 ──────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 1 · 109.00 credits " +" Product Credits " +" ● CLI 39.00 " +" ● Desktop app 38.00 " +" ● IDE extension 18.00 " +" ● Web 14.00 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_dates_distinguish_zero_missing_and_exact_details.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_dates_distinguish_zero_missing_and_exact_details.snap new file mode 100644 index 0000000000..6bc7046da5 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_dates_distinguish_zero_missing_and_exact_details.snap @@ -0,0 +1,73 @@ +--- +source: tui/src/analytics_tests.rs +expression: "screens.join(\"\\n\\n\")" +--- +▎ Credits usage history By product +────────────────────────────────────────────────────── +13,079.44 credits + + 15,000 ▄▄▄▄ + ████ + 0.00 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ + 0 ────────────────────────────────────────────── + ▲ + 27 28 29 30 31 1 2 + +────────────────────────────────────────────────────── +Aug 27 · No activity (0) +Product Credits +● Desktop app 0.00 +● CLI 0.00 +● IDE extension 0.00 +● Web 0.00 + +▎ Credits usage history By product +────────────────────────────────────────────────────── +13,079.44 credits + 12,746.44 + 15,000 ▄▄▄▄ + ████ + 0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ + 0 ────────────────────────────────────────────── + ▲ + 27 28 29 30 31 1 2 + +────────────────────────────────────────────────────── +Aug 28 · 12746.441206 credits +Product Credits +● Desktop app 12746.441206 +● CLI 0 +● IDE extension 0 +● Web 0 + +▎ Credits usage history By product +────────────────────────────────────────────────────── +13,079.44 credits + + 15,000 ▄▄▄▄ + ████ + 0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ + 0 ────────────────────────────────────────────── + ▲ + 27 28 29 30 31 1 2 + +────────────────────────────────────────────────────── +Aug 29 · Not reported +Product Credits +● Desktop app — +● CLI — +● IDE extension — +● Web — + +▎ Credits usage history By product +────────────────────────────────────────────────────── +0.00 credits +No activity reported in this range. + +────────────────────────────────────────────────────── +Aug 29 · Not reported +Product Credits + +▎ Credits usage history By product +────────────────────────────────────────────────────── +No data reported for this range. diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_day_navigation_keeps_geometry_and_legend.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_day_navigation_keeps_geometry_and_legend.snap new file mode 100644 index 0000000000..f98da9db1f --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_day_navigation_keeps_geometry_and_legend.snap @@ -0,0 +1,81 @@ +--- +source: tui/src/analytics_tests.rs +expression: "snapshots.join(\"\\n\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [Credits usage history] Token usage history Plugins " +" called Skills used " +" " +" ▎ Credits usage history By product " +" ────────────────────────────────────────────────────── " +" 347.00 credits " +" " +" 150 " +" " +" " +" ▅▅▅▅ " +" ████ " +" ▆▆▆▆ ████ " +" 75 ████ ████ ████ " +" ████ ████ ████ ████ " +" ████ ████ ████ ████ " +" ████ ████ ████ ████ " +" ████ ████ ████ ████ " +" 0 ████ — ████ ████ ████ ████ " +" 0 ───────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ────────────────────────────────────────────────────── " +" Aug 29 · Not reported " +" Product Credits " +" ● Desktop app — " +" ● CLI — " +" ● IDE extension — " +" ● Web — " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter " +" details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q " +" close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " +" " +" ▎ Credits usage history By product " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" 347.00 credits " +" " +" 150 " +" " +" " +" " +" ▇▇▇▇▇▇▇▇▇▇▇▇ " +" ████████████ " +" ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ " +" 75 ████████████ ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ " +" ████████████ ████████████ ████████████ ▃▃▃▃▃▃▃▃▃▃▃▃ " +" ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ " +" ████████████ ████████████ ████████████ ████████████ " +" ▃▃▃▃▃▃▃▃▃▃▃▃ ████████████ ████████████ ████████████ ████████████ " +" 0 ████████████ — ████████████ ████████████ ████████████ ████████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Aug 29 · Not reported " +" Product Credits " +" ● Desktop app — " +" ● CLI — " +" ● IDE extension — " +" ● Web — " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_empty_turns_keep_geometry_and_disable_details.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_empty_turns_keep_geometry_and_disable_details.snap new file mode 100644 index 0000000000..f20b0f11b7 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_empty_turns_keep_geometry_and_disable_details.snap @@ -0,0 +1,159 @@ +--- +source: tui/src/analytics_tests.rs +expression: "snapshots.join(\"\\n\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" Total usage history [Messages] Plugins called " +" Skills used " +" " +" ▎ Messages By model " +" ────────────────────────────────────────────────────── " +" 386 reported messages " +" " +" 150 " +" " +" " +" ▄▄▄▄ " +" ████ " +" ▅▅▅▅ ████ " +" 75 ████ ████ ████ " +" ▇▇▇▇ ████ ████ ████ ▅▅▅▅ " +" ████ ████ ████ ████ ████ " +" ████ ████ ████ ████ ████ " +" ████ ████ ████ ████ ████ " +" ████ ████ ████ ████ ████ 0 — " +" 0 ───────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ────────────────────────────────────────────────────── " +" No messages reported for Sep 1. " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · z dashboard · ←/→ day " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc " +" back · q close " +" " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" Total usage history [Messages] Plugins called " +" Skills used " +" " +" ▎ Messages By model " +" ────────────────────────────────────────────────────── " +" 386 reported messages " +" " +" 150 " +" " +" " +" ▄▄▄▄ " +" ████ " +" ▅▅▅▅ ████ " +" 75 ████ ████ ████ " +" ▇▇▇▇ ████ ████ ████ ▅▅▅▅ " +" ████ ████ ████ ████ ████ " +" ████ ████ ████ ████ ████ " +" ████ ████ ████ ████ ████ " +" ████ ████ ████ ████ ████ 0 — " +" 0 ───────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ────────────────────────────────────────────────────── " +" Data not reported for Sep 2. " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · z dashboard · ←/→ day " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc " +" back · q close " +" " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used " +" " +" ▎ Messages By model " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 386 reported messages " +" " +" 150 " +" " +" " +" " +" ▅▅▅▅▅▅▅▅ " +" ████████ " +" ▂▂▂▂▂▂▂▂ ████████ " +" 75 ████████ ████████ ▄▄▄▄▄▄▄▄ " +" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ▇▇▇▇▇▇▇▇ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ 0 — " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" No messages reported for Sep 1. " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · z dashboard · ←/→ day " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used " +" " +" ▎ Messages By model " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 386 reported messages " +" " +" 150 " +" " +" " +" " +" ▅▅▅▅▅▅▅▅ " +" ████████ " +" ▂▂▂▂▂▂▂▂ ████████ " +" 75 ████████ ████████ ▄▄▄▄▄▄▄▄ " +" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ▇▇▇▇▇▇▇▇ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ 0 — " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Data not reported for Sep 2. " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · z dashboard · ←/→ day " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_loading_errors_and_empty_reports_stay_independent.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_loading_errors_and_empty_reports_stay_independent.snap new file mode 100644 index 0000000000..3fcdfac7bb --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_loading_errors_and_empty_reports_stay_independent.snap @@ -0,0 +1,19 @@ +--- +source: tui/src/analytics_tests.rs +expression: panels +--- +▎ Total usage history By feature +────────────────────────────────────────────────────── +Loading… + + Plugins called +────────────────────────────────────────────────────── +No history has been reported. + + Credits usage history By product +────────────────────────────────────────────────────── +Couldn't load credits. Press R to retry. + + Messages By model +────────────────────────────────────────────────────── +No data reported for this range. diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_maximized_refunds_leave_room_for_both_bands.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_maximized_refunds_leave_room_for_both_bands.snap new file mode 100644 index 0000000000..14da46c094 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_maximized_refunds_leave_room_for_both_bands.snap @@ -0,0 +1,36 @@ +--- +source: tui/src/analytics_tests.rs +expression: output +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " +" " +" ▎ Credits usage history By product " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 367.00 credits " +" " +" 150 " +" ▂▂▂▂▂▂▂▂ ▇▇▇▇▇▇▇▇ " +" 75 ▄▄▄▄▄▄▄▄ ████████ ████████ ████████ " +" 0 ████████ ████████ ████████ ████████ ████████ " +" 0 ────────────────────────────────────────────────────────────────────────────────────────── " +" ▇▇▇▇▇▇▇▇ " +" -0.000004 " +" " +" -150 " +" " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · -0.000004 credits " +" Product Credits " +" ● CLI -0.000004 " +" ● Desktop app 0.00 " +" ● IDE extension 0.00 " +" ● Web 0.00 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_model_labels_and_tool_remainders_are_unambiguous.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_model_labels_and_tool_remainders_are_unambiguous.snap new file mode 100644 index 0000000000..3d625bc96a --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_model_labels_and_tool_remainders_are_unambiguous.snap @@ -0,0 +1,101 @@ +--- +source: tui/src/analytics_tests.rs +expression: "format!(\"{overview}\\n{skills}\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" ╭─ 1 Total usage history ──────────────────────────────────────╮ ╭─ ▸ 2 Messages ─────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ By feature │ │ By model │ " +" │ Total usage · relative units │ │ 574 reported messages │ " +" │ 86 │ │ │ " +" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ " +" │ █████ ▅▅▅▅▅ █████ ██████ │ │ 103 │ " +" │ █████ ▃▃▃▃▃ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ ▆▆▆▆▆▆ │ " +" │ 50 █████ █████ █████ █████ █████ ██████ │ │ 75 ▆▆▆▆▆ █████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ " +" │ █████ █████ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ " +" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ " +" │ █████ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ " +" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ───────────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ " +" │ ● Tasks 64.0% │ │ ● Friendly model 61 │ " +" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ 4 Skills used ──────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ 186 calls │ │ 54 uses │ " +" │ │ │ │ " +" │ 40 29 │ │ 20 │ " +" │ ▇▇▇▇▇ │ │ │ " +" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ " +" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ " +" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ " +" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " +" │ ● Slack 10 │ │ ● Testing 4 │ " +" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · enter/z maximize " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " + +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 54 uses " +" " +" 20 " +" " +" " +" 11 " +" 10 ▇▇▇▇▇▇▇▇▇ " +" ▄▄▄▄▄▄▄▄ ████████ ▄▄▄▄▄▄▄▄ ▁▁▁▁▁▁▁▁ █████████ " +" ████████ ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ▂▂▂▂▂▂▂▂ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" 0 ──────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 11 uses " +" Skill Uses " +" ● Testing 4 " +" ● Code review 3 " +" ● Other 3 " +" ● Debugging 1 " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_narrow_charts_and_small_terminals_keep_valid_cursors.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_narrow_charts_and_small_terminals_keep_valid_cursors.snap new file mode 100644 index 0000000000..916de7dfe2 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_narrow_charts_and_small_terminals_keep_valid_cursors.snap @@ -0,0 +1,24 @@ +--- +source: tui/src/analytics_tests.rs +expression: tools +--- +" Analytics · Live account " +" 7d [30d] · Aug 4–Sep 2, 2026 " +" " +" ╭─ ▸ 4 Skills used ──────────────────────────────────╮ " +" │ │ " +" │ 30d │ " +" │ 169 uses │ " +" │ 11 │ " +" │ 20 ▂▂ ▂▂ ▂▂ ▂▂ ▂▂ ▂▂ ▂▂ ▂ ▂▂ ▂▂ ▂▂ ▂▂ ▃▂ ▃▃ ▂▂ ▄ │ " +" │ 0 ────────────────────────────────────────────── │ " +" │ ▲ │ " +" │ Aug 4 Aug 11 Aug 18 Aug 25 Sep 2 │ " +" │ │ " +" │ Sep 2 · 11 uses │ " +" │ ● Testing 4 │ " +" │ ● Code review 3 │ " +" │ │ " +" tab/1–4 section · enter/z maximize " +" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q " +" close " diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_overview.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_overview.snap new file mode 100644 index 0000000000..f7f7cd87c0 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_overview.snap @@ -0,0 +1,64 @@ +--- +source: tui/src/analytics_tests.rs +expression: wide +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" ╭─ ▸ 1 Total usage history ────────────────────────────╮ ╭─ 2 Messages ───────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ By feature │ │ By model │ " +" │ Total usage · relative units │ │ 574 reported messages │ " +" │ 86 │ │ │ " +" │ 100 ▂▂▂▂ ████ │ │ 150 │ " +" │ ████ ▅▅▅▅ ████ ████ │ │ 103 │ " +" │ ████ ▃▃▃▃ ████ ████ ████ ████ │ │ ▇▇▇▇ ▆▆▆▆ │ " +" │ 50 ████ ████ ████ ████ ████ ████ │ │ 75 ▆▆▆▆ ████ ▄▄▄▄ ▇▇▇▇ ████ │ " +" │ ████ ████ ████ ████ ████ ████ │ │ ▇▇▇▇ ████ ████ ████ ▆▆▆▆ ████ ████ │ " +" │ ▃▃▃▃ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ " +" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ " +" │ 0 ─────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ " +" │ │ │ │ " +" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ " +" │ ● Tasks 64.0% │ │ ● GPT-5.6-Sol 61 │ " +" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " +" " +" ╭─ 3 Plugins called ─────────────────────────────────╮ ╭─ 4 Skills used ────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ 186 calls │ │ 54 uses │ " +" │ │ │ │ " +" │ 40 29 │ │ 20 │ " +" │ ▇▇▇▇ │ │ │ " +" │ ████ ████ ▄▄▄▄ ████ │ │ 11 │ " +" │ 20 ████ ████ ████ ████ ▆▆▆▆ ████ ████ │ │ 10 ▁▁▁▁ ▆▆▆▆ │ " +" │ ████ ████ ████ ████ ████ ████ ████ │ │ ▆▆▆▆ ████ ▆▆▆▆ ▃▃▃▃ ████ │ " +" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ▆▆▆▆ ████ │ " +" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ " +" │ 0 ──────────────────────────────────────────────── │ │ 0 ──────────────────────────────────────────────── │ " +" │ ▲ │ │ ▲ │ " +" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ " +" │ │ │ │ " +" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " +" │ ● Slack 10 │ │ ● Testing 4 │ " +" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " +" " +" " +" " +" " +" " +" " +" tab/1–4 section · enter/z maximize " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_preserves_full_denominators_and_signed_small_credits.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_preserves_full_denominators_and_signed_small_credits.snap new file mode 100644 index 0000000000..2d969be4b6 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_preserves_full_denominators_and_signed_small_credits.snap @@ -0,0 +1,32 @@ +--- +source: tui/src/analytics_tests.rs +expression: "task.into_iter().chain(credits).map(|line|\nline.to_string()).collect::>().join(\"\\n\")" +--- +▎ Total usage history By turn start +────────────────────────────────────────────────────── +Total usage · relative units +All usage by how each turn started + + 100 100 + — — — — — — ▄▄▄▄ + 0 ───────────────────────────────────────────────── + ▲ + Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 + +Sep 2 · 100 usage units +● Goals 30.0% + Credits usage history By product +────────────────────────────────────────────────────── +-0.000004 credits + + 0.000004 + — — — — — — + 0 ─────────────────────────────────────────── + ████ + -0.000004 ████ + -0.000004 + ▲ + 27 28 29 30 31 1 2 + +Sep 2 · -0.000004 credits +● CLI -0.000004 diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_series_rank_by_range_and_keep_calendar_ticks_stable.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_series_rank_by_range_and_keep_calendar_ticks_stable.snap new file mode 100644 index 0000000000..a8a0fb7e23 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_series_rank_by_range_and_keep_calendar_ticks_stable.snap @@ -0,0 +1,18 @@ +--- +source: tui/src/analytics_tests.rs +expression: crowded +--- + + 600 0.4 ▄▃ ▆ ▅█ ▆ + ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ ▅ ██ █ ██ █ + ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + 300 ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + ██ ██ █ ██ █ ▁█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █ + 0 ───────────────────────────────────────────────── + ▲ + Aug 4 Aug 11 Aug 18 Aug 25 Sep 2 diff --git a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_usage_ranks_its_own_models_after_turns_load.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_usage_ranks_its_own_models_after_turns_load.snap new file mode 100644 index 0000000000..04b4edf11a --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_usage_ranks_its_own_models_after_turns_load.snap @@ -0,0 +1,42 @@ +--- +source: tui/src/analytics_tests.rs +expression: selected_day +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used " +" " +" ▎ Total usage history By model " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" Total usage · relative units " +" Approximate · may be delayed up to 6 hours " +" 100 " +" 100 ████████ ▇▇▇▇▇▇▇▇ " +" ▅▅▅▅▅▅▅▅ ████████ ████████ " +" ████████ ▂▂▂▂▂▂▂▂ ████████ ████████ " +" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ " +" ████████ ▇▇▇▇▇▇▇▇ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" 50 ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 100 usage units " +" Model Share " +" ● Remote GPT-5.6-Sol 91.1% " +" ● Remote GPT-5.5 6.8% " +" ● Remote Other 2.1% " +" … 3 more 0.0% " +" " +" tab/1–4 section · z dashboard · ←/→ day · enter details " +" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "