mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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 "
|
||||
+381
@@ -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 "
|
||||
+28
@@ -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 "
|
||||
+65
@@ -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 "
|
||||
+117
@@ -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 "
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+102
@@ -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
|
||||
+138
@@ -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 "
|
||||
+85
@@ -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 "
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -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 "
|
||||
+73
@@ -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.
|
||||
+81
@@ -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 "
|
||||
+159
@@ -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 "
|
||||
+19
@@ -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.
|
||||
+36
@@ -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 "
|
||||
+101
@@ -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 "
|
||||
+24
@@ -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 "
|
||||
+32
@@ -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
|
||||
+18
@@ -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
|
||||
+42
@@ -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 "
|
||||
Reference in New Issue
Block a user