Add an account analytics dashboard to /usage (#45764)

## What changed

- Add “Explore analytics” to the `/usage` menu for ChatGPT accounts, opening a full-screen dashboard.
- Show usage and messages for consumer accounts, credits and token usage for Business and Enterprise accounts, and plugin and skill activity for both.
- Support overview and maximized views, 7- and 30-day ranges, report grouping, daily details, model filtering for token usage, and refresh.
- Keep report loading and errors independent, cancel pending loads on close, and retain navigation state across reopening while preserving the composer draft.

## Testing

Add regression and snapshot coverage for account-specific reports, range and grouping changes, model filtering, loading and cancellation, small terminals, terminal colors, and draft preservation.

GitOrigin-RevId: 46da94a14d6e227551e740b85f9d2d552aaf75b0
This commit is contained in:
Felipe Coury
2026-09-15 19:01:41 +00:00
committed by copyberry
parent af3bc6f796
commit ca53e19c75
68 changed files with 6357 additions and 67 deletions
+399 -1
View File
@@ -1,15 +1,413 @@
//! Account analytics data and chart primitives, staged for the dashboard integration.
//! Authenticated account analytics dashboard.
//! A maximized section and an overview share bounded report loads, selection, and inline details.
mod chart;
mod client;
mod dashboard;
mod data;
#[cfg(test)]
#[path = "analytics/test_fixtures.rs"]
mod fixture;
mod models;
mod normalize;
mod panels;
mod plot;
mod render;
mod report_data;
mod sections;
mod styles;
#[cfg(test)]
#[path = "analytics/test_support.rs"]
mod test_support;
mod tokens;
mod tool_panel;
#[cfg(test)]
#[path = "analytics_tests.rs"]
mod tests;
use crate::key_hint;
use crate::keymap::ListAction;
use crate::keymap::ListKeymap;
use crate::tui;
use crate::tui::FrameRequester;
use crate::tui::TuiEvent;
use codex_app_server_client::AppServerRequestHandle;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use data::Load;
use sections::Section;
use sections::SectionState;
use sections::SectionStates;
pub(crate) struct AnalyticsView {
model_names: std::collections::HashMap<String, String>,
sections: SectionStates,
account: Load<models::AccountKind>,
reports_started: bool,
connection: Option<(
std::sync::Arc<crate::legacy_core::config::Config>,
AppServerRequestHandle,
FrameRequester,
)>,
live: Option<std::sync::Arc<client::Live>>,
pub(crate) keymap: ListKeymap,
section: Section,
zoomed: bool,
scroll_offset: usize,
follow_selection: bool,
viewport_height: usize,
ranges: [usize; 3],
end_date: chrono::NaiveDate,
token_model: Option<String>,
group_picker: Option<usize>,
pub(crate) is_done: bool,
}
impl AnalyticsView {
pub(crate) fn new(keymap: ListKeymap) -> Self {
let mut view = Self {
model_names: std::collections::HashMap::new(),
sections: SectionStates(std::array::from_fn(|_| SectionState::default())),
account: Load::Unavailable,
reports_started: false,
connection: None,
live: None,
keymap,
section: Section::Usage,
zoomed: true,
scroll_offset: 0,
follow_selection: true,
viewport_height: 1,
ranges: [0; 3],
end_date: chrono::Utc::now().date_naive(),
token_model: None,
group_picker: None,
is_done: false,
};
view.sections[Section::Usage].group = 1;
view.sections[Section::Activity].group = 2;
view.refresh();
view
}
pub(crate) fn open(
&mut self,
handle: AppServerRequestHandle,
frame: FrameRequester,
models: Vec<codex_protocol::openai_models::ModelPreset>,
config: std::sync::Arc<crate::legacy_core::config::Config>,
) {
self.model_names = models
.into_iter()
.map(|model| (model.model, model.display_name))
.collect();
self.connection = Some((config, handle, frame));
self.is_done = false;
self.refresh();
}
pub(crate) fn refresh(&mut self) {
self.end_date = chrono::Utc::now().date_naive();
self.group_picker = None;
self.live = self.connection.as_ref().map(|(config, _, _)| {
std::sync::Arc::new(client::Live::new(
std::sync::Arc::clone(config),
self.end_date,
))
});
for section in &mut self.sections.0 {
section.history = Load::Unavailable;
}
self.reports_started = false;
self.token_model = None;
self.account = if let (Some((_, _, frame)), Some(live)) = (&self.connection, &self.live) {
let live = std::sync::Arc::clone(live);
Load::start(
async move { live.session().await.map(|session| Some(session.kind)) },
frame.clone(),
)
} else {
Load::Unavailable
};
self.start_reports();
}
/// Abort work when closing or invalidating a retained view; reopening starts fresh requests.
pub(crate) fn cancel_loads(&mut self) {
for section in &mut self.sections.0 {
section.history = Load::Unavailable;
}
self.account = Load::Unavailable;
self.live = None;
self.connection = None;
}
fn load_report(&mut self, section: Section) {
let Some(report) = section.report() else {
return;
};
let group = self.sections[section].group;
let range = self.ranges[self.range_group(section) as usize];
self.sections[section].history =
if let (Some((_, _, frame)), Some(live)) = (&self.connection, &self.live) {
let live = std::sync::Arc::clone(live);
let days = if range == 0 { 7 } else { 30 };
let model = self.token_model.clone();
Load::start(
async move {
if let Some(model) = model
.as_deref()
.filter(|_| report == models::AccountAnalyticsReport::Usage)
{
live.filtered_history(report, days, data::GROUPINGS[group], Some(model))
.await
} else {
live.history(report, days, data::GROUPINGS[group]).await
}
},
frame.clone(),
)
} else {
Load::Unavailable
};
}
fn group_options(&self) -> &[usize] {
if self.visible_sections().is_empty() {
return &[];
}
match self.section {
Section::Usage if self.business() => &[6, 2],
Section::Usage if self.consumer_attribution() => &[1, 2, 0, 3],
Section::Usage => &[0, 2],
Section::Credits => self.live.as_ref().map_or(&[0], |live| live.credit_groups()),
Section::Activity => &[2, 0],
Section::Plugins | Section::Skills => &[],
}
}
fn consumer_attribution(&self) -> bool {
!self.business()
&& self
.live
.as_ref()
.is_none_or(|live| live.attributed_usage())
}
fn section_title(&self, section: Section) -> &str {
match section {
Section::Usage if self.business() => "Token usage history",
Section::Usage if self.consumer_attribution() => "Total usage history",
Section::Usage => "Usage history",
Section::Plugins => "Plugins called",
Section::Credits => "Credits usage history",
Section::Activity => "Messages",
Section::Skills => "Skills used",
}
}
fn date_range(&self) -> std::ops::RangeInclusive<chrono::NaiveDate> {
self.section_date_range(self.section)
}
fn model_name<'a>(&'a self, model: &'a str) -> &'a str {
self.model_names
.get(model)
.filter(|name| !name.is_empty())
.map(String::as_str)
.unwrap_or(model)
}
fn row_count(&self) -> usize {
if self.ranges[self.range_group(self.section) as usize] == 0 {
7
} else {
30
}
}
fn handle_key(&mut self, key: KeyEvent) {
if key.kind == KeyEventKind::Release {
return;
}
if key_hint::plain(KeyCode::Char('q')).is_press(key)
|| key_hint::ctrl(KeyCode::Char('c')).is_press(key)
{
self.group_picker = None;
self.is_done = true;
return;
}
let action = if key_hint::plain(KeyCode::Char(' ')).is_press(key) {
Some(ListAction::Accept)
} else {
self.keymap.action_for(key)
};
if let Some(choice) = self.group_picker {
let options = self.group_options();
let Some(&group) = options.get(choice) else {
self.group_picker = None;
return;
};
match action {
Some(ListAction::MoveUp | ListAction::MoveLeft) => {
self.group_picker = Some((choice + options.len() - 1) % options.len());
}
Some(ListAction::MoveDown | ListAction::MoveRight) => {
self.group_picker = Some((choice + 1) % options.len());
}
Some(ListAction::Accept) => {
self.sections[self.section].group = group;
self.group_picker = None;
self.load_report(self.section);
}
Some(ListAction::Cancel) => self.group_picker = None,
_ => {}
}
self.follow_selection = true;
return;
}
self.follow_selection = true;
if key_hint::plain(KeyCode::Char('z')).is_press(key) {
self.zoomed = !self.zoomed;
self.scroll_offset = 0;
return;
}
if !self.zoomed && action == Some(ListAction::Accept) {
self.zoomed = true;
self.scroll_offset = 0;
return;
}
if key_hint::plain(KeyCode::Char('R')).is_press(key) {
self.refresh();
return;
}
if self.section == Section::Usage
&& self.business()
&& key_hint::plain(KeyCode::Char('m')).is_press(key)
{
let models = self
.live
.as_ref()
.map_or_else(Vec::new, |live| live.token_models());
self.token_model = match self
.token_model
.as_ref()
.and_then(|model| models.iter().position(|candidate| candidate == model))
{
Some(index) => models.get(index + 1).cloned(),
None => models.first().cloned(),
};
self.load_report(Section::Usage);
return;
}
if key_hint::plain(KeyCode::Char('r')).is_press(key) && !self.visible_sections().is_empty()
{
self.change_range();
return;
}
if key_hint::plain(KeyCode::Char('g')).is_press(key) && self.group_options().len() > 1 {
self.group_picker = self
.group_options()
.iter()
.position(|group| *group == self.sections[self.section].group);
return;
}
let visible = self.visible_sections();
if visible.is_empty() {
self.is_done |= action == Some(ListAction::Cancel);
return;
}
let current = visible
.iter()
.position(|section| *section == self.section)
.unwrap_or_default();
let target = match key.code {
KeyCode::Char(n @ '1'..='6') if key.modifiers.is_empty() => {
visible.get(n as usize - '1' as usize).copied()
}
KeyCode::Tab => Some(visible[(current + 1) % visible.len()]),
KeyCode::BackTab => Some(visible[(current + visible.len() - 1) % visible.len()]),
_ => None,
};
if let Some(section) = target {
if self.zoomed && section != self.section {
self.scroll_offset = 0;
}
self.section = section;
self.group_picker = None;
return;
}
if matches!(
self.section,
Section::Plugins | Section::Activity | Section::Skills
) && action == Some(ListAction::Accept)
&& !self.day_has_details(self.section, self.sections[self.section].cursor)
{
return;
}
let count = self.row_count().max(/*other*/ 1);
let SectionState { cursor, detail, .. } = &mut self.sections[self.section];
match action {
Some(ListAction::MoveUp) => *cursor = cursor.saturating_sub(/*rhs*/ 1),
Some(ListAction::MoveDown) => *cursor = (*cursor + 1).min(count - 1),
Some(ListAction::MoveLeft) => *cursor = cursor.saturating_sub(/*rhs*/ 1),
Some(ListAction::MoveRight) => *cursor = (*cursor + 1).min(count - 1),
Some(ListAction::JumpTop) => *cursor = 0,
Some(ListAction::JumpBottom) => *cursor = count - 1,
Some(ListAction::Accept) => {
*detail = if *detail == Some(*cursor) {
None
} else {
Some(*cursor)
};
}
Some(ListAction::Cancel) => {
self.is_done |= !self.zoomed || detail.take().is_none();
}
Some(ListAction::PageDown) => {
self.scroll_offset += self.viewport_height;
self.follow_selection = false;
}
Some(ListAction::PageUp) => {
self.scroll_offset = self.scroll_offset.saturating_sub(self.viewport_height);
self.follow_selection = false;
}
None => {}
}
if detail.is_some() {
*detail = Some(*cursor);
}
}
pub(crate) fn handle_event(
&mut self,
tui: &mut tui::Tui,
event: TuiEvent,
) -> std::io::Result<()> {
match event {
TuiEvent::Key(key) => {
self.handle_key(key);
if self.is_done {
for section in &mut self.sections.0 {
if matches!(section.history, Load::Loading(_)) {
section.history = Load::Unavailable;
}
}
self.account = Load::Unavailable;
self.connection = None;
}
tui.frame_requester().schedule_frame();
}
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) | TuiEvent::FocusGained => {
if matches!(event, TuiEvent::Resize(_)) {
self.follow_selection = true;
}
tui.draw(u16::MAX, |frame| self.render(frame.area(), frame.buffer))?;
}
_ => {}
}
Ok(())
}
}
@@ -0,0 +1,292 @@
//! Account layouts, authenticated report loading, independent ranges, and model filters.
use super::*;
use pretty_assertions::assert_eq;
use crate::analytics::sections::Section;
#[test]
fn account_layouts_show_reports_and_wrap_navigation() {
let mut screens = Vec::new();
for (kind, expected) in [
(
models::AccountKind::Consumer,
vec![
Section::Usage,
Section::Activity,
Section::Plugins,
Section::Skills,
],
),
(
models::AccountKind::Enterprise,
vec![
Section::Credits,
Section::Usage,
Section::Plugins,
Section::Skills,
],
),
] {
let mut view = fixture::view(kind);
for (index, section) in expected.iter().enumerate() {
press(
&mut view,
KeyCode::Char(char::from_digit(index as u32 + 1, /*radix*/ 10).unwrap()),
);
assert_eq!(view.section, *section);
screens.push(screen(&mut view, /*width*/ 100, /*height*/ 30));
}
press(&mut view, KeyCode::Tab);
assert_eq!(view.section, expected[0]);
press(&mut view, KeyCode::BackTab);
assert_eq!(view.section, *expected.last().unwrap());
view.zoomed = false;
screens.push(screen(&mut view, /*width*/ 140, /*height*/ 64));
}
insta::assert_snapshot!(screens.join("\n"));
}
#[tokio::test]
async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() {
let server = test_support::server().await;
let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await;
test_support::settle(&mut view).await;
press(&mut view, KeyCode::Left);
press(&mut view, KeyCode::Enter);
press(&mut view, KeyCode::Char('r'));
test_support::settle(&mut view).await;
press(&mut view, KeyCode::Char('g'));
press(&mut view, KeyCode::Down);
press(&mut view, KeyCode::Enter);
test_support::settle(&mut view).await;
assert_eq!(
(
view.ranges,
view.sections.0.each_ref().map(|state| state.cursor),
view.sections[Section::Usage].detail,
view.sections[Section::Usage].group
),
([1, 0, 0], [28, 6, 6, 6, 6], Some(28), 2)
);
let context = (
view.ranges,
view.sections.0.each_ref().map(|state| state.cursor),
view.sections.0.each_ref().map(|state| state.detail),
view.sections.0.each_ref().map(|state| state.group),
);
press(&mut view, KeyCode::Char('z'));
screen(&mut view, /*width*/ 58, /*height*/ 25);
press(&mut view, KeyCode::Char('z'));
assert_eq!(
(
view.ranges,
view.sections.0.each_ref().map(|state| state.cursor),
view.sections.0.each_ref().map(|state| state.detail),
view.sections.0.each_ref().map(|state| state.group)
),
context
);
press(&mut view, KeyCode::Char('2'));
press(&mut view, KeyCode::Char('r'));
test_support::settle(&mut view).await;
assert_eq!(view.ranges, [1, 1, 0]);
assert_eq!(
view.sections
.0
.each_ref()
.map(|state| state.history.ready().map(|history| history.data.len())),
[Some(30), Some(7), None, Some(30), Some(7)]
);
let server = test_support::server().await;
let (_business_home, _business_server, mut view) =
client::tests::connected_view(&server, "business").await;
test_support::settle(&mut view).await;
view.ranges = [1, 1, 0];
view.load_report(Section::Usage);
press(&mut view, KeyCode::Char('1'));
press(&mut view, KeyCode::Char('r'));
test_support::settle(&mut view).await;
assert_eq!(view.ranges, [0, 1, 0]);
assert_eq!(
view.sections
.0
.each_ref()
.map(|state| state.history.ready().map(|history| history.data.len())),
[Some(30), Some(7), Some(7), None, Some(7)]
);
}
#[tokio::test]
async fn business_tokens_filter_models() {
let server = test_support::server().await;
let (_home, _app_server, mut view) = client::tests::connected_view(&server, "business").await;
test_support::settle(&mut view).await;
view.end_date = fixture::END_DATE;
fixture::seed_reports(&mut view);
press(&mut view, KeyCode::Char('2'));
let all = screen(&mut view, /*width*/ 100, /*height*/ 30);
assert!(all.contains("All models") && all.contains("Cached input"));
press(&mut view, KeyCode::Char('m'));
test_support::settle(&mut view).await;
assert_eq!(
view.sections[Section::Usage]
.history
.ready()
.unwrap()
.data
.iter()
.map(|day| day.total)
.sum::<f64>(),
420.0
);
fixture::seed_reports(&mut view);
assert_eq!(view.token_model.as_deref(), Some("GPT-5.5"));
let filtered = screen(&mut view, /*width*/ 100, /*height*/ 30);
press(&mut view, KeyCode::Char('m'));
test_support::settle(&mut view).await;
fixture::seed_reports(&mut view);
assert_eq!(view.token_model, None);
insta::assert_snapshot!(format!("{all}\n{filtered}"));
}
#[tokio::test]
async fn account_pending_and_error_do_not_start_reports() {
let mut view = AnalyticsView::new(RuntimeKeymap::defaults().list);
let (send, receive) = tokio::sync::oneshot::channel();
view.account = Load::start(
async move { receive.await.unwrap() },
FrameRequester::test_dummy(),
);
screen(&mut view, /*width*/ 80, /*height*/ 24);
assert!(
view.sections
.0
.iter()
.all(|state| matches!(state.history, Load::Unavailable))
);
send.send(Err("Sign in again to view Analytics.".to_string()))
.unwrap();
tokio::task::yield_now().await;
let error = screen(&mut view, /*width*/ 80, /*height*/ 24);
assert!(error.contains("Sign in again"));
assert!(
view.sections
.0
.iter()
.all(|state| matches!(state.history, Load::Unavailable))
);
insta::assert_snapshot!(error);
}
#[tokio::test]
async fn account_reports_request_only_eligible_endpoints_and_refresh() {
for (plan, usage, activity) in [
(
"plus",
"usage/daily-token-usage-breakdown",
"analytics/daily-workspace-usage-counts",
),
(
"business",
"usage/daily-workspace-user-token-usage-breakdown",
"usage/daily-workspace-user-credit-usage",
),
] {
let server = test_support::server().await;
let (_home, _app_server, mut view) = client::tests::connected_view(&server, plan).await;
test_support::settle(&mut view).await;
let mut expected = [
"accounts/check",
usage,
activity,
"analytics/daily-plugin-usage-metrics",
"analytics/daily-skill-usage-metrics",
]
.map(|endpoint| format!("/backend-api/wham/{endpoint}"));
expected.sort();
let mut paths = server
.received_requests()
.await
.unwrap()
.into_iter()
.filter(|request| request.url.path().starts_with("/backend-api/wham/"))
.map(|request| request.url.path().to_string())
.collect::<Vec<_>>();
paths.sort();
assert_eq!(paths, expected);
let ready = view
.sections
.0
.each_ref()
.map(|state| state.history.ready().is_some());
assert_eq!(
ready,
if plan == "plus" {
[true, true, false, true, true]
} else {
[true, true, true, false, true]
}
);
press(&mut view, KeyCode::Char('R'));
test_support::settle(&mut view).await;
assert_eq!(
view.sections
.0
.each_ref()
.map(|state| state.history.ready().is_some()),
ready
);
assert_eq!(
server
.received_requests()
.await
.unwrap()
.iter()
.filter(|request| request.url.path().starts_with("/backend-api/wham/"))
.count(),
10
);
view.cancel_loads();
}
}
#[tokio::test]
async fn legacy_response_closes_an_attribution_picker_that_is_no_longer_valid() {
let server = test_support::server().await;
let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await;
view.account = Load::Ready(models::AccountKind::Consumer);
view.start_reports();
view.group_picker = Some(3);
test_support::settle(&mut view).await;
assert_eq!(
(
view.group_picker,
view.group_options(),
view.sections[Section::Usage].group
),
(None, &[0, 2][..], 0)
);
press(&mut view, KeyCode::Enter);
}
#[tokio::test]
async fn legacy_response_invalidates_picker_before_the_next_draw() {
let server = test_support::server().await;
for choice in [2, 3] {
let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await;
view.account = Load::Ready(models::AccountKind::Consumer);
view.start_reports();
view.group_picker = Some(choice);
// Complete the request without polling the view, as can happen between key events.
tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 10), async {
while view.consumer_attribution() {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
press(&mut view, KeyCode::Enter);
assert_eq!((view.group_picker, view.is_done), (None, false));
test_support::settle(&mut view).await;
assert_eq!(view.sections[Section::Usage].group, 0);
}
}
+322
View File
@@ -0,0 +1,322 @@
//! Daily history with a stable chart and a compact, separated breakdown table.
//! Chart series rank by the full range; table rows rank by the selected day and retain series colors.
//! Missing and zero days retain the same row count; only explicit expansion changes height.
use super::AnalyticsView;
use super::data;
use super::plot;
use super::render::categories;
use super::render::columns;
use super::styles::number;
use super::styles::secondary_style;
use super::styles::series_colors;
use crate::analytics::models::AccountAnalyticsUnit;
use crate::analytics::models::AccountAnalyticsValue;
use crate::analytics::sections::Section;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
impl AnalyticsView {
pub(super) fn day_has_details(&self, section: Section, day_index: usize) -> bool {
let Some(history) = self.sections[section].history.ready() else {
return false;
};
let date = self
.section_date_range(section)
.start()
.iter_days()
.nth(day_index);
history.data.iter().any(|day| {
date.is_some_and(|date| day.date == date)
&& day.values.iter().any(|value| value.value != 0.0)
})
}
pub(super) fn history_lines(
&self,
section: Section,
width: usize,
height: usize,
) -> plot::Chart {
let state = &self.sections[section].history;
let Some(history) = state.ready() else {
return plot::Chart {
lines: state
.message()
.into_iter()
.map(|message| message.to_string().set_style(secondary_style()).into())
.collect(),
bands: 0,
};
};
if history.data.is_empty() {
return plot::Chart {
lines: vec![
"No data reported for this range."
.set_style(secondary_style())
.into(),
],
bands: 0,
};
}
let range = self.section_date_range(section);
// At most 30 daily slots; index by date if report ranges grow.
let days = range
.start()
.iter_days()
.take_while(|date| date <= range.end())
.map(|date| {
let day = history.data.iter().find(|day| day.date == date);
(date, day)
})
.collect::<Vec<_>>();
let cursor = self.sections[section].cursor.min(days.len() - 1);
let selected_date = days[cursor].0.format("%b %-d").to_string();
let day = days[cursor].1;
let expanded = self.zoomed && self.sections[section].detail.is_some();
let colors = series_colors();
let mut categories = categories(history);
if matches!(section, Section::Plugins | Section::Skills) {
for category in &mut categories {
category.label = super::tool_panel::tool_name(&category.label);
}
}
if self.sections[section].group == 2 {
for category in &mut categories {
category.label = self.model_name(&category.label).to_string();
}
}
let unit = match section {
Section::Usage if history.unit == AccountAnalyticsUnit::Tokens => "tokens",
Section::Activity => "messages",
Section::Plugins => "calls",
Section::Skills => "uses",
Section::Usage | Section::Credits => "credits",
};
let empty_breakdown = matches!(
section,
Section::Plugins | Section::Activity | Section::Skills
) && day.is_none_or(|day| {
day.total == 0.0 && day.values.iter().all(|value| value.value == 0.0)
});
let relative = history.unit == AccountAnalyticsUnit::RelativeUsage;
let format_amount = if history.unit == AccountAnalyticsUnit::Credits {
data::credit_amount
} else {
data::amount
};
let mut lines = vec![if relative {
"Total usage · relative units"
.set_style(secondary_style())
.into()
} else {
number(format!(
"{} {}{unit}",
format_amount(history.data.iter().map(|day| day.total).sum()),
if section == Section::Activity {
"reported "
} else {
""
}
))
.into()
}];
if section == Section::Usage && !self.business() && self.zoomed {
lines.push(
"Approximate · may be delayed up to 6 hours"
.set_style(secondary_style())
.into(),
);
}
if self.sections[section].group == 3 {
lines.push(
"All usage by how each turn started"
.set_style(secondary_style())
.into(),
);
}
let has_activity = history
.data
.iter()
.any(|day| day.total != 0.0 || day.values.iter().any(|value| value.value != 0.0));
let mut bands = 0;
if has_activity {
let chart = plot::chart(&days, &categories, cursor, width, height, history.unit);
bands = chart.bands;
lines.extend(chart.lines);
} else {
lines.push(
if matches!(
section,
Section::Plugins | Section::Activity | Section::Skills
) {
format!("No {unit} reported in this range.")
.set_style(secondary_style())
.into()
} else {
"No activity reported in this range."
.set_style(secondary_style())
.into()
},
);
}
let table_width = width.min(/*other*/ 56);
lines.push(Line::default());
if self.zoomed {
lines.push("─".repeat(table_width).dim().into());
}
let table_start = lines.len();
let summary = match day {
None => " · Not reported".set_style(secondary_style()),
Some(day) if day.total == 0.0 && day.values.iter().all(|value| value.value == 0.0) => {
" · No activity (0)".set_style(secondary_style())
}
Some(day) if relative => number(format!(" · {} usage units", data::amount(day.total))),
Some(day) => number(format!(
" · {} {unit}",
if expanded {
day.total.to_string()
} else {
format_amount(day.total)
}
)),
};
lines.push(truncate_line_with_ellipsis_if_overflow(
vec![selected_date.clone().bold(), summary].into(),
width,
));
if self.zoomed {
lines.push(columns(
match section {
Section::Plugins => "Plugin",
Section::Skills => "Skill",
Section::Usage | Section::Credits | Section::Activity => {
self.group_label(section, self.sections[section].group)
}
}
.bold()
.into(),
if history.unit == AccountAnalyticsUnit::Tokens {
"Tokens"
} else if section == Section::Usage {
"Share"
} else if section == Section::Activity {
"Messages"
} else if section == Section::Plugins {
"Calls"
} else if section == Section::Skills {
"Uses"
} else {
"Credits"
}
.bold()
.into(),
table_width,
));
}
let mut values = categories
.iter()
.enumerate()
.map(|(index, category)| {
(
index,
AccountAnalyticsValue {
value: day
.and_then(|day| {
day.values.iter().find(|value| value.key == category.key)
})
.map_or(/*default*/ 0.0, |value| value.value),
..category.clone()
},
)
})
.collect::<Vec<_>>();
values.sort_by(|(_, a), (_, b)| b.value.abs().total_cmp(&a.value.abs()));
let remainder = if !expanded && values.len() > 4 {
let remaining = values.split_off(/*at*/ 3);
Some((
format!("{} more", remaining.len()),
remaining.iter().map(|(_, value)| value.value).sum::<f64>(),
))
} else {
None
};
let rows = values
.iter()
.map(|(index, value)| {
(
Line::from(vec![
"● ".fg(colors[(*index).min(/*other*/ 3)]),
value.label.clone().into(),
]),
value.value,
)
})
.chain(
remainder.map(|(label, value)| (Line::from(vec!["… ".dim(), label.dim()]), value)),
);
for (label, value) in rows.take(if self.zoomed { usize::MAX } else { 2 }) {
let amount = match day {
None => "—".into(),
Some(day)
if section == Section::Usage
&& history.unit != AccountAnalyticsUnit::Tokens =>
{
format!(
"{:.1}%",
if day.total == 0.0 {
0.0
} else {
value / day.total * 100.0
}
)
}
Some(_) if expanded => value.to_string(),
Some(_) => format_amount(value),
};
lines.push(columns(
truncate_line_with_ellipsis_if_overflow(
label,
table_width.saturating_sub(amount.len() + 2),
),
if day.is_none() || value == 0.0 {
amount.dim()
} else {
amount.into()
}
.into(),
table_width,
));
}
if empty_breakdown {
// Preserve the measured table allocation when the selected day has no breakdown.
let table_end = lines.len();
lines.truncate(table_start);
lines.push(truncate_line_with_ellipsis_if_overflow(
if day.is_none() {
format!("Data not reported for {selected_date}.")
} else {
format!("No {unit} reported for {selected_date}.")
}
.set_style(secondary_style())
.into(),
width,
));
lines.resize(table_end, Line::default());
}
if let Some(updated) = history
.updated_at
.and_then(|value| chrono::DateTime::from_timestamp(value, /*nsecs*/ 0))
{
lines.push(
format!("Updated {} UTC", updated.format("%b %-d %H:%M"))
.set_style(secondary_style())
.into(),
);
}
plot::Chart { lines, bands }
}
}
@@ -1,6 +1,7 @@
//! Account-scoped authentication and request identity regression coverage.
use super::*;
use crate::analytics::sections::Section;
use crate::legacy_core::config::ConfigBuilder;
use base64::Engine;
use codex_config::LoaderOverrides;
@@ -763,3 +764,58 @@ async fn invalid_cached_breakdowns_are_evicted_before_retry() {
assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 2);
server.verify().await;
}
pub(in crate::analytics) async fn connected_view(
server: &MockServer,
plan: &str,
) -> (
tempfile::TempDir,
crate::AppServerSession,
crate::analytics::AnalyticsView,
) {
let (home, live) = live(server, plan).await;
let config = Arc::clone(&live.config);
let app_server = crate::start_embedded_app_server_for_picker(&config)
.await
.unwrap();
let mut view =
crate::analytics::AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list);
view.open(
app_server.request_handle(),
crate::tui::FrameRequester::test_dummy(),
Vec::new(),
config,
);
(home, app_server, view)
}
#[tokio::test]
async fn live_account_identity_is_visible_and_closing_aborts_pending_loads() {
let server = MockServer::start().await;
let (_home, live) = live(&server, "plus").await;
live.session().await.unwrap();
let mut view =
crate::analytics::AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list);
view.live = Some(Arc::new(live));
view.sections[Section::Usage].history =
super::super::data::Load::Error("Couldn't load usage. Press R to retry.".into());
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(
/*width*/ 100, /*height*/ 24,
))
.unwrap();
terminal
.draw(|frame| view.render(frame.area(), frame.buffer_mut()))
.unwrap();
insta::assert_snapshot!(terminal.backend().to_string());
let (draw_tx, _) = tokio::sync::broadcast::channel(/*capacity*/ 1);
let (cancelled_tx, cancelled_rx) = tokio::sync::oneshot::channel::<()>();
view.sections[Section::Usage].history = super::super::data::Load::start(
async move {
let _cancelled = cancelled_tx;
std::future::pending().await
},
crate::tui::FrameRequester::new(draw_tx),
);
view.cancel_loads();
assert!(cancelled_rx.await.is_err());
}
+181
View File
@@ -0,0 +1,181 @@
//! Bordered, aligned overview cards. Detailed reports remain in maximized mode.
//! Card heights depend on the viewport, never on the selected day or load state.
use super::AnalyticsView;
use super::render::columns;
use super::render::join_columns;
use super::styles::secondary_style;
use crate::analytics::sections::Section;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow as truncate;
use crate::style::accent_style;
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_lines;
use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
use std::ops::Range;
impl AnalyticsView {
pub(super) fn dashboard_lines(
&self,
width: usize,
height: usize,
) -> (Vec<Line<'static>>, Range<usize>) {
let count = if width >= 104 { 2 } else { 1 };
let card_width = if count == 2 { (width - 4) / 2 } else { width };
let visible = self.visible_sections();
let rows = visible.len().div_ceil(count).max(/*other*/ 1);
let card_height =
(height.saturating_sub(/*rhs*/ 2) / rows).clamp(/*min*/ 16, /*max*/ 24);
let inner_width = card_width.saturating_sub(/*rhs*/ 4).max(/*other*/ 1);
let inner_height = card_height - 4;
let mut lines = Vec::new();
let mut selected = 0..0;
for (row, chunk) in visible.chunks(count).enumerate() {
let mut cards = Vec::new();
for (column, section) in chunk.iter().copied().enumerate() {
let focused = self.section == section;
let border = if focused {
accent_style()
} else {
secondary_style().dim()
};
let mut content = Vec::new();
content.push(
format!(
"{}d{}",
if self.ranges[self.range_group(section) as usize] == 0 {
7
} else {
30
},
if section == Section::Usage && self.business() {
format!(" · {}", self.token_model.as_deref().unwrap_or("All models"))
} else {
String::new()
}
)
.set_style(secondary_style())
.into(),
);
if matches!(
section,
Section::Usage | Section::Credits | Section::Activity
) {
content.push(
format!(
"By {}",
self.group_label(section, self.sections[section].group)
.to_lowercase()
)
.set_style(secondary_style())
.into(),
);
}
if focused && self.group_picker.is_some() {
content.extend(self.group_options().iter().enumerate().map(
|(index, group)| {
let label = format!(
"{} {}",
if self.group_picker == Some(index) {
"›"
} else {
" "
},
self.group_label(section, *group)
);
if self.group_picker == Some(index) {
label.set_style(accent_style()).into()
} else {
label.into()
}
},
));
} else {
content.extend(
self.history_lines(
section,
inner_width,
inner_height.saturating_sub(/*rhs*/ 13).max(/*other*/ 1),
)
.lines,
);
}
let mut content = word_wrap_lines(content, RtOptions::new(inner_width));
if content.len() > inner_height {
content.truncate(inner_height);
content[inner_height - 1] =
"… z to maximize".set_style(secondary_style()).into();
}
content.resize(inner_height, Line::default());
let label = format!(
" {} {} ",
row * count + column + 1,
self.section_title(section)
);
let title = truncate(
Line::from(vec![
" ".into(),
if focused {
"▸".set_style(accent_style())
} else {
" ".into()
},
if focused {
label.bold()
} else {
label.set_style(secondary_style())
},
]),
card_width.saturating_sub(/*rhs*/ 3),
);
let mut top = vec!["╭─".set_style(border)];
top.extend(title.spans.iter().cloned());
top.push(
"─"
.repeat(card_width.saturating_sub(title.width() + 3))
.set_style(border),
);
top.push("╮".set_style(border));
let blank = columns(
"│".set_style(border).into(),
"│".set_style(border).into(),
card_width,
);
let mut card = vec![Line::from(top), blank.clone()];
card.extend(content.into_iter().map(|line| {
let line = truncate(line, inner_width);
let padding = card_width.saturating_sub(line.width() + 3);
let mut padded = vec!["│".set_style(border), " ".into()];
padded.extend(line.spans);
padded.push(" ".repeat(padding).into());
padded.push("│".set_style(border));
Line::from(padded)
}));
card.push(blank);
card.push(
format!("╰{}╯", "─".repeat(card_width.saturating_sub(/*rhs*/ 2)))
.set_style(border)
.into(),
);
cards.push(
card.into_iter()
.map(|line| truncate(line, card_width))
.collect::<Vec<_>>(),
);
}
if chunk.contains(&self.section) {
selected = lines.len()..lines.len() + card_height;
}
lines.extend(if cards.len() == 2 {
join_columns(&cards[0], &cards[1], card_width)
} else {
cards.remove(/*index*/ 0)
});
if (row + 1) * count < visible.len() {
lines.push(Line::default());
}
}
(lines, selected)
}
}
@@ -0,0 +1,95 @@
//! Overview cards retain alignment and geometry while selection and report states change.
use super::*;
use crate::style::accent_style;
use pretty_assertions::assert_eq;
use ratatui::style::Modifier;
use crate::analytics::sections::Section;
#[test]
fn dashboard_focus_uses_marker_and_default_foreground_title() {
let mut view = fixture::view(models::AccountKind::Consumer);
view.zoomed = false;
for section in [Section::Usage, Section::Activity] {
view.section = section;
let (lines, _) = view.dashboard_lines(/*width*/ 140, /*height*/ 56);
let title = &lines[0];
let marker = title.spans.iter().find(|span| span.content == "▸").unwrap();
assert_eq!(marker.style, accent_style());
let focused_title = title
.spans
.iter()
.find(|span| span.content.contains(view.section_title(section)))
.unwrap();
assert_eq!(focused_title.style.fg, None);
assert!(focused_title.style.add_modifier.contains(Modifier::BOLD));
assert_eq!(title.to_string().matches('▸').count(), 1);
}
}
#[test]
fn dashboard_cards_align_and_keep_stable_summary_heights() {
let mut view = fixture::view(models::AccountKind::Consumer);
press(&mut view, KeyCode::Char('z'));
let wide = screen(&mut view, /*width*/ 144, /*height*/ 64);
for (left, right) in [
("1 Total usage history", "2 Messages"),
("3 Plugins called", "4 Skills used"),
] {
assert!(
wide.lines()
.any(|line| line.contains(left) && line.contains(right))
);
}
assert!(!wide.contains("Coverage from"));
let before = view.dashboard_lines(/*width*/ 140, /*height*/ 56);
press(&mut view, KeyCode::Left);
view.sections[Section::Plugins].history = Load::Error("Access denied for this report.".into());
let after = view.dashboard_lines(/*width*/ 140, /*height*/ 56);
assert_eq!((before.0.len(), before.1), (after.0.len(), after.1));
let edges = |lines: &[ratatui::text::Line<'_>]| {
lines
.iter()
.enumerate()
.filter(|(_, line)| line.to_string().starts_with(['╭', '╰']))
.map(|(index, _)| index)
.collect::<Vec<_>>()
};
assert_eq!(edges(&before.0), edges(&after.0));
for width in [1, 4, 40, 100, 140] {
assert!(
view.dashboard_lines(width, /*height*/ 56)
.0
.iter()
.all(|line| line.width() <= width)
);
}
let narrow = screen(&mut view, /*width*/ 72, /*height*/ 48);
assert!(narrow.contains("1 Total usage history"));
let context = (
view.section,
view.sections.0.each_ref().map(|state| state.cursor),
view.sections.0.each_ref().map(|state| state.group),
);
press(&mut view, KeyCode::Enter);
assert!(view.zoomed);
assert_eq!(
context,
(
view.section,
view.sections.0.each_ref().map(|state| state.cursor),
view.sections.0.each_ref().map(|state| state.group)
)
);
insta::assert_snapshot!(format!("{wide}\n{narrow}"));
}
#[test]
fn overview_closes_on_escape_with_retained_day_details() {
let mut view = fixture::view(models::AccountKind::Consumer);
press(&mut view, KeyCode::Enter);
assert_eq!(view.sections[Section::Usage].detail, Some(6));
press(&mut view, KeyCode::Char('z'));
assert!(!view.zoomed);
press(&mut view, KeyCode::Esc);
assert!(view.is_done);
}
-33
View File
@@ -101,13 +101,6 @@ impl<T: Send + 'static> Load<T> {
}
}
pub(super) fn error(error: codex_app_server_client::TypedRequestError) -> String {
match error {
codex_app_server_client::TypedRequestError::Server { source, .. } => source.message,
_ => "Couldn't load analytics. Press R to retry.".into(),
}
}
/// Keep tiny refunds visible while avoiding noise on ordinary credit amounts.
pub(super) fn amount(value: f64) -> String {
if value == 0.0 {
@@ -154,32 +147,6 @@ pub(super) fn credit_amount(value: f64) -> String {
}
}
/// Format integer millionths without floating-point rounding or hiding tiny adjustments.
pub(super) fn credits(micros: i64) -> String {
let magnitude = micros.unsigned_abs();
if magnitude > 0 && magnitude < 10_000 {
let fractional = format!("{magnitude:06}");
return format!(
"{}0.{}",
if micros < 0 { "-" } else { "" },
fractional.trim_end_matches('0')
);
}
let cents = (magnitude + 5_000) / 10_000;
let mut whole = (cents / 100).to_string();
let digits = whole.len();
for index in (1..digits).rev() {
if (digits - index).is_multiple_of(/*rhs*/ 3) {
whole.insert(index, ',');
}
}
format!(
"{}{whole}.{:02}",
if micros < 0 { "-" } else { "" },
cents % 100
)
}
pub(super) fn date(date: &str) -> String {
chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d")
.map(|date| date.format("%b %-d").to_string())
+4 -1
View File
@@ -317,7 +317,10 @@ pub(super) fn label(key: &str) -> &str {
"subagent" => "Subagents",
"image_generation" => "Image generation",
"automation" => "Automations",
"guardian_review" => "Guardian review",
"guardian_review" => "Auto review",
"guardian_classifier" => "Auto review classifier",
"thread_title" => "Thread title",
"system" => "System",
"automated_review" => "Auto review",
"agent_identity" => "Workspace agents",
"memory_consolidation" => "Memory consolidation",
+108
View File
@@ -0,0 +1,108 @@
//! Responsive panels with consistent category colors and signed daily amounts.
use super::AnalyticsView;
use super::render::columns;
use super::styles::secondary_style;
use crate::analytics::sections::Section;
use crate::style::accent_style;
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_lines;
use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
use std::ops::Range;
/// Panel content and measured geometry shared by layout and scrolling.
pub(super) struct Panel {
pub(super) lines: Vec<Line<'static>>,
pub(super) selection: Range<usize>,
pub(super) chart_bands: usize,
}
impl AnalyticsView {
pub(super) fn panel(&self, section: Section, width: usize, chart_height: usize) -> Panel {
let focused = section == self.section;
let title = format!(
"{} {}",
if focused { "▎" } else { " " },
self.section_title(section)
);
let heading = if focused {
title.set_style(accent_style()).into()
} else {
title.bold().into()
};
let group = self.sections[section].group;
let heading = if matches!(
section,
Section::Usage | Section::Credits | Section::Activity
) {
columns(
heading,
format!("By {}", self.group_label(section, group).to_lowercase())
.set_style(secondary_style())
.into(),
width,
)
} else {
heading
};
let mut lines = vec![heading, "─".repeat(width).dim().into()];
if section == Section::Usage && self.business() {
lines.push(
format!(
"{} · m change model",
self.token_model.as_deref().unwrap_or("All models")
)
.set_style(secondary_style())
.into(),
);
}
let mut selection;
if focused && let Some(choice) = self.group_picker {
lines.extend(
self.group_options()
.iter()
.enumerate()
.map(|(index, group)| {
let label = format!(
"{} {}",
if index == choice { "›" } else { " " },
self.group_label(section, *group)
);
if index == choice {
label.set_style(accent_style()).into()
} else {
label.into()
}
}),
);
}
let history = self.history_lines(section, width, chart_height);
let chart_bands = history.bands;
lines.extend(history.lines);
selection = 0..lines.len();
if focused && self.group_picker.is_some() {
selection = 0..2 + self.group_options().len();
}
let mut wrapped = Vec::new();
let mut wrapped_selection = 0..0;
for (index, line) in lines.into_iter().enumerate() {
if index == selection.start {
wrapped_selection.start = wrapped.len();
}
wrapped.extend(word_wrap_lines(
[line],
RtOptions::new(width.max(/*other*/ 1)),
));
if index + 1 == selection.end {
wrapped_selection.end = wrapped.len();
}
}
Panel {
lines: wrapped,
selection: wrapped_selection,
chart_bands,
}
}
}
+258 -3
View File
@@ -1,9 +1,55 @@
//! Shared report category aggregation and stacked-chart partitioning.
//! Maximized sections and a responsive overview share the same panels and scrolling behavior.
//! Zoom, section changes, and reflow preserve report selections and inline details.
use super::models::AccountAnalyticsHistory;
use super::models::AccountAnalyticsValue;
use super::AnalyticsView;
use super::data;
use super::styles::secondary_style;
use crate::analytics::models::AccountAnalyticsHistory;
use crate::analytics::models::AccountAnalyticsValue;
use crate::analytics::sections::Section;
use crate::keymap::ListAction;
use crate::style::accent_style;
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_lines;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint;
use ratatui::layout::Layout;
use ratatui::layout::Rect;
use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use std::collections::BTreeMap;
pub(super) fn columns(left: Line<'static>, right: Line<'static>, width: usize) -> Line<'static> {
let padding = width
.saturating_sub(left.width() + right.width())
.max(/*other*/ 1);
let mut spans = left.spans;
spans.push(" ".repeat(padding).into());
spans.extend(right.spans);
Line::from(spans)
}
/// Join independently stacked panels without leaving holes below shorter sections.
pub(super) fn join_columns(
left: &[Line<'static>],
right: &[Line<'static>],
column_width: usize,
) -> Vec<Line<'static>> {
(0..left.len().max(right.len()))
.map(|row| {
let left = left.get(row).cloned().unwrap_or_default();
let mut line = columns(left, Line::default(), column_width + 4);
if let Some(right) = right.get(row) {
line.spans.extend(right.spans.iter().cloned());
}
line
})
.collect()
}
pub(super) fn categories(history: &AccountAnalyticsHistory) -> Vec<AccountAnalyticsValue> {
let mut totals = BTreeMap::<String, AccountAnalyticsValue>::new();
for value in history.data.iter().flat_map(|day| &day.values) {
@@ -41,3 +87,212 @@ pub(super) fn parts(
}
parts
}
impl AnalyticsView {
pub(super) fn render(&mut self, area: Rect, buf: &mut Buffer) {
self.poll_reports();
let width = usize::from(area.width.saturating_sub(/*rhs*/ 4)).max(/*other*/ 1);
let show_range = !self.visible_sections().is_empty();
let hint = |action| {
self.keymap
.primary_hint(action)
.map(crate::key_hint::ShortcutHint::display_label)
.unwrap_or_default()
};
let controls = if self.visible_sections().is_empty() {
format!("R refresh · {} back · q close", hint(ListAction::Cancel))
} else if self.group_picker.is_some() {
format!(
"{}/{} group · {} select · {} cancel",
hint(ListAction::MoveUp),
hint(ListAction::MoveDown),
hint(ListAction::Accept),
hint(ListAction::Cancel)
)
} else {
let navigation = if !self.zoomed {
format!("{}/z maximize", hint(ListAction::Accept))
} else {
format!(
"{}/{} day · {} details",
hint(ListAction::MoveLeft),
hint(ListAction::MoveRight),
hint(ListAction::Accept)
)
};
format!(
"tab/1–{} section · {}{navigation}\n{}{}R refresh · {}/{} scroll · {} back · q close",
self.visible_sections().len(),
if self.zoomed { "z dashboard · " } else { "" },
if show_range { "r 7/30d · " } else { "" },
if self.group_options().len() < 2 {
""
} else {
"g group · "
},
hint(ListAction::PageUp),
hint(ListAction::PageDown),
hint(ListAction::Cancel)
)
};
let control_line = |line: &str| {
let mut spans = Vec::new();
for (index, control) in line.split(" · ").enumerate() {
if index > 0 {
spans.push(" · ".into());
}
let (keys, description) = control.rsplit_once(' ').unwrap_or((control, ""));
#[expect(
clippy::disallowed_methods,
reason = "Analytics shortcut keys intentionally use white to distinguish them from descriptions."
)]
spans.push(keys.to_owned().white().bold().not_dim());
if !description.is_empty() {
spans.push(format!(" {description}").into());
}
}
Line::from(spans)
};
let mut footer = word_wrap_lines(controls.lines().map(control_line), RtOptions::new(width));
if self.group_picker.is_none()
&& matches!(
self.section,
Section::Plugins | Section::Activity | Section::Skills
)
&& !self.day_has_details(self.section, self.sections[self.section].cursor)
{
let height = footer.len();
let controls =
controls.replace(&format!(" · {} details", hint(ListAction::Accept)), "");
footer = word_wrap_lines(controls.lines().map(control_line), RtOptions::new(width));
// Removing a contextual hint must not resize the chart on narrow terminals.
footer.resize(height, Line::default());
}
let sections = if self.zoomed {
let line = Line::from(
self.visible_sections()
.iter()
.copied()
.enumerate()
.map(|(index, section)| {
let label = self.section_title(section);
let label = if width >= 64 {
format!("{} {label}", index + 1)
} else {
label.to_string()
};
if section == self.section {
format!("[{label}] ").set_style(accent_style())
} else {
format!("{label} ").set_style(secondary_style())
}
})
.collect::<Vec<_>>(),
);
word_wrap_lines([line], RtOptions::new(width))
} else {
Vec::new()
};
let [title, range, tabs, body, hints] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(if show_range { 2 } else { 1 }),
Constraint::Length(if sections.is_empty() {
0
} else {
sections.len() as u16 + 1
}),
Constraint::Min(0),
Constraint::Length(footer.len() as u16),
])
.areas(Rect {
x: area.x + area.width.min(/*other*/ 2),
width: area.width.saturating_sub(/*rhs*/ 4),
..area
});
Line::from(vec![
"Analytics".bold(),
self.live
.as_ref()
.and_then(|live| live.account_label())
.map(|account| format!(" · Live account · {account}"))
.unwrap_or_else(|| " · Live account".into())
.set_style(secondary_style()),
])
.render(title, buf);
if show_range {
let dates = self.date_range();
let (start, end) = (dates.start(), dates.end());
Line::from(vec![
if self.ranges[self.range_group(self.section) as usize] == 0 {
"[7d]".set_style(accent_style())
} else {
"7d".into()
},
" ".into(),
if self.ranges[self.range_group(self.section) as usize] == 1 {
"[30d]".set_style(accent_style())
} else {
"30d".into()
},
format!(
" · {}–{}",
data::date(&start.to_string()),
end.format("%b %-d, %Y")
)
.set_style(secondary_style()),
])
.render(range, buf);
}
Paragraph::new(sections).render(tabs, buf);
self.viewport_height = usize::from(body.height).max(/*other*/ 1);
let (lines, selection) = if self.visible_sections().is_empty() {
let message = self
.account
.message()
.unwrap_or("Analytics is not available for this account type.");
(vec![Line::from(message.to_string())], 0..1)
} else if self.zoomed {
let chart_height =
(usize::from(body.height) / 5).clamp(/*min*/ 4, /*max*/ 10);
let mut panel = self.panel(self.section, width, chart_height);
if panel.chart_bands > 0 {
let overhead = panel.lines.len() - chart_height * panel.chart_bands;
let height =
usize::from(body.height).saturating_sub(overhead + 1) / panel.chart_bands;
let height = height.max(/*other*/ 2);
if height != chart_height {
panel = self.panel(self.section, width, height);
}
}
let selection = if panel.lines.len() <= usize::from(body.height) {
0..panel.lines.len()
} else {
panel.selection
};
panel.lines.push(Line::default());
(panel.lines, selection)
} else {
self.dashboard_lines(width, usize::from(body.height))
};
if self.follow_selection {
if selection.start < self.scroll_offset {
self.scroll_offset = selection.start;
} else {
let end = selection.start + selection.len().min(self.viewport_height);
if end > self.scroll_offset + self.viewport_height {
self.scroll_offset = end - self.viewport_height;
}
}
}
self.scroll_offset = self
.scroll_offset
.min(lines.len().saturating_sub(self.viewport_height));
self.follow_selection = false;
Paragraph::new(lines)
.scroll(/*offset*/ (self.scroll_offset as u16, 0))
.render(body, buf);
Paragraph::new(footer)
.set_style(secondary_style())
.render(hints, buf);
}
}
+212
View File
@@ -0,0 +1,212 @@
//! Account-specific section order, request eligibility, and independent report ranges.
//! Resolving the account precedes report loading; refreshing drops all account-owned data.
use super::AnalyticsView;
use super::data::Load;
use super::models::AccountKind;
/// Stable identities preserve selections while account layouts change their order.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Section {
Usage,
Plugins,
Credits,
Activity,
Skills,
}
impl Section {
pub(super) fn report(self) -> Option<super::models::AccountAnalyticsReport> {
use super::models::AccountAnalyticsReport as Report;
match self {
Self::Usage => Some(Report::Usage),
Self::Plugins => Some(Report::Plugins),
Self::Credits => Some(Report::Credits),
Self::Activity => Some(Report::Messages),
Self::Skills => Some(Report::Skills),
}
}
}
/// Section-only indexing keeps report identity separate from array positions.
pub(super) struct SectionStates(pub(super) [SectionState; 5]);
impl std::ops::Index<Section> for SectionStates {
type Output = SectionState;
fn index(&self, section: Section) -> &Self::Output {
&self.0[section as usize]
}
}
impl std::ops::IndexMut<Section> for SectionStates {
fn index_mut(&mut self, section: Section) -> &mut Self::Output {
&mut self.0[section as usize]
}
}
/// A report and its selection are replaced independently of the other sections.
pub(super) struct SectionState {
pub(super) history: Load<super::models::AccountAnalyticsHistory>,
pub(super) cursor: usize,
pub(super) detail: Option<usize>,
pub(super) group: usize,
}
impl Default for SectionState {
fn default() -> Self {
Self {
history: Load::Unavailable,
cursor: 6,
detail: None,
group: 0,
}
}
}
/// Usage, product activity, and tools each share one range across their related sections.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum RangeGroup {
Usage,
Activity,
Tools,
}
impl AnalyticsView {
pub(super) fn poll_reports(&mut self) {
for section in &mut self.sections.0 {
section.history.poll();
}
self.account.poll();
self.start_reports();
if !self.business()
&& !self.consumer_attribution()
&& ![0, 2].contains(&self.sections[Section::Usage].group)
{
self.sections[Section::Usage].group = 0;
}
if self
.group_picker
.is_some_and(|choice| choice >= self.group_options().len())
{
self.group_picker = None;
}
}
pub(super) fn range_group(&self, section: Section) -> RangeGroup {
match section {
Section::Usage if self.business() => RangeGroup::Activity,
Section::Usage | Section::Credits => RangeGroup::Usage,
Section::Activity => RangeGroup::Activity,
Section::Plugins | Section::Skills => RangeGroup::Tools,
}
}
pub(super) fn group_label(&self, section: Section, group: usize) -> &'static str {
if group == 0
&& (section == Section::Credits
|| (section == Section::Usage && !self.consumer_attribution()))
{
"Product"
} else {
super::data::GROUP_LABELS[group]
}
}
pub(super) fn business(&self) -> bool {
matches!(
self.account.ready(),
Some(AccountKind::Business | AccountKind::Enterprise)
)
}
pub(super) fn visible_sections(&self) -> &'static [Section] {
match self.account.ready() {
Some(AccountKind::Consumer) => &[
Section::Usage,
Section::Activity,
Section::Plugins,
Section::Skills,
],
Some(AccountKind::Business | AccountKind::Enterprise) => &[
Section::Credits,
Section::Usage,
Section::Plugins,
Section::Skills,
],
Some(AccountKind::Unknown) | None => &[],
}
}
pub(super) fn start_reports(&mut self) {
if self.reports_started || self.account.ready().is_none() {
return;
}
self.reports_started = true;
let visible = self.visible_sections();
let Some(first) = visible.first() else {
return;
};
if !visible.contains(&self.section) {
self.section = *first;
}
let usage_groups: &[usize] = if self.business() {
&[6, 2]
} else {
&[1, 2, 0, 3]
};
if !usage_groups.contains(&self.sections[Section::Usage].group) {
self.sections[Section::Usage].group =
if matches!(self.account.ready(), Some(AccountKind::Business)) {
2
} else {
usage_groups[0]
};
}
if let Some(live) = &self.live
&& !live
.credit_groups()
.contains(&self.sections[Section::Credits].group)
{
self.sections[Section::Credits].group = live.credit_groups()[0];
}
for section in visible {
if section.report().is_some() {
self.load_report(*section);
}
}
}
pub(super) fn section_date_range(
&self,
section: Section,
) -> std::ops::RangeInclusive<chrono::NaiveDate> {
let end = self.end_date;
let range = self.ranges[self.range_group(section) as usize];
(end - chrono::Days::new(if range == 0 { 6 } else { 29 }))..=end
}
pub(super) fn change_range(&mut self) {
let index = self.range_group(self.section);
self.ranges[index as usize] ^= 1;
for section in self.visible_sections() {
if self.range_group(*section) != index {
continue;
}
self.sections[*section].cursor = if self.ranges[index as usize] == 1 {
self.sections[*section].cursor + 23
} else {
self.sections[*section].cursor.saturating_sub(/*rhs*/ 23)
};
self.sections[*section].detail = self.sections[*section].detail.and_then(|day| {
if self.ranges[index as usize] == 1 {
Some(day + 23)
} else {
day.checked_sub(/*rhs*/ 23)
}
});
if section.report().is_some() {
self.load_report(*section);
}
}
}
}
@@ -0,0 +1,28 @@
---
source: tui/src/analytics/client_tests.rs
expression: terminal.backend().to_string()
---
" Analytics · Live account · analytics@example.test · account-a "
" "
" "
" "
" No history has been reported. "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" R refresh · esc back · q close "
@@ -0,0 +1,381 @@
---
source: tui/src/analytics/account_layout_tests.rs
expression: "screens.join(\"\\n\")"
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used "
" "
" ▎ Total usage history By feature "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" Total usage · relative units "
" Approximate · may be delayed up to 6 hours "
" 86 "
" 100 ▃▃▃▃▃▃▃▃ ████████ ▁▁▁▁▁▁▁▁ "
" ▃▃▃▃▃▃▃▃ ████████ ▆▆▆▆▆▆▆▆ ████████ ████████ "
" ████████ ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ "
" 50 ████████ ████████ ████████ ████████ ████████ ████████ "
" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 86 usage units "
" Feature Share "
" ● Tasks 64.0% "
" ● Code review 14.0% "
" ● Images 11.6% "
" ● Other 10.5% "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
" "
" ▎ Messages By model "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 574 reported messages "
" "
" 150 "
" 103 "
" ▇▇▇▇▇▇▇▇ ▆▆▆▆▆▆▆▆ "
" 75 ▆▆▆▆▆▆▆▆ ████████ ▄▄▄▄▄▄▄▄ ▇▇▇▇▇▇▇▇ ████████ "
" ▇▇▇▇▇▇▇▇ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 103 messages "
" Model Messages "
" ● GPT-5.6-Sol 61 "
" ● GPT-5.6-Terra 25 "
" ● GPT-5.5 10 "
" ● Other 7 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used "
" "
" ▎ Plugins called "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 186 calls "
" "
" 40 29 "
" ▇▇▇▇▇▇▇▇ "
" ████████ ████████ ▄▄▄▄▄▄▄▄ █████████ "
" 20 ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ ████████ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 29 calls "
" Plugin Calls "
" ● Slack 10 "
" ● Figma 10 "
" ● GitHub 5 "
" ● Notion 4 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" 11 "
" 10 ▁▁▁▁▁▁▁▁ ▆▆▆▆▆▆▆▆▆ "
" ▆▆▆▆▆▆▆▆ ████████ ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ █████████ "
" ████████ ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 11 uses "
" Skill Uses "
" ● Testing 4 "
" ● Code review 3 "
" ● Planning 3 "
" ● Debugging 1 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" ╭─ 1 Total usage history ──────────────────────────────────────╮ ╭─ 2 Messages ─────────────────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ By feature │ │ By model │ "
" │ Total usage · relative units │ │ 574 reported messages │ "
" │ 86 │ │ │ "
" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ "
" │ █████ ▅▅▅▅▅ █████ ██████ │ │ 103 │ "
" │ █████ ▃▃▃▃▃ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ ▆▆▆▆▆▆ │ "
" │ 50 █████ █████ █████ █████ █████ ██████ │ │ 75 ▆▆▆▆▆ █████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ "
" │ █████ █████ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ "
" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
" │ █████ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ───────────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ "
" │ ● Tasks 64.0% │ │ ● GPT-5.6-Sol 61 │ "
" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
" "
" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ ▸ 4 Skills used ──────────────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ 186 calls │ │ 54 uses │ "
" │ │ │ │ "
" │ 40 29 │ │ 20 │ "
" │ ▇▇▇▇▇ │ │ │ "
" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ "
" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ "
" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ "
" │ ● Slack 10 │ │ ● Testing 4 │ "
" │ ● Figma 10 │ │ ● Code review 3 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · enter/z maximize "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
" "
" ▎ Credits usage history By product "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 431.00 credits "
" "
" 150 "
" "
" ████████ "
" 75 ▇▇▇▇▇▇▇▇ ▄▄▄▄▄▄▄▄ ████████ 64.00 "
" ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ▇▇▇▇▇▇▇▇ "
" ▇▇▇▇▇▇▇▇ ████████ ████████ ████████ ████████ ████████ "
" 0 ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 64.00 credits "
" Product Credits "
" ● CLI 23.00 "
" ● Desktop app 22.00 "
" ● IDE extension 10.00 "
" ● Web 9.00 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
" "
" ▎ Token usage history By token type "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" All models · m change model "
" 4,377,500 tokens "
" "
" 1,000,000 731,000 "
" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ "
" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ "
" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ───────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 731,000 tokens "
" Token type Tokens "
" ● Cached input 602,000 "
" ● Uncached input 86,000 "
" ● Output 43,000 "
" Updated Sep 2 16:00 UTC "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used "
" "
" ▎ Plugins called "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 186 calls "
" "
" 40 29 "
" ▇▇▇▇▇▇▇▇ "
" ████████ ████████ ▄▄▄▄▄▄▄▄ █████████ "
" 20 ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ ████████ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 29 calls "
" Plugin Calls "
" ● Slack 10 "
" ● Figma 10 "
" ● GitHub 5 "
" ● Notion 4 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" 11 "
" 10 ▁▁▁▁▁▁▁▁ ▆▆▆▆▆▆▆▆▆ "
" ▆▆▆▆▆▆▆▆ ████████ ▆▆▆▆▆▆▆▆ ▃▃▃▃▃▃▃▃ █████████ "
" ████████ ████████ ████████ ████████ ████████ ▆▆▆▆▆▆▆▆ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 11 uses "
" Skill Uses "
" ● Testing 4 "
" ● Code review 3 "
" ● Planning 3 "
" ● Debugging 1 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" ╭─ 1 Credits usage history ────────────────────────────────────╮ ╭─ 2 Token usage history ──────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d · All models │ "
" │ By product │ │ By token type │ "
" │ 431.00 credits │ │ 4,377,500 tokens │ "
" │ │ │ │ "
" │ 150 │ │ 1,000,000 731,000 │ "
" │ │ │ ▃▃▃▃▃ ▇▇▇▇ │ "
" │ █████ │ │ ▂▂▂▂ █████ ▆▆▆▆ ████ █████ │ "
" │ 75 ▇▇▇▇▇ ▄▄▄▄▄ █████ 64.00 │ │ 500,000 ████ ▆▆▆▆ █████ ████ ████ █████ │ "
" │ ▅▅▅▅▅ █████ █████ █████ ▇▇▇▇▇▇ │ │ ████ ████ █████ ████ ████ █████ │ "
" │ ▇▇▇▇▇ █████ █████ █████ █████ ██████ │ │ ▁▁▁▁ ████ ████ █████ ████ ████ █████ │ "
" │ 0 █████ █████ █████ █████ █████ ██████ │ │ ████ ████ ████ █████ ████ ████ █████ │ "
" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 64.00 credits │ │ Sep 2 · 731,000 tokens │ "
" │ ● CLI 23.00 │ │ ● Cached input 602,000 │ "
" │ ● Desktop app 22.00 │ │ ● Uncached input 86,000 │ "
" │ │ │ Updated Sep 2 16:00 UTC │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
" "
" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ ▸ 4 Skills used ──────────────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ 186 calls │ │ 54 uses │ "
" │ │ │ │ "
" │ 40 29 │ │ 20 │ "
" │ ▇▇▇▇▇ │ │ │ "
" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ "
" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ "
" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ "
" │ ● Slack 10 │ │ ● Testing 4 │ "
" │ ● Figma 10 │ │ ● Code review 3 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · enter/z maximize "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,28 @@
---
source: tui/src/analytics/account_layout_tests.rs
expression: error
---
" Analytics · Live account "
" "
" "
" "
" Sign in again to view Analytics. "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" R refresh · esc back · q close "
@@ -0,0 +1,65 @@
---
source: tui/src/analytics/account_layout_tests.rs
expression: "format!(\"{all}\\n{filtered}\")"
---
" Analytics · Live account · analytics@example.test · account-a "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
" "
" ▎ Token usage history By token type "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" All models · m change model "
" 4,377,500 tokens "
" "
" 1,000,000 731,000 "
" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ "
" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ "
" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ───────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 731,000 tokens "
" Token type Tokens "
" ● Cached input 602,000 "
" ● Uncached input 86,000 "
" ● Output 43,000 "
" Updated Sep 2 16:00 UTC "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account · analytics@example.test · account-a "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
" "
" ▎ Token usage history By token type "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" GPT-5.5 · m change model "
" 4,377,500 tokens "
" "
" 1,000,000 731,000 "
" ▅▅▅▅▅▅▅▅ ▁▁▁▁▁▁▁▁ ████████ ▃▃▃▃▃▃▃▃ "
" ▅▅▅▅▅▅▅▅ ▂▂▂▂▂▂▂▂ ████████ ████████ ████████ ████████ "
" 500,000 ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ───────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 731,000 tokens "
" Token type Tokens "
" ● Cached input 602,000 "
" ● Uncached input 86,000 "
" ● Output 43,000 "
" Updated Sep 2 16:00 UTC "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,117 @@
---
source: tui/src/analytics/dashboard_tests.rs
expression: "format!(\"{wide}\\n{narrow}\")"
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" ╭─ ▸ 1 Total usage history ────────────────────────────────────────╮ ╭─ 2 Messages ───────────────────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ By feature │ │ By model │ "
" │ Total usage · relative units │ │ 574 reported messages │ "
" │ 86 │ │ │ "
" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ "
" │ █████ ▅▅▅▅▅▅ █████ ██████ │ │ 103 │ "
" │ █████ ▃▃▃▃▃▃ █████ ██████ █████ ██████ │ │ ▇▇▇▇▇▇ ▆▆▆▆▆▆ │ "
" │ 50 █████ ██████ █████ ██████ █████ ██████ │ │ 75 ▆▆▆▆▆ ██████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ "
" │ █████ ██████ █████ ██████ █████ ██████ │ │ ▇▇▇▇▇ █████ ██████ █████ ▆▆▆▆▆▆ █████ ██████ │ "
" │ ▃▃▃▃▃ █████ ██████ █████ ██████ █████ ██████ │ │ █████ █████ ██████ █████ ██████ █████ ██████ │ "
" │ █████ █████ ██████ █████ ██████ █████ ██████ │ │ █████ █████ ██████ █████ ██████ █████ ██████ │ "
" │ 0 ─────────────────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ "
" │ ● Tasks 64.0% │ │ ● GPT-5.6-Sol 61 │ "
" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ "
" "
" ╭─ 3 Plugins called ─────────────────────────────────────────────╮ ╭─ 4 Skills used ────────────────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ 186 calls │ │ 54 uses │ "
" │ │ │ │ "
" │ 40 29 │ │ 20 │ "
" │ ▇▇▇▇▇ │ │ │ "
" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ "
" │ 20 █████ ██████ █████ ██████ ▆▆▆▆▆ ██████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ "
" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ "
" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ █████ ██████ █████ ██████ █████ ▆▆▆▆▆▆ ██████ │ "
" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ █████ ██████ █████ ██████ █████ ██████ ██████ │ "
" │ 0 ──────────────────────────────────────────────────────────── │ │ 0 ──────────────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ "
" │ ● Slack 10 │ │ ● Testing 4 │ "
" │ ● Figma 10 │ │ ● Code review 3 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · enter/z maximize "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" ╭─ ▸ 1 Total usage history ────────────────────────────────────────╮ "
" │ │ "
" │ 7d │ "
" │ By feature │ "
" │ Total usage · relative units │ "
" │ 100 │ "
" │ 100 ▁▁▁▁▁ ▅▅▅▅▅ ▅▅▅▅▅▅ ▇▇▇▇▇ ▆▆▆▆▆▆ █████ ▆▆▆▆▆▆ │ "
" │ 0 ─────────────────────────────────────────────────────────── │ "
" │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ "
" │ Sep 1 · 100 usage units │ "
" │ ● Tasks 65.0% │ "
" │ ● Code review 15.0% │ "
" │ │ "
" ╰──────────────────────────────────────────────────────────────────╯ "
" "
" ╭─ 2 Messages ───────────────────────────────────────────────────╮ "
" │ │ "
" │ 7d │ "
" │ By model │ "
" │ 574 reported messages │ "
" │ 103 │ "
" │ 150 ▃▃▃▃▃ ▄▄▄▄▄ ▅▅▅▅▅▅ ▄▄▄▄▄ ▃▃▃▃▃▃ ▄▄▄▄▄ ▅▅▅▅▅▅ │ "
" │ 0 ─────────────────────────────────────────────────────────── │ "
" │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ "
" │ Sep 2 · 103 messages │ "
" │ ● GPT-5.6-Sol 61 │ "
" │ ● GPT-5.6-Terra 25 │ "
" │ │ "
" ╰──────────────────────────────────────────────────────────────────╯ "
" "
" ╭─ 3 Plugins called ─────────────────────────────────────────────╮ "
" │ │ "
" │ 7d │ "
" │ Access denied for this report. │ "
" │ │ "
" │ │ "
" │ │ "
" │ │ "
" tab/1–4 section · enter/z maximize "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q "
" close "
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
" "
" ▎ Token usage history By token type "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" All models · m change model "
" 4,377,500 tokens "
" "
" 1,000,000 "
" "
" ▂▂▂▂▂▂▂▂▂▂ "
" ██████████ 731,000 "
" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ "
" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ "
" 500,000 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 731,000 tokens "
" Token type Tokens "
" ● Cached input 602,000 "
" ● Uncached input 86,000 "
" ● Output 43,000 "
" Updated Sep 2 16:00 UTC "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 27:3 52:2 83:0
4: 0:0
5: 0:0 2:3 23:0 105:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:2 29:0
8: 0:0 2:3 18:0
9: 0:0
10: 0:0 3:2 12:0
11: 0:0
12: 0:0 90:5 100:0
13: 0:0 90:5 100:0 107:3 114:0
14: 0:0 60:5 70:0 90:6 100:0 105:5 115:0
15: 0:0 60:6 70:0 75:5 85:0 90:7 100:0 105:5 115:0
16: 0:0 30:5 40:0 60:6 70:0 75:6 85:0 90:7 100:0 105:6 115:0
17: 0:0 30:6 40:0 60:7 70:0 75:6 85:0 90:7 100:0 105:7 115:0
18: 0:0 30:6 40:0 45:5 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
19: 0:0 5:2 12:0 30:7 40:0 45:6 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
20: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
21: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
22: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
23: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
24: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
25: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
26: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
27: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
28: 0:0 11:2 12:0 13:4 118:0
29: 0:0 110:3 111:0
30: 0:0 17:2 23:0 32:2 38:0 47:2 53:0 62:2 68:0 77:2 83:0 93:2 98:0 108:8 113:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 24:0
34: 0:0 2:1 12:0 52:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0 2:2 25:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used "
" "
" ▎ Plugins called "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 186 calls "
" "
" 40 "
" "
" ▁▁▁▁▁▁▁▁▁▁ "
" ██████████ "
" ██████████ 29 "
" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ "
" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ "
" ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ "
" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 29 calls "
" Plugin Calls "
" ● Slack 10 "
" ● Figma 10 "
" ● GitHub 5 "
" ● Notion 4 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 50:3 70:2 83:0
4: 0:0
5: 0:0 2:3 18:0
6: 0:0 2:4 118:0
7: 0:0 2:3 11:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0 108:3 110:0
14: 0:0 9:5 19:0
15: 0:0 9:5 19:0
16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0
17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0
18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0
19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0
21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 18:0
34: 0:0 2:1 8:0 53:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
" "
" ▎ Credits usage history By product "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 17,240.00 credits "
" "
" 5,000 "
" "
" ▄▄▄▄▄▄▄▄▄▄ "
" ██████████ "
" ██████████ "
" ██████████ "
" ▇▇▇▇▇▇▇▇▇▇ ██████████ "
" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ "
" ██████████ ██████████ ██████████ 2,560.00 "
" 2,500 ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ "
" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 2,560.00 credits "
" Product Credits "
" ● CLI 920.00 "
" ● Desktop app 880.00 "
" ● IDE extension 400.00 "
" ● Web 360.00 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:3 29:2 83:0
4: 0:0
5: 0:0 2:3 25:0 108:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:3 19:0
8: 0:0
9: 0:0 3:2 8:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0
14: 0:0 89:5 99:0
15: 0:0 89:5 99:0
16: 0:0 89:6 99:0
17: 0:0 58:5 68:0 89:6 99:0 105:3 113:0
18: 0:0 3:2 8:0 58:5 68:0 73:5 83:0 89:6 99:0
19: 0:0 58:6 68:0 73:5 83:0 89:6 99:0
20: 0:0 58:6 68:0 73:6 83:0 89:6 99:0 105:5 115:0
21: 0:0 42:5 52:0 58:6 68:0 73:6 83:0 89:6 99:0 105:6 115:0
22: 0:0 42:6 52:0 58:6 68:0 73:6 83:0 89:7 99:0 105:6 115:0
23: 0:0 27:5 37:0 42:6 52:0 58:7 68:0 73:6 83:0 89:7 99:0 105:6 115:0
24: 0:0 27:6 37:0 42:6 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:6 115:0
25: 0:0 27:6 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 16:4 17:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 7:2 8:0 9:4 118:0
29: 0:0 109:3 110:0
30: 0:0 13:2 19:0 28:2 34:0 44:2 50:0 59:2 65:0 75:2 81:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 26:0
34: 0:0 2:1 9:0 51:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" "
" "
" "
" "
" "
" 11 "
" ▃▃▃▃▃▃▃▃▃▃ "
" 10 ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 11 uses "
" Skill Uses "
" ● Testing 4 "
" ● Code review 3 "
" ● Planning 3 "
" ● Debugging 1 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 68:3 83:0
4: 0:0
5: 0:0 2:3 15:0
6: 0:0 2:4 118:0
7: 0:0 2:3 9:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0
14: 0:0
15: 0:0
16: 0:0 108:3 110:0
17: 0:0
18: 0:0 3:2 5:0
19: 0:0 105:5 115:0
20: 0:0 41:5 51:0 105:5 115:0
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 17:0
34: 0:0 2:1 7:0 54:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used "
" "
" ▎ Total usage history By feature "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" Total usage · relative units "
" Approximate · may be delayed up to 6 hours "
" "
" 100 ██████████ "
" ▃▃▃▃▃▃▃▃▃▃ ██████████ 86 "
" ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ "
" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 50 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 86 usage units "
" Feature Share "
" ● Tasks 64.0% "
" ● Code review 14.0% "
" ● Images 11.6% "
" ● Other 10.5% "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:3 27:2 70:0
4: 0:0
5: 0:0 2:3 23:0 108:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:2 30:0
8: 0:0 2:2 44:0
9: 0:0
10: 0:0 3:2 6:0
11: 0:0 89:5 99:0 108:3 110:0
12: 0:0 89:5 99:0
13: 0:0 57:5 67:0 89:5 99:0
14: 0:0 57:5 67:0 89:6 99:0 105:5 115:0
15: 0:0 57:6 67:0 73:5 83:0 89:6 99:0 105:5 115:0
16: 0:0 25:5 35:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0
17: 0:0 25:5 35:0 57:7 67:0 73:6 83:0 89:7 99:0 105:6 115:0
18: 0:0 25:6 35:0 41:5 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
19: 0:0 4:2 6:0 25:6 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
20: 0:0 25:7 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
21: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
22: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
23: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
24: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 5:2 6:0 7:4 118:0
29: 0:0 109:3 110:0
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 24:0
34: 0:0 2:1 9:0 53:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used "
" "
" ▎ Plugins called "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 186 calls "
" "
" 40 "
" "
" ▁▁▁▁▁▁▁▁▁▁ "
" ██████████ "
" ██████████ 29 "
" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ "
" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ "
" ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ "
" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 29 calls "
" Plugin Calls "
" ● Slack 10 "
" ● Figma 10 "
" ● GitHub 5 "
" ● Notion 4 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 37:3 57:2 70:0
4: 0:0
5: 0:0 2:3 18:0
6: 0:0 2:4 118:0
7: 0:0 2:3 11:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0 108:3 110:0
14: 0:0 9:5 19:0
15: 0:0 9:5 19:0
16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0
17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0
18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0
19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0
21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 18:0
34: 0:0 2:1 8:0 53:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
" "
" ▎ Messages By model "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 574 reported messages "
" "
" 150 "
" "
" "
" "
" 103 "
" ▄▄▄▄▄▄▄▄▄▄ "
" ██████████ ██████████ "
" ██████████ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 103 messages "
" Model Messages "
" ● GPT-5.6-Sol 61 "
" ● GPT-5.6-Terra 25 "
" ● GPT-5.5 10 "
" ● Other 7 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 25:3 39:2 70:0
4: 0:0
5: 0:0 2:3 12:0 110:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:3 23:0
8: 0:0
9: 0:0 3:2 6:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0 108:3 111:0
14: 0:0
15: 0:0 41:5 51:0
16: 0:0 41:5 51:0 105:5 115:0
17: 0:0 41:6 51:0 105:6 115:0
18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0
19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0
20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0
21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0
22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 5:2 6:0 7:4 118:0
29: 0:0 109:3 110:0
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 22:0
34: 0:0 2:1 7:0 50:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" "
" "
" "
" "
" "
" 11 "
" ▃▃▃▃▃▃▃▃▃▃ "
" 10 ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 11 uses "
" Skill Uses "
" ● Testing 4 "
" ● Code review 3 "
" ● Planning 3 "
" ● Debugging 1 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Cyan|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 55:3 70:0
4: 0:0
5: 0:0 2:3 15:0
6: 0:0 2:4 118:0
7: 0:0 2:3 9:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0
14: 0:0
15: 0:0
16: 0:0 108:3 110:0
17: 0:0
18: 0:0 3:2 5:0
19: 0:0 105:5 115:0
20: 0:0 41:5 51:0 105:5 115:0
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 17:0
34: 0:0 2:1 7:0 54:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
" "
" ▎ Token usage history By token type "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" All models · m change model "
" 4,377,500 tokens "
" "
" 1,000,000 "
" "
" ▂▂▂▂▂▂▂▂▂▂ "
" ██████████ 731,000 "
" ▇▇▇▇▇▇▇▇▇▇ ██████████ ▁▁▁▁▁▁▁▁▁▁ "
" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ██████████ "
" 500,000 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 731,000 tokens "
" Token type Tokens "
" ● Cached input 602,000 "
" ● Uncached input 86,000 "
" ● Output 43,000 "
" Updated Sep 2 16:00 UTC "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 27:3 52:2 83:0
4: 0:0
5: 0:0 2:3 23:0 105:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:2 29:0
8: 0:0 2:3 18:0
9: 0:0
10: 0:0 3:2 12:0
11: 0:0
12: 0:0 90:5 100:0
13: 0:0 90:5 100:0 107:3 114:0
14: 0:0 60:5 70:0 90:6 100:0 105:5 115:0
15: 0:0 60:6 70:0 75:5 85:0 90:7 100:0 105:5 115:0
16: 0:0 30:5 40:0 60:6 70:0 75:6 85:0 90:7 100:0 105:6 115:0
17: 0:0 30:6 40:0 60:7 70:0 75:6 85:0 90:7 100:0 105:7 115:0
18: 0:0 30:6 40:0 45:5 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
19: 0:0 5:2 12:0 30:7 40:0 45:6 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
20: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
21: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
22: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
23: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
24: 0:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
25: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
26: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
27: 0:0 15:7 25:0 30:7 40:0 45:7 55:0 60:7 70:0 75:7 85:0 90:7 100:0 105:7 115:0
28: 0:0 11:2 12:0 13:4 118:0
29: 0:0 110:3 111:0
30: 0:0 17:2 23:0 32:2 38:0 47:2 53:0 62:2 68:0 77:2 83:0 93:2 98:0 108:8 113:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 24:0
34: 0:0 2:1 12:0 52:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0 2:2 25:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used "
" "
" ▎ Plugins called "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 186 calls "
" "
" 40 "
" "
" ▁▁▁▁▁▁▁▁▁▁ "
" ██████████ "
" ██████████ 29 "
" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ "
" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ "
" ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ "
" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 29 calls "
" Plugin Calls "
" ● Slack 10 "
" ● Figma 10 "
" ● GitHub 5 "
" ● Notion 4 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 50:3 70:2 83:0
4: 0:0
5: 0:0 2:3 18:0
6: 0:0 2:4 118:0
7: 0:0 2:3 11:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0 108:3 110:0
14: 0:0 9:5 19:0
15: 0:0 9:5 19:0
16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0
17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0
18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0
19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0
21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 18:0
34: 0:0 2:1 8:0 53:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
" "
" ▎ Credits usage history By product "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 17,240.00 credits "
" "
" 5,000 "
" "
" ▄▄▄▄▄▄▄▄▄▄ "
" ██████████ "
" ██████████ "
" ██████████ "
" ▇▇▇▇▇▇▇▇▇▇ ██████████ "
" ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ "
" ██████████ ██████████ ██████████ 2,560.00 "
" 2,500 ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ "
" ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ───────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 2,560.00 credits "
" Product Credits "
" ● CLI 920.00 "
" ● Desktop app 880.00 "
" ● IDE extension 400.00 "
" ● Web 360.00 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:3 29:2 83:0
4: 0:0
5: 0:0 2:3 25:0 108:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:3 19:0
8: 0:0
9: 0:0 3:2 8:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0
14: 0:0 89:5 99:0
15: 0:0 89:5 99:0
16: 0:0 89:6 99:0
17: 0:0 58:5 68:0 89:6 99:0 105:3 113:0
18: 0:0 3:2 8:0 58:5 68:0 73:5 83:0 89:6 99:0
19: 0:0 58:6 68:0 73:5 83:0 89:6 99:0
20: 0:0 58:6 68:0 73:6 83:0 89:6 99:0 105:5 115:0
21: 0:0 42:5 52:0 58:6 68:0 73:6 83:0 89:6 99:0 105:6 115:0
22: 0:0 42:6 52:0 58:6 68:0 73:6 83:0 89:7 99:0 105:6 115:0
23: 0:0 27:5 37:0 42:6 52:0 58:7 68:0 73:6 83:0 89:7 99:0 105:6 115:0
24: 0:0 27:6 37:0 42:6 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:6 115:0
25: 0:0 27:6 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 16:4 17:0 27:7 37:0 42:7 52:0 58:7 68:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 7:2 8:0 9:4 118:0
29: 0:0 109:3 110:0
30: 0:0 13:2 19:0 28:2 34:0 44:2 50:0 59:2 65:0 75:2 81:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 26:0
34: 0:0 2:1 9:0 51:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" "
" "
" "
" "
" "
" 11 "
" ▃▃▃▃▃▃▃▃▃▃ "
" 10 ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 11 uses "
" Skill Uses "
" ● Testing 4 "
" ● Code review 3 "
" ● Planning 3 "
" ● Debugging 1 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 68:3 83:0
4: 0:0
5: 0:0 2:3 15:0
6: 0:0 2:4 118:0
7: 0:0 2:3 9:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0
14: 0:0
15: 0:0
16: 0:0 108:3 110:0
17: 0:0
18: 0:0 3:2 5:0
19: 0:0 105:5 115:0
20: 0:0 41:5 51:0 105:5 115:0
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 17:0
34: 0:0 2:1 7:0 54:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used "
" "
" ▎ Total usage history By feature "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" Total usage · relative units "
" Approximate · may be delayed up to 6 hours "
" "
" 100 ██████████ "
" ▃▃▃▃▃▃▃▃▃▃ ██████████ 86 "
" ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ "
" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
" ▁▁▁▁▁▁▁▁▁▁ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 50 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 86 usage units "
" Feature Share "
" ● Tasks 64.0% "
" ● Code review 14.0% "
" ● Images 11.6% "
" ● Other 10.5% "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:3 27:2 70:0
4: 0:0
5: 0:0 2:3 23:0 108:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:2 30:0
8: 0:0 2:2 44:0
9: 0:0
10: 0:0 3:2 6:0
11: 0:0 89:5 99:0 108:3 110:0
12: 0:0 89:5 99:0
13: 0:0 57:5 67:0 89:5 99:0
14: 0:0 57:5 67:0 89:6 99:0 105:5 115:0
15: 0:0 57:6 67:0 73:5 83:0 89:6 99:0 105:5 115:0
16: 0:0 25:5 35:0 57:6 67:0 73:6 83:0 89:7 99:0 105:6 115:0
17: 0:0 25:5 35:0 57:7 67:0 73:6 83:0 89:7 99:0 105:6 115:0
18: 0:0 25:6 35:0 41:5 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
19: 0:0 4:2 6:0 25:6 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
20: 0:0 25:7 35:0 41:6 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
21: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
22: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
23: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
24: 0:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 5:2 6:0 7:4 118:0
29: 0:0 109:3 110:0
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 24:0
34: 0:0 2:1 9:0 53:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used "
" "
" ▎ Plugins called "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 186 calls "
" "
" 40 "
" "
" ▁▁▁▁▁▁▁▁▁▁ "
" ██████████ "
" ██████████ 29 "
" ██████████ ▆▆▆▆▆▆▆▆▆▆ ▆▆▆▆▆▆▆▆▆▆ "
" ██████████ ██████████ ▂▂▂▂▂▂▂▂▂▂ ██████████ "
" ██████████ ██████████ ██████████ ██████████ "
" ██████████ ▇▇▇▇▇▇▇▇▇▇ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ▇▇▇▇▇▇▇▇▇▇ ██████████ "
" 20 ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 29 calls "
" Plugin Calls "
" ● Slack 10 "
" ● Figma 10 "
" ● GitHub 5 "
" ● Notion 4 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 37:3 57:2 70:0
4: 0:0
5: 0:0 2:3 18:0
6: 0:0 2:4 118:0
7: 0:0 2:3 11:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0 108:3 110:0
14: 0:0 9:5 19:0
15: 0:0 9:5 19:0
16: 0:0 9:5 19:0 41:5 51:0 105:5 115:0
17: 0:0 9:6 19:0 41:5 51:0 105:5 115:0
18: 0:0 3:2 5:0 9:6 19:0 41:5 51:0 57:5 67:0 105:5 115:0
19: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
20: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:6 115:0
21: 0:0 9:6 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
22: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:6 115:0
23: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 18:0
34: 0:0 2:1 8:0 53:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
" "
" ▎ Messages By model "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 574 reported messages "
" "
" 150 "
" "
" "
" "
" 103 "
" ▄▄▄▄▄▄▄▄▄▄ "
" ██████████ ██████████ "
" ██████████ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ "
" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 103 messages "
" Model Messages "
" ● GPT-5.6-Sol 61 "
" ● GPT-5.6-Terra 25 "
" ● GPT-5.5 10 "
" ● Other 7 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 25:3 39:2 70:0
4: 0:0
5: 0:0 2:3 12:0 110:2 118:0
6: 0:0 2:4 118:0
7: 0:0 2:3 23:0
8: 0:0
9: 0:0 3:2 6:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0 108:3 111:0
14: 0:0
15: 0:0 41:5 51:0
16: 0:0 41:5 51:0 105:5 115:0
17: 0:0 41:6 51:0 105:6 115:0
18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0
19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0
20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0
21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0
22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 5:2 6:0 7:4 118:0
29: 0:0 109:3 110:0
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 22:0
34: 0:0 2:1 7:0 50:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
@@ -0,0 +1,102 @@
---
source: tui/src/analytics/styles_tests.rs
expression: snapshot
---
120x42
Text:
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" "
" "
" "
" "
" "
" 11 "
" ▃▃▃▃▃▃▃▃▃▃ "
" 10 ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 11 uses "
" Skill Uses "
" ● Testing 4 "
" ● Code review 3 "
" ● Planning 3 "
" ● Debugging 1 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
Palette (foreground|background|underline|modifiers):
0: Reset|Reset|Reset|NONE
1: Reset|Reset|Reset|BOLD
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: Green|Reset|Reset|NONE
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0
2: 0:0
3: 0:0 2:2 55:3 70:0
4: 0:0
5: 0:0 2:3 15:0
6: 0:0 2:4 118:0
7: 0:0 2:3 9:0
8: 0:0
9: 0:0 3:2 5:0
10: 0:0
11: 0:0
12: 0:0
13: 0:0
14: 0:0
15: 0:0
16: 0:0 108:3 110:0
17: 0:0
18: 0:0 3:2 5:0
19: 0:0 105:5 115:0
20: 0:0 41:5 51:0 105:5 115:0
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
28: 0:0 4:2 5:0 6:4 118:0
29: 0:0 109:3 110:0
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
31: 0:0
32: 0:0 2:4 58:0
33: 0:0 2:1 7:3 17:0
34: 0:0 2:1 7:0 54:1 58:0
35: 0:0 2:7 4:0
36: 0:0 2:6 4:0
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
@@ -0,0 +1,138 @@
---
source: tui/src/analytics/tool_panel_tests.rs
expression: "format!(\"{empty}\\n{missing}\\n{partial}\")"
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 38 uses "
" "
" 10 "
" "
" ▇▇▇▇▇▇▇▇▇▇▇▇ "
" ████████████ "
" ▆▆▆▆▆▆▆▆▆▆▆▆ ████████████ ▆▆▆▆▆▆▆▆▆▆▆▆ "
" ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ▅▅▅▅▅▅▅▅▅▅▅▅ "
" ████████████ ████████████ ████████████ ████████████ "
" ████████████ ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" 5 ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ — 0 "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" No uses reported for Sep 2. "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · z dashboard · ←/→ day "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 38 uses "
" "
" 10 "
" "
" ▇▇▇▇▇▇▇▇▇▇▇▇ "
" ████████████ "
" ▆▆▆▆▆▆▆▆▆▆▆▆ ████████████ ▆▆▆▆▆▆▆▆▆▆▆▆ "
" ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ▅▅▅▅▅▅▅▅▅▅▅▅ "
" ████████████ ████████████ ████████████ ████████████ "
" ████████████ ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" 5 ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ — 0 "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Data not reported for Sep 1. "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · z dashboard · ←/→ day "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" Skills could not be loaded. Press R to retry. "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · z dashboard · ←/→ day "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,85 @@
---
source: tui/src/analytics/tool_panel_tests.rs
expression: "format!(\"{wide}\\n{narrow}\")"
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used "
" "
" ▎ Plugins called "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 186 calls "
" "
" 40 "
" "
" "
" ▂▂▂▂▂▂▂▂▂▂▂▂ "
" ████████████ "
" ████████████ 29 "
" ████████████ ▁▁▁▁▁▁▁▁▁▁▁▁ ▁▁▁▁▁▁▁▁▁▁▁▁ "
" ████████████ ████████████ ████████████ "
" ████████████ ████████████ ▂▂▂▂▂▂▂▂▂▂▂▂ ████████████ "
" ████████████ ████████████ ████████████ ████████████ "
" ████████████ ▃▃▃▃▃▃▃▃▃▃▃▃ ████████████ ████████████ ▃▃▃▃▃▃▃▃▃▃▃▃ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ▆▆▆▆▆▆▆▆▆▆▆▆ ████████████ ████████████ "
" 20 ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ ████████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 29 calls "
" Plugin Calls "
" ● Slack 10 "
" ● Figma 10 "
" ● GitHub 5 "
" ● Notion 4 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" Total usage history Messages Plugins called [Skills "
" used] "
" "
" ▎ Skills used "
" ────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" ▂▂▂▂▂ "
" 10 ▃▃▃▃ ▅▅▅▅ ▃▃▃▃ 5 █████ "
" ████ ▆▆▆▆ ████ ████ ████ ▄▄▄▄ █████ "
" ████ ████ ████ ████ ████ ████ █████ "
" 0 ────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ────────────────────────────────────────────────────── "
" Sep 1 · 5 uses "
" Skill Uses "
" ● Testing 3 "
" ● Code review 1 "
" ● Planning 1 "
" ● Debugging 0 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter "
" details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q "
" close "
+153
View File
@@ -0,0 +1,153 @@
//! Render both account layouts with explicit terminal palettes, including color and selection styles.
use super::*;
use crate::analytics::sections::Section;
use crate::style::accent_style;
use crate::terminal_palette::with_test_default_colors;
use crate::terminal_probe::DefaultColors;
use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Modifier;
fn luminance(rgb: (u8, u8, u8)) -> f64 {
let [r, g, b] = [rgb.0, rgb.1, rgb.2].map(|channel| {
let value = f64::from(channel) / 255.0;
if value <= 0.04045 {
value / 12.92
} else {
((value + 0.055) / 1.055).powf(/*n*/ 2.4)
}
});
0.2126 * r + 0.7152 * g + 0.0722 * b
}
#[test]
fn analytics_sections_adapt_to_terminal_colors() {
for (theme, colors) in [
(
"light",
DefaultColors {
fg: (0, 0, 0),
bg: (255, 255, 255),
},
),
(
"dark",
DefaultColors {
fg: (230, 230, 230),
bg: (16, 16, 16),
},
),
] {
with_test_default_colors(colors, || {
for business in [false, true] {
let mut view = fixture::view(models::AccountKind::Consumer);
if business {
view = fixture::view(models::AccountKind::Enterprise);
}
// Exercise the reported credit range and preserve exact amounts below the plot.
let mut credits =
fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0);
for day in &mut credits.data {
day.total *= 40.0;
for value in &mut day.values {
value.value *= 40.0;
}
}
view.sections[Section::Credits].history = Load::Ready(credits);
for section in view.visible_sections() {
let section = *section;
view.section = section;
let area = Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 120, /*height*/ 42,
);
let mut buffer = Buffer::empty(area);
view.render(area, &mut buffer);
let heading = buffer
.content
.iter()
.find(|cell| cell.symbol() == "▎")
.unwrap();
assert_eq!(heading.fg, accent_style().fg.unwrap());
assert!(heading.modifier.contains(Modifier::BOLD));
for marker in buffer.content.iter().filter(|cell| cell.symbol() == "▲") {
assert_eq!(marker.fg, accent_style().fg.unwrap());
assert_eq!(marker.bg, Color::Reset);
assert!(marker.modifier.contains(Modifier::BOLD));
}
if theme == "light" {
for cell in &buffer.content {
if !cell.symbol().trim().is_empty()
&& let Color::Rgb(r, g, b) = cell.fg
{
let contrast =
(luminance(colors.bg) + 0.05) / (luminance((r, g, b)) + 0.05);
assert!(
contrast >= 4.5,
"{}: {:?} has contrast {contrast}",
cell.symbol(),
cell.fg
);
}
}
}
// Preserve every symbol and style while sharing repeated styles in a palette.
let mut palette = Vec::new();
let mut text = Vec::new();
let mut runs = Vec::new();
for (y, row) in buffer.content.chunks(usize::from(area.width)).enumerate() {
text.push(format!(
"{:?}",
row.iter()
.map(ratatui::buffer::Cell::symbol)
.collect::<String>()
));
let mut previous = None;
let mut changes = Vec::new();
for (x, cell) in row.iter().enumerate() {
let style = format!(
"{:?}|{:?}|{:?}|{:?}",
cell.fg, cell.bg, cell.underline_color, cell.modifier
);
let index = palette
.iter()
.position(|candidate| *candidate == style)
.unwrap_or_else(|| {
palette.push(style);
palette.len() - 1
});
if previous != Some(index) {
changes.push(format!("{x}:{index}"));
previous = Some(index);
}
}
runs.push(format!("{y}: {}", changes.join(" ")));
}
let palette = palette
.iter()
.enumerate()
.map(|(index, style)| format!("{index}: {style}"))
.collect::<Vec<_>>()
.join("\n");
let snapshot = format!(
"{}x{}\nText:\n{}\nPalette (foreground|background|underline|modifiers):\n{palette}\nRows (column:palette):\n{}",
area.width,
area.height,
text.join("\n"),
runs.join("\n")
);
insta::assert_snapshot!(
format!(
"analytics_{theme}_{}_{}",
if business { "business" } else { "consumer" },
section as usize + 1
),
snapshot
);
}
}
});
}
}
+188
View File
@@ -0,0 +1,188 @@
//! Bounded fictional dashboard data with raw daily units.
//! Charts and their totals share the same daily series; no account activity is read.
use crate::analytics::sections::Section;
pub(super) const END_DATE: chrono::NaiveDate =
match chrono::NaiveDate::from_ymd_opt(/*year*/ 2026, /*month*/ 9, /*day*/ 2) {
Some(date) => date,
None => panic!("invalid test date"),
};
pub(super) const TOOL_DAILY: [[u32; 7]; 2] = [[34, 23, 29, 26, 22, 23, 29], [8, 6, 9, 8, 7, 5, 11]];
pub(super) const GROUPS: &[(&str, [&str; 4], [u32; 4])] = &[
(
"Surface",
["Desktop app", "CLI", "IDE extension", "Web"],
[35, 36, 17, 12],
),
(
"Feature",
["Tasks", "Code review", "Images", "Other"],
[65, 15, 12, 8],
),
(
"Model",
["GPT-5.6-Sol", "GPT-5.6-Terra", "GPT-5.5", "Other"],
[60, 25, 10, 5],
),
(
"Task start",
["User messages", "Goals", "Automations", "Other"],
[29, 10, 5, 5],
),
(
"Speed",
["Standard", "Fast", "Priority", "Unknown"],
[65, 30, 5, 0],
),
(
"Reasoning",
["Low", "Medium", "High", "Other"],
[10, 30, 50, 10],
),
(
"Token type",
["Uncached input", "Cached input", "Output", "Other"],
[20, 70, 10, 0],
),
];
pub(super) fn daily(section: Section, range: usize) -> Vec<u32> {
let (earlier, recent) = match section {
Section::Usage => (40, [20, 73, 64, 91, 81, 100, 86]),
Section::Credits => (35, [0, 41, 57, 85, 75, 109, 64]),
Section::Activity => (34, [62, 83, 107, 75, 59, 85, 103]),
_ => return Vec::new(),
};
let mut values = vec![earlier; if range == 0 { 0 } else { 23 }];
values.extend(recent);
values
}
pub(super) fn breakdown(total: u32, group: usize) -> [u32; 4] {
let mut values = GROUPS[group].2.map(|weight| total * weight / 100);
if group != 3 {
values[3] = total - values[..3].iter().sum::<u32>();
}
values
}
/// Fixtures use the private report types and the same presentation path as live reports.
pub(super) fn history(
report: usize,
range: usize,
group: usize,
) -> crate::analytics::models::AccountAnalyticsHistory {
use crate::analytics::models::AccountAnalyticsDay;
use crate::analytics::models::AccountAnalyticsHistory;
use crate::analytics::models::AccountAnalyticsUnit;
use crate::analytics::models::AccountAnalyticsValue;
let section = [
Section::Usage,
Section::Credits,
Section::Activity,
Section::Plugins,
Section::Skills,
][report];
let series = if report < 3 {
daily(section, range)
} else {
let mut series = vec![if report == 3 { 20 } else { 5 }; if range == 0 { 0 } else { 23 }];
series.extend(TOOL_DAILY[report - 3]);
series
};
let start = END_DATE - chrono::Days::new(if range == 0 { 6 } else { 29 });
AccountAnalyticsHistory {
unit: if report == 0 {
AccountAnalyticsUnit::RelativeUsage
} else if report == 1 {
AccountAnalyticsUnit::Credits
} else {
AccountAnalyticsUnit::Count
},
updated_at: None,
data: series
.into_iter()
.enumerate()
.map(|(day, total)| {
let labels = if report < 3 {
GROUPS[group].1
} else if report == 3 {
["Figma", "Slack", "Notion", "GitHub"]
} else {
["Code review", "Planning", "Debugging", "Testing"]
};
let values = breakdown(total, if report < 3 { group } else { 0 });
let pairs = if report == 0 && group == 0 {
vec![
("Codex", total * 65 / 100),
("Work", total - total * 65 / 100),
]
} else {
labels.into_iter().zip(values).collect()
};
AccountAnalyticsDay {
date: start + chrono::Days::new(day as u64),
total: f64::from(total),
values: pairs
.into_iter()
.map(|(label, value)| AccountAnalyticsValue {
key: label.into(),
label: label.into(),
value: f64::from(value),
})
.collect(),
}
})
.collect(),
}
}
pub(super) fn tokens(range: usize, grouping: usize) -> super::models::AccountAnalyticsHistory {
let response = serde_json::json!({"data_freshness_ts":"2026-09-02T16:00:00Z", "data":
daily(Section::Usage, range).into_iter().enumerate().map(|(index, total)| serde_json::json!({
"date": (END_DATE - chrono::Days::new(if range == 0 { 6 } else { 29 }) + chrono::Days::new(index as u64)).to_string(),
"groups": [{"dimensions":{"model":"GPT-5.5"}, "uncached_text_input_tokens": total * 1000, "cached_text_input_tokens":total * 7000, "text_output_tokens":total * 500}]
})).collect::<Vec<_>>()
});
match super::tokens::history(
match serde_json::from_value(response) {
Ok(response) => response,
Err(_) => panic!("invalid test token response"),
},
super::data::GROUPINGS[grouping],
END_DATE - chrono::Days::new(if range == 0 { 6 } else { 29 }),
END_DATE,
) {
Ok(history) => history,
Err(_) => panic!("invalid test token history"),
}
}
/// Build a fixed account state for rendering tests; never opens a connection.
pub(super) fn view(kind: super::models::AccountKind) -> super::AnalyticsView {
let mut view = super::AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list);
view.end_date = END_DATE;
view.account = super::data::Load::Ready(kind);
view.start_reports();
seed_reports(&mut view);
view
}
/// Populate explicit report state after a test chooses its account, period, and grouping.
pub(super) fn seed_reports(view: &mut super::AnalyticsView) {
use super::data::Load;
for section in view.visible_sections() {
if let Some(report) = section.report() {
let range = view.ranges[view.range_group(*section) as usize];
let group = view.sections[*section].group;
view.sections[*section].history =
Load::Ready(if *section == Section::Usage && view.business() {
tokens(range, group)
} else {
history(report as usize, range, group)
});
}
}
}
@@ -0,0 +1,72 @@
//! Mock HTTP reports for tests that exercise the authenticated dashboard loading path.
use super::data::Load;
use serde_json::json;
pub(super) async fn server() -> wiremock::MockServer {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path_regex("^/backend-api/wham/(usage|analytics)/"))
.respond_with(|request: &wiremock::Request| {
let params = request.url.query_pairs().collect::<std::collections::HashMap<_, _>>();
let today = chrono::Utc::now().date_naive();
let start = params.get("start_date").map_or(today, |date| date.parse().unwrap());
let end = params.get("end_date").map_or(today, |date| date.parse().unwrap());
let route = request.url.path().rsplit('/').next().unwrap();
let data = start.iter_days().take_while(|date| *date <= end).map(|date| {
let date = date.to_string();
match route {
"daily-token-usage-breakdown" | "daily-workspace-user-token-usage-breakdown" => json!({
"date":date, "product_surface_usage_values":{"codex_cli":10.0},
"premium_usage_values":{
"credit_usage_credits":{"codex_cli":10.0}, "total_usage_credits":{},
"uncached_text_input_tokens_by_surface":{}, "cached_text_input_tokens_by_surface":{},
"text_output_tokens_by_surface":{}, "text_total_tokens_by_surface":{}
},
"models":[{"model":"GPT-5.5","credits":10.0,"on_demand_credits":10.0,"turns":10,"speed":"standard"}],
"groups":[{"dimensions":{"model":"GPT-5.5"},"credits":10.0,"uncached_text_input_tokens":10,"cached_text_input_tokens":20,"text_output_tokens":30}]
}),
"credit-usage-events" => json!({"date":date,"product_surface":"cli","credit_amount":10.0}),
"daily-workspace-user-credit-usage" => json!({"date":date,"values":{"codex":10.0}}),
"daily-workspace-usage-counts" => json!({
"date":date,"totals":{"users":1,"threads":1,"turns":10,"credits":10.0},"clients":[],
"models":[{"model":"GPT-5.5","credits":10.0,"turns":10}]
}),
"daily-plugin-usage-metrics" => json!({"date":date,"plugin_usage_overviews":[{
"plugin_id":null,"plugin_name":"test","marketplace":null,"display_name":"Test plugin","invocation_counts":10
}]}),
"daily-skill-usage-metrics" => json!({"date":date,"skill_usage_overviews":[{
"skill_name":"test","skill_ids":[],"display_name":"Test skill","invocation_counts":10
}]}),
_ => panic!("Unexpected mock analytics route: {route}"),
}
}).collect::<Vec<_>>();
let response = if route == "daily-workspace-user-credit-usage" {
json!({"breakdown":params["breakdown"],"series":[{"key":"codex","label":"Codex","total":data.len() as f64 * 10.0}],"data":data})
} else {
json!({"data":data})
};
wiremock::ResponseTemplate::new(/*s*/ 200).set_body_json(response)
})
.mount(&server).await;
server
}
pub(super) async fn settle(view: &mut super::AnalyticsView) {
tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 10), async {
loop {
view.poll_reports();
if !matches!(view.account, Load::Loading(_))
&& !view
.sections
.0
.iter()
.any(|state| matches!(state.history, Load::Loading(_)))
{
break;
}
tokio::time::sleep(std::time::Duration::from_millis(/*millis*/ 5)).await;
}
})
.await
.unwrap();
}
+26
View File
@@ -0,0 +1,26 @@
//! Human-readable plugin and skill labels.
pub(super) fn tool_name(label: &str) -> String {
let label = match label.split_once(':') {
Some((namespace, name)) if namespace.trim().eq_ignore_ascii_case(name.trim()) => {
name.trim()
}
_ => label,
};
label
.split_whitespace()
.map(|word| match word.to_ascii_lowercase().as_str() {
"pr" => "PR",
"cli" => "CLI",
"api" => "API",
"ide" => "IDE",
"mcp" => "MCP",
"sdk" => "SDK",
"ui" => "UI",
"tui" => "TUI",
"openai" => "OpenAI",
_ => word,
})
.collect::<Vec<_>>()
.join(" ")
}
@@ -0,0 +1,101 @@
//! Tool chart layout, independent selection, naming, and partial report states.
use super::*;
use crate::analytics::sections::Section;
use pretty_assertions::assert_eq;
#[test]
fn analytics_tools_reflow_and_keep_independent_days() {
let mut view = fixture::view(models::AccountKind::Consumer);
press(&mut view, KeyCode::Char('3'));
let wide = screen(&mut view, /*width*/ 140, /*height*/ 48);
assert!(wide.contains("186 calls"));
press(&mut view, KeyCode::Char('4'));
press(&mut view, KeyCode::Left);
press(&mut view, KeyCode::Enter);
assert_eq!(
(
view.sections[Section::Plugins].cursor,
view.sections[Section::Skills].cursor,
view.sections[Section::Skills].detail
),
(6, 5, Some(5))
);
let narrow = screen(&mut view, /*width*/ 58, /*height*/ 32);
assert!(narrow.contains("Sep 1 · 5 uses"));
press(&mut view, KeyCode::Char('r'));
assert_eq!(
(
view.sections[Section::Plugins].cursor,
view.sections[Section::Skills].cursor,
view.ranges
),
(29, 28, [0, 0, 1])
);
press(&mut view, KeyCode::Char('r'));
press(&mut view, KeyCode::Char('z'));
press(&mut view, KeyCode::Char('z'));
assert_eq!(
(
view.sections[Section::Plugins].cursor,
view.sections[Section::Skills].cursor,
view.sections[Section::Skills].detail
),
(6, 5, Some(5))
);
insta::assert_snapshot!(format!("{wide}\n{narrow}"));
}
#[test]
fn analytics_tools_handle_empty_days_and_independent_failures() {
let mut view = fixture::view(models::AccountKind::Consumer);
let mut history = fixture::history(/*report*/ 4, /*range*/ 0, /*group*/ 0);
history.data[6].total = 0.0;
for value in &mut history.data[6].values {
value.value = 0.0;
}
history.data.remove(/*index*/ 5);
view.sections[Section::Skills].history = Load::Ready(history);
press(&mut view, KeyCode::Char('4'));
let empty = screen(&mut view, /*width*/ 140, /*height*/ 44);
assert!(empty.contains("No uses reported for Sep 2."));
assert!(!empty.contains("details"));
press(&mut view, KeyCode::Enter);
assert_eq!(view.sections[Section::Skills].detail, None);
press(&mut view, KeyCode::Left);
let missing = screen(&mut view, /*width*/ 140, /*height*/ 44);
assert!(missing.contains("Data not reported for Sep 1."));
assert_eq!(
empty.lines().position(|line| line.contains('▲')),
missing.lines().position(|line| line.contains('▲')),
);
view.sections[Section::Skills].history =
Load::Error("Skills could not be loaded. Press R to retry.".into());
let partial = screen(&mut view, /*width*/ 140, /*height*/ 44);
assert!(partial.contains("Skills could not be loaded"));
press(&mut view, KeyCode::Char('3'));
assert!(screen(&mut view, /*width*/ 140, /*height*/ 44).contains("186 calls"));
insta::assert_snapshot!(format!("{empty}\n{missing}\n{partial}"));
}
#[test]
fn analytics_tool_names_preserve_acronyms_and_distinct_namespaces() {
let labels = [
"Pr Reviewer",
"Slack Cli",
"Squatch: Squatch",
"Openai Docs",
"User Writing: Writing Style",
];
assert_eq!(
labels.map(tool_panel::tool_name),
[
"PR Reviewer",
"Slack CLI",
"Squatch",
"OpenAI Docs",
"User Writing: Writing Style"
]
.map(str::to_owned),
);
}
+568 -14
View File
@@ -1,7 +1,215 @@
//! Credit formatting preserves tiny adjustments and integer precision.
//! Dashboard layout, local detail, shared ranges, grouping, and scroll/focus retention.
use super::data;
use crate::analytics::sections::Section;
#[path = "analytics/dashboard_tests.rs"]
mod dashboard;
#[path = "analytics/account_layout_tests.rs"]
mod account_layout;
#[path = "analytics/tool_panel_tests.rs"]
mod tools_panel;
#[path = "analytics/styles_tests.rs"]
mod terminal_styles;
use super::*;
use crate::keymap::RuntimeKeymap;
use crossterm::event::KeyModifiers;
use pretty_assertions::assert_eq;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
fn press(view: &mut AnalyticsView, code: KeyCode) {
view.handle_key(KeyEvent::new(code, KeyModifiers::NONE));
}
fn screen(view: &mut AnalyticsView, width: u16, height: u16) -> String {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| view.render(frame.area(), frame.buffer_mut()))
.unwrap();
terminal.backend().to_string()
}
#[test]
fn analytics_overview() {
let mut view = fixture::view(models::AccountKind::Consumer);
press(&mut view, KeyCode::Char('z'));
let wide = screen(&mut view, /*width*/ 120, /*height*/ 60);
for section in view.visible_sections() {
assert!(wide.contains(view.section_title(*section)));
}
insta::assert_snapshot!(wide);
}
#[test]
fn analytics_narrow_charts_and_small_terminals_keep_valid_cursors() {
let mut view = fixture::view(models::AccountKind::Consumer);
press(&mut view, KeyCode::Char('z'));
for section in ['1', '2', '3'] {
press(&mut view, KeyCode::Char(section));
press(&mut view, KeyCode::Char('r'));
fixture::seed_reports(&mut view);
press(&mut view, KeyCode::Home);
for _ in 0..30 {
screen(&mut view, /*width*/ 28, /*height*/ 12);
press(&mut view, KeyCode::Right);
}
assert_eq!(view.sections[view.section].cursor, 29);
}
for _ in 0..2 {
press(&mut view, KeyCode::Char('z'));
for width in [1, 4, 32, 107, 108] {
screen(&mut view, width, /*height*/ 1);
}
}
let empty = ratatui::layout::Rect::default();
view.render(empty, &mut ratatui::buffer::Buffer::empty(empty));
press(&mut view, KeyCode::Char('4'));
let tools = screen(&mut view, /*width*/ 58, /*height*/ 20);
insta::assert_snapshot!(tools);
}
#[tokio::test]
async fn analytics_credits_grouping_preserves_selected_day_and_total() {
let server = test_support::server().await;
let (_home, _app_server, mut view) = client::tests::connected_view(&server, "business").await;
test_support::settle(&mut view).await;
press(&mut view, KeyCode::Char('1'));
press(&mut view, KeyCode::Left);
let before = view.sections[Section::Credits]
.history
.ready()
.unwrap()
.data
.iter()
.map(|day| day.total)
.collect::<Vec<_>>();
press(&mut view, KeyCode::Char('g'));
view.end_date = fixture::END_DATE;
fixture::seed_reports(&mut view);
let picker = screen(&mut view, /*width*/ 85, /*height*/ 40);
press(&mut view, KeyCode::Esc);
for group in [2, 4, 5, 0] {
press(&mut view, KeyCode::Char('g'));
press(&mut view, KeyCode::Down);
press(&mut view, KeyCode::Enter);
test_support::settle(&mut view).await;
assert_eq!(
(
view.sections[Section::Credits].group,
view.sections[Section::Credits].cursor
),
(group, 5)
);
assert_eq!(
view.sections[Section::Credits]
.history
.ready()
.unwrap()
.data
.iter()
.map(|day| day.total)
.collect::<Vec<_>>(),
before
);
}
fixture::seed_reports(&mut view);
insta::assert_snapshot!(format!(
"{picker}\n{}",
screen(&mut view, /*width*/ 85, /*height*/ 40)
));
}
#[tokio::test]
async fn analytics_loading_errors_and_empty_reports_stay_independent() {
let mut view = AnalyticsView::new(RuntimeKeymap::defaults().list);
let (send, receive) = tokio::sync::oneshot::channel();
view.sections[Section::Usage].history = Load::start(
async move { receive.await.unwrap() },
FrameRequester::test_dummy(),
);
view.sections[Section::Credits].history =
Load::Error("Couldn't load credits. Press R to retry.".into());
let mut empty = fixture::history(/*report*/ 2, /*range*/ 0, /*group*/ 2);
empty.data.clear();
view.sections[Section::Activity].history = Load::Ready(empty);
let panels = [
Section::Usage,
Section::Plugins,
Section::Credits,
Section::Activity,
]
.into_iter()
.map(|section| {
view.panel(section, /*width*/ 54, /*chart_height*/ 2)
.lines
.into_iter()
.map(|line| line.to_string())
.collect::<Vec<_>>()
.join("\n")
})
.collect::<Vec<_>>()
.join("\n\n");
insta::assert_snapshot!(panels);
// Refresh drops pending work before a stale result can populate the retained view.
view.refresh();
tokio::task::yield_now().await;
assert!(send.is_closed());
assert!(matches!(
view.sections[Section::Usage].history,
Load::Unavailable
));
}
#[test]
fn analytics_preserves_full_denominators_and_signed_small_credits() {
use crate::analytics::models::AccountAnalyticsDay;
use crate::analytics::models::AccountAnalyticsValue;
let mut view = fixture::view(models::AccountKind::Consumer);
press(&mut view, KeyCode::Char('z'));
let mut history = fixture::history(/*report*/ 0, /*range*/ 0, /*group*/ 3);
history.data = vec![AccountAnalyticsDay {
date: "2026-09-02".parse().unwrap(),
total: 100.0,
values: vec![AccountAnalyticsValue {
key: "goal".into(),
label: "Goals".into(),
value: 30.0,
}],
}];
view.sections[Section::Usage].history = Load::Ready(history.clone());
view.sections[Section::Usage].group = 3;
view.sections[Section::Usage].cursor = 6;
let task = view
.panel(Section::Usage, /*width*/ 54, /*chart_height*/ 2)
.lines;
assert!(
task.iter()
.any(|line| line.to_string().contains("Goals") && line.to_string().contains("30.0%"))
);
assert!(
task.iter()
.any(|line| line.to_string() == "Sep 2 · 100 usage units")
);
history.unit = crate::analytics::models::AccountAnalyticsUnit::Credits;
history.data[0].total = -0.000004;
history.data[0].values[0].value = -0.000004;
history.data[0].values[0].label = "CLI".into();
view.sections[Section::Credits].history = Load::Ready(history);
view.sections[Section::Credits].cursor = 6;
let credits = view
.panel(Section::Credits, /*width*/ 54, /*chart_height*/ 2)
.lines;
insta::assert_snapshot!(
task.into_iter()
.chain(credits)
.map(|line| line.to_string())
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn analytics_credit_display_retains_integer_precision() {
@@ -18,18 +226,6 @@ fn analytics_credit_display_retains_integer_precision() {
]
.map(str::to_string)
);
assert_eq!(
[0, -4, 999_999_999, 19_350_041_555, i64::MIN, i64::MAX].map(data::credits),
[
"0.00",
"-0.000004",
"1,000.00",
"19,350.04",
"-9,223,372,036,854.78",
"9,223,372,036,854.78"
]
.map(str::to_string)
);
assert_eq!(
[
12_746.441206,
@@ -53,3 +249,361 @@ fn analytics_credit_display_retains_integer_precision() {
.map(str::to_string)
);
}
#[test]
fn analytics_dates_distinguish_zero_missing_and_exact_details() {
let mut view = fixture::view(models::AccountKind::Enterprise);
let mut history = fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0);
history.data[1].total = 12_746.441206;
history.data[1].values[0].value = 12_746.441206;
history.data[1].values.truncate(/*len*/ 1);
history.data.remove(/*index*/ 2);
view.sections[Section::Credits].history = Load::Ready(history.clone());
press(&mut view, KeyCode::Char('1'));
press(&mut view, KeyCode::Home);
let mut screens = Vec::new();
for keys in [
vec![],
vec![KeyCode::Right, KeyCode::Enter],
vec![KeyCode::Right],
] {
for key in keys {
press(&mut view, key);
}
screens.push(
view.panel(Section::Credits, /*width*/ 54, /*chart_height*/ 3)
.lines
.into_iter()
.map(|line| line.to_string())
.collect::<Vec<_>>()
.join("\n"),
);
}
assert!(screens[0].contains("No activity (0)"));
assert!(screens[1].contains("12746.441206 credits"));
assert!(screens[2].contains("Aug 29 · Not reported"));
for day in &mut history.data {
day.total = 0.0;
day.values.clear();
}
view.sections[Section::Credits].history = Load::Ready(history.clone());
screens.push(
view.panel(Section::Credits, /*width*/ 54, /*chart_height*/ 3)
.lines
.into_iter()
.map(|line| line.to_string())
.collect::<Vec<_>>()
.join("\n"),
);
history.data.clear();
view.sections[Section::Credits].history = Load::Ready(history);
screens.push(
view.panel(Section::Credits, /*width*/ 54, /*chart_height*/ 3)
.lines
.into_iter()
.map(|line| line.to_string())
.collect::<Vec<_>>()
.join("\n"),
);
insta::assert_snapshot!(screens.join("\n\n"));
}
#[test]
fn analytics_model_labels_and_tool_remainders_are_unambiguous() {
let mut view = fixture::view(models::AccountKind::Consumer);
press(&mut view, KeyCode::Char('z'));
view.model_names
.insert("GPT-5.6-Sol".into(), "Friendly model".into());
let mut tools = fixture::history(/*report*/ 4, /*range*/ 0, /*group*/ 0);
for day in &mut tools.data {
day.values[1].label = "Other".into();
}
view.sections[Section::Skills].history = Load::Ready(tools);
press(&mut view, KeyCode::Char('2'));
let overview = screen(&mut view, /*width*/ 140, /*height*/ 64);
assert!(overview.contains("Friendly model"));
assert!(overview.contains("Skills used"));
press(&mut view, KeyCode::Char('4'));
press(&mut view, KeyCode::Enter);
let skills = screen(&mut view, /*width*/ 100, /*height*/ 32);
assert!(skills.contains("Other"));
insta::assert_snapshot!(format!("{overview}\n{skills}"));
}
#[test]
fn analytics_maximized_refunds_leave_room_for_both_bands() {
let mut view = fixture::view(models::AccountKind::Enterprise);
let mut history = fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0);
history.data[6].total = -0.000004;
history.data[6].values = vec![crate::analytics::models::AccountAnalyticsValue {
key: "CLI".into(),
label: "CLI".into(),
value: -0.000004,
}];
view.sections[Section::Credits].history = Load::Ready(history);
press(&mut view, KeyCode::Char('1'));
let output = screen(&mut view, /*width*/ 100, /*height*/ 32);
assert!(output.contains("Sep 2 · -0.000004 credits"));
assert!(
output
.lines()
.any(|line| line.contains("● CLI") && line.contains("-0.000004"))
);
insta::assert_snapshot!(output);
}
#[test]
fn analytics_day_navigation_keeps_geometry_and_legend() {
let mut view = fixture::view(models::AccountKind::Enterprise);
let mut history = fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0);
history.data[1].values.truncate(/*len*/ 1);
history.data[1].total = history.data[1].values[0].value;
history.data.remove(/*index*/ 2);
view.sections[Section::Credits].history = Load::Ready(history);
press(&mut view, KeyCode::Char('1'));
let mut snapshots = Vec::new();
for width in [58, 160] {
let mut terminal = Terminal::new(TestBackend::new(width, /*height*/ 38)).unwrap();
for expanded in [false, true] {
press(&mut view, KeyCode::Home);
if expanded {
press(&mut view, KeyCode::Enter);
}
let mut previous = None;
for _ in 0..7 {
terminal
.draw(|frame| view.render(frame.area(), frame.buffer_mut()))
.unwrap();
let buffer = terminal.backend().buffer();
let bars = buffer
.content
.iter()
.enumerate()
.filter(|(_, cell)| {
matches!(cell.symbol(), "█" | "▁" | "▂" | "▃" | "▄" | "▅" | "▆" | "▇")
})
.map(|(index, cell)| (index, cell.clone()))
.collect::<Vec<_>>();
let output = terminal.backend().to_string();
let marker_row = output.lines().position(|line| line.contains('▲')).unwrap();
let table_row = output
.lines()
.position(|line| line.contains("Product"))
.unwrap();
let geometry = (bars, marker_row, table_row);
if let Some(previous) = &previous {
assert_eq!(&geometry, previous);
}
previous = Some(geometry);
assert!(output.contains("● CLI") && output.contains("● Desktop app"));
if view.sections[Section::Credits].cursor == 2 && !expanded {
snapshots.push(output);
}
press(&mut view, KeyCode::Right);
}
if expanded {
press(&mut view, KeyCode::Enter);
}
}
}
insta::assert_snapshot!(snapshots.join("\n"));
}
#[test]
fn analytics_series_rank_by_range_and_keep_calendar_ticks_stable() {
let mut view = fixture::view(models::AccountKind::Consumer);
screen(&mut view, /*width*/ 100, /*height*/ 38);
press(&mut view, KeyCode::Char('r'));
let mut history = fixture::history(/*report*/ 0, /*range*/ 1, /*group*/ 2);
for day in &mut history.data {
day.values[3].value += 500.0;
day.total += 500.0;
}
view.sections[Section::Usage].history = Load::Ready(history.clone());
screen(&mut view, /*width*/ 100, /*height*/ 38);
let legend = view
.history_lines(Section::Usage, /*width*/ 96, /*height*/ 10)
.lines
.into_iter()
.filter(|line| line.spans.first().is_some_and(|span| span.content == "● "))
.take(/*n*/ 3)
.map(|line| (line.spans[1].content.to_string(), line.spans[0].style.fg))
.collect::<Vec<_>>();
assert_eq!(
legend,
["Other", "GPT-5.6-Sol", "GPT-5.6-Terra"]
.into_iter()
.zip(styles::series_colors())
.map(|(label, color)| (label.to_string(), Some(color)))
.collect::<Vec<_>>()
);
let series = render::categories(&history);
history.data[8].total = 0.4;
for value in &mut history.data[8].values {
value.value = 0.1;
}
let days = history
.data
.iter()
.map(|day| (day.date, Some(day)))
.collect::<Vec<_>>();
let mut ticks = None;
for cursor in [0, 7, 8, 25, 29] {
let chart = plot::chart(
&days,
&series,
cursor,
/*width*/ 96,
/*height*/ 10,
history.unit,
);
let labels = chart.lines.last().unwrap().to_string();
if let Some(ticks) = &ticks {
assert_eq!(&labels, ticks);
}
ticks = Some(labels);
assert_eq!(
chart
.lines
.iter()
.filter(|line| line.to_string().contains('▲'))
.count(),
1
);
}
let crowded = plot::chart(
&days,
&series,
/*cursor*/ 8,
/*width*/ 54,
/*height*/ 10,
history.unit,
)
.lines
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
assert!(crowded.contains('┊'));
assert!(crowded.contains("0.4"));
insta::assert_snapshot!(crowded);
}
#[test]
fn analytics_usage_ranks_its_own_models_after_turns_load() {
let mut view = fixture::view(models::AccountKind::Consumer);
screen(&mut view, /*width*/ 100, /*height*/ 38);
press(&mut view, KeyCode::Char('g'));
press(&mut view, KeyCode::Down);
press(&mut view, KeyCode::Enter);
let mut history = fixture::history(/*report*/ 0, /*range*/ 0, /*group*/ 2);
for day in &mut history.data {
for label in ["Model E", "Model F"] {
day.values
.push(crate::analytics::models::AccountAnalyticsValue {
key: label.into(),
label: label.into(),
value: 0.0,
});
}
for value in &mut day.values {
value.key = format!("remote-{}", value.key);
value.label = format!("Remote {}", value.label);
}
}
history.data[6].total = 100.0;
for (value, amount) in history.data[6]
.values
.iter_mut()
.zip([91.1, 0.0, 6.8, 2.1, 0.0, 0.0])
{
value.value = amount;
}
view.sections[Section::Usage].history = Load::Ready(history);
let selected_day = screen(&mut view, /*width*/ 100, /*height*/ 38);
for _ in 0..7 {
screen(&mut view, /*width*/ 100, /*height*/ 38);
let expected = if view.sections[Section::Usage].cursor == 6 {
[
("Remote GPT-5.6-Sol", 0),
("Remote GPT-5.5", 2),
("Remote Other", 3),
]
} else {
[
("Remote GPT-5.6-Sol", 0),
("Remote GPT-5.6-Terra", 1),
("Remote GPT-5.5", 2),
]
}
.into_iter()
.map(|(label, color)| (label.to_string(), Some(styles::series_colors()[color])))
.collect::<Vec<_>>();
let legend = view
.history_lines(Section::Usage, /*width*/ 96, /*height*/ 10)
.lines
.into_iter()
.filter(|line| line.spans.first().is_some_and(|span| span.content == "● "))
.take(/*n*/ 3)
.map(|line| (line.spans[1].content.to_string(), line.spans[0].style.fg))
.collect::<Vec<_>>();
assert_eq!(legend, expected);
press(&mut view, KeyCode::Left);
}
insta::assert_snapshot!(selected_day);
}
#[test]
fn analytics_empty_turns_keep_geometry_and_disable_details() {
let mut view = fixture::view(models::AccountKind::Consumer);
let mut history = fixture::history(/*report*/ 2, /*range*/ 0, /*group*/ 2);
history.data[5].total = 0.0;
for value in &mut history.data[5].values {
value.value = 0.0;
}
history.data.remove(/*index*/ 6);
view.sections[Section::Activity].history = Load::Ready(history);
press(&mut view, KeyCode::Char('2'));
let mut snapshots = Vec::new();
for width in [58, 100] {
view.sections[Section::Activity].cursor = 4;
let populated = screen(&mut view, width, /*height*/ 38);
assert!(populated.contains("reported messages"));
assert!(populated.contains("details"));
let marker_row = populated.lines().position(|line| line.contains('▲'));
for cursor in [5, 6] {
view.sections[Section::Activity].cursor = cursor;
let empty = screen(&mut view, width, /*height*/ 38);
assert!(empty.contains(if cursor == 5 {
"No messages reported for"
} else {
"Data not reported for"
}));
assert!(!empty.contains("details"));
assert!(!empty.contains('●'));
assert_eq!(
empty.lines().position(|line| line.contains('▲')),
marker_row
);
press(&mut view, KeyCode::Enter);
assert_eq!(view.sections[Section::Activity].detail, None);
snapshots.push(empty);
}
view.sections[Section::Activity].cursor = 4;
press(&mut view, KeyCode::Enter);
assert_eq!(view.sections[Section::Activity].detail, Some(4));
let expanded = screen(&mut view, width, /*height*/ 38);
let marker_row = expanded.lines().position(|line| line.contains('▲'));
press(&mut view, KeyCode::Right);
let empty = screen(&mut view, width, /*height*/ 38);
assert_eq!(
empty.lines().position(|line| line.contains('▲')),
marker_row
);
let details = view.sections[Section::Activity].detail;
press(&mut view, KeyCode::Enter);
assert_eq!(view.sections[Section::Activity].detail, details);
view.sections[Section::Activity].detail = None;
}
insta::assert_snapshot!(snapshots.join("\n"));
}
+2 -1
View File
@@ -572,8 +572,9 @@ pub(crate) struct App {
last_thread_usage_status_cell: Option<history_ui::ThreadUsageStatusHistory>,
pub(crate) pending_thread_usage_history_refresh: bool,
// Pager overlay state (Transcript or Static like Diff)
// Alternate-screen overlays: transcript, diff, and analytics.
pub(crate) overlay: Option<Overlay>,
pub(crate) retained_analytics: Option<Box<crate::analytics::AnalyticsView>>,
pub(crate) deferred_history_lines: Vec<crate::terminal_hyperlinks::HyperlinkLine>,
has_emitted_history_lines: bool,
transcript_reflow: TranscriptReflowState,
@@ -235,6 +235,12 @@ impl App {
self.agents_overview.usage_disabled = false;
self.repaint_agents_overview();
self.chat_widget.cyber_policy_notice = Default::default();
if let Some(crate::pager_overlay::Overlay::Analytics(view)) = &mut self.overlay {
view.refresh();
}
if let Some(view) = &mut self.retained_analytics {
view.cancel_loads();
}
self.rate_limit_hard_stop_generation =
self.rate_limit_hard_stop_generation.wrapping_add(1);
self.rate_limit_refresh_state.invalidate_recovery();
+15
View File
@@ -1609,6 +1609,21 @@ impl App {
self.chat_widget
.add_token_activity_output(crate::chatwidget::TokenActivityView::Daily);
}
AppEvent::OpenAnalytics => {
tui.enter_alt_screen()?;
let mut view = self.retained_analytics.take().unwrap_or_else(|| {
Box::new(crate::analytics::AnalyticsView::new(self.keymap.list.clone()))
});
view.keymap = self.keymap.list.clone();
view.open(
app_server.request_handle(),
tui.frame_requester(),
self.model_catalog.try_list_models()?,
std::sync::Arc::new(self.config.clone()),
);
self.overlay = Some(Overlay::Analytics(view));
tui.frame_requester().schedule_frame();
}
AppEvent::OpenRateLimitResetCredits => {
let request_id = self.chat_widget.show_rate_limit_reset_loading_popup();
self.refresh_rate_limits(
+1
View File
@@ -755,6 +755,7 @@ See the Codex keymap documentation for supported actions and examples."
last_thread_usage_status_cell: None,
pending_thread_usage_history_refresh: false,
overlay: None,
retained_analytics: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
+1
View File
@@ -41,6 +41,7 @@ pub(super) async fn make_test_app() -> App {
last_thread_usage_status_cell: None,
pending_thread_usage_history_refresh: false,
overlay: None,
retained_analytics: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
+2
View File
@@ -5888,6 +5888,7 @@ async fn make_test_app() -> App {
last_thread_usage_status_cell: None,
pending_thread_usage_history_refresh: false,
overlay: None,
retained_analytics: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
@@ -5987,6 +5988,7 @@ pub(super) async fn make_test_app_with_channels() -> (
last_thread_usage_status_cell: None,
pending_thread_usage_history_refresh: false,
overlay: None,
retained_analytics: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
@@ -1,6 +1,7 @@
//! Regression coverage for transcript viewer input, prompt selection, and restoration.
//!
//! The default-off feature must leave the existing viewer and its draft intact.
//! Analytics also preserves the composer and stays separate from transcript backtracking.
use super::*;
use crate::test_support::test_path_display;
@@ -39,6 +40,40 @@ async fn press_key(
Ok(())
}
#[tokio::test]
async fn analytics_preserves_the_draft_and_retains_the_view_on_reopen() -> Result<()> {
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
let mut app_server = start_config_write_test_app_server(&app).await?;
let mut tui = crate::tui::test_support::make_test_tui()?;
app.chat_widget
.apply_external_edit("preserved draft".into());
app.handle_event(&mut tui, &mut app_server, AppEvent::OpenAnalytics)
.await?;
for code in [
KeyCode::Char('4'),
KeyCode::Enter,
KeyCode::Down,
KeyCode::Char('r'),
] {
press_key(&mut app, &mut tui, &mut app_server, code).await?;
assert!(matches!(app.overlay, Some(Overlay::Analytics(_))));
assert!(!app.backtrack.overlay_preview_active);
}
press_key(&mut app, &mut tui, &mut app_server, KeyCode::Char('q')).await?;
assert!(app.overlay.is_none());
assert!(app.retained_analytics.is_some());
app.handle_event(&mut tui, &mut app_server, AppEvent::OpenAnalytics)
.await?;
assert!(matches!(app.overlay, Some(Overlay::Analytics(_))));
assert!(app.retained_analytics.is_none());
press_key(&mut app, &mut tui, &mut app_server, KeyCode::Char('q')).await?;
assert_eq!(
app.chat_widget.composer_text_with_pending(),
"preserved draft"
);
Ok(())
}
#[tokio::test]
async fn transcript_flag_off_preserves_viewer_and_backtracking() -> Result<()> {
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
+10 -3
View File
@@ -84,7 +84,7 @@ pub(crate) struct BacktrackSelection {
}
impl App {
/// Route overlay events while the transcript overlay is active.
/// Route overlay events, reserving backtracking for the transcript overlay.
///
/// If backtrack preview is active, Esc / Left steps selection, Right steps forward, Enter
/// confirms. Otherwise, Esc begins preview mode and all other events are forwarded to the
@@ -95,6 +95,10 @@ impl App {
app_server: &mut AppServerSession,
event: TuiEvent,
) -> Result<bool> {
if !matches!(self.overlay, Some(Overlay::Transcript(_))) {
self.overlay_forward_event(tui, event)?;
return Ok(true);
}
self.handle_legacy_transcript_event(tui, app_server, event)
}
@@ -159,7 +163,7 @@ impl App {
tui.frame_requester().schedule_frame();
}
/// Close transcript overlay and restore normal UI.
/// Close the current overlay and restore normal UI, retaining Analytics navigation state.
pub(crate) fn close_transcript_overlay(&mut self, tui: &mut tui::Tui) {
let _ = tui.leave_alt_screen();
let was_backtrack = self.backtrack.overlay_preview_active;
@@ -170,7 +174,10 @@ impl App {
self.history_line_wrap_policy(),
);
}
self.overlay = None;
if let Some(Overlay::Analytics(mut view)) = self.overlay.take() {
view.cancel_loads();
self.retained_analytics = Some(view);
}
if self.pending_thread_usage_history_refresh
&& let Err(err) = self.refresh_thread_usage_history_tail(tui)
{
+3
View File
@@ -681,6 +681,9 @@ pub(crate) enum AppEvent {
/// Open the default token-activity view selected from the `/usage` menu.
OpenTokenActivity,
/// Open the authenticated account analytics dashboard.
OpenAnalytics,
/// Open the reset-credit flow selected from the `/usage` menu.
OpenRateLimitResetCredits,
@@ -6,6 +6,7 @@ expression: "render_bottom_popup(&chat, 80)"
View account usage or redeem an earned reset.
› 1. Show usage View recent account token usage.
2. Redeem usage limit reset You have 2 usage limit resets available.
2. Explore analytics Browse account usage and activity.
3. Redeem usage limit reset You have 2 usage limit resets available.
Press enter to confirm or esc to go back
@@ -6,6 +6,7 @@ expression: "render_bottom_popup(&chat, 80)"
View account usage or redeem an earned reset.
› 1. Show usage View recent account token usage.
2. Redeem usage limit reset Check reset availability.
2. Explore analytics Browse account usage and activity.
3. Redeem usage limit reset Check reset availability.
Press enter to confirm or esc to go back
@@ -6,6 +6,7 @@ expression: "render_bottom_popup(&chat, 80)"
View account usage or redeem an earned reset.
› 1. Show usage View recent account token usage.
2. Explore analytics Browse account usage and activity.
Redeem usage limit reset No usage limit resets available.
Press enter to confirm or esc to go back
+17 -3
View File
@@ -13,6 +13,16 @@ use uuid::Uuid;
const TEST_OVERLAY_VIEW_ID: &str = "usage-test-overlay";
#[tokio::test]
async fn usage_menu_opens_analytics() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
chat.dispatch_command(SlashCommand::Usage);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics));
}
fn reset_credits(available_count: i64) -> RateLimitResetCreditsSummary {
RateLimitResetCreditsSummary {
available_count,
@@ -148,7 +158,7 @@ async fn usage_command_disables_reset_after_cached_zero_snapshot() {
);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics));
}
#[tokio::test]
@@ -175,6 +185,7 @@ async fn usage_menu_refresh_enables_newly_available_reset() {
Ok(reset_credits(/*available_count*/ 1)),
);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));
@@ -207,7 +218,7 @@ async fn usage_menu_refresh_failure_preserves_disabled_known_zero() {
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("No usage limit resets available."));
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics));
}
#[tokio::test]
@@ -256,6 +267,7 @@ async fn usage_command_can_check_reset_availability_before_startup_refresh_finis
render_bottom_popup(&chat, /*width*/ 80)
);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));
}
@@ -268,6 +280,7 @@ async fn usage_command_can_check_reset_availability_for_workspace_accounts() {
chat.dispatch_command(SlashCommand::Usage);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));
@@ -285,6 +298,7 @@ async fn usage_menu_rate_limit_reset_entry_opens_reset_flow() {
));
chat.dispatch_command(SlashCommand::Usage);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
@@ -703,7 +717,7 @@ async fn no_credit_outcome_disables_reset_entry_in_usage_menu() {
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAnalytics));
chat.available_rate_limit_reset_credits = Some(2);
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
+14 -1
View File
@@ -45,7 +45,7 @@ impl ChatWidget {
(false, "No usage limit resets available.".to_string())
}
};
SelectionViewParams {
let mut params = SelectionViewParams {
view_id: Some(USAGE_MENU_VIEW_ID),
title: Some("Usage".to_string()),
subtitle: Some("View account usage or redeem an earned reset.".to_string()),
@@ -72,7 +72,20 @@ impl ChatWidget {
},
],
..Default::default()
};
if self.has_chatgpt_account {
params.items.insert(
/*index*/ 1,
SelectionItem {
name: "Explore analytics".to_string(),
description: Some("Browse account usage and activity.".to_string()),
actions: vec![Box::new(|tx| tx.send(AppEvent::OpenAnalytics))],
dismiss_on_select: true,
..Default::default()
},
);
}
params
}
pub(crate) fn finish_usage_menu_rate_limit_refresh(
-5
View File
@@ -102,11 +102,6 @@ pub(crate) use codex_app_server_client::legacy_core;
pub(crate) use worktree_startup::ManagedTuiWorktree;
mod additional_dirs;
// Removed when the final stack layer connects Analytics to /usage.
#[expect(
dead_code,
reason = "analytics is being introduced in independently tested stack layers"
)]
mod analytics;
mod app;
mod app_backtrack;
+3
View File
@@ -58,6 +58,7 @@ use scrolling::render_offset_content;
pub(crate) enum Overlay {
Transcript(TranscriptOverlay),
Static(StaticOverlay),
Analytics(Box<crate::analytics::AnalyticsView>),
}
impl Overlay {
@@ -85,6 +86,7 @@ impl Overlay {
match self {
Overlay::Transcript(o) => o.handle_event(tui, event),
Overlay::Static(o) => o.handle_event(tui, event),
Overlay::Analytics(o) => o.handle_event(tui, event),
}
}
@@ -92,6 +94,7 @@ impl Overlay {
match self {
Overlay::Transcript(o) => o.is_done(),
Overlay::Static(o) => o.is_done(),
Overlay::Analytics(o) => o.is_done,
}
}
}
@@ -0,0 +1,85 @@
---
source: tui/src/analytics_tests.rs
expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))"
---
" Analytics · Live account · analytics@example.test · account-a "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
" "
" ▎ Credits usage history By product "
" ───────────────────────────────────────────────────────────────────────────────── "
" › Product "
" Model "
" Speed "
" Reasoning "
" 431.00 credits "
" "
" 150 "
" "
" 109.00 "
" ▁▁▁▁▁▁▁ "
" ███████ "
" ███████ "
" ▇▇▇▇▇▇▇ ███████ "
" 75 ███████ ███████ ███████ "
" ▂▂▂▂▂▂▂ ███████ ███████ ███████ ▇▇▇▇▇▇▇ "
" ███████ ███████ ███████ ███████ ███████ "
" ▆▆▆▆▆▆▆ ███████ ███████ ███████ ███████ ███████ "
" ███████ ███████ ███████ ███████ ███████ ███████ "
" ███████ ███████ ███████ ███████ ███████ ███████ "
" 0 ███████ ███████ ███████ ███████ ███████ ███████ "
" 0 ──────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 1 · 109.00 credits "
" Product Credits "
" ● CLI 39.00 "
" ● Desktop app 38.00 "
" ● IDE extension 18.00 "
" ● Web 14.00 "
" "
" ↑/↓ group · enter select · esc cancel "
" Analytics · Live account · analytics@example.test · account-a "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
" "
" ▎ Credits usage history By product "
" ───────────────────────────────────────────────────────────────────────────────── "
" 431.00 credits "
" "
" 150 "
" "
" "
" 109.00 "
" ▂▂▂▂▂▂▂ "
" ███████ "
" ███████ "
" ▅▅▅▅▅▅▅ ███████ "
" 75 ███████ ▄▄▄▄▄▄▄ ███████ "
" ███████ ███████ ███████ ▂▂▂▂▂▂▂ "
" ▃▃▃▃▃▃▃ ███████ ███████ ███████ ███████ "
" ███████ ███████ ███████ ███████ ███████ "
" ▅▅▅▅▅▅▅ ███████ ███████ ███████ ███████ ███████ "
" ███████ ███████ ███████ ███████ ███████ ███████ "
" ███████ ███████ ███████ ███████ ███████ ███████ "
" ███████ ███████ ███████ ███████ ███████ ███████ "
" 0 ███████ ███████ ███████ ███████ ███████ ███████ "
" 0 ──────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 1 · 109.00 credits "
" Product Credits "
" ● CLI 39.00 "
" ● Desktop app 38.00 "
" ● IDE extension 18.00 "
" ● Web 14.00 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,73 @@
---
source: tui/src/analytics_tests.rs
expression: "screens.join(\"\\n\\n\")"
---
▎ Credits usage history By product
──────────────────────────────────────────────────────
13,079.44 credits
15,000 ▄▄▄▄
████
0.00 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁
0 ──────────────────────────────────────────────
▲
27 28 29 30 31 1 2
──────────────────────────────────────────────────────
Aug 27 · No activity (0)
Product Credits
● Desktop app 0.00
● CLI 0.00
● IDE extension 0.00
● Web 0.00
▎ Credits usage history By product
──────────────────────────────────────────────────────
13,079.44 credits
12,746.44
15,000 ▄▄▄▄
████
0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁
0 ──────────────────────────────────────────────
▲
27 28 29 30 31 1 2
──────────────────────────────────────────────────────
Aug 28 · 12746.441206 credits
Product Credits
● Desktop app 12746.441206
● CLI 0
● IDE extension 0
● Web 0
▎ Credits usage history By product
──────────────────────────────────────────────────────
13,079.44 credits
15,000 ▄▄▄▄
████
0 ████ — ▁▁▁▁ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁
0 ──────────────────────────────────────────────
▲
27 28 29 30 31 1 2
──────────────────────────────────────────────────────
Aug 29 · Not reported
Product Credits
● Desktop app —
● CLI —
● IDE extension —
● Web —
▎ Credits usage history By product
──────────────────────────────────────────────────────
0.00 credits
No activity reported in this range.
──────────────────────────────────────────────────────
Aug 29 · Not reported
Product Credits
▎ Credits usage history By product
──────────────────────────────────────────────────────
No data reported for this range.
@@ -0,0 +1,81 @@
---
source: tui/src/analytics_tests.rs
expression: "snapshots.join(\"\\n\")"
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [Credits usage history] Token usage history Plugins "
" called Skills used "
" "
" ▎ Credits usage history By product "
" ────────────────────────────────────────────────────── "
" 347.00 credits "
" "
" 150 "
" "
" "
" ▅▅▅▅ "
" ████ "
" ▆▆▆▆ ████ "
" 75 ████ ████ ████ "
" ████ ████ ████ ████ "
" ████ ████ ████ ████ "
" ████ ████ ████ ████ "
" ████ ████ ████ ████ "
" 0 ████ — ████ ████ ████ ████ "
" 0 ───────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ────────────────────────────────────────────────────── "
" Aug 29 · Not reported "
" Product Credits "
" ● Desktop app — "
" ● CLI — "
" ● IDE extension — "
" ● Web — "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter "
" details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q "
" close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
" "
" ▎ Credits usage history By product "
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" 347.00 credits "
" "
" 150 "
" "
" "
" "
" ▇▇▇▇▇▇▇▇▇▇▇▇ "
" ████████████ "
" ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ "
" 75 ████████████ ▄▄▄▄▄▄▄▄▄▄▄▄ ████████████ "
" ████████████ ████████████ ████████████ ▃▃▃▃▃▃▃▃▃▃▃▃ "
" ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ "
" ████████████ ████████████ ████████████ ████████████ "
" ▃▃▃▃▃▃▃▃▃▃▃▃ ████████████ ████████████ ████████████ ████████████ "
" 0 ████████████ — ████████████ ████████████ ████████████ ████████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Aug 29 · Not reported "
" Product Credits "
" ● Desktop app — "
" ● CLI — "
" ● IDE extension — "
" ● Web — "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,159 @@
---
source: tui/src/analytics_tests.rs
expression: "snapshots.join(\"\\n\")"
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" Total usage history [Messages] Plugins called "
" Skills used "
" "
" ▎ Messages By model "
" ────────────────────────────────────────────────────── "
" 386 reported messages "
" "
" 150 "
" "
" "
" ▄▄▄▄ "
" ████ "
" ▅▅▅▅ ████ "
" 75 ████ ████ ████ "
" ▇▇▇▇ ████ ████ ████ ▅▅▅▅ "
" ████ ████ ████ ████ ████ "
" ████ ████ ████ ████ ████ "
" ████ ████ ████ ████ ████ "
" ████ ████ ████ ████ ████ 0 — "
" 0 ───────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ────────────────────────────────────────────────────── "
" No messages reported for Sep 1. "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · z dashboard · ←/→ day "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc "
" back · q close "
" "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" Total usage history [Messages] Plugins called "
" Skills used "
" "
" ▎ Messages By model "
" ────────────────────────────────────────────────────── "
" 386 reported messages "
" "
" 150 "
" "
" "
" ▄▄▄▄ "
" ████ "
" ▅▅▅▅ ████ "
" 75 ████ ████ ████ "
" ▇▇▇▇ ████ ████ ████ ▅▅▅▅ "
" ████ ████ ████ ████ ████ "
" ████ ████ ████ ████ ████ "
" ████ ████ ████ ████ ████ "
" ████ ████ ████ ████ ████ 0 — "
" 0 ───────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ────────────────────────────────────────────────────── "
" Data not reported for Sep 2. "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · z dashboard · ←/→ day "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc "
" back · q close "
" "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
" "
" ▎ Messages By model "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 386 reported messages "
" "
" 150 "
" "
" "
" "
" ▅▅▅▅▅▅▅▅ "
" ████████ "
" ▂▂▂▂▂▂▂▂ ████████ "
" 75 ████████ ████████ ▄▄▄▄▄▄▄▄ "
" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ▇▇▇▇▇▇▇▇ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ 0 — "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" No messages reported for Sep 1. "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · z dashboard · ←/→ day "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
" "
" ▎ Messages By model "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 386 reported messages "
" "
" 150 "
" "
" "
" "
" ▅▅▅▅▅▅▅▅ "
" ████████ "
" ▂▂▂▂▂▂▂▂ ████████ "
" 75 ████████ ████████ ▄▄▄▄▄▄▄▄ "
" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ▇▇▇▇▇▇▇▇ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ 0 — "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Data not reported for Sep 2. "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · z dashboard · ←/→ day "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,19 @@
---
source: tui/src/analytics_tests.rs
expression: panels
---
▎ Total usage history By feature
──────────────────────────────────────────────────────
Loading…
Plugins called
──────────────────────────────────────────────────────
No history has been reported.
Credits usage history By product
──────────────────────────────────────────────────────
Couldn't load credits. Press R to retry.
Messages By model
──────────────────────────────────────────────────────
No data reported for this range.
@@ -0,0 +1,36 @@
---
source: tui/src/analytics_tests.rs
expression: output
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
" "
" ▎ Credits usage history By product "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 367.00 credits "
" "
" 150 "
" ▂▂▂▂▂▂▂▂ ▇▇▇▇▇▇▇▇ "
" 75 ▄▄▄▄▄▄▄▄ ████████ ████████ ████████ "
" 0 ████████ ████████ ████████ ████████ ████████ "
" 0 ────────────────────────────────────────────────────────────────────────────────────────── "
" ▇▇▇▇▇▇▇▇ "
" -0.000004 "
" "
" -150 "
" "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · -0.000004 credits "
" Product Credits "
" ● CLI -0.000004 "
" ● Desktop app 0.00 "
" ● IDE extension 0.00 "
" ● Web 0.00 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,101 @@
---
source: tui/src/analytics_tests.rs
expression: "format!(\"{overview}\\n{skills}\")"
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" ╭─ 1 Total usage history ──────────────────────────────────────╮ ╭─ ▸ 2 Messages ─────────────────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ By feature │ │ By model │ "
" │ Total usage · relative units │ │ 574 reported messages │ "
" │ 86 │ │ │ "
" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ "
" │ █████ ▅▅▅▅▅ █████ ██████ │ │ 103 │ "
" │ █████ ▃▃▃▃▃ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ ▆▆▆▆▆▆ │ "
" │ 50 █████ █████ █████ █████ █████ ██████ │ │ 75 ▆▆▆▆▆ █████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ "
" │ █████ █████ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ "
" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
" │ █████ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ───────────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ "
" │ ● Tasks 64.0% │ │ ● Friendly model 61 │ "
" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
" "
" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ 4 Skills used ──────────────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ 186 calls │ │ 54 uses │ "
" │ │ │ │ "
" │ 40 29 │ │ 20 │ "
" │ ▇▇▇▇▇ │ │ │ "
" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ "
" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ "
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ "
" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
" │ │ │ │ "
" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ "
" │ ● Slack 10 │ │ ● Testing 4 │ "
" │ ● Figma 10 │ │ ● Code review 3 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · enter/z maximize "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
" "
" ▎ Skills used "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" 54 uses "
" "
" 20 "
" "
" "
" 11 "
" 10 ▇▇▇▇▇▇▇▇▇ "
" ▄▄▄▄▄▄▄▄ ████████ ▄▄▄▄▄▄▄▄ ▁▁▁▁▁▁▁▁ █████████ "
" ████████ ▅▅▅▅▅▅▅▅ ████████ ████████ ████████ ▂▂▂▂▂▂▂▂ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ █████████ "
" 0 ──────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 11 uses "
" Skill Uses "
" ● Testing 4 "
" ● Code review 3 "
" ● Other 3 "
" ● Debugging 1 "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,24 @@
---
source: tui/src/analytics_tests.rs
expression: tools
---
" Analytics · Live account "
" 7d [30d] · Aug 4–Sep 2, 2026 "
" "
" ╭─ ▸ 4 Skills used ──────────────────────────────────╮ "
" │ │ "
" │ 30d │ "
" │ 169 uses │ "
" │ 11 │ "
" │ 20 ▂▂ ▂▂ ▂▂ ▂▂ ▂▂ ▂▂ ▂▂ ▂ ▂▂ ▂▂ ▂▂ ▂▂ ▃▂ ▃▃ ▂▂ ▄ │ "
" │ 0 ────────────────────────────────────────────── │ "
" │ ▲ │ "
" │ Aug 4 Aug 11 Aug 18 Aug 25 Sep 2 │ "
" │ │ "
" │ Sep 2 · 11 uses │ "
" │ ● Testing 4 │ "
" │ ● Code review 3 │ "
" │ │ "
" tab/1–4 section · enter/z maximize "
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q "
" close "
@@ -0,0 +1,64 @@
---
source: tui/src/analytics_tests.rs
expression: wide
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" ╭─ ▸ 1 Total usage history ────────────────────────────╮ ╭─ 2 Messages ───────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ By feature │ │ By model │ "
" │ Total usage · relative units │ │ 574 reported messages │ "
" │ 86 │ │ │ "
" │ 100 ▂▂▂▂ ████ │ │ 150 │ "
" │ ████ ▅▅▅▅ ████ ████ │ │ 103 │ "
" │ ████ ▃▃▃▃ ████ ████ ████ ████ │ │ ▇▇▇▇ ▆▆▆▆ │ "
" │ 50 ████ ████ ████ ████ ████ ████ │ │ 75 ▆▆▆▆ ████ ▄▄▄▄ ▇▇▇▇ ████ │ "
" │ ████ ████ ████ ████ ████ ████ │ │ ▇▇▇▇ ████ ████ ████ ▆▆▆▆ ████ ████ │ "
" │ ▃▃▃▃ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ "
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ "
" │ 0 ─────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ "
" │ │ │ │ "
" │ Sep 2 · 86 usage units │ │ Sep 2 · 103 messages │ "
" │ ● Tasks 64.0% │ │ ● GPT-5.6-Sol 61 │ "
" │ ● Code review 14.0% │ │ ● GPT-5.6-Terra 25 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ "
" "
" ╭─ 3 Plugins called ─────────────────────────────────╮ ╭─ 4 Skills used ────────────────────────────────────╮ "
" │ │ │ │ "
" │ 7d │ │ 7d │ "
" │ 186 calls │ │ 54 uses │ "
" │ │ │ │ "
" │ 40 29 │ │ 20 │ "
" │ ▇▇▇▇ │ │ │ "
" │ ████ ████ ▄▄▄▄ ████ │ │ 11 │ "
" │ 20 ████ ████ ████ ████ ▆▆▆▆ ████ ████ │ │ 10 ▁▁▁▁ ▆▆▆▆ │ "
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ▆▆▆▆ ████ ▆▆▆▆ ▃▃▃▃ ████ │ "
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ▆▆▆▆ ████ │ "
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ "
" │ 0 ──────────────────────────────────────────────── │ │ 0 ──────────────────────────────────────────────── │ "
" │ ▲ │ │ ▲ │ "
" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ "
" │ │ │ │ "
" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ "
" │ ● Slack 10 │ │ ● Testing 4 │ "
" │ ● Figma 10 │ │ ● Code review 3 │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" │ │ │ │ "
" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ "
" "
" "
" "
" "
" "
" "
" tab/1–4 section · enter/z maximize "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
@@ -0,0 +1,32 @@
---
source: tui/src/analytics_tests.rs
expression: "task.into_iter().chain(credits).map(|line|\nline.to_string()).collect::<Vec<_>>().join(\"\\n\")"
---
▎ Total usage history By turn start
──────────────────────────────────────────────────────
Total usage · relative units
All usage by how each turn started
100 100
— — — — — — ▄▄▄▄
0 ─────────────────────────────────────────────────
▲
Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2
Sep 2 · 100 usage units
● Goals 30.0%
Credits usage history By product
──────────────────────────────────────────────────────
-0.000004 credits
0.000004
— — — — — —
0 ───────────────────────────────────────────
████
-0.000004 ████
-0.000004
▲
27 28 29 30 31 1 2
Sep 2 · -0.000004 credits
● CLI -0.000004
@@ -0,0 +1,18 @@
---
source: tui/src/analytics_tests.rs
expression: crowded
---
600 0.4 ▄▃ ▆ ▅█ ▆
██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ ▅ ██ █ ██ █
██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
300 ██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
██ ██ █ ██ █ ┊█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
██ ██ █ ██ █ ▁█ ██ █ ██ █ ██ █ ██ ██ █ ██ █ ██ █
0 ─────────────────────────────────────────────────
▲
Aug 4 Aug 11 Aug 18 Aug 25 Sep 2
@@ -0,0 +1,42 @@
---
source: tui/src/analytics_tests.rs
expression: selected_day
---
" Analytics · Live account "
" [7d] 30d · Aug 27–Sep 2, 2026 "
" "
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used "
" "
" ▎ Total usage history By model "
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
" Total usage · relative units "
" Approximate · may be delayed up to 6 hours "
" 100 "
" 100 ████████ ▇▇▇▇▇▇▇▇ "
" ▅▅▅▅▅▅▅▅ ████████ ████████ "
" ████████ ▂▂▂▂▂▂▂▂ ████████ ████████ "
" ▁▁▁▁▁▁▁▁ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ "
" ████████ ▇▇▇▇▇▇▇▇ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" 50 ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Sep 2 · 100 usage units "
" Model Share "
" ● Remote GPT-5.6-Sol 91.1% "
" ● Remote GPT-5.5 6.8% "
" ● Remote Other 2.1% "
" … 3 more 0.0% "
" "
" tab/1–4 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "