From 9bd49c9dccc82491bb1ce5084f6fc6bba646d75b Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Tue, 15 Sep 2026 18:45:00 +0000 Subject: [PATCH] Add an account Summary tab to Analytics (#45769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed - Make Summary the initial Analytics tab, showing profile identity, token totals, streaks, activity insights, and most-used plugins and skills. - Move daily, weekly, and cumulative token activity charts into Summary. Replace the usage menu's separate views with “View analytics”; `/usage weekly` and the other explicit modes open Summary with the requested view, while reopening from the menu retains navigation. - Load account profiles independently of other reports, preserve missing values separately from zero, and reject responses when the active account changes. Refresh retries profile failures. - Adapt Summary to narrow terminals and compact large chart totals and axis labels. Show the account email without its ID in the Analytics header. - Flush queued transcript history before opening an overlay so startup output stays in the transcript. ## Testing Add profile decoding and account identity tests, Summary layout and navigation snapshots, refresh and cancellation coverage, and regression tests for retained navigation and transcript history when opening Analytics. GitOrigin-RevId: 6dbe6c4c98209000d8b4bc18a4e01b98fab5991a --- codex-rs/backend-client/src/client.rs | 1 + codex-rs/backend-client/src/client/profile.rs | 73 ++++ .../src/client/profile_tests.rs | 126 ++++++ codex-rs/backend-client/src/lib.rs | 6 + codex-rs/tui/src/analytics.rs | 24 +- .../tui/src/analytics/account_layout_tests.rs | 91 ++++- .../chart.rs => analytics/activity_chart.rs} | 211 ++-------- .../activity_chart}/palette.rs | 0 .../activity_chart}/palette_tests.rs | 0 .../activity_chart_tests.rs} | 93 +++-- codex-rs/tui/src/analytics/chart.rs | 17 +- .../tui/src/analytics/chat_panel_tests.rs | 8 +- codex-rs/tui/src/analytics/client.rs | 10 +- codex-rs/tui/src/analytics/client_tests.rs | 5 +- codex-rs/tui/src/analytics/compact_tests.rs | 48 +++ .../tui/src/analytics/connection_tests.rs | 237 +++++++++++ codex-rs/tui/src/analytics/dashboard.rs | 3 +- codex-rs/tui/src/analytics/dashboard_tests.rs | 12 +- codex-rs/tui/src/analytics/data.rs | 21 + codex-rs/tui/src/analytics/data_tests.rs | 36 ++ codex-rs/tui/src/analytics/panels.rs | 5 +- codex-rs/tui/src/analytics/plot.rs | 17 +- codex-rs/tui/src/analytics/plot/painting.rs | 2 +- codex-rs/tui/src/analytics/plot_tests.rs | 25 +- .../src/analytics/profile_identity_tests.rs | 28 ++ codex-rs/tui/src/analytics/render.rs | 42 +- codex-rs/tui/src/analytics/sections.rs | 28 +- ..._connection_tests__account_plan_error.snap | 45 +++ ...connection_tests__local_profile_error.snap | 52 +++ ...ection_tests__local_profile_recovered.snap | 52 +++ ...ible_and_closing_aborts_pending_loads.snap | 2 +- ...s_unknowns_and_resets_on_new_snapshot.snap | 11 +- ...escape_closes_with_retained_expansion.snap | 91 +++-- ..._axes_are_hidden_instead_of_truncated.snap | 162 ++++---- ...outs_show_reports_and_wrap_navigation.snap | 333 +++++++++------- ...s_chats_display_only_supplied_dollars.snap | 14 +- ...layout__business_tokens_filter_models.snap | 48 +-- ...ary_available_without_billing_reports.snap | 44 +++ ...plans_hide_top_chats_and_skip_loading.snap | 33 +- ...down_ranks_nonzero_groups_and_reflows.snap | 14 +- ...l_indent_and_counts_missing_estimates.snap | 5 +- ...chats_range_controls_and_visible_rows.snap | 12 +- ...etails_before_they_leave_the_viewport.snap | 5 +- ...tles_are_hidden_in_panel_and_overview.snap | 49 ++- ...adlines_and_axes_compact_large_values.snap | 96 +++++ ...er_refresh__consumer_chat_all_metrics.snap | 5 +- ...fresh__consumer_chat_available_limits.snap | 7 +- ...ing_rows_and_only_offer_known_metrics.snap | 7 +- ...align_and_keep_stable_summary_heights.snap | 133 ++++--- ...s__summary__summary_cumulative_narrow.snap | 64 +++ ...s__tests__summary__summary_daily_wide.snap | 48 +++ ...ics__tests__summary__summary_overview.snap | 68 ++++ ...tics__tests__summary__summary_partial.snap | 44 +++ ...ics__tests__summary__summary_scrolled.snap | 22 ++ ...ytics__tests__summary__summary_weekly.snap | 48 +++ ...nal_styles__analytics_dark_business_1.snap | 90 ++--- ...nal_styles__analytics_dark_business_2.snap | 6 +- ...nal_styles__analytics_dark_business_3.snap | 70 ++-- ...nal_styles__analytics_dark_business_4.snap | 6 +- ...nal_styles__analytics_dark_business_6.snap | 6 +- ...nal_styles__analytics_dark_business_8.snap | 98 +++++ ...nal_styles__analytics_dark_consumer_1.snap | 7 +- ...nal_styles__analytics_dark_consumer_2.snap | 7 +- ...nal_styles__analytics_dark_consumer_4.snap | 7 +- ...nal_styles__analytics_dark_consumer_5.snap | 7 +- ...nal_styles__analytics_dark_consumer_6.snap | 7 +- ...nal_styles__analytics_dark_consumer_8.snap | 98 +++++ ...al_styles__analytics_light_business_1.snap | 90 ++--- ...al_styles__analytics_light_business_2.snap | 6 +- ...al_styles__analytics_light_business_3.snap | 70 ++-- ...al_styles__analytics_light_business_4.snap | 6 +- ...al_styles__analytics_light_business_6.snap | 6 +- ...al_styles__analytics_light_business_8.snap | 98 +++++ ...al_styles__analytics_light_consumer_1.snap | 7 +- ...al_styles__analytics_light_consumer_2.snap | 7 +- ...al_styles__analytics_light_consumer_4.snap | 7 +- ...al_styles__analytics_light_consumer_5.snap | 7 +- ...al_styles__analytics_light_consumer_6.snap | 7 +- ...al_styles__analytics_light_consumer_8.snap | 98 +++++ ...e_empty_days_and_independent_failures.snap | 13 +- ...ools_reflow_and_keep_independent_days.snap | 11 +- codex-rs/tui/src/analytics/summary.rs | 76 ++++ codex-rs/tui/src/analytics/summary_panel.rs | 374 ++++++++++++++++++ codex-rs/tui/src/analytics/summary_tests.rs | 143 +++++++ codex-rs/tui/src/analytics/test_fixtures.rs | 5 + codex-rs/tui/src/analytics/test_support.rs | 1 + .../tui/src/analytics/tool_panel_tests.rs | 8 +- codex-rs/tui/src/analytics_tests.rs | 28 +- codex-rs/tui/src/app/background_requests.rs | 34 -- codex-rs/tui/src/app/event_dispatch.rs | 28 +- codex-rs/tui/src/app/history_ui.rs | 4 - codex-rs/tui/src/app/tests.rs | 32 -- ...ion_and_explicit_view_selects_summary.snap | 78 ++++ .../tui/src/app/tests/transcript_composer.rs | 148 ++++++- codex-rs/tui/src/app_event.rs | 19 +- codex-rs/tui/src/chatwidget.rs | 15 +- codex-rs/tui/src/chatwidget/constructor.rs | 3 - codex-rs/tui/src/chatwidget/rendering.rs | 12 +- codex-rs/tui/src/chatwidget/settings.rs | 1 - codex-rs/tui/src/chatwidget/slash_dispatch.rs | 6 +- ...fresh_renders_above_composer_snapshot.snap | 14 - ...chatwidget__tests__usage_command_menu.snap | 5 +- ...age_command_menu_before_reset_refresh.snap | 5 +- ...ts__usage_command_menu_without_resets.snap | 3 +- .../src/chatwidget/tests/slash_commands.rs | 350 ++-------------- codex-rs/tui/src/chatwidget/tests/usage.rs | 18 +- codex-rs/tui/src/chatwidget/tokens.rs | 292 -------------- codex-rs/tui/src/chatwidget/tokens_tests.rs | 39 -- codex-rs/tui/src/chatwidget/usage.rs | 22 +- codex-rs/tui/src/chatwidget/usage_history.rs | 54 +++ ...ping_preserves_selected_day_and_total.snap | 14 +- ...nguish_zero_missing_and_exact_details.snap | 44 +-- ..._navigation_keeps_geometry_and_legend.snap | 10 +- ...alytics_details_reflow_and_keep_focus.snap | 10 +- ...rns_keep_geometry_and_disable_details.snap | 21 +- ...rs_and_empty_reports_stay_independent.snap | 2 +- ...zed_refunds_leave_room_for_both_bands.snap | 14 +- ...s_and_tool_remainders_are_unambiguous.snap | 97 +++-- ...nd_small_terminals_keep_valid_cursors.snap | 5 +- ..._analytics__tests__analytics_overview.snap | 83 ++-- ...ranks_its_own_models_after_turns_load.snap | 5 +- codex-rs/tui/src/tui.rs | 18 +- 122 files changed, 3685 insertions(+), 1980 deletions(-) create mode 100644 codex-rs/backend-client/src/client/profile.rs create mode 100644 codex-rs/backend-client/src/client/profile_tests.rs rename codex-rs/tui/src/{chatwidget/tokens/chart.rs => analytics/activity_chart.rs} (56%) rename codex-rs/tui/src/{chatwidget/tokens/chart => analytics/activity_chart}/palette.rs (100%) rename codex-rs/tui/src/{chatwidget/tokens/chart => analytics/activity_chart}/palette_tests.rs (100%) rename codex-rs/tui/src/{chatwidget/tokens/chart_tests.rs => analytics/activity_chart_tests.rs} (75%) create mode 100644 codex-rs/tui/src/analytics/compact_tests.rs create mode 100644 codex-rs/tui/src/analytics/connection_tests.rs create mode 100644 codex-rs/tui/src/analytics/profile_identity_tests.rs create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__account_plan_error.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_error.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_recovered.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__unknown_plan_keeps_summary_available_without_billing_reports.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__compact__report_headlines_and_axes_compact_large_values.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_cumulative_narrow.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_daily_wide.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_overview.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_partial.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_scrolled.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_weekly.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap create mode 100644 codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap create mode 100644 codex-rs/tui/src/analytics/summary.rs create mode 100644 codex-rs/tui/src/analytics/summary_panel.rs create mode 100644 codex-rs/tui/src/analytics/summary_tests.rs create mode 100644 codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__transcript_composer__analytics_menu_reopen_preserves_navigation_and_explicit_view_selects_summary.snap delete mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pending_token_activity_refresh_renders_above_composer_snapshot.snap delete mode 100644 codex-rs/tui/src/chatwidget/tokens.rs delete mode 100644 codex-rs/tui/src/chatwidget/tokens_tests.rs create mode 100644 codex-rs/tui/src/chatwidget/usage_history.rs diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index d7b0a3e2ec..6b5fcbf9fd 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -37,6 +37,7 @@ use std::fmt; pub(crate) mod analytics; mod chatgpt_turn_cost; pub(crate) mod plan_history; +pub(crate) mod profile; mod rate_limit_resets; pub(crate) mod task_usage; mod thread_usage; diff --git a/codex-rs/backend-client/src/client/profile.rs b/codex-rs/backend-client/src/client/profile.rs new file mode 100644 index 0000000000..745efa1893 --- /dev/null +++ b/codex-rs/backend-client/src/client/profile.rs @@ -0,0 +1,73 @@ +//! Read-only profile statistics shared with the desktop account summary. +//! Optional fields retain absence; unknown invocation kinds do not invalidate other statistics. + +use super::Client; +use super::RequestError; +use crate::TokenUsageProfileStats; +use http::Method; +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct AccountProfile { + pub profile: Option, + pub metadata: Option, + pub stats: ProfileStats, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct ProfileIdentity { + pub display_name: Option, + pub username: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct ProfileMetadata { + pub stats_as_of: Option, + pub stats_error: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct ProfileStats { + #[serde(flatten)] + pub tokens: TokenUsageProfileStats, + pub fast_mode_usage_percentage: Option, + pub most_used_reasoning_effort: Option, + pub most_used_reasoning_effort_percentage: Option, + pub unique_skills_used: Option, + pub total_skills_used: Option, + pub total_threads: Option, + pub top_invocations: Option>, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct ProfileInvocation { + #[serde(rename = "type")] + pub kind: ProfileInvocationKind, + pub plugin_name: Option, + pub skill_name: Option, + pub usage_count: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ProfileInvocationKind { + Plugin, + Skill, + #[serde(other)] + Unknown, +} + +impl Client { + /// Read the full profile using the same route as account token usage. + pub async fn get_account_profile(&self) -> Result { + let url = self.token_usage_profile_url(); + let request = self.request(Method::GET, &url).headers(self.headers()); + let (body, _) = self.exec_request_detailed(request, "GET", &url).await?; + serde_json::from_str(&body) + .map_err(|_| RequestError::Other(anyhow::anyhow!("Invalid account profile response."))) + } +} + +#[cfg(test)] +#[path = "profile_tests.rs"] +mod tests; diff --git a/codex-rs/backend-client/src/client/profile_tests.rs b/codex-rs/backend-client/src/client/profile_tests.rs new file mode 100644 index 0000000000..b3f312fcff --- /dev/null +++ b/codex-rs/backend-client/src/client/profile_tests.rs @@ -0,0 +1,126 @@ +//! Full and partial profile decoding through the authenticated HTTP client. +use super::*; +use pretty_assertions::assert_eq; +use serde_json::json; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +#[tokio::test] +async fn profile_keeps_missing_zero_and_future_invocations_distinct() { + let server = MockServer::start().await; + let client = Client::new( + server.uri(), + codex_http_client::HttpClientFactory::new( + codex_http_client::OutboundProxyPolicy::ReqwestDefault, + ), + ); + let empty = AccountProfile { + profile: None, + metadata: None, + stats: ProfileStats { + tokens: TokenUsageProfileStats { + lifetime_tokens: None, + peak_daily_tokens: None, + longest_running_turn_sec: None, + current_streak_days: None, + longest_streak_days: None, + daily_usage_buckets: None, + }, + fast_mode_usage_percentage: None, + most_used_reasoning_effort: None, + most_used_reasoning_effort_percentage: None, + unique_skills_used: None, + total_skills_used: None, + total_threads: None, + top_invocations: None, + }, + }; + let full = AccountProfile { + profile: Some(ProfileIdentity { + display_name: Some("Test user".into()), + username: Some("test.user".into()), + }), + metadata: Some(ProfileMetadata { + stats_as_of: Some("2026-09-09".into()), + stats_error: Some("partial upstream data".into()), + }), + stats: ProfileStats { + tokens: TokenUsageProfileStats { + lifetime_tokens: Some(142_300_000_000), + peak_daily_tokens: Some(7_000_000_000), + longest_running_turn_sec: Some(158_760), + current_streak_days: Some(158), + longest_streak_days: Some(160), + daily_usage_buckets: Some(vec![crate::TokenUsageProfileDailyBucket { + start_date: "2026-09-09".into(), + tokens: 42, + }]), + }, + fast_mode_usage_percentage: serde_json::Number::from_f64(/*f*/ 37.5), + most_used_reasoning_effort: Some("high".into()), + most_used_reasoning_effort_percentage: Some(26.into()), + unique_skills_used: Some(147), + total_skills_used: Some(13_190), + total_threads: Some(0), + top_invocations: Some(vec![ + ProfileInvocation { + kind: ProfileInvocationKind::Unknown, + plugin_name: None, + skill_name: None, + usage_count: None, + }, + ProfileInvocation { + kind: ProfileInvocationKind::Skill, + plugin_name: None, + skill_name: Some("review".into()), + usage_count: Some(12), + }, + ProfileInvocation { + kind: ProfileInvocationKind::Plugin, + plugin_name: Some("github".into()), + skill_name: None, + usage_count: Some(34), + }, + ]), + }, + }; + for (body, expected) in [ + (json!({"stats":{}}), empty), + ( + json!({ + "profile":{"display_name":"Test user","username":"test.user"}, + "metadata":{"stats_as_of":"2026-09-09","stats_error":"partial upstream data"}, + "stats":{ + "lifetime_tokens":142_300_000_000_i64,"peak_daily_tokens":7_000_000_000_i64, + "longest_running_turn_sec":158_760,"current_streak_days":158,"longest_streak_days":160, + "daily_usage_buckets":[{"start_date":"2026-09-09","tokens":42}], + "fast_mode_usage_percentage":37.5,"most_used_reasoning_effort":"high", + "most_used_reasoning_effort_percentage":26,"unique_skills_used":147, + "total_skills_used":13_190,"total_threads":0, + "top_invocations":[{"type":"future_kind"},{"type":"skill","skill_name":"review","usage_count":12}, + {"type":"plugin","plugin_name":"github","usage_count":34}] + } + }), + full, + ), + ] { + Mock::given(method("GET")) + .and(path("/api/codex/profiles/me")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(body)) + .expect(/*r*/ 1) + .mount(&server) + .await; + assert_eq!(client.get_account_profile().await.unwrap(), expected); + server.verify().await; + server.reset().await; + } + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_string("private invalid payload")) + .mount(&server) + .await; + let error = client.get_account_profile().await.unwrap_err().to_string(); + assert_eq!(error, "Invalid account profile response."); +} diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index fa988d6dc2..1f83645989 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -13,6 +13,12 @@ pub use client::ThreadUsage; pub use client::ThreadUsageBreakdownGroup; pub use client::analytics::AnalyticsReport; pub use client::analytics::AnalyticsResponse; +pub use client::profile::AccountProfile; +pub use client::profile::ProfileIdentity; +pub use client::profile::ProfileInvocation; +pub use client::profile::ProfileInvocationKind; +pub use client::profile::ProfileMetadata; +pub use client::profile::ProfileStats; pub use client::turn_usage::ApiKeyResponseCost; pub use client::turn_usage::ApiKeyTurnCost; pub use client::turn_usage::ApiKeyTurnCostStatus; diff --git a/codex-rs/tui/src/analytics.rs b/codex-rs/tui/src/analytics.rs index ef62ea93db..99ed0b1563 100644 --- a/codex-rs/tui/src/analytics.rs +++ b/codex-rs/tui/src/analytics.rs @@ -1,7 +1,11 @@ //! Authenticated account analytics dashboard. //! A maximized section and an overview share bounded report loads, selection, and inline details. +mod activity_chart; mod chart; +mod summary; +mod summary_panel; +pub(crate) use activity_chart::TokenActivityView; mod chat_panel; mod chats; mod client; @@ -53,6 +57,7 @@ pub(crate) struct AnalyticsView { chats: Load, tasks: Load, chat_metric: usize, + profile: Load, plan: plan::State, show_zero_credit_groups: bool, account: Load, @@ -84,6 +89,7 @@ impl AnalyticsView { chats: Load::Unavailable, tasks: Load::Unavailable, chat_metric: 0, + profile: Load::Unavailable, plan: plan::State::default(), show_zero_credit_groups: false, account: Load::Unavailable, @@ -91,7 +97,7 @@ impl AnalyticsView { connection: None, live: None, keymap, - section: Section::Usage, + section: Section::Summary, zoomed: true, scroll_offset: 0, follow_selection: true, @@ -144,6 +150,7 @@ impl AnalyticsView { self.sections[Section::Chats].detail = None; self.chats = Load::Unavailable; self.tasks = Load::Unavailable; + self.profile = Load::Unavailable; self.plan.report = Load::Unavailable; self.reports_started = false; self.token_model = None; @@ -176,6 +183,7 @@ impl AnalyticsView { } self.chats = Load::Unavailable; self.tasks = Load::Unavailable; + self.profile = Load::Unavailable; self.plan.report = Load::Unavailable; self.account = Load::Unavailable; self.live = None; @@ -217,6 +225,7 @@ impl AnalyticsView { return &[]; } match self.section { + Section::Summary => &[0, 1, 2], Section::Plan => &[1, 2, 0, 3], Section::Usage if self.business() => &[6, 2], Section::Usage if self.consumer_attribution() => &[1, 2, 0, 3], @@ -246,6 +255,7 @@ impl AnalyticsView { Section::Skills => "Skills used", Section::Chats => "Top chats", Section::Plan => "Plan usage history", + Section::Summary => "Summary", } } @@ -359,7 +369,10 @@ impl AnalyticsView { return; } if key_hint::plain(KeyCode::Char('r')).is_press(key) - && !matches!(self.section, Section::Chats | Section::Plan) + && !matches!( + self.section, + Section::Chats | Section::Plan | Section::Summary + ) && !self.visible_sections().is_empty() { self.change_range(); @@ -382,7 +395,7 @@ impl AnalyticsView { .position(|section| *section == self.section) .unwrap_or_default(); let target = match key.code { - KeyCode::Char(n @ '1'..='6') if key.modifiers.is_empty() => { + KeyCode::Char(n @ '1'..='7') if key.modifiers.is_empty() => { visible.get(n as usize - '1' as usize).copied() } KeyCode::Tab => Some(visible[(current + 1) % visible.len()]), @@ -397,6 +410,10 @@ impl AnalyticsView { self.group_picker = None; return; } + if self.section == Section::Summary { + self.summary_action(action); + return; + } if self.section == Section::Plan { self.plan_action(action); return; @@ -494,6 +511,7 @@ impl AnalyticsView { } self.chats = Load::Unavailable; self.tasks = Load::Unavailable; + self.profile = Load::Unavailable; self.plan.report = Load::Unavailable; self.account = Load::Unavailable; self.connection = None; diff --git a/codex-rs/tui/src/analytics/account_layout_tests.rs b/codex-rs/tui/src/analytics/account_layout_tests.rs index 544d23693c..2d5c53ef50 100644 --- a/codex-rs/tui/src/analytics/account_layout_tests.rs +++ b/codex-rs/tui/src/analytics/account_layout_tests.rs @@ -10,6 +10,7 @@ fn account_layouts_show_reports_and_wrap_navigation() { ( models::AccountKind::Consumer, vec![ + Section::Summary, Section::Usage, Section::Activity, Section::Plugins, @@ -20,6 +21,7 @@ fn account_layouts_show_reports_and_wrap_navigation() { ( models::AccountKind::Enterprise, vec![ + Section::Summary, Section::Credits, Section::Usage, Section::Plugins, @@ -52,6 +54,7 @@ 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; + view.section = Section::Usage; press(&mut view, KeyCode::Left); press(&mut view, KeyCode::Enter); press(&mut view, KeyCode::Char('r')); @@ -67,7 +70,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() { view.sections[Section::Usage].detail, view.sections[Section::Usage].group ), - ([1, 0, 0], [28, 6, 6, 0, 6, 6, 6], Some(28), 2) + ([1, 0, 0], [28, 6, 6, 0, 6, 6, 6, 6], Some(28), 2) ); let context = ( view.ranges, @@ -87,7 +90,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() { ), context ); - press(&mut view, KeyCode::Char('2')); + press(&mut view, KeyCode::Char('3')); press(&mut view, KeyCode::Char('r')); test_support::settle(&mut view).await; assert_eq!(view.ranges, [1, 1, 0]); @@ -96,7 +99,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() { .0 .each_ref() .map(|state| state.history.ready().map(|history| history.data.len())), - [Some(30), Some(7), None, None, Some(30), Some(7), None] + [Some(30), Some(7), None, None, Some(30), Some(7), None, None] ); let server = test_support::server().await; let (_business_home, _business_server, mut view) = @@ -104,7 +107,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() { 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('2')); press(&mut view, KeyCode::Char('r')); test_support::settle(&mut view).await; assert_eq!(view.ranges, [0, 1, 0]); @@ -113,7 +116,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() { .0 .each_ref() .map(|state| state.history.ready().map(|history| history.data.len())), - [Some(30), Some(7), Some(7), None, None, Some(7), None] + [Some(30), Some(7), Some(7), None, None, Some(7), None, None] ); } @@ -124,7 +127,7 @@ async fn business_tokens_filter_models() { test_support::settle(&mut view).await; view.end_date = fixture::END_DATE; fixture::seed_reports(&mut view); - press(&mut view, KeyCode::Char('2')); + press(&mut view, KeyCode::Char('3')); let all = screen(&mut view, /*width*/ 100, /*height*/ 30); assert!(all.contains("All models") && all.contains("Cached input")); press(&mut view, KeyCode::Char('m')); @@ -194,6 +197,14 @@ async fn account_reports_request_only_eligible_endpoints_and_refresh() { ), ] { let server = test_support::server().await; + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/profiles/me")) + .respond_with( + wiremock::ResponseTemplate::new(/*s*/ 200) + .set_body_json(serde_json::json!({"stats":{}})), + ) + .mount(&server) + .await; let (_home, _app_server, mut view) = client::tests::connected_view(&server, plan).await; test_support::settle(&mut view).await; let mut expected = [ @@ -202,6 +213,7 @@ async fn account_reports_request_only_eligible_endpoints_and_refresh() { activity, "analytics/daily-plugin-usage-metrics", "analytics/daily-skill-usage-metrics", + "profiles/me", ] .map(|endpoint| format!("/backend-api/wham/{endpoint}")); expected.sort(); @@ -223,9 +235,9 @@ async fn account_reports_request_only_eligible_endpoints_and_refresh() { assert_eq!( ready, if plan == "plus" { - [true, true, false, false, true, true, false] + [true, true, false, false, true, true, false, false] } else { - [true, true, true, false, false, true, false] + [true, true, true, false, false, true, false, false] } ); press(&mut view, KeyCode::Char('R')); @@ -245,7 +257,7 @@ async fn account_reports_request_only_eligible_endpoints_and_refresh() { .iter() .filter(|request| request.url.path().starts_with("/backend-api/wham/")) .count(), - 10 + 12 ); view.cancel_loads(); } @@ -255,6 +267,7 @@ async fn account_reports_request_only_eligible_endpoints_and_refresh() { 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.section = Section::Usage; view.account = Load::Ready(codex_protocol::account::PlanType::Plus); view.start_reports(); view.group_picker = Some(3); @@ -275,6 +288,7 @@ 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.section = Section::Usage; view.account = Load::Ready(codex_protocol::account::PlanType::Plus); view.start_reports(); view.group_picker = Some(choice); @@ -305,7 +319,7 @@ fn business_chats_display_only_supplied_dollars() { usage.estimated_usage_usd_micros = None; } } - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); assert!(!screen(&mut view, /*width*/ 100, /*height*/ 30).contains("Est. $ spent")); if let Load::Ready(chats) = &mut view.chats { chats.rows[0] @@ -338,6 +352,7 @@ async fn unsupported_workspace_plans_hide_top_chats_and_skip_loading() { assert_eq!( view.visible_sections(), &[ + Section::Summary, Section::Credits, Section::Usage, Section::Plugins, @@ -347,7 +362,7 @@ async fn unsupported_workspace_plans_hide_top_chats_and_skip_loading() { ); assert!(matches!(view.chats, Load::Unavailable), "{plan}"); let selected = view.section; - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); assert_eq!(view.section, selected); } for plan in [ @@ -359,10 +374,62 @@ async fn unsupported_workspace_plans_hide_top_chats_and_skip_loading() { let (_home, _app_server, mut view) = client::tests::connected_view(&server, plan).await; test_support::settle(&mut view).await; assert!(view.visible_sections().contains(&Section::Chats), "{plan}"); - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); assert_eq!(view.section, Section::Chats); } let mut view = fixture::view(models::AccountKind::Enterprise); view.account = Load::Ready(codex_protocol::account::PlanType::Enterprise); insta::assert_snapshot!(screen(&mut view, /*width*/ 110, /*height*/ 24)); } + +#[tokio::test] +async fn unknown_plan_keeps_summary_available_without_billing_reports() { + let server = test_support::server().await; + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/profiles/me")) + .respond_with( + wiremock::ResponseTemplate::new(/*s*/ 200) + .set_body_json(serde_json::json!({"stats":{"lifetime_tokens":12345}})), + ) + .expect(/*r*/ 1) + .mount(&server) + .await; + let (_home, _app_server, mut view) = + client::tests::connected_view(&server, "future-plan").await; + test_support::settle(&mut view).await; + assert_eq!(view.visible_sections(), &[Section::Summary]); + assert_eq!( + view.profile.ready().unwrap().stats.tokens.lifetime_tokens, + Some(12345) + ); + assert!( + view.sections + .0 + .iter() + .all(|section| matches!(section.history, Load::Unavailable)) + ); + let paths = server + .received_requests() + .await + .unwrap() + .into_iter() + .filter(|request| request.url.path().starts_with("/backend-api/wham/")) + .map(|request| request.url.path().to_owned()) + .collect::>(); + assert_eq!( + paths, + [ + "/backend-api/wham/accounts/check", + "/backend-api/wham/profiles/me" + ] + ); + view.end_date = "2026-09-09".parse().unwrap(); + let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new( + /*width*/ 100, /*height*/ 40, + )) + .unwrap(); + terminal + .draw(|frame| view.render(frame.area(), frame.buffer_mut())) + .unwrap(); + insta::assert_snapshot!(terminal.backend().to_string()); +} diff --git a/codex-rs/tui/src/chatwidget/tokens/chart.rs b/codex-rs/tui/src/analytics/activity_chart.rs similarity index 56% rename from codex-rs/tui/src/chatwidget/tokens/chart.rs rename to codex-rs/tui/src/analytics/activity_chart.rs index 9a9fc27e26..8bfb704b73 100644 --- a/codex-rs/tui/src/chatwidget/tokens/chart.rs +++ b/codex-rs/tui/src/analytics/activity_chart.rs @@ -1,8 +1,4 @@ -//! Renders account token usage summaries and activity charts for `/usage`. -//! -//! This module owns the chart data bucketing and ratatui line construction. The -//! async card lifecycle stays in the parent `tokens` module so chart rendering -//! remains a pure transformation from a loaded usage response. +//! Calendar and weekly token charts for the account Summary. Bucketing is independent of view state. mod palette; @@ -11,22 +7,18 @@ use std::collections::BTreeMap; use chrono::Datelike; use chrono::Duration; use chrono::NaiveDate; -use codex_app_server_protocol::GetAccountTokenUsageResponse; use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use crate::render::highlight::foreground_style_for_scopes; -use crate::status::format_tokens_compact; use palette::TokenActivityPalette; const WEEK_COUNT: usize = 52; const DAY_COUNT: usize = 7; const CELL_COUNT: usize = WEEK_COUNT * DAY_COUNT; const CHART_LEFT_WIDTH: usize = 4; -const SUMMARY_INDENT: &str = " "; -const SUMMARY_INDENT_WIDTH: u16 = 1; /// Selects the aggregation represented by the token activity chart. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -39,10 +31,10 @@ pub(crate) enum TokenActivityView { impl TokenActivityView { /// Parses the optional `/usage` argument into a supported chart view. /// - /// An empty argument selects the daily view so `/usage` and `/usage daily` - /// behave identically. Returning `None` lets the slash-command dispatcher + /// An empty argument selects Daily when a mode is requested. The bare `/usage` + /// command opens its menu. Returning `None` lets the slash-command dispatcher /// report unsupported arguments instead of silently choosing a view. - pub(in crate::chatwidget) fn parse(value: &str) -> Option { + pub(crate) fn parse(value: &str) -> Option { match value.trim().to_ascii_lowercase().as_str() { "" | "day" | "daily" => Some(Self::Daily), "week" | "weekly" => Some(Self::Weekly), @@ -51,7 +43,7 @@ impl TokenActivityView { } } - pub(super) fn label(self) -> &'static str { + pub(crate) fn label(self) -> &'static str { match self { Self::Daily => "Daily", Self::Weekly => "Weekly", @@ -60,34 +52,9 @@ impl TokenActivityView { } } -pub(super) fn loaded_lines( +pub(super) fn chart_lines( view: TokenActivityView, - response: &GetAccountTokenUsageResponse, - today: NaiveDate, - width: u16, -) -> Vec> { - let mut lines = vec![ - vec![ - Span::from(" Token activity").bold(), - Span::styled(" last 12 months", label_style()), - ] - .into(), - ]; - lines.extend(summary_lines(response, graph_width(width))); - // Separate the headline numbers from the calendar below. - lines.push(Line::default()); - let Some(buckets) = response.daily_usage_buckets.as_ref() else { - lines.push(" Token activity history unavailable".dim().into()); - return lines; - }; - - lines.extend(chart_lines(view, buckets, today, width)); - lines -} - -fn chart_lines( - view: TokenActivityView, - buckets: &[codex_app_server_protocol::AccountTokenUsageDailyBucket], + buckets: &[codex_backend_client::TokenUsageProfileDailyBucket], today: NaiveDate, width: u16, ) -> Vec> { @@ -133,7 +100,6 @@ fn chart_lines( lines.push(bar_caption(view, &values)) } } - lines.push(view_footer(view)); lines } @@ -145,114 +111,6 @@ fn shown_columns(width: u16) -> usize { .min(WEEK_COUNT) } -fn graph_width(width: u16) -> u16 { - if width == u16::MAX { - return width; - } - (CHART_LEFT_WIDTH + shown_columns(width) * 2 - 1) as u16 -} - -fn summary_lines(response: &GetAccountTokenUsageResponse, width: u16) -> Vec> { - let summary = &response.summary; - let fields = [ - ("Lifetime", format_optional_tokens(summary.lifetime_tokens)), - ("Peak", format_optional_tokens(summary.peak_daily_tokens)), - ( - "Streak", - format_streak(summary.current_streak_days, summary.longest_streak_days), - ), - ( - "Longest task", - format_optional_duration(summary.longest_running_turn_sec), - ), - ]; - pack_fields(&fields, width) - .into_iter() - .map(|group| align_summary_line(summary_line(&fields, &group), width)) - .collect() -} - -/// Greedily pack summary fields into as few lines as fit `width`, -/// keeping field order. `u16::MAX` (raw/copy mode) always yields one line. -fn pack_fields(fields: &[(&str, String)], width: u16) -> Vec> { - if width == u16::MAX { - return vec![(0..fields.len()).collect()]; - } - let max = usize::from(width.saturating_sub(SUMMARY_INDENT_WIDTH)); - let mut groups: Vec> = Vec::new(); - let mut current: Vec = Vec::new(); - for index in 0..fields.len() { - let mut candidate = current.clone(); - candidate.push(index); - if !current.is_empty() && summary_line(fields, &candidate).width() > max { - groups.push(std::mem::take(&mut current)); - current.push(index); - } else { - current = candidate; - } - } - if !current.is_empty() { - groups.push(current); - } - groups -} - -fn summary_line(fields: &[(&str, String)], indexes: &[usize]) -> Line<'static> { - let mut spans = Vec::new(); - for (index, field_index) in indexes.iter().enumerate() { - if index > 0 { - spans.push(Span::styled(" · ", label_style())); - } - let (label, value) = &fields[*field_index]; - spans.push(Span::styled(format!("{label} "), label_style())); - spans.push(Span::styled(value.clone(), numeric_style())); - } - spans.into() -} - -fn align_summary_line(mut line: Line<'static>, width: u16) -> Line<'static> { - if width == u16::MAX { - return line; - } - line.spans.insert(/*index*/ 0, SUMMARY_INDENT.into()); - line -} - -fn format_optional_tokens(value: Option) -> String { - value - .map(format_tokens_compact) - .unwrap_or_else(|| "-".to_string()) -} - -/// Combine the current and longest streak into one field: a bare `54d` when -/// they match, otherwise `12d (best 54d)`. -fn format_streak(current: Option, longest: Option) -> String { - match (current, longest) { - (Some(current), Some(longest)) if current == longest => format!("{current}d"), - (Some(current), Some(longest)) => format!("{current}d (best {longest}d)"), - (Some(current), None) => format!("{current}d"), - (None, Some(longest)) => format!("- (best {longest}d)"), - (None, None) => "-".to_string(), - } -} - -fn format_optional_duration(value: Option) -> String { - value.map_or_else( - || "-".to_string(), - |seconds| { - let seconds = seconds.max(/*other*/ 0); - let hours = seconds / 3600; - let minutes = (seconds % 3600) / 60; - match (hours, minutes) { - (0, 0) => format!("{seconds}s"), - (0, minutes) => format!("{minutes}m"), - (hours, 0) => format!("{hours}h"), - (hours, minutes) => format!("{hours}h {minutes}m"), - } - }, - ) -} - fn numeric_style() -> Style { foreground_style_for_scopes(&["constant.numeric", "constant"]) .unwrap_or_else(|| Style::default().green()) @@ -314,7 +172,12 @@ fn bar_caption(view: TokenActivityView, values: &[i64]) -> Line<'static> { "Each column = 1 week · tallest ", weeks.iter().copied().max().unwrap_or(/*default*/ 0), ), - TokenActivityView::Cumulative => ("Running total · top ", weeks.iter().sum::()), + TokenActivityView::Cumulative => ( + "Running total · top ", + weeks + .iter() + .fold(/*init*/ 0_i64, |sum, value| sum.saturating_add(*value)), + ), TokenActivityView::Daily => ("", 0), }; if peak <= 0 { @@ -322,34 +185,11 @@ fn bar_caption(view: TokenActivityView, values: &[i64]) -> Line<'static> { } vec![ Span::styled(format!(" {lead}"), label_style()), - Span::styled(format_tokens_compact(peak), numeric_style()), + Span::styled(super::data::compact_amount(peak as f64), numeric_style()), ] .into() } -/// Dim footer that surfaces the other `/usage` views and emphasizes the -/// active one, so the weekly/cumulative modes are discoverable from the card. -fn view_footer(active: TokenActivityView) -> Line<'static> { - let mut spans = vec![Span::styled(" ", label_style())]; - let views = [ - (TokenActivityView::Daily, "daily"), - (TokenActivityView::Weekly, "weekly"), - (TokenActivityView::Cumulative, "cumulative"), - ]; - for (index, (view, name)) in views.into_iter().enumerate() { - if index > 0 { - spans.push(Span::styled(" · ", label_style())); - } - let style = if view == active { - numeric_style().bold() - } else { - label_style() - }; - spans.push(Span::styled(name, style)); - } - spans.into() -} - fn month_labels(today: NaiveDate, first_column: usize, shown_columns: usize) -> Line<'static> { let mut cells = vec![' '; shown_columns * 2 - 1]; let start = chart_start(today); @@ -382,7 +222,7 @@ fn month_labels(today: NaiveDate, first_column: usize, shown_columns: usize) -> /// Invalid, out-of-window, and future dates are ignored. Duplicate dates are /// accumulated and negative token values do not reduce activity. fn daily_values( - buckets: &[codex_app_server_protocol::AccountTokenUsageDailyBucket], + buckets: &[codex_backend_client::TokenUsageProfileDailyBucket], today: NaiveDate, ) -> Vec { let start = chart_start(today); @@ -395,7 +235,8 @@ fn daily_values( if date < start || date >= end || date > today { continue; } - *by_date.entry(date).or_insert(/*default*/ 0) += bucket.tokens.max(/*other*/ 0); + let value: &mut i64 = by_date.entry(date).or_insert(/*default*/ 0); + *value = value.saturating_add(bucket.tokens.max(/*other*/ 0)); } (0..CELL_COUNT) .map(|offset| { @@ -415,7 +256,7 @@ fn levels_for_view(values: &[i64], view: TokenActivityView) -> Vec { let cumulative = weekly_totals(values) .into_iter() .scan(/*initial_state*/ 0, |sum, value| { - *sum += value; + *sum = i64::saturating_add(*sum, value); Some(*sum) }) .collect::>(); @@ -430,9 +271,9 @@ fn graded_levels(values: &[i64]) -> Vec { .iter() .map(|value| match (*value, max) { (0, _) | (_, 0) => 0, - (value, max) if value * 4 > max * 3 => 4, - (value, max) if value * 2 > max => 3, - (value, max) if value * 4 > max => 2, + (value, max) if i128::from(value) * 4 > i128::from(max) * 3 => 4, + (value, max) if i128::from(value) * 2 > i128::from(max) => 3, + (value, max) if i128::from(value) * 4 > i128::from(max) => 2, _ => 1, }) .collect() @@ -441,7 +282,10 @@ fn graded_levels(values: &[i64]) -> Vec { fn weekly_totals(values: &[i64]) -> Vec { values .chunks(DAY_COUNT) - .map(|week| week.iter().sum()) + .map(|week| { + week.iter() + .fold(/*init*/ 0_i64, |sum, value| sum.saturating_add(*value)) + }) .collect() } @@ -453,7 +297,8 @@ fn bar_levels(totals: &[i64]) -> Vec { let height = if *value <= 0 || max <= 0 { 0 } else { - ((*value * DAY_COUNT as i64 + max - 1) / max) as usize + ((i128::from(*value) * DAY_COUNT as i128 + i128::from(max) - 1) / i128::from(max)) + as usize }; (0..DAY_COUNT).map(move |row| if DAY_COUNT - row <= height { 4 } else { 0 }) }) @@ -470,5 +315,5 @@ fn cell_date(today: NaiveDate, index: usize) -> Option { } #[cfg(test)] -#[path = "chart_tests.rs"] +#[path = "activity_chart_tests.rs"] mod tests; diff --git a/codex-rs/tui/src/chatwidget/tokens/chart/palette.rs b/codex-rs/tui/src/analytics/activity_chart/palette.rs similarity index 100% rename from codex-rs/tui/src/chatwidget/tokens/chart/palette.rs rename to codex-rs/tui/src/analytics/activity_chart/palette.rs diff --git a/codex-rs/tui/src/chatwidget/tokens/chart/palette_tests.rs b/codex-rs/tui/src/analytics/activity_chart/palette_tests.rs similarity index 100% rename from codex-rs/tui/src/chatwidget/tokens/chart/palette_tests.rs rename to codex-rs/tui/src/analytics/activity_chart/palette_tests.rs diff --git a/codex-rs/tui/src/chatwidget/tokens/chart_tests.rs b/codex-rs/tui/src/analytics/activity_chart_tests.rs similarity index 75% rename from codex-rs/tui/src/chatwidget/tokens/chart_tests.rs rename to codex-rs/tui/src/analytics/activity_chart_tests.rs index 9f706f7208..35faaa7c75 100644 --- a/codex-rs/tui/src/chatwidget/tokens/chart_tests.rs +++ b/codex-rs/tui/src/analytics/activity_chart_tests.rs @@ -1,9 +1,16 @@ +//! Calendar, aggregation, and chart rendering regressions. use super::*; -use codex_app_server_protocol::AccountTokenUsageDailyBucket; -use codex_app_server_protocol::AccountTokenUsageSummary; +use codex_backend_client::TokenUsageProfileDailyBucket as AccountTokenUsageDailyBucket; use insta::assert_snapshot; use pretty_assertions::assert_eq; +fn graph_width(width: u16) -> u16 { + if width == u16::MAX { + return width; + } + (CHART_LEFT_WIDTH + shown_columns(width) * 2 - 1) as u16 +} + #[test] fn duplicate_dates_sum_and_negative_values_clamp() { let today = @@ -86,7 +93,6 @@ fn daily_graph_snapshot_uses_distinct_empty_and_active_cells() { Sa □ □ □ □ □ □ □ □ Less □ ■ ■ ■ ■ More - daily · weekly · cumulative "); } @@ -105,9 +111,9 @@ fn daily_graph_snapshot_stays_left_aligned_in_wide_terminal() { .join("\n"); assert_snapshot!(rendered, @" - Jun Jul Aug Sep Oct Nov Dec Jan Feb Mar Apr May - Su □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ - daily · weekly · cumulative + Jun Jul Aug Sep Oct Nov Dec Jan Feb Mar Apr May + Su □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ + Less □ ■ ■ ■ ■ More "); } @@ -152,8 +158,7 @@ fn weekly_graph_snapshot_renders_bar_chart_and_caption() { 0 █ █ █ Each column = 1 week · tallest 9 - daily · weekly · cumulative - "); + "); } #[test] @@ -197,48 +202,38 @@ fn cumulative_graph_snapshot_renders_running_total_bar_chart_and_caption() { 0 █ █ █ Running total · top 18 - daily · weekly · cumulative - "); + "); } #[test] -fn summary_snapshot_left_aligns_and_splits_when_needed() { - let response = GetAccountTokenUsageResponse { - summary: AccountTokenUsageSummary { - lifetime_tokens: Some(21_400_000_000), - peak_daily_tokens: Some(835_000_000), - longest_running_turn_sec: Some(13_920), - current_streak_days: Some(54), - longest_streak_days: Some(54), - }, - daily_usage_buckets: None, - thread_usage: None, - }; - let rendered = |width| { - summary_lines(&response, graph_width(width)) - .into_iter() - .map(|line| line.to_string().trim_end().to_string()) - .collect::>() - .join("\n") - }; - - assert_snapshot!( - format!( - "wide:\n{}\n\nnarrow:\n{}\n\ntight:\n{}", - rendered(/*width*/ 120), - rendered(/*width*/ 80), - rendered(/*width*/ 62) - ), - @" - wide: - Lifetime 21.4B · Peak 835M · Streak 54d · Longest task 3h 52m - - narrow: - Lifetime 21.4B · Peak 835M · Streak 54d · Longest task 3h 52m - - tight: - Lifetime 21.4B · Peak 835M · Streak 54d - Longest task 3h 52m - " - ); +fn year_window_ignores_other_dates_and_saturates_extreme_totals() { + let today: NaiveDate = "2026-09-09".parse().unwrap(); + let buckets = [ + ("2025-01-01", 100), + ("2026-09-10", 100), + ("invalid", 100), + ("2026-09-08", i64::MAX), + ("2026-09-08", i64::MAX), + ("2026-09-09", i64::MAX), + ] + .map(|(date, tokens)| AccountTokenUsageDailyBucket { + start_date: date.into(), + tokens, + }); + let values = daily_values(&buckets, today); + let mut expected = vec![0; CELL_COUNT]; + let index = (today - chart_start(today)).num_days() as usize; + expected[index - 1] = i64::MAX; + expected[index] = i64::MAX; + assert_eq!(values, expected); + for view in [ + TokenActivityView::Daily, + TokenActivityView::Weekly, + TokenActivityView::Cumulative, + ] { + let levels = levels_for_view(&values, view); + assert!(levels.iter().all(|level| *level <= 4)); + assert!(levels.contains(&4)); + assert!(!chart_lines(view, &buckets, today, /*width*/ 110).is_empty()); + } } diff --git a/codex-rs/tui/src/analytics/chart.rs b/codex-rs/tui/src/analytics/chart.rs index 448b7ab73c..fee9d6a6f6 100644 --- a/codex-rs/tui/src/analytics/chart.rs +++ b/codex-rs/tui/src/analytics/chart.rs @@ -90,6 +90,7 @@ impl AnalyticsView { } let unit = match section { Section::Usage if history.unit == AccountAnalyticsUnit::Tokens => "tokens", + Section::Summary => "tokens", Section::Activity => "messages", Section::Plugins => "calls", Section::Skills => "uses", @@ -107,6 +108,12 @@ impl AnalyticsView { } else { data::amount }; + let total: f64 = history.data.iter().map(|day| day.total).sum(); + let formatted_total = if total.abs() >= 1_000.0 { + data::compact_amount(total) + } else { + format_amount(total) + }; let mut lines = vec![if relative { "Total usage · relative units" .set_style(secondary_style()) @@ -114,7 +121,7 @@ impl AnalyticsView { } else { number(format!( "{} {}{unit}", - format_amount(history.data.iter().map(|day| day.total).sum()), + formatted_total, if section == Section::Activity { "reported " } else { @@ -194,9 +201,11 @@ impl AnalyticsView { Section::Plugins => "Plugin", Section::Skills => "Skill", Section::Chats => "Chat", - Section::Usage | Section::Credits | Section::Activity | Section::Plan => { - self.group_label(section, self.sections[section].group) - } + Section::Summary + | Section::Usage + | Section::Credits + | Section::Activity + | Section::Plan => self.group_label(section, self.sections[section].group), } .bold() .into(), diff --git a/codex-rs/tui/src/analytics/chat_panel_tests.rs b/codex-rs/tui/src/analytics/chat_panel_tests.rs index 2a1e992f8a..709d99271b 100644 --- a/codex-rs/tui/src/analytics/chat_panel_tests.rs +++ b/codex-rs/tui/src/analytics/chat_panel_tests.rs @@ -15,7 +15,7 @@ fn chats_range_controls_and_visible_rows() { }) .collect(); view.chats = Load::Ready(chats); - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); let before = ( view.ranges, view.sections.0.each_ref().map(|state| state.cursor), @@ -48,7 +48,7 @@ fn chats_range_controls_and_visible_rows() { press(&mut view, KeyCode::Char('z')); press(&mut view, KeyCode::Char('r')); assert_eq!(view.ranges, [0; 3]); - press(&mut view, KeyCode::Char('1')); + press(&mut view, KeyCode::Char('2')); press(&mut view, KeyCode::Char('r')); press(&mut view, KeyCode::Char('z')); assert!(screen(&mut view, /*width*/ 110, /*height*/ 24).contains("[30d]")); @@ -82,7 +82,7 @@ fn chats_breakdown_ranks_nonzero_groups_and_reflows() { .map(|group| group.estimated_usage_credits_micros) .sum(); view.chats = Load::Ready(chats); - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); press(&mut view, KeyCode::Enter); let mut screens = Vec::new(); for width in [110, 58] { @@ -116,7 +116,7 @@ fn chats_long_title_keeps_detail_indent_and_counts_missing_estimates() { chats.rows[1].usage = None; chats.rows[2].usage = None; view.chats = Load::Ready(chats); - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); press(&mut view, KeyCode::Enter); let output = screen(&mut view, /*width*/ 110, /*height*/ 30); assert!(output.contains("2 chat estimates unavailable")); diff --git a/codex-rs/tui/src/analytics/client.rs b/codex-rs/tui/src/analytics/client.rs index df21094f05..f990035270 100644 --- a/codex-rs/tui/src/analytics/client.rs +++ b/codex-rs/tui/src/analytics/client.rs @@ -44,11 +44,7 @@ impl Live { pub(super) fn account_label(&self) -> Option { let session = self.session.get()?; - let account = session.backend.account(); - Some(match &account.email { - Some(email) => format!("{email} · {}", account.id), - None => account.id.clone(), - }) + session.backend.account().email.clone() } pub(super) fn credit_groups(&self) -> &[usize] { @@ -262,6 +258,10 @@ pub(super) fn request_error(error: RequestError) -> String { #[path = "client_tests.rs"] pub(super) mod tests; +#[cfg(test)] +#[path = "connection_tests.rs"] +mod connection_tests; + #[cfg(test)] #[path = "account_plan_tests.rs"] mod account_plan_tests; diff --git a/codex-rs/tui/src/analytics/client_tests.rs b/codex-rs/tui/src/analytics/client_tests.rs index 3c6181e442..872d462ed3 100644 --- a/codex-rs/tui/src/analytics/client_tests.rs +++ b/codex-rs/tui/src/analytics/client_tests.rs @@ -147,7 +147,7 @@ async fn analytics_requests_use_reloaded_credentials_for_the_same_identity() { let authenticated = &session.backend; assert_eq!( live.account_label().as_deref(), - Some("analytics@example.test · account-a") + Some("analytics@example.test") ); let auth_path = home.path().join("auth.json"); let mut auth: serde_json::Value = @@ -846,3 +846,6 @@ async fn plan_history_rejects_account_change_during_response() { ); } } + +#[path = "profile_identity_tests.rs"] +mod profile_identity; diff --git a/codex-rs/tui/src/analytics/compact_tests.rs b/codex-rs/tui/src/analytics/compact_tests.rs new file mode 100644 index 0000000000..a500d01b3f --- /dev/null +++ b/codex-rs/tui/src/analytics/compact_tests.rs @@ -0,0 +1,48 @@ +//! Large report totals and axes stay compact while selected-day details retain precision. +use super::*; + +#[test] +fn report_headlines_and_axes_compact_large_values() { + let mut screenshots = Vec::new(); + for (section, unit, total, headline) in [ + ( + Section::Usage, + models::AccountAnalyticsUnit::Tokens, + 12_280_365_226.0, + "12.3B tokens", + ), + ( + Section::Plugins, + models::AccountAnalyticsUnit::Count, + 9_749.0, + "9.7K calls", + ), + ( + Section::Skills, + models::AccountAnalyticsUnit::Count, + 2_901.0, + "2.9K uses", + ), + ] { + let mut view = fixture::view(models::AccountKind::Enterprise); + view.section = section; + view.sections[section].history = Load::Ready(models::AccountAnalyticsHistory { + unit, + updated_at: None, + data: vec![models::AccountAnalyticsDay { + date: view.end_date, + total, + values: vec![models::AccountAnalyticsValue { + key: "test".into(), + label: "Example".into(), + value: total, + }], + }], + }); + let rendered = screen(&mut view, /*width*/ 100, /*height*/ 30); + assert!(rendered.contains(headline)); + assert!(rendered.contains(&data::amount(total))); + screenshots.push(rendered); + } + insta::assert_snapshot!(screenshots.join("\n")); +} diff --git a/codex-rs/tui/src/analytics/connection_tests.rs b/codex-rs/tui/src/analytics/connection_tests.rs new file mode 100644 index 0000000000..101e8dfa79 --- /dev/null +++ b/codex-rs/tui/src/analytics/connection_tests.rs @@ -0,0 +1,237 @@ +//! Exercise local Analytics plan discovery and report recovery while connected to an app-server. + +use super::tests::live; +use codex_app_server_client::AppServerRequestHandle; +use codex_app_server_client::RemoteAppServerClient; +use codex_app_server_client::RemoteAppServerConnectArgs; +use codex_app_server_client::RemoteAppServerEndpoint; +use futures::SinkExt; +use futures::StreamExt; +use pretty_assertions::assert_eq; +use serde_json::Value; +use serde_json::json; +use tokio::net::TcpListener; +use tokio::net::TcpStream; +use tokio_tungstenite::WebSocketStream; +use tokio_tungstenite::tungstenite::Message; +use wiremock::Mock; +use wiremock::ResponseTemplate; +use wiremock::matchers::header; +use wiremock::matchers::method; + +async fn read(socket: &mut WebSocketStream) -> Value { + let frame = socket.next().await.unwrap().unwrap(); + serde_json::from_str(frame.to_text().unwrap()).unwrap() +} + +async fn remote() -> (RemoteAppServerClient, tokio::task::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap(); + let initialize = read(&mut socket).await; + assert_eq!(initialize["method"], "initialize"); + socket + .send(Message::Text( + json!({"id": initialize["id"], "result": { + "userAgent": "analytics-test", "codexHome": "/server/.codex", + }}) + .to_string() + .into(), + )) + .await + .unwrap(); + assert_eq!(read(&mut socket).await["method"], "initialized"); + // Analytics uses the local account without requesting connected-server account data. + assert!(socket.next().await.unwrap().unwrap().is_close()); + }); + let client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs { + endpoint: RemoteAppServerEndpoint::WebSocket { + websocket_url: format!("ws://{address}"), + auth_token: None, + }, + client_name: "analytics-test".into(), + client_version: "0.0.0-test".into(), + experimental_api: true, + mcp_server_openai_form_elicitation: false, + opt_out_notification_methods: Vec::new(), + channel_capacity: 8, + }) + .await + .unwrap(); + (client, server) +} + +#[tokio::test] +async fn local_reports_share_an_account_and_recover_profile_errors() { + use crate::analytics::AnalyticsView; + use crate::analytics::data::Load; + use crate::analytics::sections::Section; + + let http = crate::analytics::test_support::server().await; + let (_home, local) = live(&http, "enterprise").await; + let (client, server) = remote().await; + let failed_profile = Mock::given(method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/profiles/me")) + .and(header("chatgpt-account-id", "account-a")) + .respond_with(ResponseTemplate::new(/*s*/ 503)) + .expect(/*r*/ 1) + .mount_as_scoped(&http) + .await; + let mut view = AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list); + view.open( + AppServerRequestHandle::Remote(client.request_handle()), + crate::tui::FrameRequester::test_dummy(), + Vec::new(), + std::sync::Arc::clone(&local.config), + ); + crate::analytics::test_support::settle(&mut view).await; + assert_eq!( + view.visible_sections(), + &[ + Section::Summary, + Section::Credits, + Section::Usage, + Section::Plugins, + Section::Skills, + ] + ); + assert!(matches!(view.profile, Load::Error(_))); + for section in view.visible_sections() { + if section.report().is_some() { + assert!(view.sections[*section].history.ready().is_some()); + } + } + view.select_summary(/*view*/ None); + view.end_date = "2026-09-09".parse().unwrap(); + let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new( + /*width*/ 100, /*height*/ 48, + )) + .unwrap(); + terminal + .draw(|frame| view.render(frame.area(), frame.buffer_mut())) + .unwrap(); + insta::assert_snapshot!("local_profile_error", terminal.backend().to_string()); + drop(failed_profile); + + let profile = json!({ + "profile":{"display_name":"Local account","username":"local-user"}, + "stats":{"lifetime_tokens":12345,"peak_daily_tokens":12345, + "daily_usage_buckets":[{"start_date":"2026-09-09","tokens":12345}]} + }); + Mock::given(method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/profiles/me")) + .and(header("chatgpt-account-id", "account-a")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(profile.clone())) + .expect(/*r*/ 1) + .mount(&http) + .await; + view.refresh(); + crate::analytics::test_support::settle(&mut view).await; + assert_eq!( + view.profile.ready(), + Some(&serde_json::from_value(profile).unwrap()) + ); + view.end_date = "2026-09-09".parse().unwrap(); + terminal + .draw(|frame| view.render(frame.area(), frame.buffer_mut())) + .unwrap(); + insta::assert_snapshot!("local_profile_recovered", terminal.backend().to_string()); + for request in http.received_requests().await.unwrap() { + assert_eq!( + request.headers.get("chatgpt-account-id").unwrap(), + "account-a" + ); + } + view.cancel_loads(); + client.shutdown().await.unwrap(); + server.await.unwrap(); +} + +#[tokio::test] +async fn account_lookup_failure_recovers_on_refresh_with_the_server_plan() { + use crate::analytics::AnalyticsView; + use crate::analytics::data::Load; + use crate::analytics::sections::Section; + + let http = crate::analytics::test_support::server().await; + let (_home, local) = live(&http, "plus").await; + let (client, server) = remote().await; + Mock::given(method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/accounts/check")) + .respond_with(ResponseTemplate::new(/*s*/ 503)) + .with_priority(/*p*/ 1) + .expect(/*r*/ 1) + .up_to_n_times(/*n*/ 1) + .mount(&http) + .await; + let mut view = AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list); + view.open( + AppServerRequestHandle::Remote(client.request_handle()), + crate::tui::FrameRequester::test_dummy(), + Vec::new(), + std::sync::Arc::clone(&local.config), + ); + crate::analytics::test_support::settle(&mut view).await; + assert!(matches!(view.account, Load::Error(_))); + assert_eq!( + http.received_requests() + .await + .unwrap() + .iter() + .map(|request| request.url.path()) + .collect::>(), + ["/backend-api/wham/accounts/check"] + ); + view.end_date = "2026-09-09".parse().unwrap(); + let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new( + /*width*/ 100, /*height*/ 40, + )) + .unwrap(); + terminal + .draw(|frame| view.render(frame.area(), frame.buffer_mut())) + .unwrap(); + insta::assert_snapshot!("account_plan_error", terminal.backend().to_string()); + + Mock::given(method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/accounts/check")) + .and(header("chatgpt-account-id", "account-a")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({ + "accounts": [{"id": "account-a", "plan_type": "enterprise"}] + }))) + .with_priority(/*p*/ 1) + .expect(/*r*/ 1) + .mount(&http) + .await; + Mock::given(method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/profiles/me")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({"stats": {}}))) + .mount(&http) + .await; + view.refresh(); + crate::analytics::test_support::settle(&mut view).await; + assert_eq!( + view.visible_sections(), + &[ + Section::Summary, + Section::Credits, + Section::Usage, + Section::Plugins, + Section::Skills, + ] + ); + assert_eq!( + view.account.ready(), + Some(&codex_protocol::account::PlanType::Enterprise) + ); + for section in view.visible_sections() { + if section.report().is_some() { + assert!(view.sections[*section].history.ready().is_some()); + } + } + http.verify().await; + view.cancel_loads(); + client.shutdown().await.unwrap(); + server.await.unwrap(); +} diff --git a/codex-rs/tui/src/analytics/dashboard.rs b/codex-rs/tui/src/analytics/dashboard.rs index 099862fe3d..885208a3a6 100644 --- a/codex-rs/tui/src/analytics/dashboard.rs +++ b/codex-rs/tui/src/analytics/dashboard.rs @@ -43,7 +43,7 @@ impl AnalyticsView { secondary_style().dim() }; let mut content = Vec::new(); - if !matches!(section, Section::Chats | Section::Plan) { + if !matches!(section, Section::Chats | Section::Plan | Section::Summary) { content.push( format!( "{}d{}", @@ -100,6 +100,7 @@ impl AnalyticsView { )); } else { match section { + Section::Summary => content.extend(self.summary_lines(inner_width)), Section::Usage | Section::Plugins | Section::Credits diff --git a/codex-rs/tui/src/analytics/dashboard_tests.rs b/codex-rs/tui/src/analytics/dashboard_tests.rs index b3bd2a3cc1..066facfbfb 100644 --- a/codex-rs/tui/src/analytics/dashboard_tests.rs +++ b/codex-rs/tui/src/analytics/dashboard_tests.rs @@ -12,7 +12,10 @@ fn dashboard_focus_uses_marker_and_default_foreground_title() { for section in [Section::Usage, Section::Activity] { view.section = section; let (lines, _) = view.dashboard_lines(/*width*/ 140, /*height*/ 56); - let title = &lines[0]; + let title = lines + .iter() + .find(|line| line.spans.iter().any(|span| span.content == "▸")) + .unwrap(); let marker = title.spans.iter().find(|span| span.content == "▸").unwrap(); assert_eq!(marker.style, accent_style()); let focused_title = title @@ -32,8 +35,9 @@ fn dashboard_cards_align_and_keep_stable_summary_heights() { 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"), + ("1 Summary", "2 Total usage history"), + ("3 Messages", "4 Plugins called"), + ("5 Skills used", "6 Top chats"), ] { assert!( wide.lines() @@ -64,7 +68,7 @@ fn dashboard_cards_align_and_keep_stable_summary_heights() { ); } let narrow = screen(&mut view, /*width*/ 72, /*height*/ 48); - assert!(narrow.contains("1 Total usage history")); + assert!(narrow.contains("1 Summary")); let context = ( view.section, view.sections.0.each_ref().map(|state| state.cursor), diff --git a/codex-rs/tui/src/analytics/data.rs b/codex-rs/tui/src/analytics/data.rs index 505c756dbf..f4421db2a8 100644 --- a/codex-rs/tui/src/analytics/data.rs +++ b/codex-rs/tui/src/analytics/data.rs @@ -146,6 +146,27 @@ pub(super) fn amount(value: f64) -> String { grouped } +/// Compact headline and axis labels to one decimal, preserving small signed adjustments. +pub(super) fn compact_amount(value: f64) -> String { + if value.abs() < 1_000.0 || !value.is_finite() { + return amount(value); + } + let units = ["K", "M", "B", "T"]; + let mut scaled = value / 1_000.0; + let mut unit = 0; + // Promote before rounding would produce a label such as 1000K. + while scaled.abs() >= 999.95 && unit + 1 < units.len() { + scaled /= 1_000.0; + unit += 1; + } + let rounded = format!("{:.1}", (scaled * 10.0).round() / 10.0); + format!( + "{}{}", + rounded.trim_end_matches('0').trim_end_matches('.'), + units[unit] + ) +} + /// Align ordinary credit amounts to two decimals without hiding tiny adjustments. pub(super) fn credit_amount(value: f64) -> String { let formatted = amount(value); diff --git a/codex-rs/tui/src/analytics/data_tests.rs b/codex-rs/tui/src/analytics/data_tests.rs index beb5fda2cd..133f54440e 100644 --- a/codex-rs/tui/src/analytics/data_tests.rs +++ b/codex-rs/tui/src/analytics/data_tests.rs @@ -94,3 +94,39 @@ async fn extended_chat_load_budget_still_times_out_and_can_be_cancelled() { load.poll(); assert_eq!(load.message(), Some("Request timed out. Press R to retry.")); } + +#[test] +fn compact_counts_round_promote_units_and_preserve_small_adjustments() { + let values = [ + 0.0, + 959.0, + 1_000.0, + 9_749.0, + 2_901.0, + 12_280_365_226.0, + 142_300_000_000.0, + 999_999.0, + 999_999_999.0, + -1_250.0, + -0.000004, + 1_200_000_000_000.0, + ]; + assert_eq!( + values.map(compact_amount), + [ + "0", + "959", + "1K", + "9.7K", + "2.9K", + "12.3B", + "142.3B", + "1M", + "1B", + "-1.3K", + "-0.000004", + "1.2T" + ] + .map(str::to_string) + ); +} diff --git a/codex-rs/tui/src/analytics/panels.rs b/codex-rs/tui/src/analytics/panels.rs index d9492c29a2..ea5e6048a7 100644 --- a/codex-rs/tui/src/analytics/panels.rs +++ b/codex-rs/tui/src/analytics/panels.rs @@ -79,7 +79,10 @@ impl AnalyticsView { }), ); } - if section == Section::Plan { + if section == Section::Summary { + selection = 0..1; + lines.extend(self.summary_lines(width)); + } else if section == Section::Plan { let (plan, detail) = self.plan_lines(width); selection = lines.len() + detail.start..lines.len() + detail.end; lines.extend(plan); diff --git a/codex-rs/tui/src/analytics/plot.rs b/codex-rs/tui/src/analytics/plot.rs index bbbaa80bc6..e9f0357adb 100644 --- a/codex-rs/tui/src/analytics/plot.rs +++ b/codex-rs/tui/src/analytics/plot.rs @@ -8,6 +8,7 @@ mod painting; use self::layout::Layout; use self::painting::Band; use super::data; +use super::data::compact_amount as tick; use super::render::parts; use crate::analytics::models::AccountAnalyticsDay; use crate::analytics::models::AccountAnalyticsUnit; @@ -110,8 +111,7 @@ pub(super) fn chart( &[Band::Positive][..] }; let peak = axis_max(peak(days, categories), unit); - let label_width = - tick(peak, unit).width().max(tick(peak / 2.0, unit).width()) + bands.len() - 1 + 2; + let label_width = tick(peak).width().max(tick(peak / 2.0).width()) + bands.len() - 1 + 2; let layout = Layout::new(days.len(), cursor, width, height, label_width, bands); let renderer = Renderer { days, @@ -164,20 +164,13 @@ struct Renderer<'a> { fn amount(value: f64, unit: AccountAnalyticsUnit) -> String { match unit { AccountAnalyticsUnit::RelativeUsage => data::amount(value), - AccountAnalyticsUnit::Count | AccountAnalyticsUnit::Tokens => data::amount(value.round()), + AccountAnalyticsUnit::Count | AccountAnalyticsUnit::Tokens => { + data::compact_amount(value.round()) + } AccountAnalyticsUnit::Credits => data::credit_amount(value), } } -fn tick(value: f64, unit: AccountAnalyticsUnit) -> String { - match unit { - AccountAnalyticsUnit::Credits => data::amount(value), - AccountAnalyticsUnit::RelativeUsage - | AccountAnalyticsUnit::Count - | AccountAnalyticsUnit::Tokens => amount(value, unit), - } -} - #[cfg(test)] #[path = "plot_tests.rs"] mod tests; diff --git a/codex-rs/tui/src/analytics/plot/painting.rs b/codex-rs/tui/src/analytics/plot/painting.rs index 358bad4d0a..03f6633cf1 100644 --- a/codex-rs/tui/src/analytics/plot/painting.rs +++ b/codex-rs/tui/src/analytics/plot/painting.rs @@ -118,7 +118,7 @@ impl Renderer<'_> { ticks.push((baseline + height, -self.peak)); } for (y, value) in ticks { - let label = tick(value, self.unit); + let label = tick(value); let x = axis_width.saturating_sub(label.len() + 1); buf.set_line( x as u16, diff --git a/codex-rs/tui/src/analytics/plot_tests.rs b/codex-rs/tui/src/analytics/plot_tests.rs index 935eeea7b3..6312072ec5 100644 --- a/codex-rs/tui/src/analytics/plot_tests.rs +++ b/codex-rs/tui/src/analytics/plot_tests.rs @@ -311,14 +311,21 @@ fn partial_cells_use_the_midpoint_and_descend_from_the_baseline() { #[test] fn oversized_axes_are_hidden_instead_of_truncated() { - let day = AccountAnalyticsDay { - date: "2026-01-15".parse().unwrap(), - total: 100_000_000.0, - values: vec![value("a", /*value*/ 100_000_000.0)], - }; - let days = [(day.date, Some(&day))]; let mut snapshots = Vec::new(); - for width in [8, 30, 35, 44] { + for (width, total) in [ + (8, 100_000_000.0), + (30, 100_000_000.0), + (35, 100_000_000.0), + (44, 100_000_000.0), + (30, 1e30), + ] { + let day = AccountAnalyticsDay { + date: "2026-01-15".parse().unwrap(), + total, + values: vec![value("a", total)], + }; + let days = [(day.date, Some(&day))]; + let rendered = chart( &days, &day.values, @@ -328,9 +335,9 @@ fn oversized_axes_are_hidden_instead_of_truncated() { AccountAnalyticsUnit::Tokens, ); let baseline = rendered.lines[5].to_string(); - assert_eq!(baseline.starts_with('─'), width < 36); + assert_eq!(baseline.starts_with('─'), width < 30 || total == 1e30); if width == 8 { - assert!(rendered.lines[0].to_string().trim().is_empty()); + assert!(rendered.lines[0].to_string().contains("100M")); } snapshots.push(format!("width={width}\n{}", display(&rendered))); } diff --git a/codex-rs/tui/src/analytics/profile_identity_tests.rs b/codex-rs/tui/src/analytics/profile_identity_tests.rs new file mode 100644 index 0000000000..0b5c04301b --- /dev/null +++ b/codex-rs/tui/src/analytics/profile_identity_tests.rs @@ -0,0 +1,28 @@ +//! In-flight profile responses cannot expose data after the active account changes. +use super::*; +use pretty_assertions::assert_eq; + +#[tokio::test] +async fn profile_rejects_account_change_during_success_or_error_response() { + let server = MockServer::start().await; + for status in [200, 404, 503] { + server.reset().await; + let (home, live) = live(&server, "plus").await; + let home_path = home.path().to_path_buf(); + Mock::given(method("GET")) + .and(path("/backend-api/wham/profiles/me")) + .and(header("chatgpt-account-id", "account-a")) + .respond_with(move |_: &wiremock::Request| { + sign_in(&home_path, "account-a", "user-b", "plus"); + ResponseTemplate::new(status) + .set_body_json(json!({"stats":{"lifetime_tokens":123}})) + }) + .expect(/*r*/ 1) + .mount(&server) + .await; + assert_eq!( + live.profile().await.err().unwrap(), + "Account changed. Press R to refresh Analytics." + ); + } +} diff --git a/codex-rs/tui/src/analytics/render.rs b/codex-rs/tui/src/analytics/render.rs index fecaef1e79..ce5c1d3ca5 100644 --- a/codex-rs/tui/src/analytics/render.rs +++ b/codex-rs/tui/src/analytics/render.rs @@ -92,8 +92,10 @@ 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 = !matches!(self.section, Section::Chats | Section::Plan) - && !self.visible_sections().is_empty(); + let show_range = !matches!( + self.section, + Section::Chats | Section::Plan | Section::Summary + ) && !self.visible_sections().is_empty(); let hint = |action| { self.keymap .primary_hint(action) @@ -104,15 +106,26 @@ impl AnalyticsView { format!("R refresh · {} back · q close", hint(ListAction::Cancel)) } else if self.group_picker.is_some() { format!( - "{}/{} group · {} select · {} cancel", + "{}/{} {} · {} select · {} cancel", hint(ListAction::MoveUp), hint(ListAction::MoveDown), + if self.section == Section::Summary { + "view" + } else { + "group" + }, hint(ListAction::Accept), hint(ListAction::Cancel) ) } else { let navigation = if !self.zoomed { format!("{}/z maximize", hint(ListAction::Accept)) + } else if self.section == Section::Summary { + format!( + "{}/{} scroll", + hint(ListAction::MoveUp), + hint(ListAction::MoveDown) + ) } else if self.section == Section::Plan { format!( "{}/{} window · {}/{} period · {} details", @@ -152,6 +165,8 @@ impl AnalyticsView { if show_range { "r 7/30d · " } else { "" }, if self.group_options().len() < 2 { "" + } else if self.section == Section::Summary { + "g view · " } else { "g group · " }, @@ -275,7 +290,12 @@ impl AnalyticsView { .account .message() .unwrap_or("Analytics is not available for this account type."); - (vec![Line::from(message.to_string())], 0..1) + let lines = textwrap::wrap(message, width) + .into_iter() + .map(|line| Line::from(line.into_owned())) + .collect::>(); + let selection = 0..lines.len(); + (lines, selection) } else if self.zoomed { let chart_height = (usize::from(body.height) / 5).clamp(/*min*/ 4, /*max*/ 10); @@ -289,12 +309,24 @@ impl AnalyticsView { panel = self.panel(self.section, width, height); } } + let center_summary = self.section == Section::Summary + && self.profile.ready().is_some() + && self.group_picker.is_none(); + if center_summary { + // Keep the section heading and rule fixed; center the profile below them. + let padding = usize::from(body.height).saturating_sub(panel.lines.len()) / 2; + panel + .lines + .splice(2..2, std::iter::repeat_n(Line::default(), padding)); + } let selection = if panel.lines.len() <= usize::from(body.height) { 0..panel.lines.len() } else { panel.selection }; - panel.lines.push(Line::default()); + if !center_summary { + panel.lines.push(Line::default()); + } (panel.lines, selection) } else { self.dashboard_lines(width, usize::from(body.height)) diff --git a/codex-rs/tui/src/analytics/sections.rs b/codex-rs/tui/src/analytics/sections.rs index bb8435594a..efeebca7da 100644 --- a/codex-rs/tui/src/analytics/sections.rs +++ b/codex-rs/tui/src/analytics/sections.rs @@ -16,6 +16,7 @@ pub(super) enum Section { Activity, Skills, Plan, + Summary, } impl Section { @@ -25,7 +26,7 @@ impl Section { Self::Usage => Some(Report::Usage), Self::Plugins => Some(Report::Plugins), Self::Credits => Some(Report::Credits), - Self::Chats | Self::Plan => None, + Self::Chats | Self::Plan | Self::Summary => None, Self::Activity => Some(Report::Messages), Self::Skills => Some(Report::Skills), } @@ -33,7 +34,7 @@ impl Section { } /// Section-only indexing keeps report identity separate from array positions. -pub(super) struct SectionStates(pub(super) [SectionState; 7]); +pub(super) struct SectionStates(pub(super) [SectionState; 8]); impl std::ops::Index
for SectionStates { type Output = SectionState; @@ -82,6 +83,7 @@ impl AnalyticsView { } self.chats.poll(); self.tasks.poll(); + self.profile.poll(); self.plan.poll(); self.account.poll(); self.start_reports(); @@ -112,13 +114,20 @@ impl AnalyticsView { pub(super) fn range_group(&self, section: Section) -> RangeGroup { match section { Section::Usage if self.business() => RangeGroup::Activity, - Section::Usage | Section::Credits | Section::Chats | Section::Plan => RangeGroup::Usage, + Section::Usage + | Section::Credits + | Section::Chats + | Section::Plan + | Section::Summary => 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 section == Section::Summary { + return super::summary::VIEWS[group].label(); + } if group == 0 && (section == Section::Credits || (section == Section::Usage && !self.consumer_attribution())) @@ -146,6 +155,7 @@ impl AnalyticsView { pub(super) fn visible_sections(&self) -> &'static [Section] { match self.account_kind() { Some(AccountKind::Consumer) if self.plan.enabled => &[ + Section::Summary, Section::Usage, Section::Plan, Section::Activity, @@ -154,6 +164,7 @@ impl AnalyticsView { Section::Chats, ], Some(AccountKind::Consumer) => &[ + Section::Summary, Section::Usage, Section::Activity, Section::Plugins, @@ -164,6 +175,7 @@ impl AnalyticsView { if super::models::thread_usage_supported(self.account.ready().copied()) => { &[ + Section::Summary, Section::Credits, Section::Usage, Section::Plugins, @@ -172,12 +184,14 @@ impl AnalyticsView { ] } Some(AccountKind::Business | AccountKind::Enterprise) => &[ + Section::Summary, Section::Credits, Section::Usage, Section::Plugins, Section::Skills, ], - Some(AccountKind::Unknown) | None => &[], + Some(AccountKind::Unknown) => &[Section::Summary], + None => &[], } } @@ -206,13 +220,15 @@ impl AnalyticsView { usage_groups[0] }; } - if let Some(live) = &self.live + if visible.contains(&Section::Credits) + && let Some(live) = &self.live && !live .credit_groups() .contains(&self.sections[Section::Credits].group) { self.sections[Section::Credits].group = live.credit_groups()[0]; } + self.load_summary(); for section in visible { if section.report().is_some() { self.load_report(*section); @@ -259,7 +275,7 @@ impl AnalyticsView { let index = self.range_group(self.section); self.ranges[index as usize] ^= 1; for section in self.visible_sections() { - if matches!(section, Section::Chats | Section::Plan) + if matches!(section, Section::Chats | Section::Plan | Section::Summary) || self.range_group(*section) != index { continue; diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__account_plan_error.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__account_plan_error.snap new file mode 100644 index 0000000000..7fe9dbd95e --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__account_plan_error.snap @@ -0,0 +1,45 @@ +--- +source: tui/src/analytics/connection_tests.rs +assertion_line: 189 +expression: terminal.backend().to_string() +--- +" Analytics · Live account " +" " +" " +" " +" Couldn't load account plan. Press R to retry Analytics. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" R refresh · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_error.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_error.snap new file mode 100644 index 0000000000..abc08f1d82 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_error.snap @@ -0,0 +1,52 @@ +--- +source: tui/src/analytics/connection_tests.rs +expression: terminal.backend().to_string() +--- +" Analytics · Live account · analytics@example.test " +" " +" [1 Summary] 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" Report request failed. Press R to retry. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–5 section · z dashboard · ↑/↓ scroll " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_recovered.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_recovered.snap new file mode 100644 index 0000000000..d04262a82f --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__client__connection_tests__local_profile_recovered.snap @@ -0,0 +1,52 @@ +--- +source: tui/src/analytics/connection_tests.rs +expression: terminal.backend().to_string() +--- +" Analytics · Live account · analytics@example.test " +" " +" [1 Summary] 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" " +" " +" " +" " +" " +" " +" Local account " +" @local-user " +" Enterprise " +" " +" 12.3K 12.3K — — — " +" Lifetime tokens Peak tokens Longest chat Current streak Longest streak " +" " +" Token activity [Daily] Weekly Cumulative " +" Last 12 months " +" " +" Nov Dec Jan Feb Mar Apr May Jun Jul Aug " +" Su □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Mo □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Tu □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" We □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ ■ " +" Th □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Fr □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Sa □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" " +" Less □ ■ ■ ■ ■ More " +" " +" Activity insights Most used plugins and skills " +" " +" Fast Mode — Plugin and skill usage unavailable. " +" Most used reasoning — " +" Skills explored — " +" Total skills used — " +" Total chats — " +" " +" " +" " +" " +" " +" " +" tab/1–5 section · z dashboard · ↑/↓ scroll " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " 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 index 42810b143e..a11a26728b 100644 --- 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 @@ -2,7 +2,7 @@ source: tui/src/analytics/client_tests.rs expression: terminal.backend().to_string() --- -" Analytics · Live account · analytics@example.test · account-a " +" Analytics · Live account · analytics@example.test " " " " " " " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__period_navigation_preserves_unknowns_and_resets_on_new_snapshot.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__period_navigation_preserves_unknowns_and_resets_on_new_snapshot.snap index ad3ee03c0b..8f82620a5f 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__period_navigation_preserves_unknowns_and_resets_on_new_snapshot.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__period_navigation_preserves_unknowns_and_resets_on_new_snapshot.snap @@ -1,11 +1,10 @@ --- source: tui/src/analytics/plan_tests.rs -assertion_line: 56 expression: "output.join(\"\\n\")" --- " Analytics · Live account " " " -" 1 Total usage history [2 Plan usage history] 3 Messages 4 Plugins called 5 Skills used 6 Top chats " +" 1 Summary 2 Total usage history [3 Plan usage history] 4 Messages 5 Plugins called 6 Skills used 7 Top chats " " " " ▎ Plan usage history By feature " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -31,13 +30,13 @@ expression: "output.join(\"\\n\")" " " " " " " -" tab/1–6 section · z dashboard · ←/→ window · ↑/↓ period · enter details " +" tab/1–7 section · z dashboard · ←/→ window · ↑/↓ period · enter details " " g group · R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" Total usage history [Plan usage history] Messages " -" Plugins called Skills used Top chats " +" Summary Total usage history [Plan usage history] " +" Messages Plugins called Skills used Top chats " " " " ▎ Plan usage history By feature " " ────────────────────────────────────────────────────── " @@ -60,7 +59,7 @@ expression: "output.join(\"\\n\")" " " " " " " -" tab/1–6 section · z dashboard · ←/→ window · ↑/↓ " +" tab/1–7 section · z dashboard · ←/→ window · ↑/↓ " " period · enter details " " g group · R refresh · pgup/pgdn scroll · esc back · q " " close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__plan_overview_escape_closes_with_retained_expansion.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__plan_overview_escape_closes_with_retained_expansion.snap index 2caadc1cfb..a3266e9059 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__plan_overview_escape_closes_with_retained_expansion.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plan__tests__plan_overview_escape_closes_with_retained_expansion.snap @@ -1,65 +1,64 @@ --- source: tui/src/analytics/plan_tests.rs -assertion_line: 210 expression: output --- " Analytics · Live account " " " -" ╭─ 1 Total usage history ────────────────────────────╮ ╭─ ▸ 2 Plan usage history ─────────────────────────────╮ " +" ╭─ 1 Summary ────────────────────────────────────────╮ ╭─ 2 Total usage history ────────────────────────────╮ " " │ │ │ │ " -" │ 7d │ │ By feature │ " -" │ By feature │ │ › 5-hour limits │ " -" │ Total usage · relative units │ │ │ " -" │ 86 │ │ Latest period · 12 total 125% │ " -" │ 100 ▁▁▁▁ ▅▅▅▅ ▅▅▅▅ ▇▇▇▇ ▆▆▆▆ ████ ▆▆▆▆ │ │ │ " -" │ 0 ─────────────────────────────────────────────── │ │ Weekly limits │ " -" │ ▲ │ │ │ " -" │ 27 28 29 30 31 1 2 │ │ Latest period · 1 total Not available │ " -" │ │ │ Some periods aren't available yet │ " -" │ Sep 2 · 86 usage units │ │ Usage as of Sep 2 12:00 UTC │ " -" │ ● Tasks 64.0% │ │ Available since Sep 1 00:00 UTC │ " -" │ ● Code review 14.0% │ │ Amounts and period boundaries are approximate; │ " -" │ │ │ recent activity may be delayed │ " +" │ No history has been reported. │ │ 7d │ " +" │ │ │ By feature │ " +" │ │ │ Total usage · relative units │ " +" │ │ │ 86 │ " +" │ │ │ 100 ▁▁▁▁ ▅▅▅▅ ▅▅▅▅ ▇▇▇▇ ▆▆▆▆ ████ ▆▆▆▆ │ " +" │ │ │ 0 ─────────────────────────────────────────────── │ " +" │ │ │ ▲ │ " +" │ │ │ 27 28 29 30 31 1 2 │ " " │ │ │ │ " +" │ │ │ Sep 2 · 86 usage units │ " +" │ │ │ ● Tasks 64.0% │ " +" │ │ │ ● Code review 14.0% │ " " │ │ │ │ " " ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " " " -" ╭─ 3 Messages ───────────────────────────────────────╮ ╭─ 4 Plugins called ─────────────────────────────────╮ " +" ╭─ ▸ 3 Plan usage history ─────────────────────────────╮ ╭─ 4 Messages ───────────────────────────────────────╮ " +" │ │ │ │ " +" │ By feature │ │ 7d │ " +" │ › 5-hour limits │ │ By model │ " +" │ │ │ 574 reported messages │ " +" │ Latest period · 12 total 125% │ │ 103 │ " +" │ │ │ 150 ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ ▄▄▄▄ ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ │ " +" │ Weekly limits │ │ 0 ─────────────────────────────────────────────── │ " +" │ │ │ ▲ │ " +" │ Latest period · 1 total Not available │ │ 27 28 29 30 31 1 2 │ " +" │ Some periods aren't available yet │ │ │ " +" │ Usage as of Sep 2 12:00 UTC │ │ Sep 2 · 103 messages │ " +" │ Available since Sep 1 00:00 UTC │ │ ● GPT-5.6-Sol 61 │ " +" │ … z to maximize │ │ ● GPT-5.6-Terra 25 │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " +" " +" ╭─ 5 Plugins called ─────────────────────────────────╮ ╭─ 6 Skills used ────────────────────────────────────╮ " " │ │ │ │ " " │ 7d │ │ 7d │ " -" │ By model │ │ 186 calls │ " -" │ 574 reported messages │ │ 29 │ " -" │ 103 │ │ 40 ▆▆▆▆ ▄▄▄▄ ▅▅▅▅ ▅▅▅▅ ▄▄▄▄ ▄▄▄▄ ▅▅▅▅ │ " -" │ 150 ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ ▄▄▄▄ ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ │ │ 0 ──────────────────────────────────────────────── │ " -" │ 0 ─────────────────────────────────────────────── │ │ ▲ │ " -" │ ▲ │ │ 27 28 29 30 31 1 2 │ " -" │ 27 28 29 30 31 1 2 │ │ │ " -" │ │ │ Sep 2 · 29 calls │ " -" │ Sep 2 · 103 messages │ │ ● Slack 10 │ " -" │ ● GPT-5.6-Sol 61 │ │ ● Figma 10 │ " -" │ ● GPT-5.6-Terra 25 │ │ │ " +" │ 186 calls │ │ 54 uses │ " +" │ 29 │ │ 11 │ " +" │ 40 ▆▆▆▆ ▄▄▄▄ ▅▅▅▅ ▅▅▅▅ ▄▄▄▄ ▄▄▄▄ ▅▅▅▅ │ │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ " +" │ 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 │ " " │ │ │ │ " " │ │ │ │ " " ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " " " -" ╭─ 5 Skills used ────────────────────────────────────╮ ╭─ 6 Top chats ──────────────────────────────────────╮ " -" │ │ │ │ " -" │ 7d │ │ No history has been reported. │ " -" │ 54 uses │ │ │ " -" │ 11 │ │ │ " -" │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ │ │ " -" │ 0 ──────────────────────────────────────────────── │ │ │ " -" │ ▲ │ │ │ " -" │ 27 28 29 30 31 1 2 │ │ │ " -" │ │ │ │ " -" │ Sep 2 · 11 uses │ │ │ " -" │ ● Testing 4 │ │ │ " -" │ ● Code review 3 │ │ │ " -" │ │ │ │ " -" │ │ │ │ " -" │ │ │ │ " -" │ │ │ │ " -" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " -" tab/1–6 section · enter/z maximize " +" ╭─ 7 Top chats ──────────────────────────────────────╮ " +" │ │ " +" │ No history has been reported. │ " +" │ │ " +" │ │ " +" tab/1–7 section · enter/z maximize " " g group · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap index d77e9815bf..b55f3fc92d 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap @@ -3,7 +3,7 @@ source: tui/src/analytics/plot_tests.rs expression: "snapshots.join(\"\\n\\n\")" --- width=8 - + 100M █████ █████ █████ @@ -14,24 +14,102 @@ Jan 15 Palette: 0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) -1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) +3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): -0: 0:0 -1: 0:0 1:1 6:0 -2: 0:0 1:1 6:0 -3: 0:0 1:1 6:0 -4: 0:0 1:1 6:0 -5: 0:2 -6: 0:0 3:3 4:0 +0: 0:0 1:1 5:0 +1: 0:0 1:2 6:0 +2: 0:0 1:2 6:0 +3: 0:0 1:2 6:0 +4: 0:0 1:2 6:0 +5: 0:3 +6: 0:0 3:1 4:0 7: 0:4 6:0 width=30 - 100,000,000 - ████████████ - ████████████ + 100M + 100M ████████████ + ████████████ + 50M ████████████ + ████████████ + 0 ──────────────────────── + ▲ + Jan 15 + +Palette: +0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) +1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() +3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) +4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +Rows (column:palette): +0: 0:0 15:1 19:0 +1: 0:0 1:2 5:0 12:3 24:0 +2: 0:0 12:3 24:0 +3: 0:0 2:2 5:0 12:3 24:0 +4: 0:0 12:3 24:0 +5: 0:0 4:2 5:0 6:2 +6: 0:0 17:1 18:0 +7: 0:0 14:4 20:0 + +width=35 + 100M + 100M ████████████ + ████████████ + 50M ████████████ + ████████████ + 0 ───────────────────────────── + ▲ + Jan 15 + +Palette: +0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) +1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() +3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) +4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +Rows (column:palette): +0: 0:0 18:1 22:0 +1: 0:0 1:2 5:0 14:3 26:0 +2: 0:0 14:3 26:0 +3: 0:0 2:2 5:0 14:3 26:0 +4: 0:0 14:3 26:0 +5: 0:0 4:2 5:0 6:2 +6: 0:0 20:1 21:0 +7: 0:0 17:4 23:0 + +width=44 + 100M + 100M ████████████ + ████████████ + 50M ████████████ + ████████████ + 0 ────────────────────────────────────── + ▲ + Jan 15 + +Palette: +0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) +1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() +3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) +4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +Rows (column:palette): +0: 0:0 22:1 26:0 +1: 0:0 1:2 5:0 19:3 31:0 +2: 0:0 19:3 31:0 +3: 0:0 2:2 5:0 19:3 31:0 +4: 0:0 19:3 31:0 +5: 0:0 4:2 5:0 6:2 +6: 0:0 24:1 25:0 +7: 0:0 21:4 27:0 + +width=30 + + 1000000000000000000T + ▅▅▅▅▅▅▅▅▅▅▅▅ ████████████ ████████████ ────────────────────────────── @@ -45,63 +123,11 @@ Palette: 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): -0: 0:0 9:1 20:0 -1: 0:0 9:2 21:0 +0: 0:0 +1: 0:0 4:1 24:0 2: 0:0 9:2 21:0 3: 0:0 9:2 21:0 4: 0:0 9:2 21:0 5: 0:3 6: 0:0 14:1 15:0 7: 0:0 11:4 17:0 - -width=35 - 100,000,000 - ████████████ - ████████████ - ████████████ - ████████████ -─────────────────────────────────── - ▲ - Jan 15 - -Palette: -0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) -1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() -Rows (column:palette): -0: 0:0 12:1 23:0 -1: 0:0 11:2 23:0 -2: 0:0 11:2 23:0 -3: 0:0 11:2 23:0 -4: 0:0 11:2 23:0 -5: 0:3 -6: 0:0 17:1 18:0 -7: 0:0 14:4 20:0 - -width=44 - 100,000,000 - 100,000,000 ████████████ - ████████████ - 50,000,000 ████████████ - ████████████ - 0 ─────────────────────────────── - ▲ - Jan 15 - -Palette: -0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) -1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() -Rows (column:palette): -0: 0:0 23:1 34:0 -1: 0:0 1:2 12:0 22:3 34:0 -2: 0:0 22:3 34:0 -3: 0:0 2:2 12:0 22:3 34:0 -4: 0:0 22:3 34:0 -5: 0:0 11:2 12:0 13:2 -6: 0:0 28:1 29:0 -7: 0:0 25:4 31:0 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 index 323d7a2b1a..84f1033ec6 100644 --- 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 @@ -1,12 +1,42 @@ --- source: tui/src/analytics/account_layout_tests.rs -assertion_line: 47 expression: "screens.join(\"\\n\")" --- +" Analytics · Live account " +" " +" [1 Summary] 2 Total usage history 3 Messages 4 Plugins called 5 Skills used 6 Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" No history has been reported. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · 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 5 Top chats " +" 1 Summary [2 Total usage history] 3 Messages 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Total usage history By feature " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -31,13 +61,13 @@ expression: "screens.join(\"\\n\")" " ● Images 11.6% " " ● Other 10.5% " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 5 Top chats " +" 1 Summary 2 Total usage history [3 Messages] 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Messages By model " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -62,13 +92,13 @@ expression: "screens.join(\"\\n\")" " ● GPT-5.5 10 " " ● Other 7 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages [4 Plugins called] 5 Skills used 6 Top chats " " " " ▎ Plugins called " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -93,13 +123,13 @@ expression: "screens.join(\"\\n\")" " ● GitHub 5 " " ● Notion 4 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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] 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -124,12 +154,12 @@ expression: "screens.join(\"\\n\")" " ● Planning 3 " " ● Debugging 1 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 section · z dashboard · ←/→ day · enter details " " r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -155,89 +185,120 @@ expression: "screens.join(\"\\n\")" " " " " " " -" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · s sort · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" ╭─ 1 Total usage history ──────────────────────────────────────╮ ╭─ 2 Messages ─────────────────────────────────────────────────╮ " +" ╭─ 1 Summary ──────────────────────────────────────────────────╮ ╭─ 2 Total usage history ──────────────────────────────────────╮ " " │ │ │ │ " -" │ 7d │ │ 7d │ " -" │ By feature │ │ By model │ " -" │ Total usage · relative units │ │ 574 reported messages │ " -" │ 86 │ │ 103 │ " -" │ 100 ▃▃▃▃▃ ▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ │ 150 ▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ " -" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ " -" │ 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 │ " +" │ No history has been reported. │ │ 7d │ " +" │ │ │ By feature │ " +" │ │ │ Total usage · relative units │ " +" │ │ │ 86 │ " +" │ │ │ 100 ▃▃▃▃▃ ▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ " +" │ │ │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ " +" │ │ │ 0 ───────────────────────────────────────────────────────── │ " +" │ │ │ ▲ │ " +" │ │ │ 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 │ " +" │ │ │ Sep 2 · 86 usage units │ " +" │ │ │ ● Tasks 64.0% │ " +" │ │ │ ● Code review 14.0% │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " " " -" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ 4 Skills used ──────────────────────────────────────────────╮ " +" ╭─ 3 Messages ─────────────────────────────────────────────────╮ ╭─ 4 Plugins called ───────────────────────────────────────────╮ " " │ │ │ │ " " │ 7d │ │ 7d │ " -" │ 186 calls │ │ 54 uses │ " -" │ 29 │ │ 11 │ " -" │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ │ 20 │ " -" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ " -" │ 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 │ " +" │ By model │ │ 186 calls │ " +" │ 574 reported messages │ │ 29 │ " +" │ 103 │ │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ " +" │ 150 ▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ " +" │ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ │ 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 · 103 messages │ │ ● Slack 10 │ " +" │ ● GPT-5.6-Sol 61 │ │ ● Figma 10 │ " +" │ ● GPT-5.6-Terra 25 │ │ │ " " │ │ │ │ " -" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " -" │ ● Slack 10 │ │ ● Testing 4 │ " -" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 5 Skills used ──────────────────────────────────────────────╮ ╭─ ▸ 6 Top chats ────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ No history has been reported. │ " +" │ 54 uses │ │ │ " +" │ 11 │ │ │ " +" │ 20 │ │ │ " +" │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ │ │ " +" │ 0 ────────────────────────────────────────────────────────── │ │ │ " +" │ ▲ │ │ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ │ " +" │ │ │ │ " +" │ Sep 2 · 11 uses │ │ │ " +" │ ● Testing 4 │ │ │ " +" │ ● Code review 3 │ │ │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " " " -" ╭─ ▸ 5 Top chats ────────────────────────────────────────────────╮ " -" │ │ " -" │ No history has been reported. │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" " -" tab/1–5 section · s sort · enter/z maximize " +" tab/1–6 section · s sort · enter/z maximize " " R refresh · pgup/pgdn scroll · esc back · q close " +" Analytics · Live account " +" " +" [1 Summary] 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used 6 " +" Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" No history has been reported. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · 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 5 Top chats " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 Skills used 6 " +" Top chats " " " " ▎ Credits usage history By product " " ──────────────────────────────────────────────────────────────────────────────────────────────── " " 431.00 credits " " " " 150 " -" " -" ████████ " -" 75 ▇▇▇▇▇▇▇▇ ▄▄▄▄▄▄▄▄ ████████ 64.00 " -" ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ▇▇▇▇▇▇▇▇ " -" ▇▇▇▇▇▇▇▇ ████████ ████████ ████████ ████████ ████████ " +" ▂▂▂▂▂▂▂▂ " +" ▃▃▃▃▃▃▃▃ ████████ 64.00 " +" 75 ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ▄▄▄▄▄▄▄▄ " +" ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ████████ ████████ " " 0 ████████ ████████ ████████ ████████ ████████ ████████ " " 0 ─────────────────────────────────────────────────────────────────────────────────────────── " " ▲ " @@ -251,28 +312,28 @@ expression: "screens.join(\"\\n\")" " ● IDE extension 10.00 " " ● Web 9.00 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 5 Top chats " +" 1 Summary 2 Credits usage history [3 Token usage history] 4 Plugins called 5 Skills used 6 " +" Top chats " " " " ▎ Token usage history By token type " " ──────────────────────────────────────────────────────────────────────────────────────────────── " " All models · m change model " -" 4,377,500 tokens " +" 4.4M tokens " " " -" 1,000,000 731,000 " -" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ " -" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ " -" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ " -" ████████ ████████ ████████ ████████ ████████ ████████ " -" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " -" 0 ───────────────────────────────────────────────────────────────────────────────────── " +" 1M ▂▂▂▂▂▂▂▂ 731K " +" ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ ████████ ▅▅▅▅▅▅▅▅ " +" 500K ████████ ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ ████████ ████████ " +" 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 · 731,000 tokens " @@ -282,23 +343,23 @@ expression: "screens.join(\"\\n\")" " ● Output 43,000 " " Updated Sep 2 16:00 UTC " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 5 Top chats " +" 1 Summary 2 Credits usage history 3 Token usage history [4 Plugins called] 5 Skills used 6 " +" Top chats " " " " ▎ Plugins called " " ──────────────────────────────────────────────────────────────────────────────────────────────── " " 186 calls " " " " 40 29 " -" ▇▇▇▇▇▇▇▇ " -" ████████ ████████ ▄▄▄▄▄▄▄▄ █████████ " -" 20 ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ ████████ █████████ " -" ████████ ████████ ████████ ████████ ████████ ████████ █████████ " +" ████████ ▂▂▂▂▂▂▂▂ ▂▂▂▂▂▂▂▂▂ " +" ████████ ▃▃▃▃▃▃▃▃ ████████ ▇▇▇▇▇▇▇▇ ▂▂▂▂▂▂▂▂ ▃▃▃▃▃▃▃▃ █████████ " +" 20 ████████ ████████ ████████ ████████ ████████ ████████ █████████ " " ████████ ████████ ████████ ████████ ████████ ████████ █████████ " " ████████ ████████ ████████ ████████ ████████ ████████ █████████ " " 0 ──────────────────────────────────────────────────────────────────────────────────────────── " @@ -313,24 +374,24 @@ expression: "screens.join(\"\\n\")" " ● GitHub 5 " " ● Notion 4 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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] 5 Top chats " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called [5 Skills used] 6 " +" Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────── " " 54 uses " " " " 20 " -" " " 11 " -" 10 ▁▁▁▁▁▁▁▁ ▆▆▆▆▆▆▆▆▆ " -" ▆▆▆▆▆▆▆▆ ████████ ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ █████████ " -" ████████ ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ █████████ " +" ▂▂▂▂▂▂▂▂▂ " +" 10 ▃▃▃▃▃▃▃▃ ▅▅▅▅▅▅▅▅ ▃▃▃▃▃▃▃▃ █████████ " +" ████████ ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ▄▄▄▄▄▄▄▄ █████████ " " ████████ ████████ ████████ ████████ ████████ ████████ █████████ " " 0 ──────────────────────────────────────────────────────────────────────────────────────────── " " ▲ " @@ -344,12 +405,13 @@ expression: "screens.join(\"\\n\")" " ● Planning 3 " " ● Debugging 1 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 section · z dashboard · ←/→ day · enter details " " r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 " +" Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -374,71 +436,70 @@ expression: "screens.join(\"\\n\")" " " " " " " -" " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" ╭─ 1 Credits usage history ────────────────────────────────────╮ ╭─ 2 Token usage history ──────────────────────────────────────╮ " +" ╭─ 1 Summary ──────────────────────────────────────────────────╮ ╭─ 2 Credits usage history ────────────────────────────────────╮ " " │ │ │ │ " -" │ 7d │ │ 7d · All models │ " -" │ By product │ │ By token type │ " -" │ 431.00 credits │ │ 4,377,500 tokens │ " -" │ │ │ 731,000 │ " -" │ 150 ▁▁▁▁▁ ▃▃▃▃▃ 64.00 │ │ 1,000,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 │ " +" │ No history has been reported. │ │ 7d │ " +" │ │ │ By product │ " +" │ │ │ 431.00 credits │ " +" │ │ │ │ " +" │ │ │ 150 ▁▁▁▁▁ ▃▃▃▃▃ 64.00 │ " +" │ │ │ 0 ▄▄▄▄▄ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆▆ │ " +" │ │ │ 0 ───────────────────────────────────────────────────────── │ " +" │ │ │ ▲ │ " +" │ │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ " +" │ │ │ │ " +" │ │ │ Sep 2 · 64.00 credits │ " +" │ │ │ ● CLI 23.00 │ " +" │ │ │ ● Desktop app 22.00 │ " " │ │ │ │ " -" │ 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 ──────────────────────────────────────────────╮ " +" ╭─ 3 Token usage history ──────────────────────────────────────╮ ╭─ 4 Plugins called ───────────────────────────────────────────╮ " " │ │ │ │ " -" │ 7d │ │ 7d │ " -" │ 186 calls │ │ 54 uses │ " -" │ 29 │ │ 11 │ " -" │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ │ 20 │ " -" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ " -" │ 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 │ " +" │ 7d · All models │ │ 7d │ " +" │ By token type │ │ 186 calls │ " +" │ 4.4M tokens │ │ 29 │ " +" │ 731K │ │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ " +" │ 1M ▁▁▁▁▁ ▄▄▄▄▄ ▃▃▃▃▃ ▅▅▅▅▅ ▃▃▃▃▃ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ " +" │ ▂▂▂▂▂ █████ █████ █████ █████ █████ █████ │ │ 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 · 731,000 tokens │ │ ● Slack 10 │ " +" │ ● Cached input 602,000 │ │ ● Figma 10 │ " +" │ ● Uncached input 86,000 │ │ │ " +" │ Updated Sep 2 16:00 UTC │ │ │ " " │ │ │ │ " -" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " -" │ ● Slack 10 │ │ ● Testing 4 │ " -" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 5 Skills used ──────────────────────────────────────────────╮ ╭─ ▸ 6 Top chats ────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 30d active · lifetime credits │ " +" │ 54 uses │ │ │ " +" │ 11 │ │ Q3 planning analysis 120.00 │ " +" │ 20 │ │ Refactor billing usage 113.00 │ " +" │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ │ Other usage 109.00 │ " +" │ 0 ────────────────────────────────────────────────────────── │ │ Launch readiness review 85.00 │ " +" │ ▲ │ │ Plan the week 0.00 │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ │ " +" │ │ │ Local chats · excludes subagents │ " +" │ Sep 2 · 11 uses │ │ │ " +" │ ● Testing 4 │ │ │ " +" │ ● Code review 3 │ │ │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " " " -" ╭─ ▸ 5 Top chats ────────────────────────────────────────────────╮ " -" │ │ " -" │ 30d active · lifetime credits │ " -" │ │ " -" │ Q3 planning analysis 120.00 │ " -" │ Refactor billing usage 113.00 │ " -" │ Other usage 109.00 │ " -" │ Launch readiness review 85.00 │ " -" │ Plan the week 0.00 │ " -" │ │ " -" │ Local chats · excludes subagents │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" " -" tab/1–5 section · enter/z maximize " +" tab/1–6 section · enter/z maximize " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__business_chats_display_only_supplied_dollars.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__business_chats_display_only_supplied_dollars.snap index 581ecac8c1..3bcd7e284f 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__business_chats_display_only_supplied_dollars.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__business_chats_display_only_supplied_dollars.snap @@ -4,7 +4,8 @@ expression: "format!(\"{dollars}\\n{narrow}\")" --- " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 " +" Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -29,15 +30,14 @@ expression: "format!(\"{dollars}\\n{narrow}\")" " " " " " " -" " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" Credits usage history Token usage " -" history Plugins called Skills used " -" [Top chats] " +" Summary Credits usage history Token " +" usage history Plugins called Skills " +" used [Top chats] " " " " ▎ Top chats " " ────────────────────────────────────── " @@ -59,7 +59,7 @@ expression: "format!(\"{dollars}\\n{narrow}\")" " and subagent usage " " " " " -" tab/1–5 section · z dashboard · ↑/↓ " +" tab/1–6 section · z dashboard · ↑/↓ " " row · enter details " " R refresh · pgup/pgdn scroll · 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 index 07b9bd42cb..c8553d0590 100644 --- 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 @@ -2,25 +2,25 @@ source: tui/src/analytics/account_layout_tests.rs expression: "format!(\"{all}\\n{filtered}\")" --- -" Analytics · Live account · analytics@example.test · account-a " +" Analytics · Live account · analytics@example.test " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary 2 Credits usage history [3 Token usage history] 4 Plugins called 5 Skills used 6 " +" Top chats " " " " ▎ Token usage history By token type " " ──────────────────────────────────────────────────────────────────────────────────────────────── " " All models · m change model " -" 4,377,500 tokens " +" 4.4M tokens " " " -" 1,000,000 731,000 " -" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ " -" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ " -" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ " -" ████████ ████████ ████████ ████████ ████████ ████████ " -" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " -" 0 ───────────────────────────────────────────────────────────────────────────────────── " +" 1M ▂▂▂▂▂▂▂▂ 731K " +" ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ ████████ ▅▅▅▅▅▅▅▅ " +" 500K ████████ ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ ████████ ████████ " +" 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 · 731,000 tokens " @@ -30,28 +30,28 @@ expression: "format!(\"{all}\\n{filtered}\")" " ● Output 43,000 " " Updated Sep 2 16:00 UTC " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 " +" Analytics · Live account · analytics@example.test " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary 2 Credits usage history [3 Token usage history] 4 Plugins called 5 Skills used 6 " +" Top chats " " " " ▎ Token usage history By token type " " ──────────────────────────────────────────────────────────────────────────────────────────────── " " GPT-5.5 · m change model " -" 4,377,500 tokens " +" 4.4M tokens " " " -" 1,000,000 731,000 " -" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ " -" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ " -" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ " -" ████████ ████████ ████████ ████████ ████████ ████████ " -" ████████ ████████ ████████ ████████ ████████ ████████ ████████ " -" 0 ───────────────────────────────────────────────────────────────────────────────────── " +" 1M ▂▂▂▂▂▂▂▂ 731K " +" ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ ████████ ▅▅▅▅▅▅▅▅ " +" 500K ████████ ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ████████ " +" ████████ ████████ ████████ ████████ ████████ ████████ " +" ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ ████████ ████████ " +" 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 · 731,000 tokens " @@ -61,5 +61,5 @@ expression: "format!(\"{all}\\n{filtered}\")" " ● Output 43,000 " " Updated Sep 2 16:00 UTC " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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__account_layout__unknown_plan_keeps_summary_available_without_billing_reports.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__unknown_plan_keeps_summary_available_without_billing_reports.snap new file mode 100644 index 0000000000..a5c63118ea --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__unknown_plan_keeps_summary_available_without_billing_reports.snap @@ -0,0 +1,44 @@ +--- +source: tui/src/analytics/account_layout_tests.rs +expression: terminal.backend().to_string() +--- +" Analytics · Live account · analytics@example.test " +" " +" [1 Summary] " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" " +" " +" " +" " +" " +" " +" " +" Account " +" " +" 12.3K — — — — " +" Lifetime tokens Peak tokens Longest chat Current streak Longest streak " +" " +" Token activity [Daily] Weekly Cumulative " +" Last 12 months " +" " +" Token activity history unavailable " +" " +" Activity insights Most used plugins and skills " +" " +" Fast Mode — Plugin and skill usage unavailable. " +" Most used reasoning — " +" Skills explored — " +" Total skills used — " +" Total chats — " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–1 section · z dashboard · ↑/↓ scroll " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__unsupported_workspace_plans_hide_top_chats_and_skip_loading.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__unsupported_workspace_plans_hide_top_chats_and_skip_loading.snap index 7031c61487..b431438a4f 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__unsupported_workspace_plans_hide_top_chats_and_skip_loading.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__account_layout__unsupported_workspace_plans_hide_top_chats_and_skip_loading.snap @@ -1,29 +1,28 @@ --- source: tui/src/analytics/account_layout_tests.rs -assertion_line: 341 expression: "screen(&mut view, 110, 24)" --- " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 Skills used " " " -" ▎ Token usage history By token type " +" ▎ Credits usage history By product " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " -" All models · m change model " -" 4,377,500 tokens " -" 731,000 " -" 1,000,000 ▁▁▁▁▁▁▁▁▁ ▄▄▄▄▄▄▄▄▄ ▃▃▃▃▃▃▃▃ ▅▅▅▅▅▅▅▅▅ ▃▃▃▃▃▃▃▃▃ " -" ▂▂▂▂▂▂▂▂ █████████ ████████ █████████ ████████ █████████ █████████ " -" 0 ─────────────────────────────────────────────────────────────────────────────────────────────── " +" 431.00 credits " +" " +" 150 ▁▁▁▁▁▁▁▁▁ ▃▃▃▃▃▃▃▃▃ 64.00 " +" 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 · 731,000 tokens " -" Token type Tokens " -" ● Cached input 602,000 " -" ● Uncached input 86,000 " -" ● Output 43,000 " -" tab/1–4 section · z dashboard · ←/→ day · enter details " -" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close " +" Sep 2 · 64.00 credits " +" Product Credits " +" ● CLI 23.00 " +" ● Desktop app 22.00 " +" ● IDE extension 10.00 " +" ● Web 9.00 " +" tab/1–5 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/snapshots/codex_tui__analytics__tests__chats_table__chats_breakdown_ranks_nonzero_groups_and_reflows.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_breakdown_ranks_nonzero_groups_and_reflows.snap index dd0808f038..7041b36cd0 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_breakdown_ranks_nonzero_groups_and_reflows.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_breakdown_ranks_nonzero_groups_and_reflows.snap @@ -4,7 +4,7 @@ expression: "screens.join(\"\\n\")" --- " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -32,13 +32,13 @@ expression: "screens.join(\"\\n\")" " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" Credits usage history Token usage history Plugins " -" called Skills used [Top chats] " +" Summary Credits usage history Token usage history " +" Plugins called Skills used [Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────── " @@ -64,13 +64,13 @@ expression: "screens.join(\"\\n\")" " Local chats · excludes archived chats and subagent " " usage " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter " +" tab/1–6 section · z dashboard · ↑/↓ row · enter " " details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -98,5 +98,5 @@ expression: "screens.join(\"\\n\")" " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_long_title_keeps_detail_indent_and_counts_missing_estimates.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_long_title_keeps_detail_indent_and_counts_missing_estimates.snap index 19849695c4..4f9de2fc9e 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_long_title_keeps_detail_indent_and_counts_missing_estimates.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_long_title_keeps_detail_indent_and_counts_missing_estimates.snap @@ -1,11 +1,10 @@ --- source: tui/src/analytics/chat_panel_tests.rs -assertion_line: 123 expression: output --- " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -31,5 +30,5 @@ expression: output " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_range_controls_and_visible_rows.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_range_controls_and_visible_rows.snap index d90a6b3a9f..6271ae1f09 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_range_controls_and_visible_rows.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__chats_range_controls_and_visible_rows.snap @@ -4,7 +4,7 @@ expression: "screens.join(\"\\n\")" --- " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -24,12 +24,12 @@ expression: "screens.join(\"\\n\")" " Showing 1–9 of 30 chats " " Local chats · excludes archived chats and subagent usage " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -49,12 +49,12 @@ expression: "screens.join(\"\\n\")" " Showing 22–30 of 30 chats " " Local chats · excludes archived chats and subagent usage " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -74,5 +74,5 @@ expression: "screens.join(\"\\n\")" " Showing 26–30 of 30 chats " " Local chats · excludes archived chats and subagent usage " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__moving_chat_selection_collapses_details_before_they_leave_the_viewport.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__moving_chat_selection_collapses_details_before_they_leave_the_viewport.snap index 2eddc921df..098d8ceb15 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__moving_chat_selection_collapses_details_before_they_leave_the_viewport.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__moving_chat_selection_collapses_details_before_they_leave_the_viewport.snap @@ -1,11 +1,10 @@ --- source: tui/src/analytics/chat_panel_tests.rs -assertion_line: 205 expression: "screen(&mut view, 110, 24)" --- " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -25,5 +24,5 @@ expression: "screen(&mut view, 110, 24)" " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__unavailable_chat_titles_are_hidden_in_panel_and_overview.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__unavailable_chat_titles_are_hidden_in_panel_and_overview.snap index 29167d6127..7d71240c5f 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__unavailable_chat_titles_are_hidden_in_panel_and_overview.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__chats_table__unavailable_chat_titles_are_hidden_in_panel_and_overview.snap @@ -1,11 +1,10 @@ --- source: tui/src/analytics/chat_panel_tests.rs -assertion_line: 165 expression: "screens.join(\"\\n\")" --- " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -29,34 +28,34 @@ expression: "screens.join(\"\\n\")" " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" │ │ │ │ " -" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " -" │ ● Slack 10 │ │ ● Testing 4 │ " -" │ ● Figma 10 │ │ ● Code review 3 │ " -" │ │ │ │ " +" │ 27 28 29 30 31 1 2 │ │ │ " +" │ │ │ Sep 2 · 29 calls │ " +" │ Sep 2 · 731,000 tokens │ │ ● Slack 10 │ " +" │ ● Cached input 602,000 │ │ ● Figma 10 │ " +" │ … z to maximize │ │ │ " " │ │ │ │ " " ╰─────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────╯ " " " -" ╭─ ▸ 5 Top chats ─────────────────────────────────╮ " -" │ │ " -" │ 30d active · lifetime credits │ " -" │ │ " -" │ Chat usage unavailable — │ " -" │ │ " -" │ Local chats · excludes subagents │ " -" │ Some estimates unavailable │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" ╰─────────────────────────────────────────────────╯ " -" tab/1–5 section · enter/z maximize " +" ╭─ 5 Skills used ───────────────────────────────╮ ╭─ ▸ 6 Top chats ─────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 30d active · lifetime credits │ " +" │ 54 uses │ │ │ " +" │ 11 │ │ Chat usage unavailable — │ " +" │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ │ │ " +" │ 0 ─────────────────────────────────────────── │ │ Local chats · excludes subagents │ " +" │ ▲ │ │ Some estimates unavailable │ " +" │ 27 28 29 30 31 1 2 │ │ │ " +" │ │ │ │ " +" │ Sep 2 · 11 uses │ │ │ " +" │ ● Testing 4 │ │ │ " +" │ ● Code review 3 │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰─────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────╯ " +" tab/1–6 section · enter/z maximize " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__compact__report_headlines_and_axes_compact_large_values.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__compact__report_headlines_and_axes_compact_large_values.snap new file mode 100644 index 0000000000..b590c1762a --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__compact__report_headlines_and_axes_compact_large_values.snap @@ -0,0 +1,96 @@ +--- +source: tui/src/analytics/compact_tests.rs +expression: "screenshots.join(\"\\n\")" +--- +" Analytics · Live account " +" [7d] 30d · Aug 27–Sep 2, 2026 " +" " +" 1 Summary 2 Credits usage history [3 Token usage history] 4 Plugins called 5 Skills used 6 " +" Top chats " +" " +" ▎ Token usage history By token type " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" All models · m change model " +" 12.3B tokens " +" " +" 15B 12.3B " +" ▄▄▄▄▄▄▄▄ " +" ████████ " +" ████████ " +" 7.5B ████████ " +" ████████ " +" ████████ " +" — — — — — — ████████ " +" 0 ────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 12,280,365,226 tokens " +" Token type Tokens " +" ● Example 12,280,365,226 " +" " +" tab/1–6 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 Summary 2 Credits usage history 3 Token usage history [4 Plugins called] 5 Skills used 6 " +" Top chats " +" " +" ▎ Plugins called " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 9.7K calls " +" 9.7K " +" 10K ▆▆▆▆▆▆▆▆ " +" ████████ " +" ████████ " +" ████████ " +" 5K ████████ " +" ████████ " +" ████████ " +" ████████ " +" — — — — — — ████████ " +" 0 ─────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 9,749 calls " +" Plugin Calls " +" ● Example 9,749 " +" " +" tab/1–6 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 Summary 2 Credits usage history 3 Token usage history 4 Plugins called [5 Skills used] 6 " +" Top chats " +" " +" ▎ Skills used " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" 2.9K uses " +" 2.9K " +" 3K ▅▅▅▅▅▅▅▅ " +" ████████ " +" ████████ " +" ████████ " +" 1.5K ████████ " +" ████████ " +" ████████ " +" ████████ " +" — — — — — — ████████ " +" 0 ────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" " +" ──────────────────────────────────────────────────────── " +" Sep 2 · 2,901 uses " +" Skill Uses " +" ● Example 2,901 " +" " +" tab/1–6 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/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_all_metrics.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_all_metrics.snap index 3e6802acc4..ada4ee5ae9 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_all_metrics.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_all_metrics.snap @@ -1,11 +1,10 @@ --- source: tui/src/analytics/consumer_refresh_tests.rs -assertion_line: 61 expression: "screen(&mut view, 110, 30)" --- " Analytics · Live account " " " -" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -31,5 +30,5 @@ expression: "screen(&mut view, 110, 30)" " Showing 1–3 of 3 chats " " Local chats · includes discovered descendants · excludes archived roots " " " -" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · s sort · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_available_limits.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_available_limits.snap index 3821cac405..105eb43397 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_available_limits.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chat_available_limits.snap @@ -1,12 +1,11 @@ --- source: tui/src/analytics/consumer_refresh_tests.rs -assertion_line: 66 expression: "screen(&mut view, 58, 30)" --- " Analytics · Live account " " " -" Total usage history Messages Plugins called Skills " -" used [Top chats] " +" Summary Total usage history Messages Plugins called " +" Skills used [Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────── " @@ -30,6 +29,6 @@ expression: "screen(&mut view, 58, 30)" " " " " " " -" tab/1–5 section · s sort · z dashboard · ↑/↓ row · " +" tab/1–6 section · s sort · z dashboard · ↑/↓ row · " " enter details " " R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chats_keep_missing_rows_and_only_offer_known_metrics.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chats_keep_missing_rows_and_only_offer_known_metrics.snap index 0a34dfe3a6..501dbdab21 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chats_keep_missing_rows_and_only_offer_known_metrics.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__consumer_refresh__consumer_chats_keep_missing_rows_and_only_offer_known_metrics.snap @@ -1,11 +1,11 @@ --- source: tui/src/analytics/consumer_refresh_tests.rs -assertion_line: 43 expression: "screen(&mut view, 90, 30)" --- " Analytics · Live account " " " -" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called 5 Skills used [6 Top " +" chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────────────────────────────────────── " @@ -30,6 +30,5 @@ expression: "screen(&mut view, 90, 30)" " Local chats · includes discovered descendants · excludes archived roots " " " " " -" " -" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · s sort · z dashboard · ↑/↓ row · enter details " " 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 index 19792d8188..c76a65ecc3 100644 --- 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 @@ -1,77 +1,93 @@ --- source: tui/src/analytics/dashboard_tests.rs -assertion_line: 83 expression: "format!(\"{wide}\\n{narrow}\")" --- " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" ╭─ ▸ 1 Total usage history ────────────────────────────────────────╮ ╭─ 2 Messages ───────────────────────────────────────────────────╮ " +" ╭─ 1 Summary ────────────────────────────────────────────────────╮ ╭─ ▸ 2 Total usage history ────────────────────────────────────────╮ " " │ │ │ │ " -" │ 7d │ │ 7d │ " -" │ By feature │ │ By model │ " -" │ Total usage · relative units │ │ 574 reported messages │ " -" │ 86 │ │ 103 │ " -" │ 100 ▃▃▃▃▃ ▂▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ │ 150 ▃▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ " -" │ ▃▃▃▃▃ █████ ██████ █████ ██████ █████ ██████ │ │ ▆▆▆▆▆ █████ ██████ █████ ▆▆▆▆▆▆ █████ ██████ │ " -" │ 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 │ " +" │ No history has been reported. │ │ 7d │ " +" │ │ │ By feature │ " +" │ │ │ Total usage · relative units │ " +" │ │ │ 86 │ " +" │ │ │ 100 ▃▃▃▃▃ ▂▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ " +" │ │ │ ▃▃▃▃▃ █████ ██████ █████ ██████ █████ ██████ │ " +" │ │ │ 0 ─────────────────────────────────────────────────────────── │ " +" │ │ │ ▲ │ " +" │ │ │ 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 │ " +" │ │ │ Sep 2 · 86 usage units │ " +" │ │ │ ● Tasks 64.0% │ " +" │ │ │ ● Code review 14.0% │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " " " -" ╭─ 3 Plugins called ─────────────────────────────────────────────╮ ╭─ 4 Skills used ────────────────────────────────────────────────╮ " +" ╭─ 3 Messages ───────────────────────────────────────────────────╮ ╭─ 4 Plugins called ─────────────────────────────────────────────╮ " " │ │ │ │ " " │ 7d │ │ 7d │ " -" │ 186 calls │ │ 54 uses │ " -" │ 29 │ │ 11 │ " -" │ 40 ▅▅▅▅▅ ▁▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁▁ ▃▃▃▃▃▃ │ │ 20 │ " -" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ ▆▆▆▆▆ ▄▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄▄ ██████ │ " -" │ 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 │ " -" │ │ │ │ " +" │ By model │ │ 186 calls │ " +" │ 574 reported messages │ │ 29 │ " +" │ 103 │ │ 40 ▅▅▅▅▅ ▁▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁▁ ▃▃▃▃▃▃ │ " +" │ 150 ▃▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ │ █████ ██████ █████ ██████ █████ ██████ ██████ │ " +" │ ▆▆▆▆▆ █████ ██████ █████ ▆▆▆▆▆▆ █████ ██████ │ │ 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 · 103 messages │ │ ● Slack 10 │ " +" │ ● GPT-5.6-Sol 61 │ │ ● Figma 10 │ " +" │ ● GPT-5.6-Terra 25 │ │ │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " " " -" ╭─ 5 Top chats ──────────────────────────────────────────────────╮ " -" │ │ " -" │ No history has been reported. │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" ╰──────────────────────────────────────────────────────────────────╯ " -" tab/1–5 section · enter/z maximize " +" ╭─ 5 Skills used ────────────────────────────────────────────────╮ ╭─ 6 Top chats ──────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ No history has been reported. │ " +" │ 54 uses │ │ │ " +" │ 11 │ │ │ " +" │ 20 │ │ │ " +" │ ▆▆▆▆▆ ▄▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄▄ ██████ │ │ │ " +" │ 0 ──────────────────────────────────────────────────────────── │ │ │ " +" │ ▲ │ │ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ │ " +" │ │ │ │ " +" │ Sep 2 · 11 uses │ │ │ " +" │ ● Testing 4 │ │ │ " +" │ ● Code review 3 │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " +" tab/1–6 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 ────────────────────────────────────────╮ " +" ╭─ 1 Summary ────────────────────────────────────────────────────╮ " +" │ │ " +" │ No history has been reported. │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ ▸ 2 Total usage history ────────────────────────────────────────╮ " " │ │ " " │ 7d │ " " │ By feature │ " @@ -88,7 +104,7 @@ expression: "format!(\"{wide}\\n{narrow}\")" " │ │ " " ╰──────────────────────────────────────────────────────────────────╯ " " " -" ╭─ 2 Messages ───────────────────────────────────────────────────╮ " +" ╭─ 3 Messages ───────────────────────────────────────────────────╮ " " │ │ " " │ 7d │ " " │ By model │ " @@ -96,23 +112,6 @@ expression: "format!(\"{wide}\\n{narrow}\")" " │ 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–5 section · enter/z maximize " +" tab/1–6 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__summary__summary_cumulative_narrow.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_cumulative_narrow.snap new file mode 100644 index 0000000000..ce65f92b67 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_cumulative_narrow.snap @@ -0,0 +1,64 @@ +--- +source: tui/src/analytics/summary_tests.rs +expression: "screen(&mut view, 64, 60)" +--- +" Analytics · Live account " +" " +" [Summary] Total usage history Messages Plugins called " +" Skills used Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────── " +" " +" " +" " +" " +" " +" " +" Example User " +" @example.user " +" Plus " +" " +" 142.3B 7B 44h 7m " +" Lifetime tokens Peak tokens Longest chat " +" " +" 158 days 160 days " +" Current streak Longest streak " +" " +" Token activity Daily Weekly [Cumulative] " +" Last 12 months " +" " +" Mar Apr May Jun Jul Aug " +" max █ " +" █ " +" █ " +" █ " +" █ " +" █ " +" 0 █ " +" " +" Running total · top 300 " +" " +" Activity insights " +" " +" Fast Mode 37% " +" Most used reasoning high · 26% " +" Skills explored 147 " +" Total skills used 13,190 " +" Total chats 21,482 " +" " +" Most used plugins and skills " +" " +" $review 1,142 runs " +" @GitHub 1,059 runs " +" " +" Statistics as of Sep 9, 2026 " +" " +" " +" " +" " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_daily_wide.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_daily_wide.snap new file mode 100644 index 0000000000..7c6b33950e --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_daily_wide.snap @@ -0,0 +1,48 @@ +--- +source: tui/src/analytics/summary_tests.rs +expression: "screen(&mut view, 140, 44)" +--- +" Analytics · Live account " +" " +" [1 Summary] 2 Total usage history 3 Messages 4 Plugins called 5 Skills used 6 Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" " +" " +" " +" Example User " +" @example.user " +" Plus " +" " +" 142.3B 7B 44h 7m 158 days 160 days " +" Lifetime tokens Peak tokens Longest chat Current streak Longest streak " +" " +" Token activity [Daily] Weekly Cumulative " +" Last 12 months " +" " +" Oct Nov Dec Jan Feb Mar Apr May Jun Jul Aug " +" Su □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Mo □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Tu □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ ■ " +" We □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ ■ " +" Th □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Fr □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" Sa □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ " +" " +" Less □ ■ ■ ■ ■ More " +" " +" Activity insights Most used plugins and skills " +" " +" Fast Mode 37% $review 1,142 runs " +" Most used reasoning high · 26% @GitHub 1,059 runs " +" Skills explored 147 " +" Total skills used 13,190 " +" Total chats 21,482 " +" " +" Statistics as of Sep 9, 2026 " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_overview.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_overview.snap new file mode 100644 index 0000000000..7714fd3f35 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_overview.snap @@ -0,0 +1,68 @@ +--- +source: tui/src/analytics/summary_tests.rs +expression: "screen(&mut view, 144, 64)" +--- +" Analytics · Live account " +" " +" ╭─ ▸ 1 Summary ────────────────────────────────────────────────────╮ ╭─ 2 Total usage history ────────────────────────────────────────╮ " +" │ │ │ │ " +" │ Example User │ │ 7d │ " +" │ @example.user │ │ By feature │ " +" │ Plus │ │ Total usage · relative units │ " +" │ │ │ │ " +" │ Lifetime tokens 142.3B │ │ 1 │ " +" │ Peak tokens 7B │ │ — — — — — — — │ " +" │ Longest chat 44h 7m │ │ 0 ─────────────────────────────────────────────────────────── │ " +" │ Current streak 158 days │ │ ▲ │ " +" │ Longest streak 160 days │ │ Sep 3 Sep 4 Sep 5 Sep 6 Sep 7 Sep 8 Sep 9 │ " +" │ │ │ │ " +" │ │ │ Sep 9 · Not reported │ " +" │ │ │ ● Tasks — │ " +" │ │ │ ● Code review — │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 3 Messages ───────────────────────────────────────────────────╮ ╭─ 4 Plugins called ─────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ 7d │ " +" │ By model │ │ 186 calls │ " +" │ 574 reported messages │ │ │ " +" │ │ │ 2 │ " +" │ 2 │ │ — — — — — — — │ " +" │ — — — — — — — │ │ 0 ───────────────────────────────────────────────────────────── │ " +" │ 0 ───────────────────────────────────────────────────────────── │ │ ▲ │ " +" │ ▲ │ │ Sep 3 Sep 4 Sep 5 Sep 6 Sep 7 Sep 8 Sep 9 │ " +" │ Sep 3 Sep 4 Sep 5 Sep 6 Sep 7 Sep 8 Sep 9 │ │ │ " +" │ │ │ Data not reported for Sep 9. │ " +" │ Data not reported for Sep 9. │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " +" " +" ╭─ 5 Skills used ────────────────────────────────────────────────╮ ╭─ 6 Top chats ──────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ No history has been reported. │ " +" │ 54 uses │ │ │ " +" │ │ │ │ " +" │ 2 │ │ │ " +" │ — — — — — — — │ │ │ " +" │ 0 ───────────────────────────────────────────────────────────── │ │ │ " +" │ ▲ │ │ │ " +" │ Sep 3 Sep 4 Sep 5 Sep 6 Sep 7 Sep 8 Sep 9 │ │ │ " +" │ │ │ │ " +" │ Data not reported for Sep 9. │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ " +" " +" tab/1–6 section · enter/z maximize " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_partial.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_partial.snap new file mode 100644 index 0000000000..add561967c --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_partial.snap @@ -0,0 +1,44 @@ +--- +source: tui/src/analytics/summary_tests.rs +expression: rendered +--- +" Analytics · Live account " +" " +" [1 Summary] 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────── " +" " +" " +" " +" " +" " +" " +" Business " +" " +" 0 — — — — " +" Lifetime tokens Peak tokens Longest chat Current streak Longest streak " +" " +" Token activity [Daily] Weekly Cumulative " +" Last 12 months " +" " +" Token activity history unavailable " +" " +" Activity insights Most used plugins and skills " +" " +" Fast Mode 0% No reported plugins or skills. " +" Most used reasoning — " +" Skills explored — " +" Total skills used — " +" Total chats 0 " +" " +" Some profile statistics are unavailable. " +" " +" " +" " +" " +" " +" " +" " +" tab/1–5 section · z dashboard · ↑/↓ scroll " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_scrolled.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_scrolled.snap new file mode 100644 index 0000000000..b619593112 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_scrolled.snap @@ -0,0 +1,22 @@ +--- +source: tui/src/analytics/summary_tests.rs +expression: "screen(&mut view, 64, 18)" +--- +" Analytics · Live account " +" " +" [Summary] Total usage history Messages Plugins called " +" Skills used Top chats " +" " +" Most used reasoning high · 26% " +" Skills explored 147 " +" Total skills used 13,190 " +" Total chats 21,482 " +" " +" Most used plugins and skills " +" " +" $review 1,142 runs " +" @GitHub 1,059 runs " +" " +" Statistics as of Sep 9, 2026 " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · R refresh · pgup/pgdn scroll · esc back · q close " diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_weekly.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_weekly.snap new file mode 100644 index 0000000000..ac470baf20 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__summary__summary_weekly.snap @@ -0,0 +1,48 @@ +--- +source: tui/src/analytics/summary_tests.rs +expression: "screen(&mut view, 110, 44)" +--- +" Analytics · Live account " +" " +" [1 Summary] 2 Total usage history 3 Messages 4 Plugins called 5 Skills used 6 Top chats " +" " +" ▎ Summary " +" ────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" " +" " +" " +" Example User " +" @example.user " +" Plus " +" " +" 142.3B 7B 44h 7m 158 days 160 days " +" Lifetime tokens Peak tokens Longest chat Current streak Longest streak " +" " +" Token activity Daily [Weekly] Cumulative " +" Last 12 months " +" " +" Oct Nov Dec Jan Feb Mar Apr May Jun Jul Aug " +" max █ " +" █ " +" █ " +" █ " +" █ " +" █ " +" 0 █ " +" " +" Each column = 1 week · tallest 300 " +" " +" Activity insights Most used plugins and skills " +" " +" Fast Mode 37% $review 1,142 runs " +" Most used reasoning high · 26% @GitHub 1,059 runs " +" Skills explored 147 " +" Total skills used 13,190 " +" Total chats 21,482 " +" " +" Statistics as of Sep 9, 2026 " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · 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 index 35113d78ad..944378ba5f 100644 --- 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 @@ -7,34 +7,34 @@ 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 5 Top chats " +" 1 Summary 2 Credits usage history [3 Token usage history] 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Token usage history By token type " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " " All models · m change model " -" 4,377,500 tokens " +" 4.4M tokens " " " -" 1,000,000 " +" 1M " " " -" ▂▂▂▂▂▂▂▂▂▂ " -" ██████████ 731,000 " -" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ " -" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " -" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ " -" 500,000 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────── " -" ▲ " -" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" ▂▂▂▂▂▂▂▂▂▂ " +" ██████████ 731K " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ " +" 500K ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " " " " ──────────────────────────────────────────────────────── " " Sep 2 · 731,000 tokens " @@ -44,7 +44,7 @@ Text: " ● Output 43,000 " " Updated Sep 2 16:00 UTC " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,34 +61,34 @@ 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 96:0 +3: 0:0 2:2 38:3 63:2 107: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 +8: 0:0 2:3 13:0 9: 0:0 -10: 0:0 3:2 12:0 +10: 0:0 5:2 7: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 +12: 0:0 89:5 99:0 +13: 0:0 89:5 99:0 107:3 111: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:7 99:0 105:5 115:0 +16: 0:0 26:5 36:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +17: 0:0 26:6 36:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +18: 0:0 26:6 36:0 42:5 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +19: 0:0 3:2 7:0 26:7 36:0 42:6 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +20: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +21: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +22: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +23: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +24: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +25: 0:0 10:7 20:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 10:7 20:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 10:7 20:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 6:2 7:0 8:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 12:2 18:0 27:2 33:0 43:2 49:0 59:2 65: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 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 index ed492c231d..96c32c3861 100644 --- 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 @@ -7,7 +7,7 @@ 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 5 Top chats " +" 1 Summary 2 Credits usage history 3 Token usage history [4 Plugins called] 5 Skills used 6 Top chats " " " " ▎ Plugins called " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -44,7 +44,7 @@ Text: " ● GitHub 5 " " ● Notion 4 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,7 +61,7 @@ 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 96:0 +3: 0:0 2:2 61:3 81:2 107:0 4: 0:0 5: 0:0 2:3 18:0 6: 0:0 2:4 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 index d30f6ca4ca..6f20a96507 100644 --- 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 @@ -7,34 +7,34 @@ 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 5 Top chats " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Credits usage history By product " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " -" 17,240.00 credits " +" 17.2K credits " " " -" 5,000 " +" 5K " " " " ▄▄▄▄▄▄▄▄▄▄ " " ██████████ " " ██████████ " " ██████████ " -" ▇▇▇▇▇▇▇▇▇▇ ██████████ " -" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ " -" ██████████ ██████████ ██████████ 2,560.00 " -" 2,500 ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ " -" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ " -" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" 0 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ " +" ██████████ ██████████ ██████████ 2,560.00 " +" 2.5K ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ " +" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 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 · 2,560.00 credits " @@ -44,7 +44,7 @@ Text: " ● IDE extension 400.00 " " ● Web 360.00 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,13 +61,13 @@ 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 96:0 +3: 0:0 2:2 13:3 40:2 107: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 +7: 0:0 2:3 15:0 8: 0:0 -9: 0:0 3:2 8:0 +9: 0:0 5:2 7:0 10: 0:0 11: 0:0 12: 0:0 @@ -75,20 +75,20 @@ Rows (column:palette): 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 +17: 0:0 57:5 67:0 89:6 99:0 105:3 113:0 +18: 0:0 3:2 7:0 57:5 67:0 73:5 83:0 89:6 99:0 +19: 0:0 57:6 67:0 73:5 83:0 89:6 99:0 +20: 0:0 57:6 67:0 73:6 83:0 89:6 99:0 105:5 115:0 +21: 0:0 42:5 52:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 42:6 52:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +23: 0:0 26:5 36:0 42:6 52:0 57:7 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +24: 0:0 26:6 36:0 42:6 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:6 115:0 +25: 0:0 26:6 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 15:4 16:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 6:2 7:0 8: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 +30: 0:0 12:2 18:0 27:2 33:0 43:2 49:0 59:2 65: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 26:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap index 2e3e16d40b..67ef4f1376 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap @@ -6,7 +6,7 @@ expression: snapshot Text: " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -44,7 +44,7 @@ Text: " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE @@ -56,7 +56,7 @@ Palette (foreground|background|underline|modifiers): Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 -2: 0:0 2:2 83:3 96:0 +2: 0:0 2:2 94:3 107:0 3: 0:0 4: 0:0 2:3 13:0 5: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap index 5b4109fc5d..d89bbe591c 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap @@ -7,7 +7,7 @@ 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] 5 Top chats " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -44,7 +44,7 @@ Text: " ● Planning 3 " " ● Debugging 1 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,7 +61,7 @@ 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 85:2 96:0 +3: 0:0 2:2 79:3 96:2 107:0 4: 0:0 5: 0:0 2:3 15:0 6: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap new file mode 100644 index 0000000000..c4a8c25160 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap @@ -0,0 +1,98 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" " +" [1 Summary] 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used 6 Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" No history has been reported. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · 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: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 +2: 0:0 2:3 15:2 107:0 +3: 0:0 +4: 0:0 2:3 11:0 +5: 0:0 2:4 118:0 +6: 0:0 +7: 0:0 +8: 0:0 +9: 0:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 +17: 0:0 +18: 0:0 +19: 0:0 +20: 0:0 +21: 0:0 +22: 0:0 +23: 0:0 +24: 0:0 +25: 0:0 +26: 0:0 +27: 0:0 +28: 0:0 +29: 0:0 +30: 0:0 +31: 0:0 +32: 0:0 +33: 0:0 +34: 0:0 +35: 0:0 +36: 0:0 +37: 0:0 +38: 0:0 +39: 0:0 +40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 118:0 +41: 0:0 2:5 3:2 11:5 12:2 23:5 32:2 42:5 45:2 53:5 54: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 index c1c35c1e88..3fef73186e 100644 --- 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 @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary [2 Total usage history] 3 Messages 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Total usage history By feature " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● Images 11.6% " " ● Other 10.5% " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 83:0 +3: 0:0 2:2 13:3 38:2 94:0 4: 0:0 5: 0:0 2:3 23:0 108:2 118:0 6: 0:0 2:4 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 index 7491345cda..ae9087c3fc 100644 --- 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 @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages [4 Plugins called] 5 Skills used 6 Top chats " " " " ▎ Plugins called " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● GitHub 5 " " ● Notion 4 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 83:0 +3: 0:0 2:2 48:3 68:2 94:0 4: 0:0 5: 0:0 2:3 18:0 6: 0:0 2:4 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 index 5e99ed20a1..819465410c 100644 --- 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 @@ -1,13 +1,12 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 Text: " Analytics · Live account " " " -" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " " " " " " -" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · s sort · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE @@ -57,7 +56,7 @@ Palette (foreground|background|underline|modifiers): Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 -2: 0:0 2:2 70:3 83:0 +2: 0:0 2:2 81:3 94:0 3: 0:0 4: 0:0 2:3 13:0 5: 0:0 2:4 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 index 9af31e2ec7..86c8984388 100644 --- 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 @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary 2 Total usage history [3 Messages] 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Messages By model " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● GPT-5.5 10 " " ● Other 7 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 83:0 +3: 0:0 2:2 36:3 50:2 94:0 4: 0:0 5: 0:0 2:3 12:0 110:2 118:0 6: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap index 2f91d2fe20..008cf15a50 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● Planning 3 " " ● Debugging 1 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 72:2 83:0 +3: 0:0 2:2 66:3 83:2 94:0 4: 0:0 5: 0:0 2:3 15:0 6: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap new file mode 100644 index 0000000000..d3fe52e114 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap @@ -0,0 +1,98 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" " +" [1 Summary] 2 Total usage history 3 Messages 4 Plugins called 5 Skills used 6 Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" No history has been reported. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · 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: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 +2: 0:0 2:3 15:2 94:0 +3: 0:0 +4: 0:0 2:3 11:0 +5: 0:0 2:4 118:0 +6: 0:0 +7: 0:0 +8: 0:0 +9: 0:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 +17: 0:0 +18: 0:0 +19: 0:0 +20: 0:0 +21: 0:0 +22: 0:0 +23: 0:0 +24: 0:0 +25: 0:0 +26: 0:0 +27: 0:0 +28: 0:0 +29: 0:0 +30: 0:0 +31: 0:0 +32: 0:0 +33: 0:0 +34: 0:0 +35: 0:0 +36: 0:0 +37: 0:0 +38: 0:0 +39: 0:0 +40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 118:0 +41: 0:0 2:5 3:2 11:5 12:2 23:5 32:2 42:5 45:2 53:5 54: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 index f61c199668..493c7c3247 100644 --- 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 @@ -7,34 +7,34 @@ 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 5 Top chats " +" 1 Summary 2 Credits usage history [3 Token usage history] 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Token usage history By token type " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " " All models · m change model " -" 4,377,500 tokens " +" 4.4M tokens " " " -" 1,000,000 " +" 1M " " " -" ▂▂▂▂▂▂▂▂▂▂ " -" ██████████ 731,000 " -" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ " -" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " -" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ " -" 500,000 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────── " -" ▲ " -" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " +" ▂▂▂▂▂▂▂▂▂▂ " +" ██████████ 731K " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ " +" 500K ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 0 ────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▲ " +" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 " " " " ──────────────────────────────────────────────────────── " " Sep 2 · 731,000 tokens " @@ -44,7 +44,7 @@ Text: " ● Output 43,000 " " Updated Sep 2 16:00 UTC " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,34 +61,34 @@ 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 96:0 +3: 0:0 2:2 38:3 63:2 107: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 +8: 0:0 2:3 13:0 9: 0:0 -10: 0:0 3:2 12:0 +10: 0:0 5:2 7: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 +12: 0:0 89:5 99:0 +13: 0:0 89:5 99:0 107:3 111: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:7 99:0 105:5 115:0 +16: 0:0 26:5 36:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +17: 0:0 26:6 36:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0 +18: 0:0 26:6 36:0 42:5 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +19: 0:0 3:2 7:0 26:7 36:0 42:6 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +20: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +21: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +22: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +23: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +24: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +25: 0:0 10:7 20:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 10:7 20:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 10:7 20:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 6:2 7:0 8:4 118:0 +29: 0:0 109:3 110:0 +30: 0:0 12:2 18:0 27:2 33:0 43:2 49:0 59:2 65: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 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 index fa749d7892..3c82423e13 100644 --- 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 @@ -7,7 +7,7 @@ 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 5 Top chats " +" 1 Summary 2 Credits usage history 3 Token usage history [4 Plugins called] 5 Skills used 6 Top chats " " " " ▎ Plugins called " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -44,7 +44,7 @@ Text: " ● GitHub 5 " " ● Notion 4 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,7 +61,7 @@ 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 96:0 +3: 0:0 2:2 61:3 81:2 107:0 4: 0:0 5: 0:0 2:3 18:0 6: 0:0 2:4 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 index e9e958dfc4..fe27205f1d 100644 --- 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 @@ -7,34 +7,34 @@ 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 5 Top chats " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Credits usage history By product " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " -" 17,240.00 credits " +" 17.2K credits " " " -" 5,000 " +" 5K " " " " ▄▄▄▄▄▄▄▄▄▄ " " ██████████ " " ██████████ " " ██████████ " -" ▇▇▇▇▇▇▇▇▇▇ ██████████ " -" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ " -" ██████████ ██████████ ██████████ 2,560.00 " -" 2,500 ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ " -" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ " -" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" 0 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " -" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" ▇▇▇▇▇▇▇▇▇▇ ██████████ " +" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ " +" ██████████ ██████████ ██████████ 2,560.00 " +" 2.5K ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ " +" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ " +" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ " +" 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 · 2,560.00 credits " @@ -44,7 +44,7 @@ Text: " ● IDE extension 400.00 " " ● Web 360.00 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,13 +61,13 @@ 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 96:0 +3: 0:0 2:2 13:3 40:2 107: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 +7: 0:0 2:3 15:0 8: 0:0 -9: 0:0 3:2 8:0 +9: 0:0 5:2 7:0 10: 0:0 11: 0:0 12: 0:0 @@ -75,20 +75,20 @@ Rows (column:palette): 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 +17: 0:0 57:5 67:0 89:6 99:0 105:3 113:0 +18: 0:0 3:2 7:0 57:5 67:0 73:5 83:0 89:6 99:0 +19: 0:0 57:6 67:0 73:5 83:0 89:6 99:0 +20: 0:0 57:6 67:0 73:6 83:0 89:6 99:0 105:5 115:0 +21: 0:0 42:5 52:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0 +22: 0:0 42:6 52:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +23: 0:0 26:5 36:0 42:6 52:0 57:7 67:0 73:6 83:0 89:7 99:0 105:6 115:0 +24: 0:0 26:6 36:0 42:6 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:6 115:0 +25: 0:0 26:6 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +26: 0:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +27: 0:0 15:4 16:0 26:7 36:0 42:7 52:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0 +28: 0:0 6:2 7:0 8: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 +30: 0:0 12:2 18:0 27:2 33:0 43:2 49:0 59:2 65: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 26:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap index aa7dbd8a84..2ca8350ac8 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap @@ -6,7 +6,7 @@ expression: snapshot Text: " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -44,7 +44,7 @@ Text: " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE @@ -56,7 +56,7 @@ Palette (foreground|background|underline|modifiers): Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 -2: 0:0 2:2 83:3 96:0 +2: 0:0 2:2 94:3 107:0 3: 0:0 4: 0:0 2:3 13:0 5: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap index 7d79499033..fe76614dea 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap @@ -7,7 +7,7 @@ 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] 5 Top chats " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -44,7 +44,7 @@ Text: " ● Planning 3 " " ● Debugging 1 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -61,7 +61,7 @@ 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 85:2 96:0 +3: 0:0 2:2 79:3 96:2 107:0 4: 0:0 5: 0:0 2:3 15:0 6: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap new file mode 100644 index 0000000000..b98126a5ba --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap @@ -0,0 +1,98 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" " +" [1 Summary] 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used 6 Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" No history has been reported. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · 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: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 +2: 0:0 2:3 15:2 107:0 +3: 0:0 +4: 0:0 2:3 11:0 +5: 0:0 2:4 118:0 +6: 0:0 +7: 0:0 +8: 0:0 +9: 0:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 +17: 0:0 +18: 0:0 +19: 0:0 +20: 0:0 +21: 0:0 +22: 0:0 +23: 0:0 +24: 0:0 +25: 0:0 +26: 0:0 +27: 0:0 +28: 0:0 +29: 0:0 +30: 0:0 +31: 0:0 +32: 0:0 +33: 0:0 +34: 0:0 +35: 0:0 +36: 0:0 +37: 0:0 +38: 0:0 +39: 0:0 +40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 118:0 +41: 0:0 2:5 3:2 11:5 12:2 23:5 32:2 42:5 45:2 53:5 54: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 index de8e09d47d..607484b775 100644 --- 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 @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary [2 Total usage history] 3 Messages 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Total usage history By feature " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● Images 11.6% " " ● Other 10.5% " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 83:0 +3: 0:0 2:2 13:3 38:2 94:0 4: 0:0 5: 0:0 2:3 23:0 108:2 118:0 6: 0:0 2:4 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 index ea9d3418aa..a7a08a1bd6 100644 --- 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 @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages [4 Plugins called] 5 Skills used 6 Top chats " " " " ▎ Plugins called " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● GitHub 5 " " ● Notion 4 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 83:0 +3: 0:0 2:2 48:3 68:2 94:0 4: 0:0 5: 0:0 2:3 18:0 6: 0:0 2:4 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 index 3ce8a5171e..38012f81dd 100644 --- 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 @@ -1,13 +1,12 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 Text: " Analytics · Live account " " " -" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " " " " " " -" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · s sort · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE @@ -57,7 +56,7 @@ Palette (foreground|background|underline|modifiers): Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 -2: 0:0 2:2 70:3 83:0 +2: 0:0 2:2 81:3 94:0 3: 0:0 4: 0:0 2:3 13:0 5: 0:0 2:4 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 index e10acdd3b9..c61c2f7475 100644 --- 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 @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary 2 Total usage history [3 Messages] 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Messages By model " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● GPT-5.5 10 " " ● Other 7 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 83:0 +3: 0:0 2:2 36:3 50:2 94:0 4: 0:0 5: 0:0 2:3 12:0 110:2 118:0 6: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap index 8fd45a95d7..997757580f 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 142 expression: snapshot --- 120x42 @@ -8,7 +7,7 @@ Text: " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,7 +44,7 @@ Text: " ● Planning 3 " " ● Debugging 1 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 @@ -62,7 +61,7 @@ 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 72:2 83:0 +3: 0:0 2:2 66:3 83:2 94:0 4: 0:0 5: 0:0 2:3 15:0 6: 0:0 2:4 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap new file mode 100644 index 0000000000..464d93e875 --- /dev/null +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap @@ -0,0 +1,98 @@ +--- +source: tui/src/analytics/styles_tests.rs +expression: snapshot +--- +120x42 +Text: +" Analytics · Live account " +" " +" [1 Summary] 2 Total usage history 3 Messages 4 Plugins called 5 Skills used 6 Top chats " +" " +" ▎ Summary " +" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " +" No history has been reported. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" tab/1–6 section · z dashboard · ↑/↓ scroll " +" g view · 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: White|Reset|Reset|BOLD +Rows (column:palette): +0: 0:0 2:1 11:2 28:0 +1: 0:0 +2: 0:0 2:3 15:2 94:0 +3: 0:0 +4: 0:0 2:3 11:0 +5: 0:0 2:4 118:0 +6: 0:0 +7: 0:0 +8: 0:0 +9: 0:0 +10: 0:0 +11: 0:0 +12: 0:0 +13: 0:0 +14: 0:0 +15: 0:0 +16: 0:0 +17: 0:0 +18: 0:0 +19: 0:0 +20: 0:0 +21: 0:0 +22: 0:0 +23: 0:0 +24: 0:0 +25: 0:0 +26: 0:0 +27: 0:0 +28: 0:0 +29: 0:0 +30: 0:0 +31: 0:0 +32: 0:0 +33: 0:0 +34: 0:0 +35: 0:0 +36: 0:0 +37: 0:0 +38: 0:0 +39: 0:0 +40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 118:0 +41: 0:0 2:5 3:2 11:5 12:2 23:5 32:2 42:5 45:2 53:5 54: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 index aeb3f1a814..83eef78037 100644 --- 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 @@ -1,12 +1,11 @@ --- source: tui/src/analytics/tool_panel_tests.rs -assertion_line: 78 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] 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -45,13 +44,13 @@ expression: "format!(\"{empty}\\n{missing}\\n{partial}\")" " " " " " " -" tab/1–5 section · z dashboard · ←/→ day " +" tab/1–6 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] 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -90,13 +89,13 @@ expression: "format!(\"{empty}\\n{missing}\\n{partial}\")" " " " " " " -" tab/1–5 section · z dashboard · ←/→ day " +" tab/1–6 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] 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -135,5 +134,5 @@ expression: "format!(\"{empty}\\n{missing}\\n{partial}\")" " " " " " " -" tab/1–5 section · z dashboard · ←/→ day " +" tab/1–6 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 index df5d929d6c..67b75a08a3 100644 --- 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 @@ -1,12 +1,11 @@ --- source: tui/src/analytics/tool_panel_tests.rs -assertion_line: 46 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 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages [4 Plugins called] 5 Skills used 6 Top chats " " " " ▎ Plugins called " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -49,14 +48,14 @@ expression: "format!(\"{wide}\\n{narrow}\")" " ● GitHub 5 " " ● Notion 4 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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] Top chats " +" Summary Total usage history Messages Plugins called " +" [Skills used] Top chats " " " " ▎ Skills used " " ────────────────────────────────────────────────────── " @@ -80,7 +79,7 @@ expression: "format!(\"{wide}\\n{narrow}\")" " ● Planning 1 " " ● Debugging 0 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter " +" tab/1–6 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/summary.rs b/codex-rs/tui/src/analytics/summary.rs new file mode 100644 index 0000000000..04545b29d0 --- /dev/null +++ b/codex-rs/tui/src/analytics/summary.rs @@ -0,0 +1,76 @@ +//! Account profile loading and the Summary's year-view interaction. +//! Aggregation changes reuse the loaded profile; account refresh replaces all profile data. + +use super::AnalyticsView; +use super::activity_chart::TokenActivityView; +use super::client::Live; +use super::data::Load; +use super::sections::Section; +use crate::keymap::ListAction; +use codex_backend_client::AccountProfile; + +pub(super) const VIEWS: [TokenActivityView; 3] = [ + TokenActivityView::Daily, + TokenActivityView::Weekly, + TokenActivityView::Cumulative, +]; + +impl Live { + pub(super) async fn profile(&self) -> Result, String> { + let session = self.session().await?; + let profile = session + .backend + .request(|client| async move { client.get_account_profile().await }) + .await; + session.backend.ensure_identity().await?; + profile.map(Some).map_err(super::client::request_error) + } +} + +impl AnalyticsView { + pub(crate) fn select_summary(&mut self, view: Option) { + self.section = Section::Summary; + self.zoomed = true; + self.scroll_offset = 0; + self.group_picker = None; + if let Some(view) = view { + self.sections[Section::Summary].group = VIEWS + .iter() + .position(|item| *item == view) + .unwrap_or_default(); + } + } + + pub(super) fn load_summary(&mut self) { + if let (Some((_, _, frame)), Some(live)) = (&self.connection, &self.live) { + let live = std::sync::Arc::clone(live); + self.profile = Load::start(async move { live.profile().await }, frame.clone()); + } + } + + pub(super) fn summary_action(&mut self, action: Option) { + let step = match action { + Some(ListAction::MoveUp) => Some(-1), + Some(ListAction::MoveDown) => Some(1), + Some(ListAction::PageUp) => Some(-(self.viewport_height as isize)), + Some(ListAction::PageDown) => Some(self.viewport_height as isize), + Some(ListAction::JumpTop) => { + self.scroll_offset = 0; + Some(0) + } + Some(ListAction::JumpBottom) => { + self.scroll_offset = usize::MAX; + Some(0) + } + Some(ListAction::Cancel) => { + self.is_done = true; + None + } + Some(ListAction::MoveLeft | ListAction::MoveRight | ListAction::Accept) | None => None, + }; + if let Some(step) = step { + self.scroll_offset = self.scroll_offset.saturating_add_signed(step); + self.follow_selection = false; + } + } +} diff --git a/codex-rs/tui/src/analytics/summary_panel.rs b/codex-rs/tui/src/analytics/summary_panel.rs new file mode 100644 index 0000000000..3beec95ef6 --- /dev/null +++ b/codex-rs/tui/src/analytics/summary_panel.rs @@ -0,0 +1,374 @@ +//! Responsive profile summary using the Analytics container's typography and spacing. + +use super::AnalyticsView; +use super::activity_chart; +use super::render::columns; +use super::render::join_columns; +use super::sections::Section; +use super::styles::secondary_style; +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 codex_backend_client::ProfileInvocationKind; +use ratatui::style::Styled; +use ratatui::style::Stylize; +use ratatui::text::Line; + +impl AnalyticsView { + pub(super) fn summary_lines(&self, width: usize) -> Vec> { + let width = width.max(/*other*/ 1); + let inner_width = width.min(/*other*/ 110); + let mut lines = Vec::new(); + let Some(profile) = self.profile.ready() else { + lines.push( + self.profile + .message() + .unwrap_or("Profile statistics unavailable.") + .to_owned() + .into(), + ); + return lines; + }; + if let Some(identity) = &profile.profile { + if let Some(name) = identity + .display_name + .as_deref() + .filter(|name| !name.trim().is_empty()) + { + lines.push(center( + truncate(name.trim().to_owned().bold().into(), inner_width), + inner_width, + )); + } + if let Some(username) = identity + .username + .as_deref() + .filter(|name| !name.trim().is_empty()) + { + lines.push(center( + truncate( + format!("@{}", username.trim().trim_start_matches('@')) + .set_style(secondary_style()) + .into(), + inner_width, + ), + inner_width, + )); + } + } + let plan = self.account.ready().map(|plan| { + use codex_protocol::account::PlanType; + match plan { + PlanType::Free => "Free", + PlanType::Go => "Go", + PlanType::Plus => "Plus", + PlanType::Pro => "Pro", + PlanType::ProLite => "Pro Lite", + PlanType::Team + | PlanType::Business + | PlanType::SelfServeBusinessProLite + | PlanType::SelfServeBusinessUsageBased => "Business", + PlanType::Ent26 + | PlanType::EnterpriseCbpAutomation + | PlanType::EnterpriseCbpUsageBased + | PlanType::Enterprise => "Enterprise", + PlanType::Edu | PlanType::EduPlus | PlanType::EduPro => "Education", + PlanType::Unknown => "Account", + } + }); + if let Some(plan) = plan { + lines.push(center( + plan.set_style(secondary_style()).into(), + inner_width, + )); + } + lines.push(Line::default()); + let tokens = &profile.stats.tokens; + let fields = [ + ( + "Lifetime tokens", + tokens + .lifetime_tokens + .map(|value| super::data::compact_amount(value as f64)), + ), + ( + "Peak tokens", + tokens + .peak_daily_tokens + .map(|value| super::data::compact_amount(value as f64)), + ), + ( + "Longest chat", + tokens.longest_running_turn_sec.map(duration), + ), + ( + "Current streak", + tokens + .current_streak_days + .map(|days| format!("{days} days")), + ), + ( + "Longest streak", + tokens + .longest_streak_days + .map(|days| format!("{days} days")), + ), + ]; + if !self.zoomed { + lines.extend(fields.into_iter().map(|(label, value)| { + columns( + label.set_style(secondary_style()).into(), + value.unwrap_or_else(|| "—".into()).bold().into(), + inner_width, + ) + })); + return lines; + } + let count = if inner_width >= 90 { + 5 + } else if inner_width >= 54 { + 3 + } else if inner_width >= 36 { + 2 + } else { + 1 + }; + for chunk in fields.chunks(count) { + let cell_width = inner_width / chunk.len(); + let mut values = Line::default(); + let mut labels = Line::default(); + for (index, (label, value)) in chunk.iter().enumerate() { + for (row, cell) in [ + ( + &mut values, + value.clone().unwrap_or_else(|| "—".into()).bold(), + ), + (&mut labels, label.to_string().set_style(secondary_style())), + ] { + let cell = center(truncate(cell.into(), cell_width), cell_width); + let padding = cell_width.saturating_sub(cell.width()); + row.spans.extend(cell.spans); + if index + 1 < chunk.len() { + row.spans.push(" ".repeat(padding).into()); + } + } + } + lines.extend([values, labels, Line::default()]); + } + let selected = self.sections[Section::Summary].group; + let mut controls = Line::default(); + for (index, view) in super::summary::VIEWS.iter().enumerate() { + if index > 0 { + controls.spans.push(" ".into()); + } + controls.spans.push(if index == selected { + format!("[{}]", view.label()).set_style(accent_style()) + } else { + view.label().to_string().set_style(secondary_style()) + }); + } + if inner_width >= 58 { + lines.push(columns( + "Token activity".bold().into(), + controls, + inner_width, + )); + } else { + lines.push("Token activity".bold().into()); + lines.extend(word_wrap_lines([controls], RtOptions::new(inner_width))); + } + lines.push("Last 12 months".set_style(secondary_style()).into()); + lines.push(Line::default()); + if let Some(buckets) = &tokens.daily_usage_buckets { + lines.extend(activity_chart::chart_lines( + super::summary::VIEWS[selected], + buckets, + self.end_date, + inner_width as u16, + )); + } else { + lines.push( + "Token activity history unavailable" + .set_style(secondary_style()) + .into(), + ); + } + lines.push(Line::default()); + let stats = &profile.stats; + let reasoning = stats + .most_used_reasoning_effort + .as_deref() + .map(|effort| { + let percentage = percent(stats.most_used_reasoning_effort_percentage.as_ref()); + format!("{effort} · {percentage}") + }) + .unwrap_or_else(|| "—".into()); + let metrics = [ + ( + "Fast Mode", + percent(stats.fast_mode_usage_percentage.as_ref()), + ), + ("Most used reasoning", reasoning), + ("Skills explored", count_value(stats.unique_skills_used)), + ("Total skills used", count_value(stats.total_skills_used)), + ("Total chats", count_value(stats.total_threads)), + ]; + let two_columns = inner_width >= 76; + let column_width = if two_columns { + (inner_width - 4) / 2 + } else { + inner_width + }; + let mut insights = vec!["Activity insights".bold().into(), Line::default()]; + for (label, value) in metrics { + insights.extend(word_wrap_lines( + [columns( + label.set_style(secondary_style()).into(), + value.into(), + column_width, + )], + RtOptions::new(column_width), + )); + } + let mut plugins = vec![ + "Most used plugins and skills".bold().into(), + Line::default(), + ]; + if let Some(invocations) = &stats.top_invocations { + let mut displayed = 0; + for invocation in invocations { + let (prefix, name) = match invocation.kind { + ProfileInvocationKind::Plugin => ("@", invocation.plugin_name.as_deref()), + ProfileInvocationKind::Skill => ("$", invocation.skill_name.as_deref()), + ProfileInvocationKind::Unknown => continue, + }; + let Some(name) = name.filter(|name| !name.trim().is_empty()) else { + continue; + }; + let value = invocation + .usage_count + .map(|count| { + format!( + "{} {}", + count_value(Some(count)), + if count == 1 { "run" } else { "runs" } + ) + }) + .unwrap_or_else(|| "—".into()); + plugins.extend(word_wrap_lines( + [columns( + truncate( + format!("{prefix}{}", name.trim()).into(), + column_width.saturating_sub(value.len() + 1), + ), + value.set_style(secondary_style()).into(), + column_width, + )], + RtOptions::new(column_width), + )); + displayed += 1; + if displayed == 5 { + break; + } + } + if displayed == 0 { + plugins.push( + "No reported plugins or skills." + .set_style(secondary_style()) + .into(), + ); + } + } else { + plugins.push( + "Plugin and skill usage unavailable." + .set_style(secondary_style()) + .into(), + ); + } + if two_columns { + lines.extend(join_columns(&insights, &plugins, column_width)); + } else { + lines.extend(insights); + lines.push(Line::default()); + lines.extend(plugins); + } + if let Some(metadata) = &profile.metadata { + lines.push(Line::default()); + if let Some(date) = metadata + .stats_as_of + .as_deref() + .and_then(|date| chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d").ok()) + { + lines.push( + format!("Statistics as of {}", date.format("%b %-d, %Y")) + .set_style(secondary_style()) + .into(), + ); + } + if metadata + .stats_error + .as_deref() + .is_some_and(|error| !error.trim().is_empty()) + { + lines.push( + "Some profile statistics are unavailable." + .set_style(secondary_style()) + .into(), + ); + } + } + let padding = " ".repeat(width.saturating_sub(inner_width) / 2); + word_wrap_lines(lines, RtOptions::new(inner_width)) + .into_iter() + .map(|mut line| { + line.spans.insert(/*index*/ 0, padding.clone().into()); + line + }) + .collect() + } +} + +fn center(mut line: Line<'static>, width: usize) -> Line<'static> { + line.spans.insert( + /*index*/ 0, + " ".repeat(width.saturating_sub(line.width()) / 2).into(), + ); + line +} + +fn percent(value: Option<&serde_json::Number>) -> String { + value + .and_then(serde_json::Number::as_f64) + .filter(|value| value.is_finite() && (0.0..=100.0).contains(value)) + .map(|value| format!("{value:.0}%")) + .unwrap_or_else(|| "—".into()) +} + +fn count_value(value: Option) -> String { + let Some(value) = value else { + return "—".into(); + }; + let digits = value.to_string(); + digits + .chars() + .enumerate() + .fold(String::new(), |mut result, (index, ch)| { + if index > 0 && (digits.len() - index).is_multiple_of(/*rhs*/ 3) { + result.push(','); + } + result.push(ch); + result + }) +} + +fn duration(seconds: i64) -> String { + let seconds = seconds.max(/*other*/ 0); + let (hours, minutes) = (seconds / 3600, seconds % 3600 / 60); + match (hours, minutes) { + (0, 0) => format!("{seconds}s"), + (0, minutes) => format!("{minutes}m"), + (hours, 0) => format!("{hours}h"), + (hours, minutes) => format!("{hours}h {minutes}m"), + } +} diff --git a/codex-rs/tui/src/analytics/summary_tests.rs b/codex-rs/tui/src/analytics/summary_tests.rs new file mode 100644 index 0000000000..62f4179052 --- /dev/null +++ b/codex-rs/tui/src/analytics/summary_tests.rs @@ -0,0 +1,143 @@ +//! Summary reflow, aggregation selection, and independently unavailable profile sections. +use super::*; +use pretty_assertions::assert_eq; +use serde_json::json; + +fn profile() -> codex_backend_client::AccountProfile { + serde_json::from_value(json!({ + "profile":{"display_name":"Example User","username":"example.user"}, + "metadata":{"stats_as_of":"2026-09-09"}, + "stats":{"lifetime_tokens":142_300_000_000_i64,"peak_daily_tokens":7_000_000_000_i64, + "longest_running_turn_sec":158820,"current_streak_days":158,"longest_streak_days":160, + "daily_usage_buckets":[{"start_date":"2026-09-08","tokens":100},{"start_date":"2026-09-09","tokens":200}], + "fast_mode_usage_percentage":37.0,"most_used_reasoning_effort":"high","most_used_reasoning_effort_percentage":26.0, + "unique_skills_used":147,"total_skills_used":13190,"total_threads":21482, + "top_invocations":[{"type":"skill","skill_name":"review","usage_count":1142},{"type":"plugin","plugin_name":"GitHub","usage_count":1059},{"type":"future"}]} + })).unwrap() +} + +#[test] +fn summary_modes_reflow_and_keep_selection_across_tabs() { + let mut view = fixture::view(models::AccountKind::Consumer); + view.profile = Load::Ready(profile()); + view.end_date = "2026-09-09".parse().unwrap(); + view.select_summary(Some(crate::analytics::TokenActivityView::Daily)); + insta::assert_snapshot!( + "summary_daily_wide", + screen(&mut view, /*width*/ 140, /*height*/ 44) + ); + press(&mut view, KeyCode::Char('g')); + press(&mut view, KeyCode::Down); + press(&mut view, KeyCode::Enter); + assert_eq!(view.sections[Section::Summary].group, 1); + assert!(view.profile.ready().is_some()); + insta::assert_snapshot!( + "summary_weekly", + screen(&mut view, /*width*/ 110, /*height*/ 44) + ); + press(&mut view, KeyCode::Tab); + press(&mut view, KeyCode::BackTab); + assert_eq!(view.sections[Section::Summary].group, 1); + press(&mut view, KeyCode::Char('g')); + press(&mut view, KeyCode::Down); + press(&mut view, KeyCode::Enter); + insta::assert_snapshot!( + "summary_cumulative_narrow", + screen(&mut view, /*width*/ 64, /*height*/ 60) + ); + for width in [1, 4, 28, 64] { + let lines = view.summary_lines(width); + assert!(lines.iter().all(|line| line.width() <= width)); + } + screen(&mut view, /*width*/ 64, /*height*/ 18); + press(&mut view, KeyCode::End); + insta::assert_snapshot!( + "summary_scrolled", + screen(&mut view, /*width*/ 64, /*height*/ 18) + ); + press(&mut view, KeyCode::Home); + press(&mut view, KeyCode::Char('z')); + insta::assert_snapshot!( + "summary_overview", + screen(&mut view, /*width*/ 144, /*height*/ 64) + ); + press(&mut view, KeyCode::Esc); + assert!(view.is_done); +} + +#[test] +fn partial_summary_keeps_missing_values_and_zero() { + let mut view = fixture::view(models::AccountKind::Business); + view.select_summary(/*view*/ None); + view.profile = Load::Ready(serde_json::from_value(json!({ + "metadata":{"stats_error":"sensitive upstream error"}, + "stats":{"lifetime_tokens":0,"fast_mode_usage_percentage":0,"total_threads":0,"top_invocations":[]} + })).unwrap()); + let rendered = screen(&mut view, /*width*/ 100, /*height*/ 40); + assert!(!rendered.contains("sensitive upstream error")); + insta::assert_snapshot!("summary_partial", rendered); + view.refresh(); + assert!(view.profile.ready().is_none()); +} + +#[tokio::test] +async fn summary_loads_once_per_refresh_and_does_not_block_other_reports() { + use wiremock::Mock; + use wiremock::ResponseTemplate; + use wiremock::matchers::header; + use wiremock::matchers::method; + use wiremock::matchers::path; + let server = test_support::server().await; + let requests = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(/*v*/ 0)); + let count = std::sync::Arc::clone(&requests); + Mock::given(method("GET")) + .and(path("/backend-api/wham/profiles/me")) + .and(header("chatgpt-account-id", "account-a")) + .respond_with(move |_: &wiremock::Request| { + if count.fetch_add(/*val*/ 1, std::sync::atomic::Ordering::SeqCst) == 0 { + ResponseTemplate::new(/*s*/ 503) + } else { + ResponseTemplate::new(/*s*/ 200) + .set_body_json(json!({"stats":{"lifetime_tokens":123}})) + } + }) + .expect(/*r*/ 2) + .mount(&server) + .await; + let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await; + test_support::settle(&mut view).await; + assert_eq!(view.section, Section::Summary); + assert!(matches!(view.profile, Load::Error(_))); + assert!(view.sections[Section::Usage].history.ready().is_some()); + press(&mut view, KeyCode::Char('g')); + press(&mut view, KeyCode::Down); + press(&mut view, KeyCode::Enter); + press(&mut view, KeyCode::Tab); + press(&mut view, KeyCode::BackTab); + test_support::settle(&mut view).await; + assert_eq!(requests.load(std::sync::atomic::Ordering::SeqCst), 1); + press(&mut view, KeyCode::Char('R')); + test_support::settle(&mut view).await; + assert_eq!( + view.profile.ready().unwrap().stats.tokens.lifetime_tokens, + Some(123) + ); + assert_eq!(view.sections[Section::Summary].group, 1); + view.cancel_loads(); + assert!(matches!(view.profile, Load::Unavailable)); +} + +#[tokio::test] +async fn closing_summary_aborts_its_pending_profile() { + let mut view = fixture::view(models::AccountKind::Consumer); + let (cancelled_tx, cancelled_rx) = tokio::sync::oneshot::channel::<()>(); + view.profile = Load::start( + async move { + let _cancelled = cancelled_tx; + std::future::pending().await + }, + crate::tui::FrameRequester::test_dummy(), + ); + view.cancel_loads(); + assert!(cancelled_rx.await.is_err()); +} diff --git a/codex-rs/tui/src/analytics/test_fixtures.rs b/codex-rs/tui/src/analytics/test_fixtures.rs index e1e16b9495..07ff4eab17 100644 --- a/codex-rs/tui/src/analytics/test_fixtures.rs +++ b/codex-rs/tui/src/analytics/test_fixtures.rs @@ -208,6 +208,11 @@ pub(super) fn view(kind: super::models::AccountKind) -> super::AnalyticsView { } super::models::AccountKind::Unknown => codex_protocol::account::PlanType::Unknown, }); + view.section = if view.business() { + super::sections::Section::Credits + } else { + super::sections::Section::Usage + }; view.start_reports(); seed_reports(&mut view); view diff --git a/codex-rs/tui/src/analytics/test_support.rs b/codex-rs/tui/src/analytics/test_support.rs index 9e51ba2699..82b605a1a3 100644 --- a/codex-rs/tui/src/analytics/test_support.rs +++ b/codex-rs/tui/src/analytics/test_support.rs @@ -63,6 +63,7 @@ pub(super) async fn settle(view: &mut super::AnalyticsView) { .any(|state| matches!(state.history, Load::Loading(_))) && !matches!(view.chats, Load::Loading(_)) && !matches!(view.tasks, Load::Loading(_)) + && !matches!(view.profile, Load::Loading(_)) && !matches!(view.plan.report, Load::Loading(_)) { break; diff --git a/codex-rs/tui/src/analytics/tool_panel_tests.rs b/codex-rs/tui/src/analytics/tool_panel_tests.rs index b13ea6cd15..87a5f0cc7c 100644 --- a/codex-rs/tui/src/analytics/tool_panel_tests.rs +++ b/codex-rs/tui/src/analytics/tool_panel_tests.rs @@ -7,10 +7,10 @@ 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')); + press(&mut view, KeyCode::Char('4')); let wide = screen(&mut view, /*width*/ 140, /*height*/ 48); assert!(wide.contains("186 calls")); - press(&mut view, KeyCode::Char('4')); + press(&mut view, KeyCode::Char('5')); press(&mut view, KeyCode::Left); press(&mut view, KeyCode::Enter); assert_eq!( @@ -56,7 +56,7 @@ fn analytics_tools_handle_empty_days_and_independent_failures() { } history.data.remove(/*index*/ 5); view.sections[Section::Skills].history = Load::Ready(history); - press(&mut view, KeyCode::Char('4')); + press(&mut view, KeyCode::Char('5')); let empty = screen(&mut view, /*width*/ 140, /*height*/ 44); assert!(empty.contains("No uses reported for Sep 2.")); assert!(!empty.contains("details")); @@ -73,7 +73,7 @@ fn analytics_tools_handle_empty_days_and_independent_failures() { 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')); + press(&mut view, KeyCode::Char('4')); assert!(screen(&mut view, /*width*/ 140, /*height*/ 44).contains("186 calls")); insta::assert_snapshot!(format!("{empty}\n{missing}\n{partial}")); } diff --git a/codex-rs/tui/src/analytics_tests.rs b/codex-rs/tui/src/analytics_tests.rs index 06b412017c..5d8969b7cb 100644 --- a/codex-rs/tui/src/analytics_tests.rs +++ b/codex-rs/tui/src/analytics_tests.rs @@ -1,8 +1,12 @@ //! Dashboard layout, local detail, shared ranges, grouping, and scroll/focus retention. use crate::analytics::sections::Section; +#[path = "analytics/compact_tests.rs"] +mod compact; #[path = "analytics/dashboard_tests.rs"] mod dashboard; +#[path = "analytics/summary_tests.rs"] +mod summary; #[path = "analytics/account_layout_tests.rs"] mod account_layout; @@ -50,7 +54,7 @@ fn analytics_overview() { 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'] { + for section in ['2', '3', '4'] { press(&mut view, KeyCode::Char(section)); press(&mut view, KeyCode::Char('r')); fixture::seed_reports(&mut view); @@ -69,7 +73,7 @@ fn analytics_narrow_charts_and_small_terminals_keep_valid_cursors() { } let empty = ratatui::layout::Rect::default(); view.render(empty, &mut ratatui::buffer::Buffer::empty(empty)); - press(&mut view, KeyCode::Char('4')); + press(&mut view, KeyCode::Char('5')); let tools = screen(&mut view, /*width*/ 58, /*height*/ 20); insta::assert_snapshot!(tools); } @@ -79,7 +83,7 @@ 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::Char('2')); press(&mut view, KeyCode::Left); let before = view.sections[Section::Credits] .history @@ -274,7 +278,7 @@ fn analytics_dates_distinguish_zero_missing_and_exact_details() { 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::Char('2')); press(&mut view, KeyCode::Home); let mut screens = Vec::new(); for keys in [ @@ -334,11 +338,11 @@ fn analytics_model_labels_and_tool_remainders_are_unambiguous() { day.values[1].label = "Other".into(); } view.sections[Section::Skills].history = Load::Ready(tools); - press(&mut view, KeyCode::Char('2')); + press(&mut view, KeyCode::Char('3')); 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::Char('5')); press(&mut view, KeyCode::Enter); let skills = screen(&mut view, /*width*/ 100, /*height*/ 32); assert!(skills.contains("Other")); @@ -356,7 +360,7 @@ fn analytics_maximized_refunds_leave_room_for_both_bands() { value: -0.000004, }]; view.sections[Section::Credits].history = Load::Ready(history); - press(&mut view, KeyCode::Char('1')); + press(&mut view, KeyCode::Char('2')); let output = screen(&mut view, /*width*/ 100, /*height*/ 32); assert!(output.contains("Sep 2 · -0.000004 credits")); assert!( @@ -375,7 +379,7 @@ fn analytics_day_navigation_keeps_geometry_and_legend() { 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')); + press(&mut view, KeyCode::Char('2')); let mut snapshots = Vec::new(); for width in [58, 160] { let mut terminal = Terminal::new(TestBackend::new(width, /*height*/ 38)).unwrap(); @@ -578,7 +582,7 @@ fn analytics_empty_turns_keep_geometry_and_disable_details() { } history.data.remove(/*index*/ 6); view.sections[Section::Activity].history = Load::Ready(history); - press(&mut view, KeyCode::Char('2')); + press(&mut view, KeyCode::Char('3')); let mut snapshots = Vec::new(); for width in [58, 100] { view.sections[Section::Activity].cursor = 4; @@ -631,7 +635,7 @@ fn analytics_details_reflow_and_keep_focus() { let mut view = fixture::view(models::AccountKind::Enterprise); view.chats = Load::Ready(fixture::chats()); press(&mut view, KeyCode::Char('z')); - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); press(&mut view, KeyCode::Enter); assert!(view.zoomed); press(&mut view, KeyCode::Enter); @@ -656,7 +660,7 @@ fn analytics_details_reflow_and_keep_focus() { before ); press(&mut view, KeyCode::Tab); - press(&mut view, KeyCode::Char('5')); + press(&mut view, KeyCode::Char('6')); assert_eq!( ( view.section, @@ -671,7 +675,7 @@ fn analytics_details_reflow_and_keep_focus() { view.is_done, view.sections.0.each_ref().map(|state| state.detail) ), - (false, [None; 7]) + (false, [None; 8]) ); press(&mut view, KeyCode::Esc); assert!(view.is_done); diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 19f3308d47..d1d51ebffd 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -30,8 +30,6 @@ use crate::hooks_rpc::write_hook_trust; use crate::hooks_rpc::write_hook_trusts; use codex_utils_absolute_path::AbsolutePathBuf; -const TOKEN_ACTIVITY_FETCH_TIMEOUT: std::time::Duration = - std::time::Duration::from_secs(/*secs*/ 15); pub(super) const THREAD_USAGE_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(/*secs*/ 65); const RATE_LIMIT_RESET_REQUEST_TIMEOUT: std::time::Duration = @@ -124,25 +122,6 @@ impl App { }); } - pub(super) fn refresh_token_activity( - &mut self, - app_server: &AppServerSession, - request_id: u64, - ) { - let request_handle = app_server.request_handle(); - let app_event_tx = self.app_event_tx.clone(); - tokio::spawn(async move { - let result = tokio::time::timeout( - TOKEN_ACTIVITY_FETCH_TIMEOUT, - fetch_account_token_activity(request_handle), - ) - .await - .map_err(|_| "account/usage/read timed out in TUI".to_string()) - .and_then(|result| result.map_err(|err| err.to_string())); - app_event_tx.send(AppEvent::TokenActivityLoaded { request_id, result }); - }); - } - pub(super) fn refresh_thread_usage( &mut self, app_server: &AppServerSession, @@ -831,19 +810,6 @@ pub(super) async fn fetch_account_rate_limits( result.wrap_err("account/rateLimits/read failed in TUI") } -pub(super) async fn fetch_account_token_activity( - request_handle: AppServerRequestHandle, -) -> Result { - let request_id = RequestId::String(format!("account-token-usage-{}", Uuid::new_v4())); - request_handle - .request_typed(ClientRequest::GetAccountTokenUsage { - request_id, - params: None, - }) - .await - .wrap_err("account/usage/read failed in TUI") -} - pub(super) async fn fetch_thread_usage( request_handle: AppServerRequestHandle, thread_id: ThreadId, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index ec26f4ccec..ee4041233f 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1413,9 +1413,6 @@ impl App { self.refresh_rate_limits(app_server, RateLimitRefreshOrigin::Periodic); } } - AppEvent::RefreshTokenActivity { request_id } => { - self.refresh_token_activity(app_server, request_id); - } AppEvent::RefreshThreadUsage { thread_id, request_id, @@ -1605,16 +1602,15 @@ impl App { self.chat_widget.finish_rate_limit_recovery(); } }, - AppEvent::OpenTokenActivity => { - self.chat_widget - .add_token_activity_output(crate::chatwidget::TokenActivityView::Daily); - } - AppEvent::OpenAnalytics => { + AppEvent::OpenAnalytics { view: summary_view } => { 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(); + if summary_view.is_some() { + view.select_summary(summary_view); + } view.open( app_server.request_handle(), tui.frame_requester(), @@ -1692,22 +1688,6 @@ impl App { ); } } - AppEvent::TokenActivityLoaded { request_id, result } => { - if let Err(err) = &result { - tracing::warn!("account/usage/read failed during TUI refresh: {err}"); - } - if self - .chat_widget - .finish_token_activity_refresh(request_id, result) - { - // Commit synchronously so an already queued /clear cannot overtake this card. - // Do not route through ChatWidget::add_to_history: /usage may complete during - // active work, and flushing an in-progress tool cell would corrupt its lifecycle. - // If an answer stream is active, keep the settled card transient until its - // provisional transcript cells have been consolidated. - self.insert_pending_usage_output_if_ready(tui); - } - } AppEvent::ThreadUsageLoaded { thread_id, request_id, diff --git a/codex-rs/tui/src/app/history_ui.rs b/codex-rs/tui/src/app/history_ui.rs index 9d63c68a1c..d79f83ff71 100644 --- a/codex-rs/tui/src/app/history_ui.rs +++ b/codex-rs/tui/src/app/history_ui.rs @@ -197,9 +197,6 @@ impl App { } fn insert_pending_usage_output(&mut self, tui: &mut tui::Tui) { - if let Some(cell) = self.chat_widget.take_completed_token_activity_output() { - self.insert_history_cell(tui, Box::new(cell)); - } if let Some(cell) = self.chat_widget.take_pending_rate_limit_reset_hint() { self.insert_history_cell(tui, Box::new(cell)); } @@ -324,7 +321,6 @@ impl App { self.deferred_history_lines.clear(); self.has_emitted_history_lines = false; self.transcript_reflow.clear(); - self.chat_widget.clear_pending_token_activity_refreshes(); self.chat_widget.clear_pending_rate_limit_reset_hint(); self.initial_history_replay_buffer = None; self.scrollback_has_older_history = false; diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index cfa991bd2b..cf696d6d93 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -8538,38 +8538,6 @@ async fn refreshed_snapshot_session_persists_resumed_turns() { ); } -#[tokio::test] -async fn late_usage_result_can_follow_finalized_plan() { - let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; - app.chat_widget - .add_token_activity_output(crate::chatwidget::TokenActivityView::Daily); - let request_id = match app_event_rx.try_recv() { - Ok(AppEvent::RefreshTokenActivity { request_id }) => request_id, - other => panic!("expected token activity refresh request, got {other:?}"), - }; - - app.chat_widget.note_stream_consolidation_queued(); - app.transcript_cells - .push(Arc::new(history_cell::new_proposed_plan_stream( - vec![Line::from("finalized plan")], - /*is_stream_continuation*/ false, - ))); - app.chat_widget.note_stream_consolidation_completed(); - - assert!( - app.chat_widget.finish_token_activity_refresh( - request_id, - Err("token activity unavailable".to_string()), - ) - ); - assert!(!app.pending_usage_output_insertion_blocked()); - assert!( - app.chat_widget - .take_completed_token_activity_output() - .is_some() - ); -} - #[tokio::test] async fn new_session_requests_shutdown_for_previous_conversation() { Box::pin(async { diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__transcript_composer__analytics_menu_reopen_preserves_navigation_and_explicit_view_selects_summary.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__transcript_composer__analytics_menu_reopen_preserves_navigation_and_explicit_view_selects_summary.snap new file mode 100644 index 0000000000..f7b6079b54 --- /dev/null +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__transcript_composer__analytics_menu_reopen_preserves_navigation_and_explicit_view_selects_summary.snap @@ -0,0 +1,78 @@ +--- +source: tui/src/app/tests/transcript_composer.rs +expression: snapshot +--- + Analytics · Live account + [7d] 30d · [date range] + + 1 Summary 2 Total usage history 3 Messages [4 Plugins called] 5 Skills + used 6 Top chats + + ▎ Plugins called + ──────────────────────────────────────────────────────────────────────────── + No data reported for this range. + + + + + + + + + + + + + + tab/1–6 section · z dashboard · ←/→ day + r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close + + Analytics · Live account + [7d] 30d · [date range] + + 1 Summary 2 Total usage history 3 Messages [4 Plugins called] 5 Skills + used 6 Top chats + + ▎ Plugins called + ──────────────────────────────────────────────────────────────────────────── + No data reported for this range. + + + + + + + + + + + + + + tab/1–6 section · z dashboard · ←/→ day + r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close + + Analytics · Live account + + [1 Summary] 2 Total usage history 3 Messages 4 Plugins called 5 Skills + used 6 Top chats + + ▎ Summary + ──────────────────────────────────────────────────────────────────────────── + Plus + + 12.3K — — + Lifetime tokens Peak tokens Longest chat + + — — + Current streak Longest streak + + Token activity Daily [Weekly] Cumulative + Last 12 months + + Token activity history unavailable + + Activity insights Most used plugins and skills + + tab/1–6 section · z dashboard · ↑/↓ scroll + g view · R refresh · pgup/pgdn scroll · esc back · q close diff --git a/codex-rs/tui/src/app/tests/transcript_composer.rs b/codex-rs/tui/src/app/tests/transcript_composer.rs index 7e72da277e..922a05da91 100644 --- a/codex-rs/tui/src/app/tests/transcript_composer.rs +++ b/codex-rs/tui/src/app/tests/transcript_composer.rs @@ -47,8 +47,12 @@ async fn analytics_preserves_the_draft_and_retains_the_view_on_reopen() -> Resul 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?; + app.handle_event( + &mut tui, + &mut app_server, + AppEvent::OpenAnalytics { view: None }, + ) + .await?; for code in [ KeyCode::Char('4'), KeyCode::Enter, @@ -62,8 +66,12 @@ async fn analytics_preserves_the_draft_and_retains_the_view_on_reopen() -> Resul 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?; + app.handle_event( + &mut tui, + &mut app_server, + AppEvent::OpenAnalytics { view: None }, + ) + .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?; @@ -74,6 +82,138 @@ async fn analytics_preserves_the_draft_and_retains_the_view_on_reopen() -> Resul Ok(()) } +#[tokio::test] +async fn analytics_menu_reopen_preserves_navigation_and_explicit_view_selects_summary() -> Result<()> +{ + let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await; + let http = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/backend-api/wham/accounts/check")) + .and(wiremock::matchers::header( + "chatgpt-account-id", + "test-account", + )) + .respond_with(wiremock::ResponseTemplate::new(/*s*/ 200).set_body_json( + serde_json::json!({"accounts": [{"id": "test-account", "plan_type": "plus"}]}), + )) + .expect(/*r*/ 3) + .mount(&http) + .await; + wiremock::Mock::given(wiremock::matchers::method("GET")) + .respond_with( + wiremock::ResponseTemplate::new(/*s*/ 200) + .set_body_json(serde_json::json!({"stats":{"lifetime_tokens":12345},"data":[]})), + ) + .mount(&http) + .await; + app.config.chatgpt_base_url = format!("{}/backend-api", http.uri()); + app.config.cli_auth_credentials_store_mode = codex_login::AuthCredentialsStoreMode::File; + app_test_support::write_chatgpt_auth( + &app.config.codex_home, + app_test_support::ChatGptAuthFixture::new("test-access-token") + .account_id("test-account") + .chatgpt_account_id("test-account") + .chatgpt_user_id("test-user") + .plan_type("plus"), + codex_login::AuthCredentialsStoreMode::File, + ) + .unwrap(); + let mut app_server = start_config_write_test_app_server(&app).await?; + let mut tui = crate::tui::test_support::make_test_tui()?; + let mut screens = Vec::new(); + for explicit in [ + None, + None, + Some(crate::analytics::TokenActivityView::Weekly), + ] { + app.handle_event( + &mut tui, + &mut app_server, + AppEvent::OpenAnalytics { view: explicit }, + ) + .await?; + tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 10), async { + loop { + app.handle_tui_event(&mut tui, &mut app_server, TuiEvent::Draw) + .await?; + let text = buffer_text(crate::custom_terminal::test_support::last_rendered_buffer( + &tui.terminal, + )); + if screens.is_empty() && text.contains("Lifetime tokens") { + press_key(&mut app, &mut tui, &mut app_server, KeyCode::Char('4')).await?; + continue; + } + if text.contains("Lifetime tokens") + || text.contains("No activity") + || text.contains("No data reported") + { + return Ok::<(), color_eyre::Report>(()); + } + tokio::time::sleep(std::time::Duration::from_millis(/*millis*/ 5)).await; + } + }) + .await??; + let screen = buffer_text(crate::custom_terminal::test_support::last_rendered_buffer( + &tui.terminal, + )); + screens.push(screen); + press_key(&mut app, &mut tui, &mut app_server, KeyCode::Char('q')).await?; + } + assert_eq!(screens[0], screens[1]); + assert!(screens[1].contains("[4 Plugins called]")); + assert!(screens[2].contains("[1 Summary]")); + assert!(screens[2].contains("[Weekly]")); + let snapshot = screens.join("\n\n"); + let snapshot = snapshot + .lines() + .map(|line| { + if line.starts_with(" [7d] 30d · ") { + " [7d] 30d · [date range]" + } else { + line + } + }) + .collect::>() + .join("\n"); + insta::assert_snapshot!(snapshot); + Ok(()) +} + +#[tokio::test] +async fn analytics_keeps_queued_and_late_startup_history_out_of_the_overlay() -> Result<()> { + for alternate_screen in [true, false] { + 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()?; + tui.set_alt_screen_enabled(alternate_screen); + app.insert_history_cell( + &mut tui, + Box::new(PlainHistoryCell::new(vec!["Startup header".into()])), + ); + assert!(!tui.pending_history_lines_for_test().is_empty()); + app.handle_event( + &mut tui, + &mut app_server, + AppEvent::OpenAnalytics { view: None }, + ) + .await?; + assert!(tui.pending_history_lines_for_test().is_empty()); + app.insert_history_cell( + &mut tui, + Box::new(PlainHistoryCell::new(vec![ + "Late startup announcement".into(), + ])), + ); + assert!(tui.pending_history_lines_for_test().is_empty()); + let deferred = app.deferred_history_lines.clone(); + assert!(!deferred.is_empty()); + press_key(&mut app, &mut tui, &mut app_server, KeyCode::Char('q')).await?; + assert_eq!(tui.pending_history_lines_for_test(), deferred); + assert_eq!(app.transcript_cells.len(), 2); + } + 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_event.rs b/codex-rs/tui/src/app_event.rs index 40c4462564..1544b3e199 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -19,7 +19,6 @@ use codex_app_server_protocol::AddCreditsNudgeEmailStatus; use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse; use codex_app_server_protocol::DynamicToolCallResponse; use codex_app_server_protocol::GetAccountRateLimitsResponse; -use codex_app_server_protocol::GetAccountTokenUsageResponse; use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::MarketplaceRemoveResponse; use codex_app_server_protocol::MarketplaceUpgradeResponse; @@ -678,11 +677,10 @@ pub(crate) enum AppEvent { result: Result, }, - /// Open the default token-activity view selected from the `/usage` menu. - OpenTokenActivity, - /// Open the authenticated account analytics dashboard. - OpenAnalytics, + OpenAnalytics { + view: Option, + }, /// Open the reset-credit flow selected from the `/usage` menu. OpenRateLimitResetCredits, @@ -711,17 +709,6 @@ pub(crate) enum AppEvent { result: Result, }, - /// Fetch account-wide token activity for a `/usage` history card. - RefreshTokenActivity { - request_id: u64, - }, - - /// Result of fetching account-wide token activity. - TokenActivityLoaded { - request_id: u64, - result: Result, - }, - /// Fetch backend-estimated usage for the currently visible enterprise thread. RefreshThreadUsage { thread_id: ThreadId, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3cbe36586e..0222a34771 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -445,12 +445,11 @@ use self::status_surfaces::CachedProjectRootName; mod thread_title_status; mod thread_usage; pub(crate) use self::thread_usage::ThreadUsageOutcome; -mod tokens; -pub(crate) use self::tokens::TokenActivityView; mod tool_lifecycle; mod tool_requests; mod transcript; mod transcript_export; +mod usage_history; use self::transcript::TranscriptState; mod turn_lifecycle; mod turn_runtime; @@ -610,9 +609,6 @@ pub(crate) struct ChatWidget { rate_limit_snapshots_by_limit_id: BTreeMap, refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>, next_status_refresh_request_id: u64, - refreshing_token_activity_output: Option, - completed_token_activity_output: Option, - next_token_activity_request_id: u64, pending_rate_limit_reset_request_id: Option, pending_rate_limit_reset_idempotency_key: Option, rate_limit_reset_picker_request_id: Option, @@ -1985,7 +1981,6 @@ impl ChatWidget { pub(crate) fn active_cell_transcript_key(&self) -> Option { let cell = self.transcript.active_cell.as_ref(); let mut realtime_cells = self.realtime_conversation.live_transcript_cells(); - let token_activity_cell = self.pending_token_activity_output(); let rate_limit_reset_hint = self.pending_rate_limit_reset_hint(); if cell.is_none() && self @@ -1994,7 +1989,6 @@ impl ChatWidget { .next() .is_none() && self.realtime_conversation.pending_history_cells.is_empty() - && token_activity_cell.is_none() && rate_limit_reset_hint.is_none() { return None; @@ -2036,13 +2030,6 @@ impl ChatWidget { } lines.extend(realtime_lines); } - if let Some(token_activity_cell) = self.pending_token_activity_output() { - let token_activity_lines = token_activity_cell.transcript_hyperlink_lines(width); - if !token_activity_lines.is_empty() && !lines.is_empty() { - lines.push(HyperlinkLine::from("")); - } - lines.extend(token_activity_lines); - } if let Some(rate_limit_reset_hint) = self.pending_rate_limit_reset_hint() { let hint_lines = rate_limit_reset_hint.transcript_hyperlink_lines(width); if !hint_lines.is_empty() && !lines.is_empty() { diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index 3ed23be896..97ede3622f 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -145,9 +145,6 @@ impl ChatWidget { rate_limit_snapshots_by_limit_id: BTreeMap::new(), refreshing_status_outputs: Vec::new(), next_status_refresh_request_id: 0, - refreshing_token_activity_output: None, - completed_token_activity_output: None, - next_token_activity_request_id: 0, pending_rate_limit_reset_request_id: None, pending_rate_limit_reset_idempotency_key: None, rate_limit_reset_picker_request_id: None, diff --git a/codex-rs/tui/src/chatwidget/rendering.rs b/codex-rs/tui/src/chatwidget/rendering.rs index 7752f51727..103ea9642c 100644 --- a/codex-rs/tui/src/chatwidget/rendering.rs +++ b/codex-rs/tui/src/chatwidget/rendering.rs @@ -174,17 +174,7 @@ impl ChatWidget { })), ); } - if let Some(cell) = self.pending_token_activity_output() { - flex.push( - /*flex*/ 1, - RenderableItem::Owned(Box::new(TranscriptAreaRenderable { - child: cell, - top: 1, - right: active_cell_right_reserve, - persistent_layout: None, - })), - ); - } + if let Some(cell) = self.pending_rate_limit_reset_hint() { flex.push( /*flex*/ 1, diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index 78dac65a9d..61817a46c9 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -213,7 +213,6 @@ impl ChatWidget { self.model_popup_request_id = None; self.invalidate_permission_discovery(); self.invalidate_connector_scope(); - self.clear_pending_token_activity_refreshes(); self.clear_pending_rate_limit_reset_requests(); self.clear_backend_banner(); self.luna_reserve_notice_account_id = None; diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index e18a0c50cf..156c671ffe 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -747,8 +747,10 @@ impl ChatWidget { } SlashCommand::Usage => { if self.ensure_usage_command_available() { - match tokens::TokenActivityView::parse(trimmed) { - Some(view) => self.add_token_activity_output(view), + match crate::analytics::TokenActivityView::parse(trimmed) { + Some(view) => self + .app_event_tx + .send(AppEvent::OpenAnalytics { view: Some(view) }), None => self.add_error_message( "Usage: /usage [daily|weekly|cumulative]".to_string(), ), diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pending_token_activity_refresh_renders_above_composer_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pending_token_activity_refresh_renders_above_composer_snapshot.snap deleted file mode 100644 index 3197796d1f..0000000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__pending_token_activity_refresh_renders_above_composer_snapshot.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: tui/src/chatwidget/tests/slash_commands.rs -expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) ---- - -/usage daily - - Token activity - Loading... - - -› Ask Codex to do anything - - gpt-5.6-sol default · /tmp/project 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 cc2fb465bb..459f6d04e5 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 @@ -5,8 +5,7 @@ expression: "render_bottom_popup(&chat, 80)" Usage View account usage or redeem an earned reset. -› 1. Show usage View recent account token usage. - 2. Explore analytics Browse account usage and activity. - 3. Redeem usage limit reset You have 2 usage limit resets available. +› 1. View analytics Browse account usage and activity. + 2. 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 ef503e1b9f..f21a8798a0 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 @@ -5,8 +5,7 @@ expression: "render_bottom_popup(&chat, 80)" Usage View account usage or redeem an earned reset. -› 1. Show usage View recent account token usage. - 2. Explore analytics Browse account usage and activity. - 3. Redeem usage limit reset Check reset availability. +› 1. View analytics Browse account usage and activity. + 2. 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 f2f78e64a3..aec39060ae 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 @@ -5,8 +5,7 @@ expression: "render_bottom_popup(&chat, 80)" Usage View account usage or redeem an earned reset. -› 1. Show usage View recent account token usage. - 2. Explore analytics Browse account usage and activity. +› 1. View 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/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 557002b211..6dcc09bf7e 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -79,21 +79,6 @@ fn recall_latest_after_clearing(chat: &mut ChatWidget) -> String { chat.bottom_pane.composer_text() } -fn dispatch_usage_and_expect_refresh( - chat: &mut ChatWidget, - rx: &mut tokio::sync::mpsc::UnboundedReceiver, -) -> u64 { - chat.dispatch_command_with_args(SlashCommand::Usage, "daily".to_string(), Vec::new()); - expect_token_activity_refresh(rx) -} - -fn expect_token_activity_refresh(rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> u64 { - match rx.try_recv() { - Ok(AppEvent::RefreshTokenActivity { request_id }) => request_id, - other => panic!("expected token activity refresh request, got {other:?}"), - } -} - fn next_add_to_history_event(rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> String { loop { match rx.try_recv() { @@ -1419,6 +1404,24 @@ async fn usage_command_with_invalid_view_reports_usage_snapshot() { assert_eq!(recall_latest_after_clearing(&mut chat), "/usage monthly"); } +#[tokio::test] +async fn usage_views_open_the_requested_summary_mode() { + use crate::analytics::TokenActivityView; + for (argument, expected) in [ + ("daily", TokenActivityView::Daily), + ("weekly", TokenActivityView::Weekly), + ("cumulative", TokenActivityView::Cumulative), + ] { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + set_chatgpt_auth(&mut chat); + chat.dispatch_command_with_args(SlashCommand::Usage, argument.to_string(), Vec::new()); + match rx.try_recv().unwrap() { + AppEvent::OpenAnalytics { view } => assert_eq!(view, Some(expected)), + other => panic!("expected Analytics, got {other:?}"), + } + } +} + #[tokio::test] async fn usage_command_runs_with_backend_auth_without_chatgpt_account_flag() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -1429,7 +1432,12 @@ async fn usage_command_runs_with_backend_auth_without_chatgpt_account_flag() { chat.dispatch_command_with_args(SlashCommand::Usage, "daily".to_string(), Vec::new()); - assert_matches!(rx.try_recv(), Ok(AppEvent::RefreshTokenActivity { .. })); + assert_matches!( + rx.try_recv(), + Ok(AppEvent::OpenAnalytics { + view: Some(crate::analytics::TokenActivityView::Daily) + }) + ); assert!(!chat.has_chatgpt_account()); } @@ -1445,314 +1453,16 @@ async fn usage_command_runs_with_backend_auth_from_widget_init() { chat.dispatch_command_with_args(SlashCommand::Usage, "daily".to_string(), Vec::new()); - assert_matches!(rx.try_recv(), Ok(AppEvent::RefreshTokenActivity { .. })); + assert_matches!( + rx.try_recv(), + Ok(AppEvent::OpenAnalytics { + view: Some(crate::analytics::TokenActivityView::Daily) + }) + ); assert!(!chat.has_chatgpt_account()); assert!(chat.has_codex_backend_auth()); } -#[tokio::test] -async fn clearing_pending_token_activity_refreshes_discards_late_result() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - assert_eq!( - chat.pending_token_activity_output() - .map(|cell| lines_to_single_string(&cell.display_lines(u16::MAX))), - Some("/usage daily\n\n Token activity\n Loading...\n".to_string()), - ); - assert_eq!( - chat.active_cell_transcript_lines(u16::MAX) - .map(|lines| lines_to_single_string(&lines)), - Some("/usage daily\n\n Token activity\n Loading...\n".to_string()), - ); - - chat.clear_pending_token_activity_refreshes(); - - assert!( - !chat.finish_token_activity_refresh( - request_id, - Err("stale token activity result".to_string()), - ) - ); -} - -#[tokio::test] -async fn account_state_change_discards_pending_token_activity_refresh() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - assert!(chat.pending_token_activity_output().is_some()); - - chat.update_account_state( - Some(crate::status::StatusAccountDisplay::ChatGpt { - email: Some("new-account@example.com".to_string()), - plan: None, - }), - /*plan_type*/ None, - /*has_chatgpt_account*/ true, - /*has_codex_backend_auth*/ true, - ); - - assert!(chat.pending_token_activity_output().is_none()); - assert!( - !chat.finish_token_activity_refresh( - request_id, - Err("stale token activity result".to_string()), - ) - ); -} - -#[tokio::test] -async fn pending_token_activity_refresh_renders_above_composer_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - - let width: u16 = 80; - let height = chat.desired_height(width); - let backend = VT100Backend::new(width, height); - let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal"); - term.set_viewport_area(Rect::new(/*x*/ 0, /*y*/ 0, width, height)); - term.draw(|f| { - chat.render(f.area(), f.buffer_mut()); - }) - .unwrap(); - assert_chatwidget_snapshot!( - "pending_token_activity_refresh_renders_above_composer_snapshot", - normalize_snapshot_paths(term.backend().vt100().screen().contents()) - ); -} - -#[tokio::test] -async fn completed_token_activity_refresh_returns_one_history_cell() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - assert!(chat.pending_token_activity_output().is_some()); - - assert!( - chat.finish_token_activity_refresh( - request_id, - Err("token activity unavailable".to_string()), - ) - ); - assert!(chat.pending_token_activity_output().is_some()); - let cell = chat - .take_completed_token_activity_output() - .expect("completed token activity cell"); - - assert!(chat.pending_token_activity_output().is_none()); - assert_eq!( - lines_to_single_string(&cell.display_lines(u16::MAX)), - "/usage daily\n\n Token activity\n Token activity unavailable\n", - ); -} - -#[tokio::test] -async fn completed_token_activity_refresh_waits_for_active_stream() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - chat.on_agent_message_delta("partial response".to_string()); - assert!(chat.usage_history_insertion_blocked()); - - assert!( - chat.finish_token_activity_refresh( - request_id, - Err("token activity unavailable".to_string()), - ) - ); - assert_eq!( - chat.pending_token_activity_output() - .map(|cell| lines_to_single_string(&cell.display_lines(u16::MAX))), - Some("/usage daily\n\n Token activity\n Token activity unavailable\n".to_string()), - ); - - chat.finalize_turn(); - assert!(chat.usage_history_insertion_blocked()); - chat.note_stream_consolidation_completed(); - assert!(!chat.usage_history_insertion_blocked()); - assert!( - std::iter::from_fn(|| rx.try_recv().ok()) - .any(|event| matches!(event, AppEvent::CommitPendingUsageOutputAfterStreamShutdown)) - ); - assert!(chat.take_completed_token_activity_output().is_some()); -} - -#[tokio::test] -async fn completed_token_activity_refresh_waits_for_queued_stream_consolidation() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - chat.on_agent_message_delta("partial response".to_string()); - chat.finalize_completed_assistant_message(/*message*/ None); - assert!(chat.pending_stream_consolidations > 0); - - assert!( - chat.finish_token_activity_refresh( - request_id, - Err("token activity unavailable".to_string()), - ) - ); - assert!(chat.usage_history_insertion_blocked()); - - chat.note_stream_consolidation_completed(); - - assert!(!chat.usage_history_insertion_blocked()); -} - -#[tokio::test] -async fn completed_token_activity_refresh_waits_for_active_history_cell() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - chat.transcript.active_cell = Some(Box::new(PlainHistoryCell::new(vec![Line::from( - "active tool", - )]))); - assert!( - chat.finish_token_activity_refresh( - request_id, - Err("token activity unavailable".to_string()), - ) - ); - assert!(chat.usage_history_insertion_blocked()); - - chat.flush_active_cell(); - - assert_matches!(rx.try_recv(), Ok(AppEvent::InsertHistoryCell(_))); - assert_matches!(rx.try_recv(), Ok(AppEvent::CommitPendingUsageOutput)); -} - -#[tokio::test] -async fn completed_token_activity_refresh_waits_for_active_hook() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - handle_hook_started( - &mut chat, - hook_run( - "post-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - codex_app_server_protocol::HookRunStatus::Running, - "checking output policy", - Vec::new(), - ), - ); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - assert!( - chat.finish_token_activity_refresh( - request_id, - Err("token activity unavailable".to_string()), - ) - ); - assert!(chat.usage_history_insertion_blocked()); - - handle_hook_completed( - &mut chat, - hook_run( - "post-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - codex_app_server_protocol::HookRunStatus::Completed, - "checking output policy", - vec![codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Context, - text: "hook context".to_string(), - }], - ), - ); - - assert_matches!(rx.try_recv(), Ok(AppEvent::CommitPendingUsageOutput)); -} - -#[tokio::test] -async fn completed_token_activity_refresh_retries_after_plan_item_completion() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - let mut controller = crate::streaming::controller::PlanStreamController::new( - /*width*/ None, - &chat.config.cwd, - chat.history_render_mode(), - ); - controller.push("Plan details"); - chat.plan_stream_controller = Some(controller); - assert!( - chat.finish_token_activity_refresh( - request_id, - Err("token activity unavailable".to_string()), - ) - ); - - chat.on_plan_item_completed("Plan details".to_string()); - - assert!( - std::iter::from_fn(|| rx.try_recv().ok()) - .any(|event| matches!(event, AppEvent::CommitPendingUsageOutputAfterStreamShutdown)) - ); -} - -#[tokio::test] -async fn pending_token_activity_refresh_keeps_composer_visible_in_short_viewport() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - chat.transcript.active_cell = Some(Box::new(PlainHistoryCell::new( - std::iter::repeat_n(Line::from("active output"), /*n*/ 20).collect(), - ))); - - dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - - let width: u16 = 80; - let height: u16 = 8; - let backend = VT100Backend::new(width, height); - let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal"); - term.set_viewport_area(Rect::new(/*x*/ 0, /*y*/ 0, width, height)); - term.draw(|f| { - chat.render(f.area(), f.buffer_mut()); - }) - .unwrap(); - - assert!( - term.backend() - .vt100() - .screen() - .contents() - .contains("Ask Codex to do anything") - ); -} - -#[tokio::test] -async fn repeated_token_activity_refreshes_keep_only_latest_card() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - set_chatgpt_auth(&mut chat); - - let first_request_id = dispatch_usage_and_expect_refresh(&mut chat, &mut rx); - - chat.dispatch_command_with_args(SlashCommand::Usage, "weekly".to_string(), Vec::new()); - let second_request_id = expect_token_activity_refresh(&mut rx); - - assert_eq!( - chat.pending_token_activity_output() - .map(|cell| lines_to_single_string(&cell.display_lines(u16::MAX))), - Some("/usage weekly\n\n Token activity\n Loading...\n".to_string()), - ); - assert!(!chat.finish_token_activity_refresh( - first_request_id, - Err("stale token activity result".to_string()), - )); - assert!(chat.finish_token_activity_refresh( - second_request_id, - Err("token activity unavailable".to_string()), - )); -} - #[tokio::test] async fn unrecognized_slash_command_is_not_added_to_local_recall() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tests/usage.rs b/codex-rs/tui/src/chatwidget/tests/usage.rs index 5ea2f1910d..6bea412c31 100644 --- a/codex-rs/tui/src/chatwidget/tests/usage.rs +++ b/codex-rs/tui/src/chatwidget/tests/usage.rs @@ -18,9 +18,8 @@ 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)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics { view: None })); } fn reset_credits(available_count: i64) -> RateLimitResetCreditsSummary { @@ -130,7 +129,7 @@ async fn usage_command_opens_menu_when_reset_is_available_snapshot() { render_bottom_popup(&chat, /*width*/ 80) ); 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 { view: None })); } #[tokio::test] @@ -156,9 +155,8 @@ async fn usage_command_disables_reset_after_cached_zero_snapshot() { origin: RateLimitRefreshOrigin::UsageMenu { request_id: 1 } }) ); - 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)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics { view: None })); } #[tokio::test] @@ -185,7 +183,6 @@ 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)); @@ -216,9 +213,8 @@ 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::OpenAnalytics)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics { view: None })); } #[tokio::test] @@ -267,7 +263,6 @@ 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)); } @@ -280,7 +275,6 @@ 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)); @@ -298,7 +292,6 @@ 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)); @@ -714,10 +707,9 @@ async fn no_credit_outcome_disables_reset_entry_in_usage_menu() { origin: RateLimitRefreshOrigin::UsageMenu { request_id: 2 } }) ); - 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)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics { view: None })); 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/tokens.rs b/codex-rs/tui/src/chatwidget/tokens.rs deleted file mode 100644 index 52db0a60f6..0000000000 --- a/codex-rs/tui/src/chatwidget/tokens.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! Coordinates asynchronous `/usage` cards in the chat widget. -//! -//! The slash command builds a composite history cell immediately, but the widget -//! keeps that cell transient while the account request runs. The transient card is -//! rendered above the composer through [`ChatWidget::pending_token_activity_output`] -//! so loading never requires clearing or rewriting transcript history. When the -//! matching response arrives, [`TokenActivityHandle`] updates the shared card state -//! and [`ChatWidget::finish_token_activity_refresh`] moves the cell into a completed -//! slot. Event dispatch commits that completed cell into history only after active -//! output and stream consolidation no longer block insertion. -//! -//! Pure chart rendering and date bucketing live in [`chart`]. This module owns -//! request correlation, transient/completed card state, and integration with -//! `ChatWidget` history insertion. - -mod chart; - -use std::sync::Arc; -use std::sync::RwLock; - -use chrono::NaiveDate; -use chrono::Utc; -use codex_app_server_protocol::GetAccountTokenUsageResponse; -use ratatui::style::Stylize; -use ratatui::text::Line; - -use super::ChatWidget; -use crate::app_event::AppEvent; -use crate::history_cell::CompositeHistoryCell; -use crate::history_cell::HistoryCell; -use crate::history_cell::PlainHistoryCell; -use crate::history_cell::plain_lines; - -pub(crate) use chart::TokenActivityView; - -/// Tracks the renderable lifecycle of one token activity history cell. -#[derive(Debug)] -enum TokenActivityState { - Loading, - Loaded { - response: GetAccountTokenUsageResponse, - today: NaiveDate, - }, - Error, -} - -/// Completes an asynchronously rendered token activity history cell. -/// -/// Clones share the same card state, allowing the background request path to -/// update a cell still owned by the widget's transient-output state. The widget -/// remains responsible for request-ID matching, redraws, and history insertion. -#[derive(Clone, Debug)] -pub(super) struct TokenActivityHandle { - state: Arc>, -} - -/// Holds the one transient token activity card waiting on its background response. -/// -/// The request ID prevents late results from mutating a newer `/usage` card. The -/// cell stays out of transcript history until the matching response completes and -/// the widget confirms that active output no longer blocks insertion. -pub(super) struct PendingTokenActivityOutput { - request_id: u64, - cell: CompositeHistoryCell, - handle: TokenActivityHandle, -} - -impl TokenActivityHandle { - /// Replaces the loading state with either fetched activity or an unavailable state. - /// - /// This method intentionally discards the error string because the TUI exposes - /// one stable unavailable message. Calling it more than once replaces the prior - /// terminal state, so request-ID matching should happen before completion. - pub(super) fn finish(&self, result: Result) { - self.finish_with_today(result, Utc::now().date_naive()); - } - - fn finish_with_today( - &self, - result: Result, - today: NaiveDate, - ) { - let state = match result { - Ok(response) => TokenActivityState::Loaded { response, today }, - Err(_) => TokenActivityState::Error, - }; - #[expect(clippy::expect_used)] - let mut current = self.state.write().expect("token activity state poisoned"); - *current = state; - } -} - -/// Renders one `/usage` card from shared asynchronous state. -#[derive(Debug)] -struct TokenActivityHistoryCell { - view: TokenActivityView, - state: Arc>, -} - -/// Creates the card contents and completion handle for one `/usage` invocation. -/// -/// The composite cell includes the echoed slash command and a loading card from -/// the start. Callers must retain the returned handle and complete it when the -/// matching background response arrives; otherwise the transient card stays loading. -pub(super) fn new_token_activity_output( - view: TokenActivityView, -) -> (CompositeHistoryCell, TokenActivityHandle) { - let command = PlainHistoryCell::new(vec![ - format!("/usage {}", view.label().to_lowercase()) - .magenta() - .into(), - ]); - let state = Arc::new(RwLock::new(TokenActivityState::Loading)); - let handle = TokenActivityHandle { - state: Arc::clone(&state), - }; - let card = TokenActivityHistoryCell { view, state }; - ( - CompositeHistoryCell::new(vec![Box::new(command), Box::new(card)]), - handle, - ) -} - -impl HistoryCell for TokenActivityHistoryCell { - fn display_lines(&self, width: u16) -> Vec> { - #[expect(clippy::expect_used)] - let state = self.state.read().expect("token activity state poisoned"); - match &*state { - TokenActivityState::Loading => { - vec![ - " Token activity".bold().into(), - " Loading...".dim().into(), - ] - } - TokenActivityState::Error => vec![ - " Token activity".bold().into(), - " Token activity unavailable".dim().into(), - ], - TokenActivityState::Loaded { response, today } => { - chart::loaded_lines(self.view, response, *today, width) - } - } - } - - fn raw_lines(&self) -> Vec> { - plain_lines(self.display_lines(u16::MAX)) - } -} - -impl ChatWidget { - /// Starts a token activity refresh and replaces the current transient card. - /// - /// Each invocation receives a request ID so background responses update only - /// their own card. The card remains outside transcript history until completion, - /// which keeps loading visible without disturbing existing transcript content. - pub(crate) fn add_token_activity_output(&mut self, view: TokenActivityView) { - let request_id = self.next_token_activity_request_id; - self.next_token_activity_request_id = - self.next_token_activity_request_id.wrapping_add(/*rhs*/ 1); - let (cell, handle) = new_token_activity_output(view); - self.completed_token_activity_output = None; - self.refreshing_token_activity_output = Some(PendingTokenActivityOutput { - request_id, - cell, - handle, - }); - self.bump_active_cell_revision(); - self.request_redraw(); - self.app_event_tx - .send(AppEvent::RefreshTokenActivity { request_id }); - } - - /// Returns the transient token activity card that should render above the composer. - /// - /// A loading card takes precedence over a completed card waiting for history - /// insertion. Callers should render the returned cell but leave ownership with - /// the widget so completion and insertion can update it safely. - pub(super) fn pending_token_activity_output(&self) -> Option<&dyn HistoryCell> { - self.refreshing_token_activity_output - .as_ref() - .map(|output| &output.cell as &dyn HistoryCell) - .or_else(|| { - self.completed_token_activity_output - .as_ref() - .map(|cell| cell as &dyn HistoryCell) - }) - } - - /// Applies a background token activity result to its matching transient card. - /// - /// Returns `true` when the pending request matched and moved into the completed - /// slot. Late responses return `false`, including responses for cards replaced - /// by a newer `/usage` invocation or cleared during transcript changes. - pub(crate) fn finish_token_activity_refresh( - &mut self, - request_id: u64, - result: Result, - ) -> bool { - let Some(output) = self.refreshing_token_activity_output.take() else { - return false; - }; - if output.request_id != request_id { - self.refreshing_token_activity_output = Some(output); - return false; - } - output.handle.finish(result); - self.completed_token_activity_output = Some(output.cell); - self.bump_active_cell_revision(); - self.request_redraw(); - true - } - - /// Reports whether completed asynchronous usage output must wait before insertion. - /// - /// Inserting while a stream, queued consolidation, or active transcript cell is - /// present can reorder output relative to visible work, so callers retry once - /// these barriers clear. - pub(crate) fn usage_history_insertion_blocked(&self) -> bool { - self.stream_controller.is_some() - || self.plan_stream_controller.is_some() - || self.pending_stream_consolidations > 0 - || self.transcript.active_cell.is_some() - || self.active_hook_cell.is_some() - } - - /// Records a stream consolidation barrier that delays token card insertion. - /// - /// Each queued consolidation should eventually call - /// [`ChatWidget::note_stream_consolidation_completed`]. - pub(crate) fn note_stream_consolidation_queued(&mut self) { - self.pending_stream_consolidations = - self.pending_stream_consolidations.saturating_add(/*rhs*/ 1); - } - - /// Releases one queued stream consolidation barrier. - /// - /// The counter saturates at zero so an unmatched completion does not underflow, - /// but paired queue/completion calls are still the intended contract. - pub(crate) fn note_stream_consolidation_completed(&mut self) { - self.pending_stream_consolidations = - self.pending_stream_consolidations.saturating_sub(/*rhs*/ 1); - } - - /// Transfers the completed token activity card into the history insertion path. - /// - /// Callers should use this only after - /// [`ChatWidget::usage_history_insertion_blocked`] returns `false`; - /// taking the card removes it from the transient render area. - pub(crate) fn take_completed_token_activity_output(&mut self) -> Option { - let output = self.completed_token_activity_output.take()?; - self.bump_active_cell_revision(); - Some(output) - } - - /// Requests another insertion attempt when completed usage output is waiting. - /// - /// This is used after stream or history lifecycle events that may have cleared - /// the insertion barriers without directly owning the completed output. - pub(crate) fn request_pending_usage_output_insertion(&self) { - if self.completed_token_activity_output.is_some() - || self.pending_rate_limit_reset_hint().is_some() - { - self.app_event_tx.send(AppEvent::CommitPendingUsageOutput); - } - } - - pub(crate) fn request_pending_usage_output_insertion_after_stream_shutdown(&self) { - if self.completed_token_activity_output.is_some() - || self.pending_rate_limit_reset_hint().is_some() - { - self.app_event_tx - .send(AppEvent::CommitPendingUsageOutputAfterStreamShutdown); - } - } - - /// Drops transient and completed token cards that must no longer update. - /// - /// Late background responses cannot mutate cards after a transcript reset, - /// backtrack, or replacement flow clears this widget-owned state. - pub(crate) fn clear_pending_token_activity_refreshes(&mut self) { - let cleared_refresh = self.refreshing_token_activity_output.take().is_some(); - let cleared_completed = self.completed_token_activity_output.take().is_some(); - if cleared_refresh || cleared_completed { - self.bump_active_cell_revision(); - self.request_redraw(); - } - } -} - -#[cfg(test)] -#[path = "tokens_tests.rs"] -mod tests; diff --git a/codex-rs/tui/src/chatwidget/tokens_tests.rs b/codex-rs/tui/src/chatwidget/tokens_tests.rs deleted file mode 100644 index 4c8d741391..0000000000 --- a/codex-rs/tui/src/chatwidget/tokens_tests.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::*; -use codex_app_server_protocol::AccountTokenUsageSummary; -use pretty_assertions::assert_eq; - -#[test] -fn loaded_state_freezes_chart_anchor_date_at_completion() { - let state = Arc::new(RwLock::new(TokenActivityState::Loading)); - let handle = TokenActivityHandle { - state: Arc::clone(&state), - }; - let today = - NaiveDate::from_ymd_opt(/*year*/ 2026, /*month*/ 5, /*day*/ 29).expect("valid date"); - - handle.finish_with_today( - Ok(GetAccountTokenUsageResponse { - summary: AccountTokenUsageSummary { - lifetime_tokens: None, - peak_daily_tokens: None, - longest_running_turn_sec: None, - current_streak_days: None, - longest_streak_days: None, - }, - daily_usage_buckets: None, - thread_usage: None, - }), - today, - ); - - let state = state.read().expect("token activity state poisoned"); - match &*state { - TokenActivityState::Loaded { - today: loaded_today, - .. - } => { - assert_eq!(*loaded_today, today); - } - other => panic!("expected loaded state, got {other:?}"), - } -} diff --git a/codex-rs/tui/src/chatwidget/usage.rs b/codex-rs/tui/src/chatwidget/usage.rs index 88b9150b7c..a45aee4a88 100644 --- a/codex-rs/tui/src/chatwidget/usage.rs +++ b/codex-rs/tui/src/chatwidget/usage.rs @@ -45,17 +45,18 @@ impl ChatWidget { (false, "No usage limit resets available.".to_string()) } }; - let mut params = SelectionViewParams { + + 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()), footer_hint: Some(standard_popup_hint_line()), items: vec![ SelectionItem { - name: "Show usage".to_string(), - description: Some("View recent account token usage.".to_string()), + name: "View analytics".to_string(), + description: Some("Browse account usage and activity.".to_string()), actions: vec![Box::new(|tx| { - tx.send(AppEvent::OpenTokenActivity); + tx.send(AppEvent::OpenAnalytics { view: None }); })], dismiss_on_select: true, ..Default::default() @@ -72,20 +73,7 @@ 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/chatwidget/usage_history.rs b/codex-rs/tui/src/chatwidget/usage_history.rs new file mode 100644 index 0000000000..882211f4ee --- /dev/null +++ b/codex-rs/tui/src/chatwidget/usage_history.rs @@ -0,0 +1,54 @@ +//! Defer reset hints until active transcript output and stream consolidation have settled. + +use super::ChatWidget; +use crate::app_event::AppEvent; + +impl ChatWidget { + /// Reports whether completed asynchronous usage output must wait before insertion. + /// + /// Inserting while a stream, queued consolidation, or active transcript cell is + /// present can reorder output relative to visible work, so callers retry once + /// these barriers clear. + pub(crate) fn usage_history_insertion_blocked(&self) -> bool { + self.stream_controller.is_some() + || self.plan_stream_controller.is_some() + || self.pending_stream_consolidations > 0 + || self.transcript.active_cell.is_some() + || self.active_hook_cell.is_some() + } + + /// Records a stream consolidation barrier that delays reset hint insertion. + /// + /// Each queued consolidation should eventually call + /// [`ChatWidget::note_stream_consolidation_completed`]. + pub(crate) fn note_stream_consolidation_queued(&mut self) { + self.pending_stream_consolidations = + self.pending_stream_consolidations.saturating_add(/*rhs*/ 1); + } + + /// Releases one queued stream consolidation barrier. + /// + /// The counter saturates at zero so an unmatched completion does not underflow, + /// but paired queue/completion calls are still the intended contract. + pub(crate) fn note_stream_consolidation_completed(&mut self) { + self.pending_stream_consolidations = + self.pending_stream_consolidations.saturating_sub(/*rhs*/ 1); + } + + /// Requests another insertion attempt when completed usage output is waiting. + /// + /// This is used after stream or history lifecycle events that may have cleared + /// the insertion barriers without directly owning the completed output. + pub(crate) fn request_pending_usage_output_insertion(&self) { + if self.pending_rate_limit_reset_hint().is_some() { + self.app_event_tx.send(AppEvent::CommitPendingUsageOutput); + } + } + + pub(crate) fn request_pending_usage_output_insertion_after_stream_shutdown(&self) { + if self.pending_rate_limit_reset_hint().is_some() { + self.app_event_tx + .send(AppEvent::CommitPendingUsageOutputAfterStreamShutdown); + } + } +} 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 index 73f1ca7f57..a05dade7ae 100644 --- 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 @@ -2,11 +2,11 @@ source: tui/src/analytics_tests.rs expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))" --- -" Analytics · Live account · analytics@example.test · account-a " +" Analytics · Live account · analytics@example.test " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " -" 5 Top chats " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 " +" Skills used 6 Top chats " " " " ▎ Credits usage history By product " " ───────────────────────────────────────────────────────────────────────────────── " @@ -43,11 +43,11 @@ expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))" " " " ↑/↓ group · enter select · esc cancel " -" Analytics · Live account · analytics@example.test · account-a " +" Analytics · Live account · analytics@example.test " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used " -" 5 Top chats " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 " +" Skills used 6 Top chats " " " " ▎ Credits usage history By product " " ───────────────────────────────────────────────────────────────────────────────── " @@ -81,5 +81,5 @@ expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))" " ● IDE extension 18.00 " " ● Web 14.00 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 index 6bc7046da5..fe32fe284a 100644 --- 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 @@ -4,14 +4,14 @@ expression: "screens.join(\"\\n\\n\")" --- ▎ Credits usage history By product ────────────────────────────────────────────────────── -13,079.44 credits +13.1K credits - 15,000 ▄▄▄▄ - ████ - 0.00 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ - 0 ────────────────────────────────────────────── - ▲ - 27 28 29 30 31 1 2 + 15K ▄▄▄▄ + ████ + 0.00 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ + 0 ──────────────────────────────────────────────── + ▲ + 27 28 29 30 31 1 2 ────────────────────────────────────────────────────── Aug 27 · No activity (0) @@ -23,14 +23,14 @@ Product Credits ▎ Credits usage history By product ────────────────────────────────────────────────────── -13,079.44 credits - 12,746.44 - 15,000 ▄▄▄▄ - ████ - 0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ - 0 ────────────────────────────────────────────── - ▲ - 27 28 29 30 31 1 2 +13.1K credits + 12,746.44 + 15K ▄▄▄▄ + ████ + 0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ + 0 ──────────────────────────────────────────────── + ▲ + 27 28 29 30 31 1 2 ────────────────────────────────────────────────────── Aug 28 · 12746.441206 credits @@ -42,14 +42,14 @@ Product Credits ▎ Credits usage history By product ────────────────────────────────────────────────────── -13,079.44 credits +13.1K credits - 15,000 ▄▄▄▄ - ████ - 0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ - 0 ────────────────────────────────────────────── - ▲ - 27 28 29 30 31 1 2 + 15K ▄▄▄▄ + ████ + 0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ + 0 ──────────────────────────────────────────────── + ▲ + 27 28 29 30 31 1 2 ────────────────────────────────────────────────────── Aug 29 · Not reported 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 index 65e5fd4a69..9d55188eae 100644 --- 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 @@ -5,8 +5,8 @@ expression: "snapshots.join(\"\\n\")" " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" [Credits usage history] Token usage history Plugins " -" called Skills used Top chats " +" Summary [Credits usage history] Token usage history " +" Plugins called Skills used Top chats " " " " ▎ Credits usage history By product " " ────────────────────────────────────────────────────── " @@ -36,7 +36,7 @@ expression: "snapshots.join(\"\\n\")" " ● IDE extension — " " ● Web — " " " -" tab/1–5 section · z dashboard · ←/→ day · enter " +" tab/1–6 section · z dashboard · ←/→ day · enter " " details " " r 7/30d · R refresh · pgup/pgdn scroll · esc back · q " " close " @@ -44,7 +44,7 @@ expression: "snapshots.join(\"\\n\")" " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Credits usage history By product " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -77,5 +77,5 @@ expression: "snapshots.join(\"\\n\")" " ● IDE extension — " " ● Web — " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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_details_reflow_and_keep_focus.snap b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_details_reflow_and_keep_focus.snap index 12c1dd3580..782eafa7f2 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_details_reflow_and_keep_focus.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__analytics__tests__analytics_details_reflow_and_keep_focus.snap @@ -4,7 +4,7 @@ expression: "format!(\"{wide}\\n{narrow}\")" --- " Analytics · Live account " " " -" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] " +" 1 Summary 2 Credits usage history 3 Token usage history 4 Plugins called 5 Skills used [6 Top chats] " " " " ▎ Top chats " " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── " @@ -42,13 +42,13 @@ expression: "format!(\"{wide}\\n{narrow}\")" " " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter details " +" tab/1–6 section · z dashboard · ↑/↓ row · enter details " " R refresh · pgup/pgdn scroll · esc back · q close " " Analytics · Live account " " " -" Credits usage history Token usage history Plugins " -" called Skills used [Top chats] " +" Summary Credits usage history Token usage history " +" Plugins called Skills used [Top chats] " " " " ▎ Top chats " " ────────────────────────────────────────────────────── " @@ -62,6 +62,6 @@ expression: "format!(\"{wide}\\n{narrow}\")" " high · standard " " " " " -" tab/1–5 section · z dashboard · ↑/↓ row · enter " +" tab/1–6 section · z dashboard · ↑/↓ row · enter " " details " " 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 index 506043edaf..ca697bb1d7 100644 --- 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 @@ -1,13 +1,12 @@ --- source: tui/src/analytics_tests.rs -assertion_line: 628 expression: "snapshots.join(\"\\n\")" --- " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" Total usage history [Messages] Plugins called " -" Skills used Top chats " +" Summary Total usage history [Messages] Plugins " +" called Skills used Top chats " " " " ▎ Messages By model " " ────────────────────────────────────────────────────── " @@ -37,7 +36,7 @@ expression: "snapshots.join(\"\\n\")" " " " " " " -" tab/1–5 section · z dashboard · ←/→ day " +" tab/1–6 section · z dashboard · ←/→ day " " r 7/30d · g group · R refresh · pgup/pgdn scroll · esc " " back · q close " " " @@ -45,8 +44,8 @@ expression: "snapshots.join(\"\\n\")" " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" Total usage history [Messages] Plugins called " -" Skills used Top chats " +" Summary Total usage history [Messages] Plugins " +" called Skills used Top chats " " " " ▎ Messages By model " " ────────────────────────────────────────────────────── " @@ -76,7 +75,7 @@ expression: "snapshots.join(\"\\n\")" " " " " " " -" tab/1–5 section · z dashboard · ←/→ day " +" tab/1–6 section · z dashboard · ←/→ day " " r 7/30d · g group · R refresh · pgup/pgdn scroll · esc " " back · q close " " " @@ -84,7 +83,7 @@ expression: "snapshots.join(\"\\n\")" " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats " +" 1 Summary 2 Total usage history [3 Messages] 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Messages By model " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -117,13 +116,13 @@ expression: "snapshots.join(\"\\n\")" " " " " " " -" tab/1–5 section · z dashboard · ←/→ day " +" tab/1–6 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 5 Top chats " +" 1 Summary 2 Total usage history [3 Messages] 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Messages By model " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -156,5 +155,5 @@ expression: "snapshots.join(\"\\n\")" " " " " " " -" tab/1–5 section · z dashboard · ←/→ day " +" tab/1–6 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 index 3fcdfac7bb..827070b4b4 100644 --- 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 @@ -2,7 +2,7 @@ source: tui/src/analytics_tests.rs expression: panels --- -▎ Total usage history By feature + Total usage history By feature ────────────────────────────────────────────────────── Loading… 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 index e944d7275d..0cd1f42a1e 100644 --- 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 @@ -5,20 +5,19 @@ 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 5 Top chats " +" 1 Summary [2 Credits usage history] 3 Token usage history 4 Plugins called 5 Skills used 6 " +" Top chats " " " " ▎ Credits usage history By product " " ──────────────────────────────────────────────────────────────────────────────────────────────── " " 367.00 credits " " " -" 150 " -" ▂▂▂▂▂▂▂▂ ▇▇▇▇▇▇▇▇ " -" 75 ▄▄▄▄▄▄▄▄ ████████ ████████ ████████ " -" 0 ████████ ████████ ████████ ████████ ████████ " +" 150 ▁▁▁▁▁▁▁▁ " +" ▁▁▁▁▁▁▁▁ ▅▅▅▅▅▅▅▅ ▄▄▄▄▄▄▄▄ ████████ " +" 0 ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ " " 0 ────────────────────────────────────────────────────────────────────────────────────────── " " ▇▇▇▇▇▇▇▇ " " -0.000004 " -" " " -150 " " " " ▲ " @@ -32,5 +31,6 @@ expression: output " ● IDE extension 0.00 " " ● Web 0.00 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" " +" tab/1–6 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 index 5b65050c75..600dffda5a 100644 --- 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 @@ -1,77 +1,76 @@ --- source: tui/src/analytics_tests.rs -assertion_line: 345 expression: "format!(\"{overview}\\n{skills}\")" --- " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" ╭─ 1 Total usage history ──────────────────────────────────────╮ ╭─ ▸ 2 Messages ─────────────────────────────────────────────────╮ " +" ╭─ 1 Summary ──────────────────────────────────────────────────╮ ╭─ 2 Total usage history ──────────────────────────────────────╮ " " │ │ │ │ " -" │ 7d │ │ 7d │ " -" │ By feature │ │ By model │ " -" │ Total usage · relative units │ │ 574 reported messages │ " -" │ 86 │ │ 103 │ " -" │ 100 ▃▃▃▃▃ ▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ │ 150 ▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ " -" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ " -" │ 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 │ " +" │ No history has been reported. │ │ 7d │ " +" │ │ │ By feature │ " +" │ │ │ Total usage · relative units │ " +" │ │ │ 86 │ " +" │ │ │ 100 ▃▃▃▃▃ ▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ " +" │ │ │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ " +" │ │ │ 0 ───────────────────────────────────────────────────────── │ " +" │ │ │ ▲ │ " +" │ │ │ 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 │ " +" │ │ │ Sep 2 · 86 usage units │ " +" │ │ │ ● Tasks 64.0% │ " +" │ │ │ ● Code review 14.0% │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " " " -" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ 4 Skills used ──────────────────────────────────────────────╮ " +" ╭─ ▸ 3 Messages ─────────────────────────────────────────────────╮ ╭─ 4 Plugins called ───────────────────────────────────────────╮ " " │ │ │ │ " " │ 7d │ │ 7d │ " -" │ 186 calls │ │ 54 uses │ " -" │ 29 │ │ 11 │ " -" │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ │ 20 │ " -" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ " -" │ 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 │ " -" │ │ │ │ " +" │ By model │ │ 186 calls │ " +" │ 574 reported messages │ │ 29 │ " +" │ 103 │ │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ " +" │ 150 ▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ " +" │ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ │ 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 · 103 messages │ │ ● Slack 10 │ " +" │ ● Friendly model 61 │ │ ● Figma 10 │ " +" │ ● GPT-5.6-Terra 25 │ │ │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " " " -" ╭─ 5 Top chats ────────────────────────────────────────────────╮ " -" │ │ " -" │ No history has been reported. │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" tab/1–5 section · enter/z maximize " +" ╭─ 5 Skills used ──────────────────────────────────────────────╮ ╭─ 6 Top chats ────────────────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ No history has been reported. │ " +" │ 54 uses │ │ │ " +" │ 11 │ │ │ " +" │ 20 │ │ │ " +" │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ │ │ " +" │ 0 ────────────────────────────────────────────────────────── │ │ │ " +" │ ▲ │ │ │ " +" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ │ " +" │ │ │ │ " +" │ Sep 2 · 11 uses │ │ │ " +" │ ● Testing 4 │ │ │ " +" │ ● Code review 3 │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" │ │ │ │ " +" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ " +" tab/1–6 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] 5 Top chats " +" 1 Summary 2 Total usage history 3 Messages 4 Plugins called [5 Skills used] 6 Top chats " " " " ▎ Skills used " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -98,5 +97,5 @@ expression: "format!(\"{overview}\\n{skills}\")" " ● Other 3 " " ● Debugging 1 " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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 index 1d55c137a6..a5b20f4a04 100644 --- 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 @@ -1,12 +1,11 @@ --- source: tui/src/analytics_tests.rs -assertion_line: 74 expression: tools --- " Analytics · Live account " " 7d [30d] · Aug 4–Sep 2, 2026 " " " -" ╭─ ▸ 4 Skills used ──────────────────────────────────╮ " +" ╭─ ▸ 5 Skills used ──────────────────────────────────╮ " " │ │ " " │ 30d │ " " │ 169 uses │ " @@ -20,6 +19,6 @@ expression: tools " │ ● Testing 4 │ " " │ ● Code review 3 │ " " │ │ " -" tab/1–5 section · enter/z maximize " +" tab/1–6 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 index 956414af48..d287e1cd57 100644 --- 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 @@ -1,65 +1,64 @@ --- source: tui/src/analytics_tests.rs -assertion_line: 46 expression: wide --- " Analytics · Live account " " [7d] 30d · Aug 27–Sep 2, 2026 " " " -" ╭─ ▸ 1 Total usage history ────────────────────────────╮ ╭─ 2 Messages ───────────────────────────────────────╮ " +" ╭─ 1 Summary ────────────────────────────────────────╮ ╭─ ▸ 2 Total usage history ────────────────────────────╮ " " │ │ │ │ " -" │ 7d │ │ 7d │ " -" │ By feature │ │ By model │ " -" │ Total usage · relative units │ │ 574 reported messages │ " -" │ 86 │ │ 103 │ " -" │ 100 ▁▁▁▁ ▅▅▅▅ ▅▅▅▅ ▇▇▇▇ ▆▆▆▆ ████ ▆▆▆▆ │ │ 150 ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ ▄▄▄▄ ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ │ " -" │ 0 ─────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────── │ " -" │ ▲ │ │ ▲ │ " -" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ " +" │ No history has been reported. │ │ 7d │ " +" │ │ │ By feature │ " +" │ │ │ Total usage · relative units │ " +" │ │ │ 86 │ " +" │ │ │ 100 ▁▁▁▁ ▅▅▅▅ ▅▅▅▅ ▇▇▇▇ ▆▆▆▆ ████ ▆▆▆▆ │ " +" │ │ │ 0 ─────────────────────────────────────────────── │ " +" │ │ │ ▲ │ " +" │ │ │ 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 │ " +" │ │ │ Sep 2 · 86 usage units │ " +" │ │ │ ● Tasks 64.0% │ " +" │ │ │ ● Code review 14.0% │ " " │ │ │ │ " " │ │ │ │ " " ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " " " -" ╭─ 3 Plugins called ─────────────────────────────────╮ ╭─ 4 Skills used ────────────────────────────────────╮ " +" ╭─ 3 Messages ───────────────────────────────────────╮ ╭─ 4 Plugins called ─────────────────────────────────╮ " " │ │ │ │ " " │ 7d │ │ 7d │ " -" │ 186 calls │ │ 54 uses │ " -" │ 29 │ │ 11 │ " -" │ 40 ▆▆▆▆ ▄▄▄▄ ▅▅▅▅ ▅▅▅▅ ▄▄▄▄ ▄▄▄▄ ▅▅▅▅ │ │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ " -" │ 0 ──────────────────────────────────────────────── │ │ 0 ──────────────────────────────────────────────── │ " -" │ ▲ │ │ ▲ │ " -" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ " +" │ By model │ │ 186 calls │ " +" │ 574 reported messages │ │ 29 │ " +" │ 103 │ │ 40 ▆▆▆▆ ▄▄▄▄ ▅▅▅▅ ▅▅▅▅ ▄▄▄▄ ▄▄▄▄ ▅▅▅▅ │ " +" │ 150 ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ ▄▄▄▄ ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ │ │ 0 ──────────────────────────────────────────────── │ " +" │ 0 ─────────────────────────────────────────────── │ │ ▲ │ " +" │ ▲ │ │ 27 28 29 30 31 1 2 │ " +" │ 27 28 29 30 31 1 2 │ │ │ " +" │ │ │ Sep 2 · 29 calls │ " +" │ Sep 2 · 103 messages │ │ ● Slack 10 │ " +" │ ● GPT-5.6-Sol 61 │ │ ● Figma 10 │ " +" │ ● GPT-5.6-Terra 25 │ │ │ " " │ │ │ │ " -" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ " -" │ ● Slack 10 │ │ ● Testing 4 │ " -" │ ● Figma 10 │ │ ● Code review 3 │ " +" │ │ │ │ " +" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " +" " +" ╭─ 5 Skills used ────────────────────────────────────╮ ╭─ 6 Top chats ──────────────────────────────────────╮ " +" │ │ │ │ " +" │ 7d │ │ No history has been reported. │ " +" │ 54 uses │ │ │ " +" │ 11 │ │ │ " +" │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ │ │ " +" │ 0 ──────────────────────────────────────────────── │ │ │ " +" │ ▲ │ │ │ " +" │ 27 28 29 30 31 1 2 │ │ │ " +" │ │ │ │ " +" │ Sep 2 · 11 uses │ │ │ " +" │ ● Testing 4 │ │ │ " +" │ ● Code review 3 │ │ │ " " │ │ │ │ " " │ │ │ │ " " │ │ │ │ " " ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ " " " -" ╭─ 5 Top chats ──────────────────────────────────────╮ " -" │ │ " -" │ No history has been reported. │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" │ │ " -" ╰──────────────────────────────────────────────────────╯ " " " -" " -" tab/1–5 section · enter/z maximize " +" tab/1–6 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_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 index 716ca7ecf4..c19b3448e1 100644 --- 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 @@ -1,12 +1,11 @@ --- source: tui/src/analytics_tests.rs -assertion_line: 573 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 5 Top chats " +" 1 Summary [2 Total usage history] 3 Messages 4 Plugins called 5 Skills used 6 Top chats " " " " ▎ Total usage history By model " " ──────────────────────────────────────────────────────────────────────────────────────────────── " @@ -39,5 +38,5 @@ expression: selected_day " ● Remote Other 2.1% " " … 3 more 0.0% " " " -" tab/1–5 section · z dashboard · ←/→ day · enter details " +" tab/1–6 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/tui.rs b/codex-rs/tui/src/tui.rs index 223e285fce..3c4f6df198 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -606,7 +606,7 @@ pub struct Tui { notification_backend: Option, notification_condition: NotificationCondition, scrollback: ScrollbackStrategy, - // When false, enter_alt_screen() becomes a no-op. + // When false, overlays stay on the inline screen. alt_screen_enabled: bool, // Keeps unmanaged process stderr writes out of the inline viewport. _stderr_guard: terminal_stderr::TerminalStderrGuard, @@ -673,7 +673,7 @@ impl Tui { } } - /// Set whether alternate screen is enabled. When false, enter_alt_screen() becomes a no-op. + /// Set whether overlays switch to the alternate screen or stay inline. pub fn set_alt_screen_enabled(&mut self, enabled: bool) { self.alt_screen_enabled = enabled; } @@ -842,7 +842,19 @@ impl Tui { /// Enter alternate screen and expand the viewport to full terminal size, saving the current /// inline viewport for restoration when leaving. pub fn enter_alt_screen(&mut self) -> Result<()> { - if !self.alt_screen_enabled || self.is_alt_screen_active() { + if self.is_alt_screen_active() { + return Ok(()); + } + // History queued before opening an overlay belongs to the inline transcript. + // Flush before switching screens or expanding an inline overlay's viewport. + let screen_size = self.terminal.last_known_screen_size; + Self::flush_pending_history_lines( + &mut self.terminal, + &mut self.pending_history_lines, + self.scrollback, + screen_size, + )?; + if !self.alt_screen_enabled { return Ok(()); } let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen);