Add an account Summary tab to Analytics (#45769)

## 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
This commit is contained in:
Felipe Coury
2026-09-15 19:13:08 +00:00
committed by copyberry
parent 8f0d2459ac
commit 9bd49c9dcc
122 changed files with 3685 additions and 1980 deletions
+1
View File
@@ -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;
@@ -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<ProfileIdentity>,
pub metadata: Option<ProfileMetadata>,
pub stats: ProfileStats,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct ProfileIdentity {
pub display_name: Option<String>,
pub username: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct ProfileMetadata {
pub stats_as_of: Option<String>,
pub stats_error: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct ProfileStats {
#[serde(flatten)]
pub tokens: TokenUsageProfileStats,
pub fast_mode_usage_percentage: Option<serde_json::Number>,
pub most_used_reasoning_effort: Option<String>,
pub most_used_reasoning_effort_percentage: Option<serde_json::Number>,
pub unique_skills_used: Option<u64>,
pub total_skills_used: Option<u64>,
pub total_threads: Option<u64>,
pub top_invocations: Option<Vec<ProfileInvocation>>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct ProfileInvocation {
#[serde(rename = "type")]
pub kind: ProfileInvocationKind,
pub plugin_name: Option<String>,
pub skill_name: Option<String>,
pub usage_count: Option<u64>,
}
#[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<AccountProfile, RequestError> {
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;
@@ -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.");
}
+6
View File
@@ -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;
+21 -3
View File
@@ -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<chats::Chats>,
tasks: Load<tasks::Chats>,
chat_metric: usize,
profile: Load<codex_backend_client::AccountProfile>,
plan: plan::State,
show_zero_credit_groups: bool,
account: Load<codex_protocol::account::PlanType>,
@@ -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;
@@ -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::<Vec<_>>();
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());
}
@@ -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<Self> {
pub(crate) fn parse(value: &str) -> Option<Self> {
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<Line<'static>> {
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<Line<'static>> {
@@ -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<Line<'static>> {
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<Vec<usize>> {
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<usize>> = Vec::new();
let mut current: Vec<usize> = 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<i64>) -> 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<i64>, longest: Option<i64>) -> 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<i64>) -> 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::<i64>()),
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<i64> {
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<usize> {
let cumulative = weekly_totals(values)
.into_iter()
.scan(/*initial_state*/ 0, |sum, value| {
*sum += value;
*sum = i64::saturating_add(*sum, value);
Some(*sum)
})
.collect::<Vec<_>>();
@@ -430,9 +271,9 @@ fn graded_levels(values: &[i64]) -> Vec<usize> {
.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<usize> {
fn weekly_totals(values: &[i64]) -> Vec<i64> {
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<usize> {
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<NaiveDate> {
}
#[cfg(test)]
#[path = "chart_tests.rs"]
#[path = "activity_chart_tests.rs"]
mod tests;
@@ -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::<Vec<_>>()
.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());
}
}
+13 -4
View File
@@ -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(),
@@ -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"));
+5 -5
View File
@@ -44,11 +44,7 @@ impl Live {
pub(super) fn account_label(&self) -> Option<String> {
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;
+4 -1
View File
@@ -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;
@@ -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"));
}
@@ -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<TcpStream>) -> 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::<Vec<_>>(),
["/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();
}
+2 -1
View File
@@ -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
@@ -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),
+21
View File
@@ -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);
+36
View File
@@ -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)
);
}
+4 -1
View File
@@ -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);
+5 -12
View File
@@ -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;
+1 -1
View File
@@ -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,
+16 -9
View File
@@ -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)));
}
@@ -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."
);
}
}
+37 -5
View File
@@ -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::<Vec<_>>();
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))
+22 -6
View File
@@ -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<Section> 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;
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
" "
" "
" "
@@ -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 "
@@ -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 "
@@ -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
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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 "
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 "
@@ -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 "
+76
View File
@@ -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<Option<AccountProfile>, 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<TokenActivityView>) {
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<ListAction>) {
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;
}
}
}
+374
View File
@@ -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<Line<'static>> {
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<u64>) -> 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"),
}
}
+143
View File
@@ -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());
}
@@ -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
@@ -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;
@@ -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}"));
}
+16 -12
View File
@@ -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);
@@ -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<codex_app_server_protocol::GetAccountTokenUsageResponse> {
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,
+4 -24
View File
@@ -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,
-4
View File
@@ -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;
-32
View File
@@ -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 {
@@ -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
@@ -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::<Vec<_>>()
.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;
+3 -16
View File
@@ -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<GetAccountRateLimitsResponse, String>,
},
/// Open the default token-activity view selected from the `/usage` menu.
OpenTokenActivity,
/// Open the authenticated account analytics dashboard.
OpenAnalytics,
OpenAnalytics {
view: Option<crate::analytics::TokenActivityView>,
},
/// Open the reset-credit flow selected from the `/usage` menu.
OpenRateLimitResetCredits,
@@ -711,17 +709,6 @@ pub(crate) enum AppEvent {
result: Result<ConsumeAccountRateLimitResetCreditResponse, String>,
},
/// 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<GetAccountTokenUsageResponse, String>,
},
/// Fetch backend-estimated usage for the currently visible enterprise thread.
RefreshThreadUsage {
thread_id: ThreadId,
+1 -14
View File
@@ -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<String, RateLimitSnapshotDisplay>,
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
next_status_refresh_request_id: u64,
refreshing_token_activity_output: Option<tokens::PendingTokenActivityOutput>,
completed_token_activity_output: Option<history_cell::CompositeHistoryCell>,
next_token_activity_request_id: u64,
pending_rate_limit_reset_request_id: Option<u64>,
pending_rate_limit_reset_idempotency_key: Option<String>,
rate_limit_reset_picker_request_id: Option<u64>,
@@ -1985,7 +1981,6 @@ impl ChatWidget {
pub(crate) fn active_cell_transcript_key(&self) -> Option<ActiveCellTranscriptKey> {
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() {
@@ -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,
+1 -11
View File
@@ -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,
-1
View File
@@ -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;
@@ -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(),
),

Some files were not shown because too many files have changed in this diff Show More