mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
Add consumer Top chats usage analytics (#45768)
## Why Consumer analytics lacked a per-chat view of allowance usage and balance credit debits. ## What changed - Add a Top chats panel for up to 100 local chats active in the past 30 days, including discovered descendants and excluding archived roots. - Show weekly and five-hour usage as percentages of current full limits, plus balance credits including adjustments. Preserve exact credit decimals and distinguish missing values from zero. - Support cycling sort metrics with `s`, a top-five dashboard summary, and expandable model, reasoning effort, and speed breakdowns. - Query task usage through `query_v2` with bounded, disjoint descendant groups. Retain partial and unavailable rows, hide titles when usage is unavailable, and report ranking coverage and freshness. ## Testing Add backend and TUI tests covering decimal precision, request and response validation, paginated archived descendants, unavailable responses, metric selection, title hiding, and top-five rendering. GitOrigin-RevId: b8f233166576389c8212bb97eceb7c6be7f5f51d
This commit is contained in:
@@ -38,6 +38,7 @@ pub(crate) mod analytics;
|
||||
mod chatgpt_turn_cost;
|
||||
pub(crate) mod plan_history;
|
||||
mod rate_limit_resets;
|
||||
pub(crate) mod task_usage;
|
||||
mod thread_usage;
|
||||
pub(crate) mod turn_usage;
|
||||
|
||||
|
||||
229
codex-rs/backend-client/src/client/task_usage.rs
Normal file
229
codex-rs/backend-client/src/client/task_usage.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
//! Consumer task accounting: current allowance percentages and actual balance debits.
|
||||
//! This contract is deliberately separate from enterprise estimated lifetime credits.
|
||||
|
||||
use super::Client;
|
||||
use super::PathStyle;
|
||||
use super::RequestError;
|
||||
use http::Method;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct TaskUsageThread {
|
||||
pub thread_id: String,
|
||||
pub created_at: Option<String>,
|
||||
pub descendant_thread_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TaskUsageResponse {
|
||||
pub data_as_of: Option<String>,
|
||||
pub threads: Vec<TaskUsage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskUsageStatus {
|
||||
Available,
|
||||
Partial,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TaskUsage {
|
||||
pub thread_id: String,
|
||||
pub data_status: TaskUsageStatus,
|
||||
pub usage_source: String,
|
||||
#[serde(flatten)]
|
||||
pub amounts: TaskUsageAmounts,
|
||||
pub groups: Vec<TaskUsageGroup>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TaskUsageAmounts {
|
||||
#[serde(default, deserialize_with = "percentage")]
|
||||
pub five_hour_limit_percent: Option<f64>,
|
||||
#[serde(default, deserialize_with = "percentage")]
|
||||
pub weekly_limit_percent: Option<f64>,
|
||||
pub balance_usage_credits: Option<TaskCredits>,
|
||||
}
|
||||
|
||||
// Flattened Serde fields buffer arbitrary-precision JSON numbers as maps.
|
||||
// Decode through Number so decimal percentages work with either serde_json configuration.
|
||||
fn percentage<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<f64>, D::Error> {
|
||||
Option::<serde_json::Number>::deserialize(deserializer)?
|
||||
.map(|value| {
|
||||
value
|
||||
.as_f64()
|
||||
.filter(|value| value.is_finite())
|
||||
.ok_or_else(|| serde::de::Error::custom("Invalid task percentage."))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TaskUsageGroup {
|
||||
pub product_experience: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub reasoning_effort: Option<String>,
|
||||
pub speed: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub amounts: TaskUsageAmounts,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Queries disjoint tasks, including legacy descendants, without selecting a comparison plan.
|
||||
pub async fn get_task_usage(
|
||||
&self,
|
||||
threads: &[TaskUsageThread],
|
||||
) -> Result<TaskUsageResponse, RequestError> {
|
||||
let mut ids = HashSet::new();
|
||||
if threads.is_empty()
|
||||
|| threads.len() > 100
|
||||
|| threads.iter().any(|thread| {
|
||||
std::iter::once(&thread.thread_id)
|
||||
.chain(&thread.descendant_thread_ids)
|
||||
.any(|id| id.trim().is_empty() || id.len() > 512 || !ids.insert(id.as_str()))
|
||||
})
|
||||
|| ids.len() > 1_000
|
||||
{
|
||||
return Err(RequestError::Other(anyhow::anyhow!(
|
||||
"Expected at most 100 disjoint tasks and 1,000 thread IDs."
|
||||
)));
|
||||
}
|
||||
let prefix = match self.path_style {
|
||||
PathStyle::CodexApi => "api/codex",
|
||||
PathStyle::ChatGptApi => "wham",
|
||||
};
|
||||
let url = format!("{}/{prefix}/usage/thread_usage/query_v2", self.base_url);
|
||||
#[derive(Serialize)]
|
||||
struct Query<'a> {
|
||||
threads: &'a [TaskUsageThread],
|
||||
}
|
||||
let request = self
|
||||
.request(Method::POST, &url)
|
||||
.headers(self.headers())
|
||||
.json(&Query { threads });
|
||||
let (body, _) = self.exec_request_detailed(request, "POST", &url).await?;
|
||||
let response: TaskUsageResponse = serde_json::from_str(&body)
|
||||
.map_err(|_| RequestError::Other(anyhow::anyhow!("Invalid task usage response.")))?;
|
||||
let roots: HashSet<_> = threads
|
||||
.iter()
|
||||
.map(|thread| thread.thread_id.as_str())
|
||||
.collect();
|
||||
let mut seen = HashSet::new();
|
||||
if response.threads.iter().any(|thread| {
|
||||
!roots.contains(thread.thread_id.as_str())
|
||||
|| !seen.insert(&thread.thread_id)
|
||||
|| std::iter::once(&thread.amounts)
|
||||
.chain(thread.groups.iter().map(|group| &group.amounts))
|
||||
.any(|amounts| {
|
||||
[
|
||||
amounts.five_hour_limit_percent,
|
||||
amounts.weekly_limit_percent,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|value| !value.is_finite())
|
||||
})
|
||||
}) {
|
||||
return Err(RequestError::Other(anyhow::anyhow!(
|
||||
"Invalid task usage response."
|
||||
)));
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decimal strings retain all backend digits for both rendering and sorting.
|
||||
#[derive(Clone, Debug, Eq)]
|
||||
pub struct TaskCredits(String);
|
||||
|
||||
impl TaskCredits {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn parts(&self) -> (bool, i64, String) {
|
||||
let (mantissa, exponent) = self.0.split_once(['e', 'E']).unwrap_or((&self.0, "0"));
|
||||
let negative = mantissa.starts_with('-');
|
||||
let unsigned = mantissa.trim_start_matches(['-', '+']);
|
||||
let fraction = unsigned
|
||||
.split_once('.')
|
||||
.map_or(/*default*/ 0, |(_, fraction)| fraction.len());
|
||||
let digits = unsigned.replace('.', "");
|
||||
let digits = digits.trim_start_matches('0');
|
||||
let power =
|
||||
exponent.parse::<i64>().unwrap_or_default() - fraction as i64 + digits.len() as i64;
|
||||
(negative && !digits.is_empty(), power, digits.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TaskCredits {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let value = String::deserialize(deserializer)?;
|
||||
let mantissa = value
|
||||
.split(['e', 'E'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim_start_matches(['-', '+']);
|
||||
let exponent = value
|
||||
.split_once(['e', 'E'])
|
||||
.map_or("0", |(_, exponent)| exponent);
|
||||
if value.len() > 128
|
||||
|| mantissa.is_empty()
|
||||
|| !mantissa.chars().any(|ch| ch.is_ascii_digit())
|
||||
|| !mantissa.chars().all(|ch| ch.is_ascii_digit() || ch == '.')
|
||||
|| value.parse::<f64>().is_err()
|
||||
|| exponent.parse::<i32>().is_err()
|
||||
{
|
||||
return Err(serde::de::Error::custom("Invalid decimal credits."));
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TaskCredits {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let (negative, power, digits) = self.parts();
|
||||
let (other_negative, other_power, other_digits) = other.parts();
|
||||
let magnitude = match (digits.is_empty(), other_digits.is_empty()) {
|
||||
(true, true) => std::cmp::Ordering::Equal,
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
(false, false) => power.cmp(&other_power).then_with(|| {
|
||||
let width = digits.len().max(other_digits.len());
|
||||
digits
|
||||
.bytes()
|
||||
.chain(std::iter::repeat(b'0'))
|
||||
.take(width)
|
||||
.cmp(
|
||||
other_digits
|
||||
.bytes()
|
||||
.chain(std::iter::repeat(b'0'))
|
||||
.take(width),
|
||||
)
|
||||
}),
|
||||
};
|
||||
other_negative.cmp(&negative).then(if negative {
|
||||
magnitude.reverse()
|
||||
} else {
|
||||
magnitude
|
||||
})
|
||||
}
|
||||
}
|
||||
impl PartialOrd for TaskCredits {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
impl PartialEq for TaskCredits {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "task_usage_tests.rs"]
|
||||
mod tests;
|
||||
132
codex-rs/backend-client/src/client/task_usage_tests.rs
Normal file
132
codex-rs/backend-client/src/client/task_usage_tests.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! Exact decimal preservation and sorting for consumer task credits.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn credits_preserve_precision_and_order_signed_decimal_strings() {
|
||||
let mut amounts: Vec<TaskCredits> = serde_json::from_value(json!([
|
||||
"1.000000000000000002",
|
||||
"-0.00025",
|
||||
"0",
|
||||
"1.000000000000000001",
|
||||
"1e-20",
|
||||
"-1E+3"
|
||||
]))
|
||||
.unwrap();
|
||||
amounts.sort();
|
||||
assert_eq!(
|
||||
amounts.iter().map(TaskCredits::as_str).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"-1E+3",
|
||||
"-0.00025",
|
||||
"0",
|
||||
"1e-20",
|
||||
"1.000000000000000001",
|
||||
"1.000000000000000002"
|
||||
]
|
||||
);
|
||||
for invalid in ["NaN", "inf", "1..0", "", "1e999999999999"] {
|
||||
assert!(serde_json::from_value::<TaskCredits>(json!(invalid)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn task_queries_validate_request_groups_and_response_ownership() {
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
let server = MockServer::start().await;
|
||||
let client = Client::new(
|
||||
server.uri(),
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
);
|
||||
let query = TaskUsageThread {
|
||||
thread_id: "root".into(),
|
||||
created_at: None,
|
||||
descendant_thread_ids: vec!["child".into()],
|
||||
};
|
||||
let task = json!({"thread_id":"root", "data_status":"available", "usage_source":"plan_and_credits",
|
||||
"five_hour_limit_percent":150.0, "weekly_limit_percent":0.0,
|
||||
"balance_usage_credits":"-0.000000000000000001", "groups":[{
|
||||
"product_experience":"codex", "model":"gpt-5.5", "reasoning_effort":"high", "speed":"standard",
|
||||
"five_hour_limit_percent":150.25, "weekly_limit_percent":0.125, "balance_usage_credits":"0"
|
||||
}]});
|
||||
for ids in [vec!["root"], vec!["other"], vec!["root", "root"]] {
|
||||
server.reset().await;
|
||||
let response = json!({"data_as_of":null, "threads":ids.iter().map(|id| {
|
||||
let mut task=task.clone();task["thread_id"]=json!(id);task
|
||||
}).collect::<Vec<_>>()});
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/usage/thread_usage/query_v2"))
|
||||
.respond_with(move |request: &wiremock::Request| {
|
||||
assert_eq!(
|
||||
request.body_json::<serde_json::Value>().unwrap(),
|
||||
json!({"threads":[{
|
||||
"thread_id":"root", "created_at":null, "descendant_thread_ids":["child"]
|
||||
}]})
|
||||
);
|
||||
ResponseTemplate::new(/*s*/ 200).set_body_json(&response)
|
||||
})
|
||||
.expect(/*r*/ 1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let result = client.get_task_usage(std::slice::from_ref(&query)).await;
|
||||
if ids == ["root"] {
|
||||
let response = result.unwrap();
|
||||
assert_eq!((response.data_as_of, response.threads.len()), (None, 1));
|
||||
let row = &response.threads[0];
|
||||
assert_eq!(
|
||||
(
|
||||
row.thread_id.as_str(),
|
||||
row.data_status,
|
||||
row.amounts.five_hour_limit_percent,
|
||||
row.amounts.weekly_limit_percent,
|
||||
row.amounts
|
||||
.balance_usage_credits
|
||||
.as_ref()
|
||||
.map(TaskCredits::as_str)
|
||||
),
|
||||
(
|
||||
"root",
|
||||
TaskUsageStatus::Available,
|
||||
Some(150.0),
|
||||
Some(0.0),
|
||||
Some("-0.000000000000000001")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
row.groups[0].amounts.five_hour_limit_percent,
|
||||
row.groups[0].amounts.weekly_limit_percent
|
||||
),
|
||||
(Some(150.25), Some(0.125))
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Invalid task usage response.")
|
||||
);
|
||||
}
|
||||
}
|
||||
server.reset().await;
|
||||
for queries in [
|
||||
vec![],
|
||||
vec![query.clone(), query.clone()],
|
||||
vec![TaskUsageThread {
|
||||
thread_id: "root".into(),
|
||||
created_at: None,
|
||||
descendant_thread_ids: (0..1_000).map(|n| format!("child-{n}")).collect(),
|
||||
}],
|
||||
] {
|
||||
assert!(client.get_task_usage(&queries).await.is_err());
|
||||
}
|
||||
assert!(server.received_requests().await.unwrap().is_empty());
|
||||
}
|
||||
@@ -47,4 +47,12 @@ pub use client::plan_history::PlanLimitDimension;
|
||||
pub use client::plan_history::PlanLimitHistory;
|
||||
pub use client::plan_history::PlanLimitPeriod;
|
||||
pub use client::plan_history::PlanLimitValue;
|
||||
|
||||
pub use client::task_usage::TaskCredits;
|
||||
pub use client::task_usage::TaskUsage;
|
||||
pub use client::task_usage::TaskUsageAmounts;
|
||||
pub use client::task_usage::TaskUsageGroup;
|
||||
pub use client::task_usage::TaskUsageResponse;
|
||||
pub use client::task_usage::TaskUsageStatus;
|
||||
pub use client::task_usage::TaskUsageThread;
|
||||
pub use codex_backend_openapi_models::models::analytics as analytics_models;
|
||||
|
||||
@@ -20,6 +20,8 @@ mod render;
|
||||
mod report_data;
|
||||
mod sections;
|
||||
mod styles;
|
||||
mod task_panel;
|
||||
mod tasks;
|
||||
#[cfg(test)]
|
||||
#[path = "analytics/test_support.rs"]
|
||||
mod test_support;
|
||||
@@ -49,6 +51,8 @@ pub(crate) struct AnalyticsView {
|
||||
model_names: std::collections::HashMap<String, String>,
|
||||
sections: SectionStates,
|
||||
chats: Load<chats::Chats>,
|
||||
tasks: Load<tasks::Chats>,
|
||||
chat_metric: usize,
|
||||
plan: plan::State,
|
||||
show_zero_credit_groups: bool,
|
||||
account: Load<codex_protocol::account::PlanType>,
|
||||
@@ -78,6 +82,8 @@ impl AnalyticsView {
|
||||
model_names: std::collections::HashMap::new(),
|
||||
sections: SectionStates(std::array::from_fn(|_| SectionState::default())),
|
||||
chats: Load::Unavailable,
|
||||
tasks: Load::Unavailable,
|
||||
chat_metric: 0,
|
||||
plan: plan::State::default(),
|
||||
show_zero_credit_groups: false,
|
||||
account: Load::Unavailable,
|
||||
@@ -137,6 +143,7 @@ impl AnalyticsView {
|
||||
}
|
||||
self.sections[Section::Chats].detail = None;
|
||||
self.chats = Load::Unavailable;
|
||||
self.tasks = Load::Unavailable;
|
||||
self.plan.report = Load::Unavailable;
|
||||
self.reports_started = false;
|
||||
self.token_model = None;
|
||||
@@ -168,6 +175,7 @@ impl AnalyticsView {
|
||||
section.history = Load::Unavailable;
|
||||
}
|
||||
self.chats = Load::Unavailable;
|
||||
self.tasks = Load::Unavailable;
|
||||
self.plan.report = Load::Unavailable;
|
||||
self.account = Load::Unavailable;
|
||||
self.live = None;
|
||||
@@ -254,7 +262,11 @@ impl AnalyticsView {
|
||||
}
|
||||
|
||||
fn row_count(&self) -> usize {
|
||||
if self.section == Section::Chats {
|
||||
if self.section == Section::Chats && !self.business() {
|
||||
self.tasks
|
||||
.ready()
|
||||
.map_or(/*default*/ 0, |chats| chats.rows.len())
|
||||
} else if self.section == Section::Chats {
|
||||
self.chats
|
||||
.ready()
|
||||
.map_or(/*default*/ 0, |chats| chats.rows.len())
|
||||
@@ -389,6 +401,19 @@ impl AnalyticsView {
|
||||
self.plan_action(action);
|
||||
return;
|
||||
}
|
||||
if self.section == Section::Chats
|
||||
&& !self.business()
|
||||
&& key_hint::plain(KeyCode::Char('s')).is_press(key)
|
||||
{
|
||||
let metrics = self.task_metrics();
|
||||
let index = metrics
|
||||
.iter()
|
||||
.position(|metric| *metric == self.task_metric())
|
||||
.unwrap_or_default();
|
||||
self.chat_metric = metrics[(index + 1) % metrics.len()];
|
||||
self.sections[Section::Chats].detail = None;
|
||||
return;
|
||||
}
|
||||
if matches!(
|
||||
self.section,
|
||||
Section::Plugins | Section::Activity | Section::Skills
|
||||
@@ -398,14 +423,19 @@ impl AnalyticsView {
|
||||
return;
|
||||
}
|
||||
if self.section == Section::Chats
|
||||
&& self.business()
|
||||
&& matches!(action, Some(ListAction::Accept | ListAction::MoveRight))
|
||||
&& self
|
||||
.chats
|
||||
.ready()
|
||||
.and_then(|chats| chats.rows.get(self.sections[Section::Chats].cursor))
|
||||
.and_then(|chat| chat.usage.as_ref())
|
||||
.is_none()
|
||||
&& if self.business() {
|
||||
self.chats
|
||||
.ready()
|
||||
.and_then(|chats| chats.rows.get(self.sections[Section::Chats].cursor))
|
||||
.and_then(|chat| chat.usage.as_ref())
|
||||
.is_none()
|
||||
} else {
|
||||
self.task_rows()
|
||||
.get(self.sections[Section::Chats].cursor)
|
||||
.and_then(|chat| task_panel::available(chat))
|
||||
.is_none()
|
||||
}
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -463,6 +493,7 @@ impl AnalyticsView {
|
||||
}
|
||||
}
|
||||
self.chats = Load::Unavailable;
|
||||
self.tasks = Load::Unavailable;
|
||||
self.plan.report = Load::Unavailable;
|
||||
self.account = Load::Unavailable;
|
||||
self.connection = None;
|
||||
|
||||
@@ -14,6 +14,7 @@ fn account_layouts_show_reports_and_wrap_navigation() {
|
||||
Section::Activity,
|
||||
Section::Plugins,
|
||||
Section::Skills,
|
||||
Section::Chats,
|
||||
],
|
||||
),
|
||||
(
|
||||
|
||||
105
codex-rs/tui/src/analytics/consumer_refresh_tests.rs
Normal file
105
codex-rs/tui/src/analytics/consumer_refresh_tests.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Consumer chat columns follow reported metrics without substituting billing estimates.
|
||||
use super::*;
|
||||
use crate::analytics::tasks::Chat;
|
||||
use crate::analytics::tasks::Chats;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn consumer_chats_keep_missing_rows_and_only_offer_known_metrics() {
|
||||
let mut view = fixture::view(models::AccountKind::Consumer);
|
||||
view.section = Section::Chats;
|
||||
view.tasks = Load::Ready(Chats {
|
||||
rows: vec![
|
||||
Chat { title: "Private title from another account".into(), task: None },
|
||||
Chat { title: "Unverified backend row".into(), task: Some(serde_json::from_value(json!({
|
||||
"thread_id":"unavailable", "data_status":"unavailable", "usage_source":"unknown",
|
||||
"weekly_limit_percent":null,"five_hour_limit_percent":null,"balance_usage_credits":null,"groups":[]
|
||||
})).unwrap()) },
|
||||
Chat { title: "Refunded task".into(), task: Some(serde_json::from_value(json!({
|
||||
"thread_id":"refund", "data_status":"partial", "usage_source":"credits",
|
||||
"weekly_limit_percent":null,"five_hour_limit_percent":null,"balance_usage_credits":"-0.000004","groups":[{
|
||||
"product_experience":"codex", "model":"gpt-5.5", "reasoning_effort":"high", "speed":"fast",
|
||||
"weekly_limit_percent":null,"five_hour_limit_percent":null,"balance_usage_credits":"-0.000004"
|
||||
}]
|
||||
})).unwrap()) },
|
||||
], ..Chats::default()
|
||||
});
|
||||
assert_eq!(view.task_metrics(), vec![2]);
|
||||
assert_eq!(
|
||||
view.task_rows()
|
||||
.iter()
|
||||
.map(|row| row.title.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"Refunded task",
|
||||
"Private title from another account",
|
||||
"Unverified backend row"
|
||||
]
|
||||
);
|
||||
press(&mut view, KeyCode::Down);
|
||||
press(&mut view, KeyCode::Enter);
|
||||
assert_eq!(view.sections[Section::Chats].detail, None);
|
||||
let hidden = screen(&mut view, /*width*/ 90, /*height*/ 30);
|
||||
assert!(!hidden.contains("Private title") && !hidden.contains("Unverified backend row"));
|
||||
press(&mut view, KeyCode::Up);
|
||||
press(&mut view, KeyCode::Enter);
|
||||
insta::assert_snapshot!(screen(&mut view, /*width*/ 90, /*height*/ 30));
|
||||
let chats = match &mut view.tasks {
|
||||
Load::Ready(chats) => chats,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
chats.rows[2]
|
||||
.task
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.amounts
|
||||
.five_hour_limit_percent = Some(0.0);
|
||||
chats.rows[2]
|
||||
.task
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.amounts
|
||||
.weekly_limit_percent = Some(125.0);
|
||||
assert_eq!(view.task_metrics(), vec![0, 1, 2]);
|
||||
insta::assert_snapshot!(
|
||||
"consumer_chat_all_metrics",
|
||||
screen(&mut view, /*width*/ 110, /*height*/ 30)
|
||||
);
|
||||
press(&mut view, KeyCode::Char('s'));
|
||||
insta::assert_snapshot!(
|
||||
"consumer_chat_available_limits",
|
||||
screen(&mut view, /*width*/ 58, /*height*/ 30)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumer_overview_shows_top_five_and_closes_with_retained_details() {
|
||||
let mut view = fixture::view(models::AccountKind::Consumer);
|
||||
view.section = Section::Chats;
|
||||
view.tasks = Load::Ready(Chats {
|
||||
rows: (0..6).map(|index| Chat {
|
||||
title: format!("Task {index}"),
|
||||
task: Some(serde_json::from_value(json!({
|
||||
"thread_id":format!("task-{index}"), "data_status":"available", "usage_source":"credits",
|
||||
"balance_usage_credits":index.to_string(), "groups":[]
|
||||
})).unwrap()),
|
||||
}).collect(),
|
||||
..Chats::default()
|
||||
});
|
||||
press(&mut view, KeyCode::End);
|
||||
press(&mut view, KeyCode::Enter);
|
||||
assert_eq!(view.sections[Section::Chats].detail, Some(5));
|
||||
press(&mut view, KeyCode::Char('z'));
|
||||
let content = view
|
||||
.task_lines(/*width*/ 70)
|
||||
.0
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(content.contains("Task 5") && !content.contains("Task 0"));
|
||||
insta::assert_snapshot!(content);
|
||||
press(&mut view, KeyCode::Esc);
|
||||
assert!(view.is_done);
|
||||
}
|
||||
@@ -113,6 +113,9 @@ impl AnalyticsView {
|
||||
.lines,
|
||||
),
|
||||
Section::Plan => content.extend(self.plan_lines(inner_width).0),
|
||||
Section::Chats if !self.business() => {
|
||||
content.extend(self.task_lines(inner_width).0)
|
||||
}
|
||||
Section::Chats => {
|
||||
content.push(
|
||||
"30d active · lifetime credits"
|
||||
|
||||
@@ -84,7 +84,11 @@ impl AnalyticsView {
|
||||
selection = lines.len() + detail.start..lines.len() + detail.end;
|
||||
lines.extend(plan);
|
||||
} else if section == Section::Chats {
|
||||
let (chats, detail) = self.chat_lines(width);
|
||||
let (chats, detail) = if self.business() {
|
||||
self.chat_lines(width)
|
||||
} else {
|
||||
self.task_lines(width)
|
||||
};
|
||||
selection = lines.len() + detail.start..lines.len() + detail.end;
|
||||
lines.extend(chats);
|
||||
} else {
|
||||
|
||||
@@ -140,7 +140,15 @@ impl AnalyticsView {
|
||||
format!(
|
||||
"tab/1–{} section · {}{navigation}\n{}{}R refresh · {}/{} scroll · {} back · q close",
|
||||
self.visible_sections().len(),
|
||||
if self.zoomed { "z dashboard · " } else { "" },
|
||||
if self.section == Section::Chats && !self.business() && !self.zoomed {
|
||||
"s sort · "
|
||||
} else if self.section == Section::Chats && !self.business() {
|
||||
"s sort · z dashboard · "
|
||||
} else if self.zoomed {
|
||||
"z dashboard · "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if show_range { "r 7/30d · " } else { "" },
|
||||
if self.group_options().len() < 2 {
|
||||
""
|
||||
|
||||
@@ -81,6 +81,7 @@ impl AnalyticsView {
|
||||
section.history.poll();
|
||||
}
|
||||
self.chats.poll();
|
||||
self.tasks.poll();
|
||||
self.plan.poll();
|
||||
self.account.poll();
|
||||
self.start_reports();
|
||||
@@ -96,6 +97,11 @@ impl AnalyticsView {
|
||||
{
|
||||
self.group_picker = None;
|
||||
}
|
||||
if let Some(chats) = self.tasks.ready() {
|
||||
self.sections[Section::Chats].cursor = self.sections[Section::Chats]
|
||||
.cursor
|
||||
.min(chats.rows.len().saturating_sub(/*rhs*/ 1));
|
||||
}
|
||||
if let Some(chats) = self.chats.ready() {
|
||||
self.sections[Section::Chats].cursor = self.sections[Section::Chats]
|
||||
.cursor
|
||||
@@ -145,12 +151,14 @@ impl AnalyticsView {
|
||||
Section::Activity,
|
||||
Section::Plugins,
|
||||
Section::Skills,
|
||||
Section::Chats,
|
||||
],
|
||||
Some(AccountKind::Consumer) => &[
|
||||
Section::Usage,
|
||||
Section::Activity,
|
||||
Section::Plugins,
|
||||
Section::Skills,
|
||||
Section::Chats,
|
||||
],
|
||||
Some(AccountKind::Business | AccountKind::Enterprise)
|
||||
if super::models::thread_usage_supported(self.account.ready().copied()) =>
|
||||
@@ -216,7 +224,15 @@ impl AnalyticsView {
|
||||
let live = std::sync::Arc::clone(live);
|
||||
self.plan.report = Load::start(async move { live.plan_history().await }, frame.clone());
|
||||
}
|
||||
if visible.contains(&Section::Chats) {
|
||||
if visible.contains(&Section::Chats) && !self.business() {
|
||||
if let (Some((_, handle, frame)), Some(live)) = (&self.connection, &self.live) {
|
||||
self.tasks = Load::start_with_timeout(
|
||||
super::tasks::read(handle.clone(), std::sync::Arc::clone(live)),
|
||||
frame.clone(),
|
||||
std::time::Duration::from_secs(/*secs*/ 120),
|
||||
);
|
||||
}
|
||||
} else if visible.contains(&Section::Chats) {
|
||||
self.chats =
|
||||
if let (Some((_, handle, frame)), Some(live)) = (&self.connection, &self.live) {
|
||||
Load::start_with_timeout(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
source: tui/src/analytics/plan_tests.rs
|
||||
assertion_line: 53
|
||||
assertion_line: 56
|
||||
expression: "output.join(\"\\n\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Total usage history [2 Plan usage history] 3 Messages 4 Plugins called 5 Skills used "
|
||||
" 1 Total usage history [2 Plan usage history] 3 Messages 4 Plugins called 5 Skills used 6 Top chats "
|
||||
" "
|
||||
" ▎ Plan usage history By feature "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -31,13 +31,13 @@ expression: "output.join(\"\\n\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ←/→ window · ↑/↓ period · enter details "
|
||||
" tab/1–6 section · z dashboard · ←/→ window · ↑/↓ period · enter details "
|
||||
" g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" Total usage history [Plan usage history] Messages "
|
||||
" Plugins called Skills used "
|
||||
" Plugins called Skills used Top chats "
|
||||
" "
|
||||
" ▎ Plan usage history By feature "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
@@ -60,7 +60,7 @@ expression: "output.join(\"\\n\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ←/→ window · ↑/↓ "
|
||||
" tab/1–6 section · z dashboard · ←/→ window · ↑/↓ "
|
||||
" period · enter details "
|
||||
" g group · R refresh · pgup/pgdn scroll · esc back · q "
|
||||
" close "
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/plan_tests.rs
|
||||
assertion_line: 183
|
||||
assertion_line: 210
|
||||
expression: output
|
||||
---
|
||||
" Analytics · Live account "
|
||||
@@ -43,23 +43,23 @@ expression: output
|
||||
" │ │ │ │ "
|
||||
" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" ╭─ 5 Skills used ────────────────────────────────────╮ "
|
||||
" │ │ "
|
||||
" │ 7d │ "
|
||||
" │ 54 uses │ "
|
||||
" │ 11 │ "
|
||||
" │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ "
|
||||
" │ 0 ──────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ "
|
||||
" │ 27 28 29 30 31 1 2 │ "
|
||||
" │ │ "
|
||||
" │ Sep 2 · 11 uses │ "
|
||||
" │ ● Testing 4 │ "
|
||||
" │ ● Code review 3 │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" ╰──────────────────────────────────────────────────────╯ "
|
||||
" tab/1–5 section · enter/z maximize "
|
||||
" ╭─ 5 Skills used ────────────────────────────────────╮ ╭─ 6 Top chats ──────────────────────────────────────╮ "
|
||||
" │ │ │ │ "
|
||||
" │ 7d │ │ No history has been reported. │ "
|
||||
" │ 54 uses │ │ │ "
|
||||
" │ 11 │ │ │ "
|
||||
" │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ │ │ "
|
||||
" │ 0 ──────────────────────────────────────────────── │ │ │ "
|
||||
" │ ▲ │ │ │ "
|
||||
" │ 27 28 29 30 31 1 2 │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" │ Sep 2 · 11 uses │ │ │ "
|
||||
" │ ● Testing 4 │ │ │ "
|
||||
" │ ● Code review 3 │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ "
|
||||
" tab/1–6 section · enter/z maximize "
|
||||
" g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
source: tui/src/analytics/account_layout_tests.rs
|
||||
assertion_line: 47
|
||||
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 "
|
||||
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Total usage history By feature "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -30,13 +31,13 @@ expression: "screens.join(\"\\n\")"
|
||||
" ● Images 11.6% "
|
||||
" ● Other 10.5% "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 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 "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -61,13 +62,13 @@ expression: "screens.join(\"\\n\")"
|
||||
" ● GPT-5.5 10 "
|
||||
" ● Other 7 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 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 "
|
||||
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Plugins called "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -92,13 +93,13 @@ expression: "screens.join(\"\\n\")"
|
||||
" ● GitHub 5 "
|
||||
" ● Notion 4 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 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] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -123,25 +124,50 @@ expression: "screens.join(\"\\n\")"
|
||||
" ● Planning 3 "
|
||||
" ● Debugging 1 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" No history has been reported. "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details "
|
||||
" 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 ▆▆▆▆▆ █████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ "
|
||||
" │ █████ █████ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ "
|
||||
" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
|
||||
" │ █████ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
|
||||
" │ 86 │ │ 103 │ "
|
||||
" │ 100 ▃▃▃▃▃ ▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ │ 150 ▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ "
|
||||
" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ "
|
||||
" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ───────────────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
|
||||
@@ -154,18 +180,13 @@ expression: "screens.join(\"\\n\")"
|
||||
" │ │ │ │ "
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ ▸ 4 Skills used ──────────────────────────────────────────────╮ "
|
||||
" ╭─ 3 Plugins called ───────────────────────────────────────────╮ ╭─ 4 Skills used ──────────────────────────────────────────────╮ "
|
||||
" │ │ │ │ "
|
||||
" │ 7d │ │ 7d │ "
|
||||
" │ 186 calls │ │ 54 uses │ "
|
||||
" │ │ │ │ "
|
||||
" │ 40 29 │ │ 20 │ "
|
||||
" │ ▇▇▇▇▇ │ │ │ "
|
||||
" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ "
|
||||
" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ "
|
||||
" │ 29 │ │ 11 │ "
|
||||
" │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ │ 20 │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ "
|
||||
" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
|
||||
@@ -179,17 +200,28 @@ expression: "screens.join(\"\\n\")"
|
||||
" │ │ │ │ "
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" ╭─ ▸ 5 Top chats ────────────────────────────────────────────────╮ "
|
||||
" │ │ "
|
||||
" │ No history has been reported. │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" ╰────────────────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · enter/z maximize "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
" tab/1–5 section · s sort · enter/z maximize "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tui/src/analytics/consumer_refresh_tests.rs
|
||||
assertion_line: 61
|
||||
expression: "screen(&mut view, 110, 30)"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Usage estimates · active in past 30 days · sorted by Weekly % "
|
||||
" Recorded usage / current full limit · may exceed 100% "
|
||||
" Partial ranking · 1 partial · 2 unavailable "
|
||||
" "
|
||||
" Chat Weekly % 5-hour % Balance credits "
|
||||
" › Refunded task · partial 125.0% 0.0% -0.000004 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Refunded task "
|
||||
" Weekly % 125.0% "
|
||||
" 5-hour % 0.0% "
|
||||
" Balance credits -0.000004 "
|
||||
" "
|
||||
" Model / effort / speed Weekly % "
|
||||
" gpt-5.5 — "
|
||||
" high · fast · codex "
|
||||
" "
|
||||
" Chat usage unavailable — — — "
|
||||
" Chat usage unavailable — — — "
|
||||
" "
|
||||
" Showing 1–3 of 3 chats "
|
||||
" Local chats · includes discovered descendants · excludes archived roots "
|
||||
" "
|
||||
" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tui/src/analytics/consumer_refresh_tests.rs
|
||||
assertion_line: 66
|
||||
expression: "screen(&mut view, 58, 30)"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" Total usage history Messages Plugins called Skills "
|
||||
" used [Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
" Usage estimates · active in past 30 days · sorted by "
|
||||
" 5-hour % "
|
||||
" Recorded usage / current full limit · may exceed 100% "
|
||||
" Partial ranking · 1 partial · 2 unavailable "
|
||||
" "
|
||||
" Chat 5-hour % "
|
||||
" › Refunded task · partial 0.0% "
|
||||
" Chat usage unavailable — "
|
||||
" Chat usage unavailable — "
|
||||
" "
|
||||
" Showing 1–3 of 3 chats "
|
||||
" Local chats · includes discovered descendants · "
|
||||
" excludes archived roots "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · s sort · z dashboard · ↑/↓ row · "
|
||||
" enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tui/src/analytics/consumer_refresh_tests.rs
|
||||
assertion_line: 43
|
||||
expression: "screen(&mut view, 90, 30)"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Usage estimates · active in past 30 days · sorted by Balance credits "
|
||||
" Credits debited from balance · includes adjustments "
|
||||
" Partial ranking · 1 partial · 2 unavailable "
|
||||
" "
|
||||
" Chat Balance credits "
|
||||
" › Refunded task · partial -0.000004 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Refunded task "
|
||||
" Balance credits -0.000004 "
|
||||
" "
|
||||
" Model / effort / speed Balance credits "
|
||||
" gpt-5.5 -0.000004 "
|
||||
" high · fast · codex "
|
||||
" "
|
||||
" Chat usage unavailable — "
|
||||
" Chat usage unavailable — "
|
||||
" "
|
||||
" Showing 1–3 of 3 chats "
|
||||
" Local chats · includes discovered descendants · excludes archived roots "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: tui/src/analytics/consumer_refresh_tests.rs
|
||||
assertion_line: 93
|
||||
expression: content
|
||||
---
|
||||
Usage estimates · active in past 30 days · sorted by Balance credits
|
||||
Credits debited from balance · includes adjustments
|
||||
|
||||
Chat Balance credits
|
||||
Task 5 5
|
||||
Task 4 4
|
||||
Task 3 3
|
||||
Task 2 2
|
||||
Task 1 1
|
||||
|
||||
Showing top 5 of 6 chats
|
||||
Local chats · includes discovered descendants · excludes archived
|
||||
roots
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/dashboard_tests.rs
|
||||
assertion_line: 83
|
||||
expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
@@ -10,14 +11,9 @@ expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
" │ 7d │ │ 7d │ "
|
||||
" │ By feature │ │ By model │ "
|
||||
" │ Total usage · relative units │ │ 574 reported messages │ "
|
||||
" │ 86 │ │ │ "
|
||||
" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ "
|
||||
" │ █████ ▅▅▅▅▅▅ █████ ██████ │ │ 103 │ "
|
||||
" │ █████ ▃▃▃▃▃▃ █████ ██████ █████ ██████ │ │ ▇▇▇▇▇▇ ▆▆▆▆▆▆ │ "
|
||||
" │ 50 █████ ██████ █████ ██████ █████ ██████ │ │ 75 ▆▆▆▆▆ ██████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ "
|
||||
" │ █████ ██████ █████ ██████ █████ ██████ │ │ ▇▇▇▇▇ █████ ██████ █████ ▆▆▆▆▆▆ █████ ██████ │ "
|
||||
" │ ▃▃▃▃▃ █████ ██████ █████ ██████ █████ ██████ │ │ █████ █████ ██████ █████ ██████ █████ ██████ │ "
|
||||
" │ █████ █████ ██████ █████ ██████ █████ ██████ │ │ █████ █████ ██████ █████ ██████ █████ ██████ │ "
|
||||
" │ 86 │ │ 103 │ "
|
||||
" │ 100 ▃▃▃▃▃ ▂▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ │ 150 ▃▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ "
|
||||
" │ ▃▃▃▃▃ █████ ██████ █████ ██████ █████ ██████ │ │ ▆▆▆▆▆ █████ ██████ █████ ▆▆▆▆▆▆ █████ ██████ │ "
|
||||
" │ 0 ─────────────────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
|
||||
@@ -34,14 +30,9 @@ expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
" │ │ │ │ "
|
||||
" │ 7d │ │ 7d │ "
|
||||
" │ 186 calls │ │ 54 uses │ "
|
||||
" │ │ │ │ "
|
||||
" │ 40 29 │ │ 20 │ "
|
||||
" │ ▇▇▇▇▇ │ │ │ "
|
||||
" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ "
|
||||
" │ 20 █████ ██████ █████ ██████ ▆▆▆▆▆ ██████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ "
|
||||
" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ "
|
||||
" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ █████ ██████ █████ ██████ █████ ▆▆▆▆▆▆ ██████ │ "
|
||||
" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ █████ ██████ █████ ██████ █████ ██████ ██████ │ "
|
||||
" │ 29 │ │ 11 │ "
|
||||
" │ 40 ▅▅▅▅▅ ▁▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁▁ ▃▃▃▃▃▃ │ │ 20 │ "
|
||||
" │ █████ ██████ █████ ██████ █████ ██████ ██████ │ │ ▆▆▆▆▆ ▄▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄▄ ██████ │ "
|
||||
" │ 0 ──────────────────────────────────────────────────────────── │ │ 0 ──────────────────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
|
||||
@@ -55,16 +46,26 @@ expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
" │ │ │ │ "
|
||||
" ╰──────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · enter/z maximize "
|
||||
" ╭─ 5 Top chats ──────────────────────────────────────────────────╮ "
|
||||
" │ │ "
|
||||
" │ No history has been reported. │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" ╰──────────────────────────────────────────────────────────────────╯ "
|
||||
" tab/1–5 section · enter/z maximize "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
@@ -112,6 +113,6 @@ expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" tab/1–4 section · enter/z maximize "
|
||||
" tab/1–5 section · enter/z maximize "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q "
|
||||
" close "
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used "
|
||||
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Total usage history By feature "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● Images 11.6% "
|
||||
" ● Other 10.5% "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:3 27:2 70:0
|
||||
3: 0:0 2:3 27:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 23:0 108:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used "
|
||||
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Plugins called "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● GitHub 5 "
|
||||
" ● Notion 4 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 37:3 57:2 70:0
|
||||
3: 0:0 2:2 37:3 57:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 18:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
Text:
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" No history has been reported. "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
1: Reset|Reset|Reset|BOLD
|
||||
2: Rgb(165, 165, 165)|Reset|Reset|NONE
|
||||
3: Cyan|Reset|Reset|BOLD
|
||||
4: Reset|Reset|Reset|DIM
|
||||
5: White|Reset|Reset|BOLD
|
||||
Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0
|
||||
2: 0:0 2:2 70:3 83:0
|
||||
3: 0:0
|
||||
4: 0:0 2:3 13:0
|
||||
5: 0:0 2:4 118:0
|
||||
6: 0:0
|
||||
7: 0:0
|
||||
8: 0:0
|
||||
9: 0:0
|
||||
10: 0:0
|
||||
11: 0:0
|
||||
12: 0:0
|
||||
13: 0:0
|
||||
14: 0:0
|
||||
15: 0:0
|
||||
16: 0:0
|
||||
17: 0:0
|
||||
18: 0:0
|
||||
19: 0:0
|
||||
20: 0:0
|
||||
21: 0:0
|
||||
22: 0:0
|
||||
23: 0:0
|
||||
24: 0:0
|
||||
25: 0:0
|
||||
26: 0:0
|
||||
27: 0:0
|
||||
28: 0:0
|
||||
29: 0:0
|
||||
30: 0:0
|
||||
31: 0:0
|
||||
32: 0:0
|
||||
33: 0:0
|
||||
34: 0:0
|
||||
35: 0:0
|
||||
36: 0:0
|
||||
37: 0:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:5 9:2 20:5 21:2 29:5 30:2 43:5 46:2 53:5 58:2 118:0
|
||||
41: 0:0 2:5 3:2 14:5 23:2 33:5 36:2 44:5 45:2 118:0
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● GPT-5.5 10 "
|
||||
" ● Other 7 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 25:3 39:2 70:0
|
||||
3: 0:0 2:2 25:3 39:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 12:0 110:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● Planning 3 "
|
||||
" ● Debugging 1 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 55:3 70:0
|
||||
3: 0:0 2:2 55:3 72:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 15:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used "
|
||||
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Total usage history By feature "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● Images 11.6% "
|
||||
" ● Other 10.5% "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:3 27:2 70:0
|
||||
3: 0:0 2:3 27:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 23:0 108:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used "
|
||||
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Plugins called "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● GitHub 5 "
|
||||
" ● Notion 4 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 37:3 57:2 70:0
|
||||
3: 0:0 2:2 37:3 57:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 18:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
Text:
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" No history has been reported. "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · s sort · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
1: Reset|Reset|Reset|BOLD
|
||||
2: Rgb(76, 76, 76)|Reset|Reset|NONE
|
||||
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
|
||||
4: Reset|Reset|Reset|DIM
|
||||
5: White|Reset|Reset|BOLD
|
||||
Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0
|
||||
2: 0:0 2:2 70:3 83:0
|
||||
3: 0:0
|
||||
4: 0:0 2:3 13:0
|
||||
5: 0:0 2:4 118:0
|
||||
6: 0:0
|
||||
7: 0:0
|
||||
8: 0:0
|
||||
9: 0:0
|
||||
10: 0:0
|
||||
11: 0:0
|
||||
12: 0:0
|
||||
13: 0:0
|
||||
14: 0:0
|
||||
15: 0:0
|
||||
16: 0:0
|
||||
17: 0:0
|
||||
18: 0:0
|
||||
19: 0:0
|
||||
20: 0:0
|
||||
21: 0:0
|
||||
22: 0:0
|
||||
23: 0:0
|
||||
24: 0:0
|
||||
25: 0:0
|
||||
26: 0:0
|
||||
27: 0:0
|
||||
28: 0:0
|
||||
29: 0:0
|
||||
30: 0:0
|
||||
31: 0:0
|
||||
32: 0:0
|
||||
33: 0:0
|
||||
34: 0:0
|
||||
35: 0:0
|
||||
36: 0:0
|
||||
37: 0:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:5 9:2 20:5 21:2 29:5 30:2 43:5 46:2 53:5 58:2 118:0
|
||||
41: 0:0 2:5 3:2 14:5 23:2 33:5 36:2 44:5 45:2 118:0
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● GPT-5.5 10 "
|
||||
" ● Other 7 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 25:3 39:2 70:0
|
||||
3: 0:0 2:2 25:3 39:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 12:0 110:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
assertion_line: 142
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
@@ -7,7 +8,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +45,7 @@ Text:
|
||||
" ● Planning 3 "
|
||||
" ● Debugging 1 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
@@ -61,7 +62,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 55:3 70:0
|
||||
3: 0:0 2:2 55:3 72:2 83:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 15:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
source: tui/src/analytics/tool_panel_tests.rs
|
||||
assertion_line: 78
|
||||
expression: "format!(\"{empty}\\n{missing}\\n{partial}\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,13 +45,13 @@ expression: "format!(\"{empty}\\n{missing}\\n{partial}\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day "
|
||||
" tab/1–5 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] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -89,13 +90,13 @@ expression: "format!(\"{empty}\\n{missing}\\n{partial}\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day "
|
||||
" tab/1–5 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] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -134,5 +135,5 @@ expression: "format!(\"{empty}\\n{missing}\\n{partial}\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day "
|
||||
" tab/1–5 section · z dashboard · ←/→ day "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
source: tui/src/analytics/tool_panel_tests.rs
|
||||
assertion_line: 46
|
||||
expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used "
|
||||
" 1 Total usage history 2 Messages [3 Plugins called] 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Plugins called "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -48,14 +49,14 @@ expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
" ● GitHub 5 "
|
||||
" ● Notion 4 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 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] "
|
||||
" used] Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
@@ -79,7 +80,7 @@ expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
" ● Planning 1 "
|
||||
" ● Debugging 0 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter "
|
||||
" details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q "
|
||||
" close "
|
||||
|
||||
344
codex-rs/tui/src/analytics/task_panel.rs
Normal file
344
codex-rs/tui/src/analytics/task_panel.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
//! Consumer chat metrics retain actual balance debits, current-limit comparisons, and partial states.
|
||||
|
||||
use super::AnalyticsView;
|
||||
use super::render::columns;
|
||||
use super::sections::Section;
|
||||
use super::styles::number;
|
||||
use super::styles::secondary_style;
|
||||
use super::tasks::Chat;
|
||||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow as truncate;
|
||||
use crate::style::accent_style;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
use codex_backend_client::TaskUsageAmounts;
|
||||
use codex_backend_client::TaskUsageStatus;
|
||||
use ratatui::style::Styled;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
|
||||
pub(super) const METRICS: [&str; 3] = ["Weekly %", "5-hour %", "Balance credits"];
|
||||
|
||||
fn compare(a: Option<&TaskUsageAmounts>, b: Option<&TaskUsageAmounts>, metric: usize) -> Ordering {
|
||||
if metric == 2 {
|
||||
a.and_then(|a| a.balance_usage_credits.as_ref())
|
||||
.cmp(&b.and_then(|b| b.balance_usage_credits.as_ref()))
|
||||
} else {
|
||||
let value = |amounts: &TaskUsageAmounts| {
|
||||
if metric == 0 {
|
||||
amounts.weekly_limit_percent
|
||||
} else {
|
||||
amounts.five_hour_limit_percent
|
||||
}
|
||||
};
|
||||
a.and_then(value)
|
||||
.partial_cmp(&b.and_then(value))
|
||||
.unwrap_or(Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn amount(amounts: Option<&TaskUsageAmounts>, metric: usize) -> String {
|
||||
let Some(amounts) = amounts else {
|
||||
return "—".into();
|
||||
};
|
||||
if metric == 2 {
|
||||
amounts
|
||||
.balance_usage_credits
|
||||
.as_ref()
|
||||
.map(|amount| amount.as_str().to_string())
|
||||
.unwrap_or_else(|| "—".into())
|
||||
} else {
|
||||
(if metric == 0 {
|
||||
amounts.weekly_limit_percent
|
||||
} else {
|
||||
amounts.five_hour_limit_percent
|
||||
})
|
||||
.map(|value| format!("{value:.1}%"))
|
||||
.unwrap_or_else(|| "—".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn available(chat: &Chat) -> Option<&TaskUsageAmounts> {
|
||||
chat.task
|
||||
.as_ref()
|
||||
.filter(|task| task.data_status != TaskUsageStatus::Unavailable)
|
||||
.map(|task| &task.amounts)
|
||||
}
|
||||
|
||||
impl AnalyticsView {
|
||||
pub(super) fn task_metrics(&self) -> Vec<usize> {
|
||||
(0..3)
|
||||
.filter(|metric| {
|
||||
*metric == 2
|
||||
|| self.tasks.ready().is_some_and(|chats| {
|
||||
chats.rows.iter().any(|chat| {
|
||||
available(chat).is_some_and(|amounts| {
|
||||
if *metric == 0 {
|
||||
amounts.weekly_limit_percent.is_some()
|
||||
} else {
|
||||
amounts.five_hour_limit_percent.is_some()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn task_metric(&self) -> usize {
|
||||
let metrics = self.task_metrics();
|
||||
if metrics.contains(&self.chat_metric) {
|
||||
self.chat_metric
|
||||
} else {
|
||||
metrics[0]
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn task_rows(&self) -> Vec<&Chat> {
|
||||
let mut rows = self
|
||||
.tasks
|
||||
.ready()
|
||||
.map(|chats| chats.rows.iter().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
rows.sort_by(|a, b| compare(available(b), available(a), self.task_metric()));
|
||||
rows
|
||||
}
|
||||
|
||||
pub(super) fn task_lines(&self, width: usize) -> (Vec<Line<'static>>, Range<usize>) {
|
||||
let width = width.clamp(/*min*/ 1, /*max*/ 110);
|
||||
let wrap = |lines| word_wrap_lines(lines, RtOptions::new(width));
|
||||
let Some(chats) = self.tasks.ready() else {
|
||||
return (
|
||||
wrap(vec![
|
||||
self.tasks
|
||||
.message()
|
||||
.unwrap_or("No task usage reported.")
|
||||
.to_string()
|
||||
.into(),
|
||||
]),
|
||||
0..1,
|
||||
);
|
||||
};
|
||||
let metric = self.task_metric();
|
||||
let mut lines = wrap(vec![
|
||||
format!(
|
||||
"Usage estimates · active in past 30 days · sorted by {}",
|
||||
METRICS[metric]
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
if metric == 2 {
|
||||
"Credits debited from balance · includes adjustments"
|
||||
} else {
|
||||
"Recorded usage / current full limit · may exceed 100%"
|
||||
}
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
]);
|
||||
if chats.rows.is_empty() {
|
||||
lines.push("No recent local chats.".into());
|
||||
return (lines, 0..1);
|
||||
}
|
||||
let missing = chats
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|chat| amount(available(chat), metric) == "—")
|
||||
.count();
|
||||
let partial = chats
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|chat| {
|
||||
chat.task
|
||||
.as_ref()
|
||||
.is_some_and(|task| task.data_status == TaskUsageStatus::Partial)
|
||||
})
|
||||
.count();
|
||||
if missing + partial > 0 {
|
||||
lines.extend(wrap(vec![
|
||||
format!("Partial ranking · {partial} partial · {missing} unavailable")
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
]));
|
||||
}
|
||||
let metrics = self.task_metrics();
|
||||
let widths = metrics
|
||||
.iter()
|
||||
.map(|index| {
|
||||
chats
|
||||
.rows
|
||||
.iter()
|
||||
.map(|chat| amount(available(chat), *index).len())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
.max(METRICS[*index].len())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let show_all = widths.iter().sum::<usize>() + widths.len() * 2 + 24 <= width;
|
||||
let metric_text = |chat: Option<&Chat>| {
|
||||
if !show_all {
|
||||
return chat.map_or_else(
|
||||
|| METRICS[metric].to_string(),
|
||||
|chat| amount(available(chat), metric),
|
||||
);
|
||||
}
|
||||
metrics
|
||||
.iter()
|
||||
.zip(&widths)
|
||||
.map(|(index, column_width)| {
|
||||
let value = chat.map_or_else(
|
||||
|| METRICS[*index].to_string(),
|
||||
|chat| amount(available(chat), *index),
|
||||
);
|
||||
format!("{value:>column_width$}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
};
|
||||
lines.push(Line::default());
|
||||
lines.push(columns(
|
||||
" Chat".bold().into(),
|
||||
metric_text(/*chat*/ None).bold().into(),
|
||||
width,
|
||||
));
|
||||
let rows = self
|
||||
.task_rows()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, chat)| {
|
||||
let selected =
|
||||
self.section == Section::Chats && index == self.sections[Section::Chats].cursor;
|
||||
let value = metric_text(Some(chat));
|
||||
let status = if chat
|
||||
.task
|
||||
.as_ref()
|
||||
.is_some_and(|task| task.data_status == TaskUsageStatus::Partial)
|
||||
{
|
||||
" · partial"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let label = format!(
|
||||
"{} {}{status}",
|
||||
if selected { "›" } else { " " },
|
||||
chat.display_title()
|
||||
);
|
||||
let label: Line<'static> = if selected {
|
||||
label.set_style(accent_style()).into()
|
||||
} else {
|
||||
label.into()
|
||||
};
|
||||
let mut row = vec![columns(
|
||||
truncate(label, width.saturating_sub(value.len() + 1)),
|
||||
if value == "—" {
|
||||
value.set_style(secondary_style()).into()
|
||||
} else if selected {
|
||||
number(value).into()
|
||||
} else {
|
||||
value.into()
|
||||
},
|
||||
width,
|
||||
)];
|
||||
if self.zoomed && self.sections[Section::Chats].detail == Some(index) {
|
||||
row.push("─".repeat(width).dim().into());
|
||||
row.extend(word_wrap_lines(
|
||||
[chat.display_title().to_owned().bold()],
|
||||
RtOptions::new(width)
|
||||
.initial_indent(" ".into())
|
||||
.subsequent_indent(" ".into()),
|
||||
));
|
||||
for index in self.task_metrics() {
|
||||
let label = METRICS[index];
|
||||
row.push(columns(
|
||||
format!(" {label}").into(),
|
||||
amount(available(chat), index).into(),
|
||||
width,
|
||||
));
|
||||
}
|
||||
if let Some(task) = &chat.task {
|
||||
if task.data_status != TaskUsageStatus::Unavailable
|
||||
&& !task.groups.is_empty()
|
||||
{
|
||||
let mut groups = task.groups.iter().collect::<Vec<_>>();
|
||||
groups.sort_by(|a, b| {
|
||||
compare(Some(&b.amounts), Some(&a.amounts), metric)
|
||||
});
|
||||
row.push(Line::default());
|
||||
row.push(columns(
|
||||
" Model / effort / speed".bold().into(),
|
||||
METRICS[metric].bold().into(),
|
||||
width,
|
||||
));
|
||||
for group in groups {
|
||||
let model = self
|
||||
.model_name(group.model.as_deref().unwrap_or("Not reported"));
|
||||
let value = amount(Some(&group.amounts), metric);
|
||||
row.push(columns(
|
||||
truncate(
|
||||
format!(" {model}").bold().into(),
|
||||
width.saturating_sub(value.len() + 1),
|
||||
),
|
||||
value.into(),
|
||||
width,
|
||||
));
|
||||
row.push(
|
||||
format!(
|
||||
" {} · {} · {}",
|
||||
group.reasoning_effort.as_deref().unwrap_or("Not reported"),
|
||||
group.speed.as_deref().unwrap_or("Not reported"),
|
||||
group
|
||||
.product_experience
|
||||
.as_deref()
|
||||
.unwrap_or("Not reported")
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row.push(
|
||||
" Task usage unavailable."
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
row.push(Line::default());
|
||||
}
|
||||
wrap(row)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut coverage = vec![
|
||||
"Local chats · includes discovered descendants · excludes archived roots"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
];
|
||||
if chats.truncated {
|
||||
coverage.push(
|
||||
"Partial ranking · 100 most recently active chats"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(time) = chats.updated_at {
|
||||
coverage.push(
|
||||
format!(
|
||||
"Updated {} UTC · recent activity may be delayed",
|
||||
time.format("%b %-d %H:%M")
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if !self.zoomed {
|
||||
let count = rows.len();
|
||||
lines.extend(rows.into_iter().take(/*n*/ 5).flatten());
|
||||
lines.push(Line::default());
|
||||
lines.push(format!("Showing top {} of {count} chats", count.min(/*other*/ 5)).into());
|
||||
lines.extend(wrap(coverage));
|
||||
(lines, 0..1)
|
||||
} else {
|
||||
self.chat_window(lines, rows, wrap(coverage), width)
|
||||
}
|
||||
}
|
||||
}
|
||||
244
codex-rs/tui/src/analytics/tasks.rs
Normal file
244
codex-rs/tui/src/analytics/tasks.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
//! Consumer task queries preserve missing amounts and require complete descendant groups.
|
||||
use super::client::Live;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_backend_client::TaskUsage;
|
||||
use codex_backend_client::TaskUsageThread;
|
||||
use futures::StreamExt;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) struct Chat {
|
||||
pub title: String,
|
||||
pub task: Option<TaskUsage>,
|
||||
}
|
||||
impl Chat {
|
||||
/// Local history can span accounts; unavailable task data does not verify ownership.
|
||||
pub(super) fn display_title(&self) -> &str {
|
||||
if self.task.as_ref().is_some_and(|task| {
|
||||
task.data_status != codex_backend_client::TaskUsageStatus::Unavailable
|
||||
}) {
|
||||
&self.title
|
||||
} else {
|
||||
"Chat usage unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Chats {
|
||||
pub rows: Vec<Chat>,
|
||||
pub truncated: bool,
|
||||
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
pub(super) async fn read(
|
||||
handle: AppServerRequestHandle,
|
||||
live: Arc<Live>,
|
||||
) -> Result<Option<Chats>, String> {
|
||||
let session = live.session().await?;
|
||||
if session.kind != super::models::AccountKind::Consumer {
|
||||
return Ok(None);
|
||||
}
|
||||
session.backend.ensure_identity().await?;
|
||||
let mut roots = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(/*secs*/ 60),
|
||||
super::chats::roots(&handle),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "Chat listing timed out. Press R to retry.".to_string())??;
|
||||
session.backend.ensure_identity().await?;
|
||||
roots.sort_by(|a, b| {
|
||||
b.updated_at
|
||||
.cmp(&a.updated_at)
|
||||
.then_with(|| a.id.cmp(&b.id))
|
||||
});
|
||||
let truncated = roots.len() > 100;
|
||||
roots.truncate(/*len*/ 100);
|
||||
let discoveries = roots
|
||||
.iter()
|
||||
.map(|root| descendants(&handle, root))
|
||||
.collect::<Vec<_>>();
|
||||
let mut pending = futures::stream::iter(discoveries).buffer_unordered(/*n*/ 2);
|
||||
let mut queries = Vec::new();
|
||||
// Keep complete groups collected before the deadline; never query partial descendants.
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(/*secs*/ 25);
|
||||
while let Ok(Some(result)) = tokio::time::timeout_at(deadline, pending.next()).await {
|
||||
if let Ok(query) = result {
|
||||
queries.push(query);
|
||||
}
|
||||
}
|
||||
drop(pending);
|
||||
let mut seen = HashSet::new();
|
||||
let mut batches = Vec::new();
|
||||
let mut batch = Vec::new();
|
||||
let mut count = 0;
|
||||
for query in queries {
|
||||
let ids = std::iter::once(&query.thread_id).chain(&query.descendant_thread_ids);
|
||||
if ids.clone().any(|id| !seen.insert(id.clone())) {
|
||||
return Err("Task descendant groups overlap.".into());
|
||||
}
|
||||
let size = 1 + query.descendant_thread_ids.len();
|
||||
if count + size > 1_000 || batch.len() == 100 {
|
||||
batches.push(std::mem::take(&mut batch));
|
||||
count = 0;
|
||||
}
|
||||
count += size;
|
||||
batch.push(query);
|
||||
}
|
||||
if !batch.is_empty() {
|
||||
batches.push(batch);
|
||||
}
|
||||
let requests = batches
|
||||
.into_iter()
|
||||
.map(|threads| async move {
|
||||
session
|
||||
.backend
|
||||
.request(|client| {
|
||||
let threads = threads.clone();
|
||||
async move { client.get_task_usage(&threads).await }
|
||||
})
|
||||
.await
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected_batches = requests.len();
|
||||
let mut completed_batches = 0;
|
||||
let mut pending = futures::stream::iter(requests).buffer_unordered(/*n*/ 2);
|
||||
let mut usage = HashMap::new();
|
||||
let mut freshness = Vec::new();
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(/*secs*/ 25);
|
||||
while let Ok(Some(result)) = tokio::time::timeout_at(deadline, pending.next()).await {
|
||||
completed_batches += 1;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
freshness.push(
|
||||
response
|
||||
.data_as_of
|
||||
.as_deref()
|
||||
.map(chrono::DateTime::parse_from_rfc3339)
|
||||
.transpose()
|
||||
.map_err(|_| "Invalid task usage timestamp.")?
|
||||
.map(|time| time.with_timezone(&chrono::Utc)),
|
||||
);
|
||||
usage.extend(
|
||||
response
|
||||
.threads
|
||||
.into_iter()
|
||||
.map(|task| (task.thread_id.clone(), task)),
|
||||
);
|
||||
}
|
||||
Err(error)
|
||||
if error
|
||||
.status()
|
||||
.is_some_and(|status| matches!(status.as_u16(), 404 | 503)) =>
|
||||
{
|
||||
freshness.push(/*value*/ None);
|
||||
}
|
||||
Err(error) => return Err(super::client::request_error(error)),
|
||||
}
|
||||
}
|
||||
if completed_batches < expected_batches {
|
||||
freshness.push(/*value*/ None);
|
||||
}
|
||||
drop(pending);
|
||||
session.backend.ensure_identity().await?;
|
||||
Ok(Some(Chats {
|
||||
truncated,
|
||||
updated_at: if freshness.iter().all(Option::is_some) {
|
||||
freshness.into_iter().flatten().min()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
rows: roots
|
||||
.into_iter()
|
||||
.map(|thread| Chat {
|
||||
task: usage.remove(&thread.id),
|
||||
title: thread
|
||||
.name
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
if thread.preview.trim().is_empty() {
|
||||
"Untitled chat".into()
|
||||
} else {
|
||||
thread.preview
|
||||
}
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn descendants(
|
||||
handle: &AppServerRequestHandle,
|
||||
root: &Thread,
|
||||
) -> Result<TaskUsageThread, String> {
|
||||
let mut ids = HashSet::new();
|
||||
let mut parents = HashMap::new();
|
||||
for archived in [false, true] {
|
||||
let mut cursor = None;
|
||||
let mut seen = HashSet::new();
|
||||
loop {
|
||||
let page: ThreadListResponse = handle
|
||||
.request_typed(ClientRequest::ThreadList {
|
||||
request_id: RequestId::String(uuid::Uuid::new_v4().to_string()),
|
||||
params: ThreadListParams {
|
||||
ancestor_thread_id: Some(root.id.clone()),
|
||||
archived: Some(archived),
|
||||
cursor,
|
||||
limit: Some(100),
|
||||
..super::chats::list_params()
|
||||
},
|
||||
})
|
||||
.await
|
||||
.map_err(super::data::error)?;
|
||||
for thread in page.data {
|
||||
let parent = thread
|
||||
.parent_thread_id
|
||||
.filter(|_| thread.id != root.id)
|
||||
.ok_or("Task descendant discovery is unsupported.")?;
|
||||
parents.insert(thread.id.clone(), parent);
|
||||
ids.insert(thread.id);
|
||||
}
|
||||
if ids.len() >= 1_000 {
|
||||
return Err("Task exceeds the reporting limit.".into());
|
||||
}
|
||||
match page.next_cursor {
|
||||
Some(next) if seen.insert(next.clone()) => cursor = Some(next),
|
||||
Some(_) => return Err("Task listing repeated a cursor.".into()),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Older servers can ignore the ancestor filter. Prove every returned chain reaches this root.
|
||||
for id in &ids {
|
||||
let mut current = id;
|
||||
let mut chain = HashSet::new();
|
||||
while current != &root.id {
|
||||
if !chain.insert(current) {
|
||||
return Err("Task descendant cycle.".into());
|
||||
}
|
||||
current = parents
|
||||
.get(current)
|
||||
.ok_or("Task descendant discovery is incomplete.")?;
|
||||
}
|
||||
}
|
||||
let mut descendant_thread_ids = ids.into_iter().collect::<Vec<_>>();
|
||||
descendant_thread_ids.sort();
|
||||
Ok(TaskUsageThread {
|
||||
thread_id: root.id.clone(),
|
||||
created_at: (root.created_at > 0)
|
||||
.then(|| chrono::DateTime::from_timestamp(root.created_at, /*nsecs*/ 0))
|
||||
.flatten()
|
||||
.map(|time| time.to_rfc3339()),
|
||||
descendant_thread_ids,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tasks_tests.rs"]
|
||||
mod tests;
|
||||
132
codex-rs/tui/src/analytics/tasks_tests.rs
Normal file
132
codex-rs/tui/src/analytics/tasks_tests.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! Consumer HTTP contract, descendant discovery, and partial-result regression coverage.
|
||||
|
||||
use super::super::client::tests::live;
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
#[tokio::test]
|
||||
async fn consumer_roots_include_paginated_archived_descendants_and_keep_missing_usage() {
|
||||
for (unsupported, status) in [(false, 200), (true, 200), (false, 404), (false, 503)] {
|
||||
use codex_app_server_client::RemoteAppServerClient;
|
||||
use codex_app_server_client::RemoteAppServerConnectArgs;
|
||||
use codex_app_server_client::RemoteAppServerEndpoint;
|
||||
use futures::SinkExt;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let (_home, live) = live(&server, "plus").await;
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let websocket_url = format!("ws://{}", listener.local_addr().unwrap());
|
||||
let rpc = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.unwrap();
|
||||
let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
|
||||
let mut pages = 0;
|
||||
while let Some(Ok(Message::Text(text))) = socket.next().await {
|
||||
let request: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
let result = match request["method"].as_str() {
|
||||
Some("initialize") => {
|
||||
json!({"userAgent":"analytics-test", "codexHome":"/unused"})
|
||||
}
|
||||
Some("thread/list") => {
|
||||
pages += 1;
|
||||
if pages == 1 {
|
||||
tokio::time::pause();
|
||||
tokio::time::advance(std::time::Duration::from_secs(/*secs*/ 31)).await;
|
||||
tokio::time::resume();
|
||||
}
|
||||
let params = &request["params"];
|
||||
assert_eq!(params["modelProviders"], json!([]));
|
||||
assert_ne!(params["useStateDbOnly"], json!(true));
|
||||
let ancestor = params["ancestorThreadId"].as_str();
|
||||
let archived = params["archived"] == true;
|
||||
let cursor = params["cursor"].as_str();
|
||||
let ids = match (ancestor, archived, cursor) {
|
||||
(None, false, None) => vec!["root", "missing"],
|
||||
(Some("root"), false, None) if unsupported => vec!["unrelated"],
|
||||
(Some("root"), false, None) => vec!["child"],
|
||||
(Some("root"), false, Some("next")) => vec!["grandchild"],
|
||||
(Some("root"), true, None) => vec!["archived-child"],
|
||||
(Some("missing"), _, None) => Vec::new(),
|
||||
_ => panic!("Unexpected thread listing: {params}"),
|
||||
};
|
||||
let data = ids.into_iter().map(|id| json!({
|
||||
"id":id, "sessionId":id, "parentThreadId":if id == "unrelated" { None } else if id == "grandchild" { Some("child") } else { ancestor }, "preview":id, "ephemeral":false,
|
||||
"modelProvider":"openai", "createdAt":1788220800_i64,
|
||||
"updatedAt":chrono::Utc::now().timestamp(), "status":{"type":"idle"},
|
||||
"cwd":std::env::temp_dir(), "cliVersion":"test", "source":"cli", "turns":[]
|
||||
})).collect::<Vec<_>>();
|
||||
json!({"data":data, "nextCursor":if ancestor == Some("root") && !archived && cursor.is_none() { Some("next") } else { None }})
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
socket
|
||||
.send(Message::Text(
|
||||
json!({"jsonrpc":"2.0", "id":request["id"], "result":result})
|
||||
.to_string()
|
||||
.into(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
if pages == if unsupported { 4 } else { 6 } {
|
||||
break;
|
||||
}
|
||||
}
|
||||
pages
|
||||
});
|
||||
let remote = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
|
||||
endpoint: RemoteAppServerEndpoint::WebSocket {
|
||||
websocket_url,
|
||||
auth_token: None,
|
||||
},
|
||||
client_name: "analytics-test".into(),
|
||||
client_version: "test".into(),
|
||||
experimental_api: false,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: 16,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
Mock::given(method("POST")).and(path("/backend-api/wham/usage/thread_usage/query_v2"))
|
||||
.respond_with(move |request:&wiremock::Request| {
|
||||
let mut body:serde_json::Value = request.body_json().unwrap();
|
||||
body["threads"].as_array_mut().unwrap().sort_by_key(|thread| thread["thread_id"].as_str().unwrap().to_string());
|
||||
let mut expected=json!({"threads":[
|
||||
{"thread_id":"missing", "created_at":"2026-09-01T00:00:00+00:00", "descendant_thread_ids":[]},
|
||||
{"thread_id":"root", "created_at":"2026-09-01T00:00:00+00:00", "descendant_thread_ids":["archived-child", "child", "grandchild"]}
|
||||
]});
|
||||
if unsupported { expected["threads"].as_array_mut().unwrap().retain(|thread| thread["thread_id"] == "missing"); }
|
||||
assert_eq!(body, expected);
|
||||
let mut response=json!({"data_as_of":null,"threads":[]});
|
||||
if !unsupported { response["threads"] = json!([{ "thread_id":"root", "data_status":"partial", "usage_source":"included_plan", "weekly_limit_percent":125, "five_hour_limit_percent":null, "balance_usage_credits":null, "groups":[] }]); }
|
||||
ResponseTemplate::new(status).set_body_json(response)
|
||||
}).expect(/*r*/ 1).mount(&server).await;
|
||||
let handle = AppServerRequestHandle::Remote(remote.request_handle());
|
||||
let chats = read(handle, Arc::new(live)).await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
chats.rows.len(),
|
||||
chats
|
||||
.rows
|
||||
.iter()
|
||||
.filter_map(|chat| chat.task.as_ref().map(|task| task.data_status))
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
(
|
||||
2,
|
||||
if unsupported || status != 200 {
|
||||
vec![]
|
||||
} else {
|
||||
vec![codex_backend_client::TaskUsageStatus::Partial]
|
||||
}
|
||||
)
|
||||
);
|
||||
assert_eq!(rpc.await.unwrap(), if unsupported { 4 } else { 6 });
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ pub(super) async fn settle(view: &mut super::AnalyticsView) {
|
||||
.iter()
|
||||
.any(|state| matches!(state.history, Load::Loading(_)))
|
||||
&& !matches!(view.chats, Load::Loading(_))
|
||||
&& !matches!(view.tasks, Load::Loading(_))
|
||||
&& !matches!(view.plan.report, Load::Loading(_))
|
||||
{
|
||||
break;
|
||||
|
||||
@@ -7,6 +7,9 @@ mod dashboard;
|
||||
#[path = "analytics/account_layout_tests.rs"]
|
||||
mod account_layout;
|
||||
|
||||
#[path = "analytics/consumer_refresh_tests.rs"]
|
||||
mod consumer_refresh;
|
||||
|
||||
#[path = "analytics/tool_panel_tests.rs"]
|
||||
mod tools_panel;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tui/src/analytics_tests.rs
|
||||
assertion_line: 628
|
||||
expression: "snapshots.join(\"\\n\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" Total usage history [Messages] Plugins called "
|
||||
" Skills used "
|
||||
" Skills used Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
@@ -36,7 +37,7 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day "
|
||||
" tab/1–5 section · z dashboard · ←/→ day "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc "
|
||||
" back · q close "
|
||||
" "
|
||||
@@ -45,7 +46,7 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" Total usage history [Messages] Plugins called "
|
||||
" Skills used "
|
||||
" Skills used Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
@@ -75,7 +76,7 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day "
|
||||
" tab/1–5 section · z dashboard · ←/→ day "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc "
|
||||
" back · q close "
|
||||
" "
|
||||
@@ -83,7 +84,7 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -116,13 +117,13 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day "
|
||||
" tab/1–5 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 "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -155,5 +156,5 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day "
|
||||
" tab/1–5 section · z dashboard · ←/→ day "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics_tests.rs
|
||||
assertion_line: 345
|
||||
expression: "format!(\"{overview}\\n{skills}\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
@@ -10,14 +11,9 @@ expression: "format!(\"{overview}\\n{skills}\")"
|
||||
" │ 7d │ │ 7d │ "
|
||||
" │ By feature │ │ By model │ "
|
||||
" │ Total usage · relative units │ │ 574 reported messages │ "
|
||||
" │ 86 │ │ │ "
|
||||
" │ 100 ▂▂▂▂▂ █████ │ │ 150 │ "
|
||||
" │ █████ ▅▅▅▅▅ █████ ██████ │ │ 103 │ "
|
||||
" │ █████ ▃▃▃▃▃ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ ▆▆▆▆▆▆ │ "
|
||||
" │ 50 █████ █████ █████ █████ █████ ██████ │ │ 75 ▆▆▆▆▆ █████ ▄▄▄▄▄ ▇▇▇▇▇ ██████ │ "
|
||||
" │ █████ █████ █████ █████ █████ ██████ │ │ ▇▇▇▇▇ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ "
|
||||
" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
|
||||
" │ █████ █████ █████ █████ █████ █████ ██████ │ │ █████ █████ █████ █████ █████ █████ ██████ │ "
|
||||
" │ 86 │ │ 103 │ "
|
||||
" │ 100 ▃▃▃▃▃ ▂▂▂▂▂ ▆▆▆▆▆ ▄▄▄▄▄ █████ ▅▅▅▅▅▅ │ │ 150 ▃▃▃▃▃ ▁▁▁▁▁ ▂▂▂▂▂▂ │ "
|
||||
" │ ▃▃▃▃▃ █████ █████ █████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆ █████ ██████ │ "
|
||||
" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ───────────────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
|
||||
@@ -34,14 +30,9 @@ expression: "format!(\"{overview}\\n{skills}\")"
|
||||
" │ │ │ │ "
|
||||
" │ 7d │ │ 7d │ "
|
||||
" │ 186 calls │ │ 54 uses │ "
|
||||
" │ │ │ │ "
|
||||
" │ 40 29 │ │ 20 │ "
|
||||
" │ ▇▇▇▇▇ │ │ │ "
|
||||
" │ █████ █████ ▄▄▄▄▄▄ ██████ │ │ 11 │ "
|
||||
" │ 20 █████ █████ █████ ██████ ▆▆▆▆▆ █████ ██████ │ │ 10 ▁▁▁▁▁ ▆▆▆▆▆▆ │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ █████ ▆▆▆▆▆▆ ▃▃▃▃▃ ██████ │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ ▆▆▆▆▆ ██████ │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ █████ █████ █████ ██████ █████ █████ ██████ │ "
|
||||
" │ 29 │ │ 11 │ "
|
||||
" │ 40 ▅▅▅▅▅ ▁▁▁▁▁ ▃▃▃▃▃ ▂▂▂▂▂▂ ▁▁▁▁▁ ▃▃▃▃▃▃ │ │ 20 │ "
|
||||
" │ █████ █████ █████ ██████ █████ █████ ██████ │ │ ▆▆▆▆▆ ▄▄▄▄▄ ▇▇▇▇▇ ▆▆▆▆▆▆ ▅▅▅▅▅ ▄▄▄▄▄ ██████ │ "
|
||||
" │ 0 ────────────────────────────────────────────────────────── │ │ 0 ────────────────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
|
||||
@@ -55,22 +46,32 @@ expression: "format!(\"{overview}\\n{skills}\")"
|
||||
" │ │ │ │ "
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · enter/z maximize "
|
||||
" ╭─ 5 Top chats ────────────────────────────────────────────────╮ "
|
||||
" │ │ "
|
||||
" │ No history has been reported. │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" ╰────────────────────────────────────────────────────────────────╯ "
|
||||
" tab/1–5 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] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -97,5 +98,5 @@ expression: "format!(\"{overview}\\n{skills}\")"
|
||||
" ● Other 3 "
|
||||
" ● Debugging 1 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics_tests.rs
|
||||
assertion_line: 74
|
||||
expression: tools
|
||||
---
|
||||
" Analytics · Live account "
|
||||
@@ -19,6 +20,6 @@ expression: tools
|
||||
" │ ● Testing 4 │ "
|
||||
" │ ● Code review 3 │ "
|
||||
" │ │ "
|
||||
" tab/1–4 section · enter/z maximize "
|
||||
" tab/1–5 section · enter/z maximize "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q "
|
||||
" close "
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/analytics_tests.rs
|
||||
assertion_line: 46
|
||||
expression: wide
|
||||
---
|
||||
" Analytics · Live account "
|
||||
@@ -10,14 +11,8 @@ expression: wide
|
||||
" │ 7d │ │ 7d │ "
|
||||
" │ By feature │ │ By model │ "
|
||||
" │ Total usage · relative units │ │ 574 reported messages │ "
|
||||
" │ 86 │ │ │ "
|
||||
" │ 100 ▂▂▂▂ ████ │ │ 150 │ "
|
||||
" │ ████ ▅▅▅▅ ████ ████ │ │ 103 │ "
|
||||
" │ ████ ▃▃▃▃ ████ ████ ████ ████ │ │ ▇▇▇▇ ▆▆▆▆ │ "
|
||||
" │ 50 ████ ████ ████ ████ ████ ████ │ │ 75 ▆▆▆▆ ████ ▄▄▄▄ ▇▇▇▇ ████ │ "
|
||||
" │ ████ ████ ████ ████ ████ ████ │ │ ▇▇▇▇ ████ ████ ████ ▆▆▆▆ ████ ████ │ "
|
||||
" │ ▃▃▃▃ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ "
|
||||
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ "
|
||||
" │ 86 │ │ 103 │ "
|
||||
" │ 100 ▁▁▁▁ ▅▅▅▅ ▅▅▅▅ ▇▇▇▇ ▆▆▆▆ ████ ▆▆▆▆ │ │ 150 ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ ▄▄▄▄ ▃▃▃▃ ▄▄▄▄ ▅▅▅▅ │ "
|
||||
" │ 0 ─────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ "
|
||||
@@ -27,21 +22,14 @@ expression: wide
|
||||
" │ ● 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 ▁▁▁▁ ▆▆▆▆ │ "
|
||||
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ▆▆▆▆ ████ ▆▆▆▆ ▃▃▃▃ ████ │ "
|
||||
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ▆▆▆▆ ████ │ "
|
||||
" │ ████ ████ ████ ████ ████ ████ ████ │ │ ████ ████ ████ ████ ████ ████ ████ │ "
|
||||
" │ 29 │ │ 11 │ "
|
||||
" │ 40 ▆▆▆▆ ▄▄▄▄ ▅▅▅▅ ▅▅▅▅ ▄▄▄▄ ▄▄▄▄ ▅▅▅▅ │ │ 20 ▃▃▃▃ ▂▂▂▂ ▃▃▃▃ ▃▃▃▃ ▂▂▂▂ ▂▂▂▂ ▄▄▄▄ │ "
|
||||
" │ 0 ──────────────────────────────────────────────── │ │ 0 ──────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ 27 28 29 30 31 1 2 │ │ 27 28 29 30 31 1 2 │ "
|
||||
@@ -52,13 +40,26 @@ expression: wide
|
||||
" │ │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" ╰──────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" ╭─ 5 Top chats ──────────────────────────────────────╮ "
|
||||
" │ │ "
|
||||
" │ No history has been reported. │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" ╰──────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · enter/z maximize "
|
||||
" tab/1–5 section · enter/z maximize "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
source: tui/src/analytics_tests.rs
|
||||
assertion_line: 573
|
||||
expression: selected_day
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used "
|
||||
" [1 Total usage history] 2 Messages 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Total usage history By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -38,5 +39,5 @@ expression: selected_day
|
||||
" ● Remote Other 2.1% "
|
||||
" … 3 more 0.0% "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" tab/1–5 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