mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
Add Top chats to usage analytics (#45765)
## What changed - Show local chats active in the past 30 days, ranked by estimated lifetime credits, for supported Business and Enterprise plans. Exclude archived chats and subagents. - Add expandable model, reasoning effort, and speed breakdowns, optional zero-credit groups, and dollar estimates when supplied by the backend. - Load estimates in bounded batches of up to 100 distinct threads, retaining available results when some estimates are unavailable. Hide chat titles without a returned estimate and distinguish missing usage from zero. ## Testing Add coverage for batch validation, partial failures, request limits, pagination and ranking, plan eligibility, title visibility, and responsive table navigation and details. GitOrigin-RevId: 783b960a780f4e0fd39f57e2fa78fb31b3090c44
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! Authoritative estimated credit and dollar usage for an individual Codex thread.
|
||||
//! Authoritative estimated credit and dollar usage for bounded batches of Codex threads.
|
||||
|
||||
use super::Client;
|
||||
use super::PathStyle;
|
||||
@@ -36,39 +36,77 @@ pub struct ThreadUsage {
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ThreadUsageQueryRequest<'a> {
|
||||
thread_ids: [&'a str; 1],
|
||||
thread_ids: &'a [&'a str],
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ThreadUsageQueryResponse {
|
||||
threads: Vec<ThreadUsage>,
|
||||
threads: Vec<ThreadUsageEstimate>,
|
||||
}
|
||||
|
||||
// Preserve unavailable rows until all returned IDs have been validated.
|
||||
#[derive(Deserialize)]
|
||||
struct ThreadUsageEstimate {
|
||||
thread_id: String,
|
||||
estimated_usage_credits_micros: Option<i64>,
|
||||
estimated_usage_usd_micros: Option<i64>,
|
||||
groups: Option<Vec<ThreadUsageBreakdownGroup>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Reads authoritative estimated totals without maintaining a second usage ledger.
|
||||
pub async fn get_thread_usage(&self, thread_id: &str) -> Result<ThreadUsage, RequestError> {
|
||||
self.get_threads_usage(&[thread_id])
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|usage| usage.thread_id == thread_id)
|
||||
.ok_or_else(|| {
|
||||
RequestError::from(anyhow!("thread usage response omitted requested thread"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads at most 100 distinct threads; omitted results are unavailable, not zero.
|
||||
pub async fn get_threads_usage(
|
||||
&self,
|
||||
thread_ids: &[&str],
|
||||
) -> Result<Vec<ThreadUsage>, RequestError> {
|
||||
let requested: std::collections::HashSet<_> = thread_ids.iter().copied().collect();
|
||||
if thread_ids.is_empty() || thread_ids.len() > 100 || requested.len() != thread_ids.len() {
|
||||
return Err(RequestError::from(anyhow!(
|
||||
"expected 1–100 distinct thread IDs"
|
||||
)));
|
||||
}
|
||||
let url = self.thread_usage_url();
|
||||
let request = self
|
||||
.request(Method::POST, &url)
|
||||
.headers(self.headers())
|
||||
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
|
||||
.json(&ThreadUsageQueryRequest {
|
||||
thread_ids: [thread_id],
|
||||
});
|
||||
let (body, content_type) = self.exec_request_detailed(request, "POST", &url).await?;
|
||||
let response = self
|
||||
.decode_json::<ThreadUsageQueryResponse>(&url, &content_type, &body)
|
||||
.map_err(RequestError::from)?;
|
||||
|
||||
response
|
||||
.json(&ThreadUsageQueryRequest { thread_ids });
|
||||
let (body, _) = self.exec_request_detailed(request, "POST", &url).await?;
|
||||
let response: ThreadUsageQueryResponse = serde_json::from_str(&body)
|
||||
.map_err(|_| RequestError::Other(anyhow!("Invalid thread usage response.")))?;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
if response
|
||||
.threads
|
||||
.iter()
|
||||
.any(|row| !requested.contains(row.thread_id.as_str()) || !seen.insert(&row.thread_id))
|
||||
{
|
||||
return Err(RequestError::from(anyhow!(
|
||||
"thread usage returned unexpected threads"
|
||||
)));
|
||||
}
|
||||
Ok(response
|
||||
.threads
|
||||
.into_iter()
|
||||
.find(|usage| usage.thread_id == thread_id)
|
||||
.ok_or_else(|| {
|
||||
RequestError::from(anyhow!(
|
||||
"thread usage response did not contain requested thread {thread_id}"
|
||||
))
|
||||
.filter_map(|row| {
|
||||
Some(ThreadUsage {
|
||||
thread_id: row.thread_id,
|
||||
estimated_usage_credits_micros: row.estimated_usage_credits_micros?,
|
||||
estimated_usage_usd_micros: row.estimated_usage_usd_micros,
|
||||
groups: row.groups.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn thread_usage_url(&self) -> String {
|
||||
|
||||
@@ -23,7 +23,7 @@ fn thread_usage_contract_uses_expected_paths_and_payload() {
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(ThreadUsageQueryRequest {
|
||||
thread_ids: ["thread-123"],
|
||||
thread_ids: &["thread-123"],
|
||||
})
|
||||
.expect("serialize thread usage request"),
|
||||
json!({ "thread_ids": ["thread-123"] })
|
||||
@@ -142,5 +142,59 @@ async fn get_thread_usage_rejects_totals_for_another_thread() {
|
||||
.get_thread_usage("thread-123")
|
||||
.await
|
||||
.expect_err("reject usage for a different thread");
|
||||
assert!(error.to_string().contains("requested thread thread-123"));
|
||||
assert!(error.to_string().contains("unexpected threads"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_usage_rejects_invalid_requests_before_http() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.respond_with(ResponseTemplate::new(/*s*/ 500))
|
||||
.expect(/*r*/ 0)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let client = Client::new(
|
||||
server.uri(),
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
);
|
||||
let too_many = (0..101)
|
||||
.map(|id| format!("thread-{id}"))
|
||||
.collect::<Vec<_>>();
|
||||
for ids in [
|
||||
Vec::new(),
|
||||
vec!["duplicate", "duplicate"],
|
||||
too_many.iter().map(String::as_str).collect(),
|
||||
] {
|
||||
let error = client.get_threads_usage(&ids).await.unwrap_err();
|
||||
assert!(error.to_string().contains("1–100 distinct thread IDs"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_usage_rejects_duplicate_and_unrequested_response_rows() {
|
||||
for returned in [["first", "first"], ["first", "unexpected"]] {
|
||||
for second_amount in [Some(1), None] {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(body_json(json!({"thread_ids": ["first", "second"]})))
|
||||
.respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({
|
||||
"threads": returned.iter().enumerate().map(|(index, thread_id)| json!({
|
||||
"thread_id": thread_id,
|
||||
"estimated_usage_credits_micros": if index == 0 { Some(1) } else { second_amount }
|
||||
})).collect::<Vec<_>>()
|
||||
})))
|
||||
.expect(/*r*/ 1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let client = Client::new(
|
||||
server.uri(),
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
);
|
||||
let error = client
|
||||
.get_threads_usage(&["first", "second"])
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("unexpected threads"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! A maximized section and an overview share bounded report loads, selection, and inline details.
|
||||
|
||||
mod chart;
|
||||
mod chat_panel;
|
||||
mod chats;
|
||||
mod client;
|
||||
mod dashboard;
|
||||
mod data;
|
||||
@@ -44,7 +46,9 @@ use sections::SectionStates;
|
||||
pub(crate) struct AnalyticsView {
|
||||
model_names: std::collections::HashMap<String, String>,
|
||||
sections: SectionStates,
|
||||
account: Load<models::AccountKind>,
|
||||
chats: Load<chats::Chats>,
|
||||
show_zero_credit_groups: bool,
|
||||
account: Load<codex_protocol::account::PlanType>,
|
||||
reports_started: bool,
|
||||
connection: Option<(
|
||||
std::sync::Arc<crate::legacy_core::config::Config>,
|
||||
@@ -70,6 +74,8 @@ impl AnalyticsView {
|
||||
let mut view = Self {
|
||||
model_names: std::collections::HashMap::new(),
|
||||
sections: SectionStates(std::array::from_fn(|_| SectionState::default())),
|
||||
chats: Load::Unavailable,
|
||||
show_zero_credit_groups: false,
|
||||
account: Load::Unavailable,
|
||||
reports_started: false,
|
||||
connection: None,
|
||||
@@ -87,6 +93,7 @@ impl AnalyticsView {
|
||||
is_done: false,
|
||||
};
|
||||
view.sections[Section::Usage].group = 1;
|
||||
view.sections[Section::Chats].cursor = 0;
|
||||
view.sections[Section::Activity].group = 2;
|
||||
view.refresh();
|
||||
view
|
||||
@@ -120,12 +127,24 @@ impl AnalyticsView {
|
||||
for section in &mut self.sections.0 {
|
||||
section.history = Load::Unavailable;
|
||||
}
|
||||
self.sections[Section::Chats].detail = None;
|
||||
self.chats = Load::Unavailable;
|
||||
self.reports_started = false;
|
||||
self.token_model = None;
|
||||
self.account = if let (Some((_, _, frame)), Some(live)) = (&self.connection, &self.live) {
|
||||
let live = std::sync::Arc::clone(live);
|
||||
Load::start(
|
||||
async move { live.session().await.map(|session| Some(session.kind)) },
|
||||
async move {
|
||||
live.session().await.map(|session| {
|
||||
Some(
|
||||
session
|
||||
.backend
|
||||
.account()
|
||||
.plan_type
|
||||
.unwrap_or(codex_protocol::account::PlanType::Unknown),
|
||||
)
|
||||
})
|
||||
},
|
||||
frame.clone(),
|
||||
)
|
||||
} else {
|
||||
@@ -139,6 +158,7 @@ impl AnalyticsView {
|
||||
for section in &mut self.sections.0 {
|
||||
section.history = Load::Unavailable;
|
||||
}
|
||||
self.chats = Load::Unavailable;
|
||||
self.account = Load::Unavailable;
|
||||
self.live = None;
|
||||
self.connection = None;
|
||||
@@ -184,7 +204,7 @@ impl AnalyticsView {
|
||||
Section::Usage => &[0, 2],
|
||||
Section::Credits => self.live.as_ref().map_or(&[0], |live| live.credit_groups()),
|
||||
Section::Activity => &[2, 0],
|
||||
Section::Plugins | Section::Skills => &[],
|
||||
Section::Plugins | Section::Skills | Section::Chats => &[],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +225,7 @@ impl AnalyticsView {
|
||||
Section::Credits => "Credits usage history",
|
||||
Section::Activity => "Messages",
|
||||
Section::Skills => "Skills used",
|
||||
Section::Chats => "Top chats",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +242,11 @@ impl AnalyticsView {
|
||||
}
|
||||
|
||||
fn row_count(&self) -> usize {
|
||||
if self.ranges[self.range_group(self.section) as usize] == 0 {
|
||||
if self.section == Section::Chats {
|
||||
self.chats
|
||||
.ready()
|
||||
.map_or(/*default*/ 0, |chats| chats.rows.len())
|
||||
} else if self.ranges[self.range_group(self.section) as usize] == 0 {
|
||||
7
|
||||
} else {
|
||||
30
|
||||
@@ -283,6 +308,13 @@ impl AnalyticsView {
|
||||
self.refresh();
|
||||
return;
|
||||
}
|
||||
if self.section == Section::Chats
|
||||
&& self.sections[Section::Chats].detail.is_some()
|
||||
&& key_hint::plain(KeyCode::Char('a')).is_press(key)
|
||||
{
|
||||
self.show_zero_credit_groups = !self.show_zero_credit_groups;
|
||||
return;
|
||||
}
|
||||
if self.section == Section::Usage
|
||||
&& self.business()
|
||||
&& key_hint::plain(KeyCode::Char('m')).is_press(key)
|
||||
@@ -302,7 +334,9 @@ impl AnalyticsView {
|
||||
self.load_report(Section::Usage);
|
||||
return;
|
||||
}
|
||||
if key_hint::plain(KeyCode::Char('r')).is_press(key) && !self.visible_sections().is_empty()
|
||||
if key_hint::plain(KeyCode::Char('r')).is_press(key)
|
||||
&& self.section != Section::Chats
|
||||
&& !self.visible_sections().is_empty()
|
||||
{
|
||||
self.change_range();
|
||||
return;
|
||||
@@ -347,22 +381,37 @@ 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()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let chart = self.section != Section::Chats;
|
||||
let count = self.row_count().max(/*other*/ 1);
|
||||
let SectionState { cursor, detail, .. } = &mut self.sections[self.section];
|
||||
let previous_cursor = *cursor;
|
||||
match action {
|
||||
Some(ListAction::MoveUp) => *cursor = cursor.saturating_sub(/*rhs*/ 1),
|
||||
Some(ListAction::MoveDown) => *cursor = (*cursor + 1).min(count - 1),
|
||||
Some(ListAction::MoveLeft) => *cursor = cursor.saturating_sub(/*rhs*/ 1),
|
||||
Some(ListAction::MoveRight) => *cursor = (*cursor + 1).min(count - 1),
|
||||
Some(ListAction::MoveLeft) if chart => *cursor = cursor.saturating_sub(/*rhs*/ 1),
|
||||
Some(ListAction::MoveRight) if chart => *cursor = (*cursor + 1).min(count - 1),
|
||||
Some(ListAction::JumpTop) => *cursor = 0,
|
||||
Some(ListAction::JumpBottom) => *cursor = count - 1,
|
||||
Some(ListAction::Accept) => {
|
||||
Some(ListAction::Accept | ListAction::MoveRight) => {
|
||||
*detail = if *detail == Some(*cursor) {
|
||||
None
|
||||
} else {
|
||||
Some(*cursor)
|
||||
};
|
||||
}
|
||||
Some(ListAction::MoveLeft) => *detail = None,
|
||||
Some(ListAction::Cancel) => {
|
||||
self.is_done |= !self.zoomed || detail.take().is_none();
|
||||
}
|
||||
@@ -376,8 +425,10 @@ impl AnalyticsView {
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if detail.is_some() {
|
||||
if chart && detail.is_some() {
|
||||
*detail = Some(*cursor);
|
||||
} else if !chart && *cursor != previous_cursor {
|
||||
*detail = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,6 +446,7 @@ impl AnalyticsView {
|
||||
section.history = Load::Unavailable;
|
||||
}
|
||||
}
|
||||
self.chats = Load::Unavailable;
|
||||
self.account = Load::Unavailable;
|
||||
self.connection = None;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ fn account_layouts_show_reports_and_wrap_navigation() {
|
||||
Section::Usage,
|
||||
Section::Plugins,
|
||||
Section::Skills,
|
||||
Section::Chats,
|
||||
],
|
||||
),
|
||||
] {
|
||||
@@ -65,7 +66,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() {
|
||||
view.sections[Section::Usage].detail,
|
||||
view.sections[Section::Usage].group
|
||||
),
|
||||
([1, 0, 0], [28, 6, 6, 6, 6], Some(28), 2)
|
||||
([1, 0, 0], [28, 6, 6, 0, 6, 6], Some(28), 2)
|
||||
);
|
||||
let context = (
|
||||
view.ranges,
|
||||
@@ -94,7 +95,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() {
|
||||
.0
|
||||
.each_ref()
|
||||
.map(|state| state.history.ready().map(|history| history.data.len())),
|
||||
[Some(30), Some(7), None, Some(30), Some(7)]
|
||||
[Some(30), Some(7), None, None, Some(30), Some(7)]
|
||||
);
|
||||
let server = test_support::server().await;
|
||||
let (_business_home, _business_server, mut view) =
|
||||
@@ -111,7 +112,7 @@ async fn changing_ranges_and_grouping_preserves_other_reports_and_focus() {
|
||||
.0
|
||||
.each_ref()
|
||||
.map(|state| state.history.ready().map(|history| history.data.len())),
|
||||
[Some(30), Some(7), Some(7), None, Some(7)]
|
||||
[Some(30), Some(7), Some(7), None, None, Some(7)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,9 +222,9 @@ async fn account_reports_request_only_eligible_endpoints_and_refresh() {
|
||||
assert_eq!(
|
||||
ready,
|
||||
if plan == "plus" {
|
||||
[true, true, false, true, true]
|
||||
[true, true, false, false, true, true]
|
||||
} else {
|
||||
[true, true, true, false, true]
|
||||
[true, true, true, false, false, true]
|
||||
}
|
||||
);
|
||||
press(&mut view, KeyCode::Char('R'));
|
||||
@@ -253,7 +254,7 @@ async fn account_reports_request_only_eligible_endpoints_and_refresh() {
|
||||
async fn legacy_response_closes_an_attribution_picker_that_is_no_longer_valid() {
|
||||
let server = test_support::server().await;
|
||||
let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await;
|
||||
view.account = Load::Ready(models::AccountKind::Consumer);
|
||||
view.account = Load::Ready(codex_protocol::account::PlanType::Plus);
|
||||
view.start_reports();
|
||||
view.group_picker = Some(3);
|
||||
test_support::settle(&mut view).await;
|
||||
@@ -273,7 +274,7 @@ async fn legacy_response_invalidates_picker_before_the_next_draw() {
|
||||
let server = test_support::server().await;
|
||||
for choice in [2, 3] {
|
||||
let (_home, _app_server, mut view) = client::tests::connected_view(&server, "plus").await;
|
||||
view.account = Load::Ready(models::AccountKind::Consumer);
|
||||
view.account = Load::Ready(codex_protocol::account::PlanType::Plus);
|
||||
view.start_reports();
|
||||
view.group_picker = Some(choice);
|
||||
// Complete the request without polling the view, as can happen between key events.
|
||||
@@ -290,3 +291,77 @@ async fn legacy_response_invalidates_picker_before_the_next_draw() {
|
||||
assert_eq!(view.sections[Section::Usage].group, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn business_chats_display_only_supplied_dollars() {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
let chats = match &mut view.chats {
|
||||
Load::Ready(chats) => chats,
|
||||
_ => panic!("fixture chats"),
|
||||
};
|
||||
for chat in &mut chats.rows {
|
||||
if let Some(usage) = &mut chat.usage {
|
||||
usage.estimated_usage_usd_micros = None;
|
||||
}
|
||||
}
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
assert!(!screen(&mut view, /*width*/ 100, /*height*/ 30).contains("Est. $ spent"));
|
||||
if let Load::Ready(chats) = &mut view.chats {
|
||||
chats.rows[0]
|
||||
.usage
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.estimated_usage_usd_micros = Some(123_450_000);
|
||||
}
|
||||
let dollars = screen(&mut view, /*width*/ 100, /*height*/ 30);
|
||||
assert!(dollars.contains("$123.45") && dollars.contains("Est. $ spent"));
|
||||
let narrow = screen(&mut view, /*width*/ 42, /*height*/ 30);
|
||||
insta::assert_snapshot!(format!("{dollars}\n{narrow}"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unsupported_workspace_plans_hide_top_chats_and_skip_loading() {
|
||||
for plan in [
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"enterprise",
|
||||
"ent26",
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
] {
|
||||
let server = test_support::server().await;
|
||||
let (_home, _app_server, mut view) = client::tests::connected_view(&server, plan).await;
|
||||
test_support::settle(&mut view).await;
|
||||
assert_eq!(
|
||||
view.visible_sections(),
|
||||
&[
|
||||
Section::Credits,
|
||||
Section::Usage,
|
||||
Section::Plugins,
|
||||
Section::Skills
|
||||
],
|
||||
"{plan}"
|
||||
);
|
||||
assert!(matches!(view.chats, Load::Unavailable), "{plan}");
|
||||
let selected = view.section;
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
assert_eq!(view.section, selected);
|
||||
}
|
||||
for plan in [
|
||||
"business",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise_cbp_automation",
|
||||
] {
|
||||
let server = test_support::server().await;
|
||||
let (_home, _app_server, mut view) = client::tests::connected_view(&server, plan).await;
|
||||
test_support::settle(&mut view).await;
|
||||
assert!(view.visible_sections().contains(&Section::Chats), "{plan}");
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
assert_eq!(view.section, Section::Chats);
|
||||
}
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
view.account = Load::Ready(codex_protocol::account::PlanType::Enterprise);
|
||||
insta::assert_snapshot!(screen(&mut view, /*width*/ 110, /*height*/ 24));
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ impl AnalyticsView {
|
||||
Section::Activity => "messages",
|
||||
Section::Plugins => "calls",
|
||||
Section::Skills => "uses",
|
||||
Section::Usage | Section::Credits => "credits",
|
||||
Section::Usage | Section::Credits | Section::Chats => "credits",
|
||||
};
|
||||
let empty_breakdown = matches!(
|
||||
section,
|
||||
@@ -193,6 +193,7 @@ impl AnalyticsView {
|
||||
match section {
|
||||
Section::Plugins => "Plugin",
|
||||
Section::Skills => "Skill",
|
||||
Section::Chats => "Chat",
|
||||
Section::Usage | Section::Credits | Section::Activity => {
|
||||
self.group_label(section, self.sections[section].group)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
//! Bounded chat tables with inline credit breakdowns and a viewport-sized row window.
|
||||
//! Zero-credit groups are optional; missing dimensions and signed adjustments stay distinct.
|
||||
|
||||
use super::AnalyticsView;
|
||||
use super::data;
|
||||
use super::render::columns;
|
||||
use super::styles::number;
|
||||
use super::styles::secondary_style;
|
||||
use crate::analytics::sections::Section;
|
||||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
|
||||
use crate::style::accent_style;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
use ratatui::style::Styled;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use std::ops::Range;
|
||||
|
||||
impl AnalyticsView {
|
||||
pub(super) fn chat_lines(&self, width: usize) -> (Vec<Line<'static>>, Range<usize>) {
|
||||
let width = width.clamp(/*min*/ 1, /*max*/ 96);
|
||||
let wrap = |lines: Vec<Line<'static>>| word_wrap_lines(lines, RtOptions::new(width));
|
||||
let mut lines = wrap(vec![
|
||||
"Active in past 30 days · sorted by lifetime credits"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
]);
|
||||
if let Some(message) = self.chats.message() {
|
||||
lines.extend(wrap(vec![
|
||||
message.to_string().set_style(secondary_style()).into(),
|
||||
]));
|
||||
}
|
||||
let Some(chats) = self.chats.ready() else {
|
||||
return (lines, 0..1);
|
||||
};
|
||||
if chats.rows.is_empty() {
|
||||
lines.extend(wrap(vec![
|
||||
"No recent local chats.".set_style(secondary_style()).into(),
|
||||
]));
|
||||
return (lines, 0..1);
|
||||
}
|
||||
let missing = chats.rows.iter().filter(|row| row.usage.is_none()).count();
|
||||
if missing > 0 {
|
||||
lines.extend(wrap(vec![
|
||||
format!(
|
||||
"{missing} chat estimate{} unavailable",
|
||||
if missing == 1 { "" } else { "s" }
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
]));
|
||||
}
|
||||
lines.push(Line::default());
|
||||
let show_usd = chats.rows.iter().any(|chat| {
|
||||
chat.usage
|
||||
.as_ref()
|
||||
.is_some_and(|usage| usage.estimated_usage_usd_micros.is_some())
|
||||
});
|
||||
let stacked_amounts = show_usd && width < 70;
|
||||
lines.extend(wrap(vec![columns(
|
||||
" Chat".bold().into(),
|
||||
if stacked_amounts {
|
||||
"Est. $ / credits"
|
||||
} else if show_usd {
|
||||
"Est. $ spent Lifetime credits"
|
||||
} else {
|
||||
"Lifetime credits"
|
||||
}
|
||||
.bold()
|
||||
.into(),
|
||||
width,
|
||||
)]));
|
||||
let rows = chats
|
||||
.rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, chat)| {
|
||||
let selected =
|
||||
self.section == Section::Chats && index == self.sections[Section::Chats].cursor;
|
||||
let amount = chat.usage.as_ref().map_or_else(
|
||||
|| "—".into(),
|
||||
|usage| data::credits(usage.estimated_usage_credits_micros),
|
||||
);
|
||||
let amount = if show_usd {
|
||||
let usd = chat
|
||||
.usage
|
||||
.as_ref()
|
||||
.and_then(|usage| usage.estimated_usage_usd_micros)
|
||||
.map(|micros| format!("${:.2}", micros as f64 / 1_000_000.0))
|
||||
.unwrap_or_else(|| "—".into());
|
||||
format!("{usd:>12} {amount:>16}")
|
||||
} else {
|
||||
amount
|
||||
};
|
||||
let label = format!(
|
||||
"{} {}",
|
||||
if selected { "›" } else { " " },
|
||||
chat.display_title()
|
||||
);
|
||||
let label: Line<'static> = if selected {
|
||||
label.set_style(accent_style()).into()
|
||||
} else {
|
||||
label.into()
|
||||
};
|
||||
let label_width = if stacked_amounts {
|
||||
width
|
||||
} else {
|
||||
width.saturating_sub(amount.len() + 1)
|
||||
};
|
||||
let truncated = label.width() > label_width;
|
||||
let mut row = if stacked_amounts {
|
||||
vec![
|
||||
truncate_line_with_ellipsis_if_overflow(label, label_width),
|
||||
format!(" {}", amount.trim()).into(),
|
||||
]
|
||||
} else {
|
||||
vec![columns(
|
||||
truncate_line_with_ellipsis_if_overflow(label, label_width),
|
||||
if chat.usage.is_none() {
|
||||
amount.set_style(secondary_style()).into()
|
||||
} else if selected {
|
||||
number(amount).into()
|
||||
} else {
|
||||
amount.into()
|
||||
},
|
||||
width,
|
||||
)]
|
||||
};
|
||||
if self.sections[Section::Chats].detail == Some(index)
|
||||
&& let Some(usage) = &chat.usage
|
||||
{
|
||||
row.push(
|
||||
format!(" {}", "─".repeat(width.saturating_sub(/*rhs*/ 2)))
|
||||
.dim()
|
||||
.into(),
|
||||
);
|
||||
if truncated {
|
||||
row.extend(word_wrap_lines(
|
||||
[chat.display_title().to_string().bold()],
|
||||
RtOptions::new(width)
|
||||
.initial_indent(" ".into())
|
||||
.subsequent_indent(" ".into()),
|
||||
));
|
||||
}
|
||||
let zeros = usage
|
||||
.groups
|
||||
.iter()
|
||||
.filter(|group| group.estimated_usage_credits_micros == 0)
|
||||
.count();
|
||||
let mut groups = usage
|
||||
.groups
|
||||
.iter()
|
||||
.filter(|group| {
|
||||
self.show_zero_credit_groups
|
||||
|| group.estimated_usage_credits_micros != 0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
groups.sort_by_key(|group| {
|
||||
std::cmp::Reverse(group.estimated_usage_credits_micros)
|
||||
});
|
||||
// On narrow terminals, retain every dimension on a second line.
|
||||
let table = width >= 70;
|
||||
if !groups.is_empty() {
|
||||
row.push(columns(
|
||||
if table {
|
||||
format!(
|
||||
" {:<model_width$} {:<12} {:<12}",
|
||||
"Model",
|
||||
"Effort",
|
||||
"Speed",
|
||||
model_width = width - 46
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into()
|
||||
} else {
|
||||
" Model / effort / speed"
|
||||
.set_style(secondary_style())
|
||||
.into()
|
||||
},
|
||||
"Credits".bold().into(),
|
||||
width,
|
||||
));
|
||||
}
|
||||
for group in groups {
|
||||
let model =
|
||||
self.model_name(group.model.as_deref().unwrap_or("Not reported"));
|
||||
let effort = group.reasoning_effort.as_deref().unwrap_or("Not reported");
|
||||
let speed = group.speed.as_deref().unwrap_or("Not reported");
|
||||
let amount = data::credits(group.estimated_usage_credits_micros);
|
||||
let label = if table {
|
||||
// Wrap long model names instead of hiding distinctions between groups.
|
||||
let model_width = width - 46;
|
||||
let models = textwrap::wrap(model, model_width);
|
||||
for model in models.iter().take(models.len().saturating_sub(/*rhs*/ 1))
|
||||
{
|
||||
row.push(format!(" {model}").bold().into());
|
||||
}
|
||||
vec![
|
||||
format!(
|
||||
" {:<model_width$}",
|
||||
models
|
||||
.last()
|
||||
.map(std::convert::AsRef::as_ref)
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.bold(),
|
||||
format!(" {effort:<12} {speed:<12}").set_style(secondary_style()),
|
||||
]
|
||||
.into()
|
||||
} else {
|
||||
let label = Line::from(format!(" {model}").bold());
|
||||
if label.width() + amount.len() + 1 > width {
|
||||
row.push(label);
|
||||
" ".into()
|
||||
} else {
|
||||
label
|
||||
}
|
||||
};
|
||||
row.push(columns(label, amount.into(), width));
|
||||
if !table {
|
||||
row.push(
|
||||
format!(" {effort} · {speed}")
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if usage.groups.is_empty() {
|
||||
row.push(
|
||||
" Breakdown unavailable"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if zeros > 0 {
|
||||
row.push(
|
||||
format!(
|
||||
" {zeros} zero-credit {}{} · a {}",
|
||||
if zeros == 1 { "group" } else { "groups" },
|
||||
if self.show_zero_credit_groups {
|
||||
""
|
||||
} else {
|
||||
" hidden"
|
||||
},
|
||||
if self.show_zero_credit_groups {
|
||||
"hide zeros"
|
||||
} else {
|
||||
"show all"
|
||||
},
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
row.push(Line::default());
|
||||
}
|
||||
wrap(row)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let coverage = wrap(vec![
|
||||
"Local chats · excludes archived chats and subagent usage"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
]);
|
||||
self.chat_window(lines, rows, coverage, width)
|
||||
}
|
||||
|
||||
pub(super) fn chat_window(
|
||||
&self,
|
||||
mut lines: Vec<Line<'static>>,
|
||||
rows: Vec<Vec<Line<'static>>>,
|
||||
coverage: Vec<Line<'static>>,
|
||||
width: usize,
|
||||
) -> (Vec<Line<'static>>, Range<usize>) {
|
||||
let wrap = |lines: Vec<Line<'static>>| word_wrap_lines(lines, RtOptions::new(width));
|
||||
let count = rows.len();
|
||||
let range_height = wrap(vec![
|
||||
format!("Showing {count}–{count} of {count} chats").into(),
|
||||
])
|
||||
.len();
|
||||
// Measure wrapped details before choosing neighbors so the range describes the rendered rows.
|
||||
let budget = if self.zoomed {
|
||||
self.viewport_height
|
||||
.saturating_sub(lines.len() + coverage.len() + range_height + 4)
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
let row_limit = if self.zoomed { count } else { 5 };
|
||||
let cursor = self.sections[Section::Chats].cursor.min(count - 1);
|
||||
let mut first = cursor;
|
||||
let mut end = cursor + 1;
|
||||
let mut used = rows[cursor].len();
|
||||
while first > 0 && end - first < row_limit && used + rows[first - 1].len() <= budget {
|
||||
first -= 1;
|
||||
used += rows[first].len();
|
||||
}
|
||||
while end < count && end - first < row_limit && used + rows[end].len() <= budget {
|
||||
used += rows[end].len();
|
||||
end += 1;
|
||||
}
|
||||
let mut selection = 0..1;
|
||||
for (index, row) in rows.into_iter().enumerate().take(end).skip(first) {
|
||||
if index == cursor {
|
||||
selection = lines.len()..lines.len() + row.len();
|
||||
}
|
||||
lines.extend(row);
|
||||
}
|
||||
lines.push(Line::default());
|
||||
lines.extend(wrap(vec![
|
||||
format!("Showing {}–{end} of {count} chats", first + 1)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
]));
|
||||
lines.extend(coverage);
|
||||
(lines, selection)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Chat list coverage, responsive expansion, and controls for fixed-period data.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn chats_range_controls_and_visible_rows() {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
let mut chats = fixture::chats();
|
||||
let usage = chats.rows[0].usage.clone();
|
||||
chats.rows = (1..=30)
|
||||
.map(|index| chats::Chat {
|
||||
title: format!("Chat {index:02}"),
|
||||
usage: usage.clone(),
|
||||
})
|
||||
.collect();
|
||||
view.chats = Load::Ready(chats);
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
let before = (
|
||||
view.ranges,
|
||||
view.sections.0.each_ref().map(|state| state.cursor),
|
||||
view.sections.0.each_ref().map(|state| state.detail),
|
||||
);
|
||||
press(&mut view, KeyCode::Char('r'));
|
||||
assert_eq!(
|
||||
(
|
||||
view.ranges,
|
||||
view.sections.0.each_ref().map(|state| state.cursor),
|
||||
view.sections.0.each_ref().map(|state| state.detail)
|
||||
),
|
||||
before
|
||||
);
|
||||
let mut screens = Vec::new();
|
||||
for key in [KeyCode::Home, KeyCode::End, KeyCode::Enter] {
|
||||
press(&mut view, key);
|
||||
let output = screen(&mut view, /*width*/ 110, /*height*/ 24);
|
||||
assert!(!output.contains("7d") && !output.contains("r 7/30d"));
|
||||
let visible = (1..=30)
|
||||
.filter(|index| output.contains(&format!("Chat {index:02}")))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(output.contains(&format!(
|
||||
"Showing {}–{} of 30 chats",
|
||||
visible.first().unwrap(),
|
||||
visible.last().unwrap()
|
||||
)));
|
||||
screens.push(output);
|
||||
}
|
||||
press(&mut view, KeyCode::Char('z'));
|
||||
press(&mut view, KeyCode::Char('r'));
|
||||
assert_eq!(view.ranges, [0; 3]);
|
||||
press(&mut view, KeyCode::Char('1'));
|
||||
press(&mut view, KeyCode::Char('r'));
|
||||
press(&mut view, KeyCode::Char('z'));
|
||||
assert!(screen(&mut view, /*width*/ 110, /*height*/ 24).contains("[30d]"));
|
||||
insta::assert_snapshot!(screens.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chats_breakdown_ranks_nonzero_groups_and_reflows() {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
let mut chats = fixture::chats();
|
||||
let usage = chats.rows[0].usage.as_mut().unwrap();
|
||||
let seed = usage.groups[0].clone();
|
||||
usage.groups = [
|
||||
("Zero model", 0),
|
||||
("Small model", 215_610_920),
|
||||
("Top model", 19_134_430_635),
|
||||
("Refund model", -4),
|
||||
]
|
||||
.map(
|
||||
|(model, credits)| codex_backend_client::ThreadUsageBreakdownGroup {
|
||||
model: Some(model.into()),
|
||||
speed: (credits != 19_134_430_635).then(|| "standard".into()),
|
||||
estimated_usage_credits_micros: credits,
|
||||
..seed.clone()
|
||||
},
|
||||
)
|
||||
.to_vec();
|
||||
usage.estimated_usage_credits_micros = usage
|
||||
.groups
|
||||
.iter()
|
||||
.map(|group| group.estimated_usage_credits_micros)
|
||||
.sum();
|
||||
view.chats = Load::Ready(chats);
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
press(&mut view, KeyCode::Enter);
|
||||
let mut screens = Vec::new();
|
||||
for width in [110, 58] {
|
||||
view.follow_selection = true;
|
||||
let output = screen(&mut view, width, /*height*/ 32);
|
||||
assert!(output.find("Top model").unwrap() < output.find("Small model").unwrap());
|
||||
assert!(output.find("Small model").unwrap() < output.find("Refund model").unwrap());
|
||||
assert!(!output.contains("Zero model"));
|
||||
assert!(output.contains("-0.000004") && output.contains("Not reported"));
|
||||
assert!(output.contains("19,350.04") && output.contains("19,134.43"));
|
||||
assert!(output.contains("1 zero-credit group hidden · a show all"));
|
||||
assert!(output.contains("Refactor billing usage"));
|
||||
screens.push(output);
|
||||
}
|
||||
press(&mut view, KeyCode::Char('a'));
|
||||
let expanded = screen(&mut view, /*width*/ 110, /*height*/ 32);
|
||||
assert!(expanded.contains("Zero model") && expanded.contains("a hide zeros"));
|
||||
screens.push(expanded);
|
||||
press(&mut view, KeyCode::Char('a'));
|
||||
assert!(!view.show_zero_credit_groups);
|
||||
let wide = view.chat_lines(/*width*/ 220).0;
|
||||
assert!(wide.iter().all(|line| line.width() <= 96));
|
||||
insta::assert_snapshot!(screens.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chats_long_title_keeps_detail_indent_and_counts_missing_estimates() {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
let mut chats = fixture::chats();
|
||||
chats.rows[0].title = "Create a comprehensive list of areas to investigate across the runtime and describe how each area affects performance in a long conversation".into();
|
||||
chats.rows[1].usage = None;
|
||||
chats.rows[2].usage = None;
|
||||
view.chats = Load::Ready(chats);
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
press(&mut view, KeyCode::Enter);
|
||||
let output = screen(&mut view, /*width*/ 110, /*height*/ 30);
|
||||
assert!(output.contains("2 chat estimates unavailable"));
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unavailable_chats_do_not_open_invisible_details() {
|
||||
for key in [KeyCode::Enter, KeyCode::Right] {
|
||||
for state in 0..5 {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
view.section = Section::Chats;
|
||||
view.chats = match state {
|
||||
0 => Load::Unavailable,
|
||||
1 => Load::Error("Temporary failure".into()),
|
||||
2 => Load::Ready(chats::Chats::default()),
|
||||
3 => Load::Ready(chats::Chats {
|
||||
rows: vec![chats::Chat {
|
||||
title: "Another workspace's private title".into(),
|
||||
usage: None,
|
||||
}],
|
||||
}),
|
||||
_ => Load::start(std::future::pending(), FrameRequester::test_dummy()),
|
||||
};
|
||||
press(&mut view, key);
|
||||
assert_eq!(view.sections[Section::Chats].detail, None);
|
||||
press(&mut view, KeyCode::Esc);
|
||||
assert!(view.is_done);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_chat_titles_are_hidden_in_panel_and_overview() {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
view.section = Section::Chats;
|
||||
view.chats = Load::Ready(chats::Chats {
|
||||
rows: vec![chats::Chat {
|
||||
title: "Another workspace's private title".into(),
|
||||
usage: None,
|
||||
}],
|
||||
});
|
||||
let mut screens = Vec::new();
|
||||
for zoomed in [true, false] {
|
||||
view.zoomed = zoomed;
|
||||
view.follow_selection = true;
|
||||
let output = screen(&mut view, /*width*/ 110, /*height*/ 28);
|
||||
assert!(!output.contains("Another workspace's private title"));
|
||||
assert!(output.contains("Chat usage unavailable"));
|
||||
screens.push(output);
|
||||
}
|
||||
insta::assert_snapshot!(screens.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refreshing_chats_clears_details_before_replacing_the_ranked_rows() {
|
||||
for unavailable in [true, false] {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
view.section = Section::Chats;
|
||||
press(&mut view, KeyCode::Enter);
|
||||
assert_eq!(view.sections[Section::Chats].detail, Some(0));
|
||||
press(&mut view, KeyCode::Char('R'));
|
||||
assert_eq!(view.sections[Section::Chats].detail, None);
|
||||
view.account = Load::Ready(codex_protocol::account::PlanType::EnterpriseCbpUsageBased);
|
||||
view.chats = if unavailable {
|
||||
Load::Error("Temporarily unavailable".into())
|
||||
} else {
|
||||
Load::Ready(chats::Chats::default())
|
||||
};
|
||||
press(&mut view, KeyCode::Esc);
|
||||
assert!(view.is_done);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_chat_selection_collapses_details_before_they_leave_the_viewport() {
|
||||
for (start, key) in [
|
||||
(0, KeyCode::Down),
|
||||
(1, KeyCode::Up),
|
||||
(0, KeyCode::End),
|
||||
(2, KeyCode::Home),
|
||||
] {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
view.section = Section::Chats;
|
||||
view.sections[Section::Chats].cursor = start;
|
||||
press(&mut view, KeyCode::Enter);
|
||||
assert_eq!(view.sections[Section::Chats].detail, Some(start));
|
||||
press(&mut view, key);
|
||||
assert_eq!(view.sections[Section::Chats].detail, None);
|
||||
if key == KeyCode::End {
|
||||
insta::assert_snapshot!(screen(&mut view, /*width*/ 110, /*height*/ 24));
|
||||
}
|
||||
press(&mut view, KeyCode::Esc);
|
||||
assert!(view.is_done);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//! Discover recent local roots and load business chat credit estimates.
|
||||
|
||||
use super::client::Live;
|
||||
use super::client::Session;
|
||||
use super::data;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SortDirection;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_backend_client::ThreadUsage;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) struct Chat {
|
||||
pub title: String,
|
||||
pub usage: Option<ThreadUsage>,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
/// Local history can span accounts; expose titles only with a returned account estimate.
|
||||
pub(super) fn display_title(&self) -> &str {
|
||||
if self.usage.is_some() {
|
||||
&self.title
|
||||
} else {
|
||||
"Chat usage unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Chats {
|
||||
pub rows: Vec<Chat>,
|
||||
}
|
||||
|
||||
pub(super) async fn read(
|
||||
handle: AppServerRequestHandle,
|
||||
live: Arc<Live>,
|
||||
) -> Result<Option<Chats>, String> {
|
||||
let session = live.session().await?;
|
||||
if !super::models::thread_usage_supported(session.backend.account().plan_type) {
|
||||
return Ok(None);
|
||||
}
|
||||
session.backend.ensure_identity().await?;
|
||||
let threads = tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 60), roots(&handle))
|
||||
.await
|
||||
.map_err(|_| "Chat listing timed out. Press R to retry.".to_string())??;
|
||||
session.backend.ensure_identity().await?;
|
||||
if threads.is_empty() {
|
||||
return Ok(Some(Chats { rows: Vec::new() }));
|
||||
}
|
||||
let ids = threads
|
||||
.iter()
|
||||
.map(|thread| thread.id.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
// Repairing local history has its own budget; it must not consume estimate time.
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(/*secs*/ 25);
|
||||
let mut data = Vec::new();
|
||||
for pair in ids.chunks(/*chunk_size*/ 200) {
|
||||
let midpoint = pair.len().min(/*other*/ 100);
|
||||
let (first, second) = tokio::join!(
|
||||
tokio::time::timeout_at(deadline, estimates(session, &pair[..midpoint])),
|
||||
tokio::time::timeout_at(deadline, estimates(session, &pair[midpoint..])),
|
||||
);
|
||||
for batch in [first, second].into_iter().flatten() {
|
||||
data.extend(batch?);
|
||||
}
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
break;
|
||||
}
|
||||
}
|
||||
session.backend.ensure_identity().await?;
|
||||
let mut usage: HashMap<_, _> = data
|
||||
.into_iter()
|
||||
.map(|row| (row.thread_id.clone(), row))
|
||||
.collect();
|
||||
let mut rows = threads
|
||||
.into_iter()
|
||||
.map(|thread| Chat {
|
||||
usage: 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::<Vec<_>>();
|
||||
rows.sort_by_key(|row| {
|
||||
std::cmp::Reverse(
|
||||
row.usage
|
||||
.as_ref()
|
||||
.map(|usage| usage.estimated_usage_credits_micros),
|
||||
)
|
||||
});
|
||||
Ok(Some(Chats { rows }))
|
||||
}
|
||||
|
||||
/// Recent local roots are shared by consumer task and business credit reporting.
|
||||
pub(super) async fn roots(
|
||||
handle: &AppServerRequestHandle,
|
||||
) -> Result<Vec<codex_app_server_protocol::Thread>, String> {
|
||||
let cutoff = chrono::Utc::now().timestamp() - 30 * 24 * 60 * 60;
|
||||
let mut threads = Vec::new();
|
||||
let mut cursor = None;
|
||||
let mut seen_cursors = HashSet::new();
|
||||
loop {
|
||||
let page: ThreadListResponse = handle
|
||||
.request_typed(ClientRequest::ThreadList {
|
||||
request_id: RequestId::String(uuid::Uuid::new_v4().to_string()),
|
||||
params: ThreadListParams {
|
||||
cursor,
|
||||
..list_params()
|
||||
},
|
||||
})
|
||||
.await
|
||||
.map_err(data::error)?;
|
||||
let reached_cutoff = page.data.iter().any(|thread| thread.updated_at < cutoff);
|
||||
threads.extend(page.data.into_iter().filter(|thread| {
|
||||
thread.updated_at >= cutoff && !thread.ephemeral && thread.parent_thread_id.is_none()
|
||||
}));
|
||||
match page.next_cursor {
|
||||
Some(next) if !seen_cursors.insert(next.clone()) => {
|
||||
return Err("Chat listing repeated a cursor. Press R to retry.".into());
|
||||
}
|
||||
Some(next) if !reached_cutoff => cursor = Some(next),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
// Keep repeated rows from overlapping pages out of the backend's distinct-ID batches.
|
||||
let mut seen_ids = HashSet::new();
|
||||
threads.retain(|thread| seen_ids.insert(thread.id.clone()));
|
||||
Ok(threads)
|
||||
}
|
||||
|
||||
pub(super) fn list_params() -> ThreadListParams {
|
||||
ThreadListParams {
|
||||
limit: Some(100),
|
||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||
sort_direction: Some(SortDirection::Desc),
|
||||
model_providers: Some(Vec::new()),
|
||||
source_kinds: None,
|
||||
originators: None,
|
||||
archived: Some(false),
|
||||
cursor: None,
|
||||
section_id: None,
|
||||
project_id: None,
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: None,
|
||||
parent_thread_id: None,
|
||||
ancestor_thread_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Isolate unavailable estimates within the existing concurrency slot; keep healthy rows.
|
||||
async fn estimates(session: &Session, ids: &[&str]) -> Result<Vec<ThreadUsage>, String> {
|
||||
let mut pending = if ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![ids]
|
||||
};
|
||||
let mut rows = Vec::new();
|
||||
// At most 15 requests per batch; leave remaining estimates unavailable during an outage.
|
||||
for _ in 0..15 {
|
||||
let Some(ids) = pending.pop() else { break };
|
||||
match session
|
||||
.backend
|
||||
.request(|client| async move { client.get_threads_usage(ids).await })
|
||||
.await
|
||||
{
|
||||
Ok(batch) => rows.extend(batch),
|
||||
Err(error) if error.status().is_some_and(|status| status.as_u16() == 503) => {
|
||||
if ids.len() > 1 {
|
||||
let midpoint = ids.len().div_ceil(/*rhs*/ 2);
|
||||
pending.push(&ids[midpoint..]);
|
||||
pending.push(&ids[..midpoint]);
|
||||
}
|
||||
}
|
||||
Err(error) => return Err(super::client::request_error(error)),
|
||||
}
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "chats_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,178 @@
|
||||
//! Verify bounded chat discovery, account eligibility, and partial estimate handling.
|
||||
|
||||
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 incomplete_chat_batches_retain_available_estimates_without_inventing_zeroes() {
|
||||
let server = MockServer::start().await;
|
||||
let (_home, live) = live(&server, "enterprise_cbp_usage_based").await;
|
||||
Mock::given(method("POST")).and(path("/backend-api/wham/usage/thread_usage/query"))
|
||||
.respond_with(|request: &wiremock::Request| {
|
||||
let body: serde_json::Value = request.body_json().unwrap();
|
||||
let ids = body["thread_ids"].as_array().unwrap();
|
||||
if ids.len() > 1 || ids[0] == "incomplete" {
|
||||
ResponseTemplate::new(/*s*/ 503).set_body_json(json!({"detail": "Thread usage is temporarily unavailable."}))
|
||||
} else {
|
||||
ResponseTemplate::new(/*s*/ 200).set_body_json(json!({"threads": [{
|
||||
"thread_id": ids[0], "estimated_usage_credits_micros": if ids[0] == "missing" { None } else { Some(1250000) },
|
||||
"estimated_usage_usd_micros": null, "groups": null,
|
||||
}]}))
|
||||
}
|
||||
}).expect(/*r*/ 5).mount(&server).await;
|
||||
let session = live.session().await.unwrap();
|
||||
assert_eq!(
|
||||
estimates(session, &["available", "incomplete", "missing"])
|
||||
.await
|
||||
.unwrap(),
|
||||
vec![ThreadUsage {
|
||||
thread_id: "available".into(),
|
||||
estimated_usage_credits_micros: 1250000,
|
||||
estimated_usage_usd_micros: None,
|
||||
groups: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slow_repairing_chat_listing_keeps_estimate_budget_and_ranks_across_pages() {
|
||||
use codex_app_server_client::RemoteAppServerClient;
|
||||
use codex_app_server_client::RemoteAppServerConnectArgs;
|
||||
use codex_app_server_client::RemoteAppServerEndpoint;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let (_home, live) = live(&server, "enterprise_cbp_usage_based").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 = Vec::new();
|
||||
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") => {
|
||||
let params = &request["params"];
|
||||
pages.push(json!([
|
||||
params["cursor"],
|
||||
params["limit"],
|
||||
params["useStateDbOnly"].as_bool().unwrap_or_default(),
|
||||
params["modelProviders"]
|
||||
]));
|
||||
if pages.len() == 1 {
|
||||
// Simulate repair exceeding the HTTP estimate budget without a wall-clock wait.
|
||||
tokio::time::pause();
|
||||
tokio::time::advance(std::time::Duration::from_secs(/*secs*/ 26)).await;
|
||||
tokio::time::resume();
|
||||
}
|
||||
let range = if pages.len() == 1 { 0..100 } else { 100..101 };
|
||||
let data = range.map(|index| json!({
|
||||
"id": format!("chat-{index}"), "sessionId": format!("chat-{index}"),
|
||||
"preview": "", "name": format!("Chat {index}"), "ephemeral": false,
|
||||
"modelProvider": "openai", "createdAt": 0,
|
||||
"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 pages.len() == 1 { 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.len() == 2 {
|
||||
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"))
|
||||
.respond_with(|request: &wiremock::Request| {
|
||||
let body: serde_json::Value = request.body_json().unwrap();
|
||||
let ids = body["thread_ids"].as_array().unwrap();
|
||||
assert!(ids.len() <= 100);
|
||||
if ids.iter().any(|id| id == "chat-35") {
|
||||
return ResponseTemplate::new(/*s*/ 503)
|
||||
.set_body_json(json!({"detail": "Thread usage is temporarily unavailable."}));
|
||||
}
|
||||
let threads = ids.iter().map(|id| json!({
|
||||
"thread_id": id,
|
||||
"estimated_usage_credits_micros": id.as_str().unwrap().strip_prefix("chat-").unwrap().parse::<i64>().unwrap(),
|
||||
})).collect::<Vec<_>>();
|
||||
ResponseTemplate::new(/*s*/ 200).set_body_json(json!({"threads": threads}))
|
||||
}).expect(/*r*/ 2..=16).mount(&server).await;
|
||||
let chats = read(
|
||||
AppServerRequestHandle::Remote(remote.request_handle()),
|
||||
Arc::new(live),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
chats.rows.len(),
|
||||
chats.rows[0].title.as_str(),
|
||||
chats.rows.last().unwrap().title.as_str(),
|
||||
chats.rows.last().unwrap().usage.clone()
|
||||
),
|
||||
(101, "Chat 100", "Chat 35", None)
|
||||
);
|
||||
assert_eq!(
|
||||
rpc.await.unwrap(),
|
||||
vec![
|
||||
json!([null, 100, false, []]),
|
||||
json!(["next", 100, false, []])
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unavailable_chat_batches_bound_requests_during_an_outage() {
|
||||
let server = MockServer::start().await;
|
||||
let (_home, live) = live(&server, "enterprise_cbp_usage_based").await;
|
||||
Mock::given(method("POST"))
|
||||
.respond_with(ResponseTemplate::new(/*s*/ 503))
|
||||
.expect(/*r*/ 15)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let ids = (0..100).map(|i| format!("chat-{i}")).collect::<Vec<_>>();
|
||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
estimates(live.session().await.unwrap(), &ids)
|
||||
.await
|
||||
.unwrap(),
|
||||
Vec::<ThreadUsage>::new()
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
//! Card heights depend on the viewport, never on the selected day or load state.
|
||||
|
||||
use super::AnalyticsView;
|
||||
use super::data;
|
||||
use super::render::columns;
|
||||
use super::render::join_columns;
|
||||
use super::styles::number;
|
||||
use super::styles::secondary_style;
|
||||
use crate::analytics::sections::Section;
|
||||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow as truncate;
|
||||
@@ -41,23 +43,28 @@ impl AnalyticsView {
|
||||
secondary_style().dim()
|
||||
};
|
||||
let mut content = Vec::new();
|
||||
content.push(
|
||||
format!(
|
||||
"{}d{}",
|
||||
if self.ranges[self.range_group(section) as usize] == 0 {
|
||||
7
|
||||
} else {
|
||||
30
|
||||
},
|
||||
if section == Section::Usage && self.business() {
|
||||
format!(" · {}", self.token_model.as_deref().unwrap_or("All models"))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
if !matches!(section, Section::Chats) {
|
||||
content.push(
|
||||
format!(
|
||||
"{}d{}",
|
||||
if self.ranges[self.range_group(section) as usize] == 0 {
|
||||
7
|
||||
} else {
|
||||
30
|
||||
},
|
||||
if section == Section::Usage && self.business() {
|
||||
format!(
|
||||
" · {}",
|
||||
self.token_model.as_deref().unwrap_or("All models")
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
)
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if matches!(
|
||||
section,
|
||||
Section::Usage | Section::Credits | Section::Activity
|
||||
@@ -92,14 +99,70 @@ impl AnalyticsView {
|
||||
},
|
||||
));
|
||||
} else {
|
||||
content.extend(
|
||||
self.history_lines(
|
||||
section,
|
||||
inner_width,
|
||||
inner_height.saturating_sub(/*rhs*/ 13).max(/*other*/ 1),
|
||||
)
|
||||
.lines,
|
||||
);
|
||||
match section {
|
||||
Section::Usage
|
||||
| Section::Plugins
|
||||
| Section::Credits
|
||||
| Section::Activity
|
||||
| Section::Skills => content.extend(
|
||||
self.history_lines(
|
||||
section,
|
||||
inner_width,
|
||||
inner_height.saturating_sub(/*rhs*/ 13).max(/*other*/ 1),
|
||||
)
|
||||
.lines,
|
||||
),
|
||||
Section::Chats => {
|
||||
content.push(
|
||||
"30d active · lifetime credits"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
content.push(Line::default());
|
||||
if let Some(chats) = self.chats.ready() {
|
||||
if chats.rows.is_empty() {
|
||||
content.push(
|
||||
"No recent local chats."
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
for chat in chats.rows.iter().take(/*n*/ 5) {
|
||||
let amount = chat
|
||||
.usage
|
||||
.as_ref()
|
||||
.map(|usage| {
|
||||
data::credits(usage.estimated_usage_credits_micros)
|
||||
})
|
||||
.unwrap_or_else(|| "—".into());
|
||||
content.push(columns(
|
||||
truncate(
|
||||
chat.display_title().to_string().into(),
|
||||
inner_width.saturating_sub(amount.len() + 1),
|
||||
),
|
||||
number(amount).into(),
|
||||
inner_width,
|
||||
));
|
||||
}
|
||||
content.push(Line::default());
|
||||
content.push(
|
||||
"Local chats · excludes subagents"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
if chats.rows.iter().any(|chat| chat.usage.is_none()) {
|
||||
content.push(
|
||||
"Some estimates unavailable"
|
||||
.set_style(secondary_style())
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
} else if let Some(message) = self.chats.message() {
|
||||
content
|
||||
.push(message.to_string().set_style(secondary_style()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut content = word_wrap_lines(content, RtOptions::new(inner_width));
|
||||
if content.len() > inner_height {
|
||||
|
||||
@@ -50,22 +50,29 @@ impl<T: Send + 'static> Load<T> {
|
||||
pub(super) fn start(
|
||||
future: impl std::future::Future<Output = Result<Option<T>, String>> + Send + 'static,
|
||||
frame: FrameRequester,
|
||||
) -> Self {
|
||||
Self::start_with_timeout(future, frame, std::time::Duration::from_secs(/*secs*/ 30))
|
||||
}
|
||||
|
||||
pub(super) fn start_with_timeout(
|
||||
future: impl std::future::Future<Output = Result<Option<T>, String>> + Send + 'static,
|
||||
frame: FrameRequester,
|
||||
timeout: std::time::Duration,
|
||||
) -> Self {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let task = tokio::spawn(async move {
|
||||
let result = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(/*secs*/ 30),
|
||||
AssertUnwindSafe(future).catch_unwind(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(_)) => Err("Request interrupted. Press R to retry.".into()),
|
||||
Err(_) => Err("Request timed out. Press R to retry.".into()),
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
frame.schedule_frame();
|
||||
});
|
||||
let task =
|
||||
tokio::spawn(async move {
|
||||
let result =
|
||||
match tokio::time::timeout(timeout, AssertUnwindSafe(future).catch_unwind())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(_)) => Err("Request interrupted. Press R to retry.".into()),
|
||||
Err(_) => Err("Request timed out. Press R to retry.".into()),
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
frame.schedule_frame();
|
||||
});
|
||||
Self::Loading(Pending { receiver, task })
|
||||
}
|
||||
|
||||
@@ -101,6 +108,11 @@ impl<T: Send + 'static> Load<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep untyped server and transport diagnostics out of account reports.
|
||||
pub(super) fn error(_error: codex_app_server_client::TypedRequestError) -> String {
|
||||
"Couldn't load analytics. Press R to retry.".into()
|
||||
}
|
||||
|
||||
/// Keep tiny refunds visible while avoiding noise on ordinary credit amounts.
|
||||
pub(super) fn amount(value: f64) -> String {
|
||||
if value == 0.0 {
|
||||
@@ -147,6 +159,31 @@ pub(super) fn credit_amount(value: f64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn credits(micros: i64) -> String {
|
||||
let magnitude = micros.unsigned_abs();
|
||||
if magnitude > 0 && magnitude < 10_000 {
|
||||
let fractional = format!("{magnitude:06}");
|
||||
return format!(
|
||||
"{}0.{}",
|
||||
if micros < 0 { "-" } else { "" },
|
||||
fractional.trim_end_matches('0')
|
||||
);
|
||||
}
|
||||
let cents = (magnitude + 5_000) / 10_000;
|
||||
let mut whole = (cents / 100).to_string();
|
||||
let digits = whole.len();
|
||||
for index in (1..digits).rev() {
|
||||
if (digits - index).is_multiple_of(/*rhs*/ 3) {
|
||||
whole.insert(index, ',');
|
||||
}
|
||||
}
|
||||
format!(
|
||||
"{}{whole}.{:02}",
|
||||
if micros < 0 { "-" } else { "" },
|
||||
cents % 100
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn date(date: &str) -> String {
|
||||
chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d")
|
||||
.map(|date| date.format("%b %-d").to_string())
|
||||
|
||||
@@ -57,3 +57,40 @@ async fn panicking_loads_request_a_frame_and_report_interruption() {
|
||||
Some("Request interrupted. Press R to retry.")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_errors_do_not_expose_server_diagnostics() {
|
||||
let error = codex_app_server_client::TypedRequestError::Server {
|
||||
method: "thread/list".into(),
|
||||
source: codex_app_server_protocol::JSONRPCErrorError {
|
||||
code: -32603,
|
||||
message: "Failed opening /private/workspace: token=secret".into(),
|
||||
data: Some(serde_json::json!({"credential": "secret"})),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
super::error(error),
|
||||
"Couldn't load analytics. Press R to retry."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn extended_chat_load_budget_still_times_out_and_can_be_cancelled() {
|
||||
let (cancelled_tx, cancelled_rx) = oneshot::channel::<()>();
|
||||
let mut load = Load::<()>::start_with_timeout(
|
||||
async move {
|
||||
let _cancelled = cancelled_tx;
|
||||
std::future::pending().await
|
||||
},
|
||||
FrameRequester::test_dummy(),
|
||||
std::time::Duration::from_secs(/*secs*/ 90),
|
||||
);
|
||||
tokio::task::yield_now().await;
|
||||
tokio::time::advance(std::time::Duration::from_secs(/*secs*/ 31)).await;
|
||||
load.poll();
|
||||
assert!(matches!(load, Load::Loading(_)));
|
||||
tokio::time::advance(std::time::Duration::from_secs(/*secs*/ 60)).await;
|
||||
assert!(cancelled_rx.await.is_err());
|
||||
load.poll();
|
||||
assert_eq!(load.message(), Some("Request timed out. Press R to retry."));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,18 @@ impl From<Option<PlanType>> for AccountKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Match the existing CLI thread-usage capability without widening other workspace reports.
|
||||
pub(super) fn thread_usage_supported(plan: Option<PlanType>) -> bool {
|
||||
matches!(
|
||||
plan,
|
||||
Some(
|
||||
PlanType::Business
|
||||
| PlanType::EnterpriseCbpUsageBased
|
||||
| PlanType::EnterpriseCbpAutomation
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum AccountAnalyticsReport {
|
||||
Usage,
|
||||
|
||||
@@ -59,6 +59,7 @@ impl AnalyticsView {
|
||||
);
|
||||
}
|
||||
let mut selection;
|
||||
let mut chart_bands = 0;
|
||||
if focused && let Some(choice) = self.group_picker {
|
||||
lines.extend(
|
||||
self.group_options()
|
||||
@@ -78,10 +79,16 @@ impl AnalyticsView {
|
||||
}),
|
||||
);
|
||||
}
|
||||
let history = self.history_lines(section, width, chart_height);
|
||||
let chart_bands = history.bands;
|
||||
lines.extend(history.lines);
|
||||
selection = 0..lines.len();
|
||||
if section == Section::Chats {
|
||||
let (chats, detail) = self.chat_lines(width);
|
||||
selection = lines.len() + detail.start..lines.len() + detail.end;
|
||||
lines.extend(chats);
|
||||
} else {
|
||||
let history = self.history_lines(section, width, chart_height);
|
||||
chart_bands = history.bands;
|
||||
lines.extend(history.lines);
|
||||
selection = 0..lines.len();
|
||||
}
|
||||
if focused && self.group_picker.is_some() {
|
||||
selection = 0..2 + self.group_options().len();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ impl AnalyticsView {
|
||||
pub(super) fn render(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
self.poll_reports();
|
||||
let width = usize::from(area.width.saturating_sub(/*rhs*/ 4)).max(/*other*/ 1);
|
||||
let show_range = !self.visible_sections().is_empty();
|
||||
let show_range = self.section != Section::Chats && !self.visible_sections().is_empty();
|
||||
let hint = |action| {
|
||||
self.keymap
|
||||
.primary_hint(action)
|
||||
@@ -112,13 +112,20 @@ impl AnalyticsView {
|
||||
} else {
|
||||
let navigation = if !self.zoomed {
|
||||
format!("{}/z maximize", hint(ListAction::Accept))
|
||||
} else {
|
||||
} else if self.section != Section::Chats {
|
||||
format!(
|
||||
"{}/{} day · {} details",
|
||||
hint(ListAction::MoveLeft),
|
||||
hint(ListAction::MoveRight),
|
||||
hint(ListAction::Accept)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}/{} row · {} details",
|
||||
hint(ListAction::MoveUp),
|
||||
hint(ListAction::MoveDown),
|
||||
hint(ListAction::Accept)
|
||||
)
|
||||
};
|
||||
format!(
|
||||
"tab/1–{} section · {}{navigation}\n{}{}R refresh · {}/{} scroll · {} back · q close",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Resolving the account precedes report loading; refreshing drops all account-owned data.
|
||||
|
||||
use super::AnalyticsView;
|
||||
use super::chats;
|
||||
use super::data::Load;
|
||||
use super::models::AccountKind;
|
||||
|
||||
@@ -11,6 +12,7 @@ pub(super) enum Section {
|
||||
Usage,
|
||||
Plugins,
|
||||
Credits,
|
||||
Chats,
|
||||
Activity,
|
||||
Skills,
|
||||
}
|
||||
@@ -22,6 +24,7 @@ impl Section {
|
||||
Self::Usage => Some(Report::Usage),
|
||||
Self::Plugins => Some(Report::Plugins),
|
||||
Self::Credits => Some(Report::Credits),
|
||||
Self::Chats => None,
|
||||
Self::Activity => Some(Report::Messages),
|
||||
Self::Skills => Some(Report::Skills),
|
||||
}
|
||||
@@ -29,7 +32,7 @@ impl Section {
|
||||
}
|
||||
|
||||
/// Section-only indexing keeps report identity separate from array positions.
|
||||
pub(super) struct SectionStates(pub(super) [SectionState; 5]);
|
||||
pub(super) struct SectionStates(pub(super) [SectionState; 6]);
|
||||
|
||||
impl std::ops::Index<Section> for SectionStates {
|
||||
type Output = SectionState;
|
||||
@@ -76,6 +79,7 @@ impl AnalyticsView {
|
||||
for section in &mut self.sections.0 {
|
||||
section.history.poll();
|
||||
}
|
||||
self.chats.poll();
|
||||
self.account.poll();
|
||||
self.start_reports();
|
||||
if !self.business()
|
||||
@@ -90,12 +94,17 @@ impl AnalyticsView {
|
||||
{
|
||||
self.group_picker = None;
|
||||
}
|
||||
if let Some(chats) = self.chats.ready() {
|
||||
self.sections[Section::Chats].cursor = self.sections[Section::Chats]
|
||||
.cursor
|
||||
.min(chats.rows.len().saturating_sub(/*rhs*/ 1));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn range_group(&self, section: Section) -> RangeGroup {
|
||||
match section {
|
||||
Section::Usage if self.business() => RangeGroup::Activity,
|
||||
Section::Usage | Section::Credits => RangeGroup::Usage,
|
||||
Section::Usage | Section::Credits | Section::Chats => RangeGroup::Usage,
|
||||
Section::Activity => RangeGroup::Activity,
|
||||
Section::Plugins | Section::Skills => RangeGroup::Tools,
|
||||
}
|
||||
@@ -112,21 +121,39 @@ impl AnalyticsView {
|
||||
}
|
||||
}
|
||||
|
||||
fn account_kind(&self) -> Option<AccountKind> {
|
||||
self.account
|
||||
.ready()
|
||||
.copied()
|
||||
.map(|plan| AccountKind::from(Some(plan)))
|
||||
}
|
||||
|
||||
pub(super) fn business(&self) -> bool {
|
||||
matches!(
|
||||
self.account.ready(),
|
||||
self.account_kind(),
|
||||
Some(AccountKind::Business | AccountKind::Enterprise)
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn visible_sections(&self) -> &'static [Section] {
|
||||
match self.account.ready() {
|
||||
match self.account_kind() {
|
||||
Some(AccountKind::Consumer) => &[
|
||||
Section::Usage,
|
||||
Section::Activity,
|
||||
Section::Plugins,
|
||||
Section::Skills,
|
||||
],
|
||||
Some(AccountKind::Business | AccountKind::Enterprise)
|
||||
if super::models::thread_usage_supported(self.account.ready().copied()) =>
|
||||
{
|
||||
&[
|
||||
Section::Credits,
|
||||
Section::Usage,
|
||||
Section::Plugins,
|
||||
Section::Skills,
|
||||
Section::Chats,
|
||||
]
|
||||
}
|
||||
Some(AccountKind::Business | AccountKind::Enterprise) => &[
|
||||
Section::Credits,
|
||||
Section::Usage,
|
||||
@@ -156,7 +183,7 @@ impl AnalyticsView {
|
||||
};
|
||||
if !usage_groups.contains(&self.sections[Section::Usage].group) {
|
||||
self.sections[Section::Usage].group =
|
||||
if matches!(self.account.ready(), Some(AccountKind::Business)) {
|
||||
if matches!(self.account_kind(), Some(AccountKind::Business)) {
|
||||
2
|
||||
} else {
|
||||
usage_groups[0]
|
||||
@@ -174,6 +201,18 @@ impl AnalyticsView {
|
||||
self.load_report(*section);
|
||||
}
|
||||
}
|
||||
if visible.contains(&Section::Chats) {
|
||||
self.chats =
|
||||
if let (Some((_, handle, frame)), Some(live)) = (&self.connection, &self.live) {
|
||||
Load::start_with_timeout(
|
||||
chats::read(handle.clone(), std::sync::Arc::clone(live)),
|
||||
frame.clone(),
|
||||
std::time::Duration::from_secs(/*secs*/ 90),
|
||||
)
|
||||
} else {
|
||||
Load::Unavailable
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn section_date_range(
|
||||
@@ -189,7 +228,7 @@ impl AnalyticsView {
|
||||
let index = self.range_group(self.section);
|
||||
self.ranges[index as usize] ^= 1;
|
||||
for section in self.visible_sections() {
|
||||
if self.range_group(*section) != index {
|
||||
if matches!(section, Section::Chats) || self.range_group(*section) != index {
|
||||
continue;
|
||||
}
|
||||
self.sections[*section].cursor = if self.ranges[index as usize] == 1 {
|
||||
|
||||
+67
-36
@@ -194,7 +194,7 @@ expression: "screens.join(\"\\n\")"
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -219,13 +219,13 @@ expression: "screens.join(\"\\n\")"
|
||||
" ● IDE extension 10.00 "
|
||||
" ● Web 9.00 "
|
||||
" "
|
||||
" 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 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Token usage history By token type "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -250,13 +250,13 @@ expression: "screens.join(\"\\n\")"
|
||||
" ● Output 43,000 "
|
||||
" Updated Sep 2 16:00 UTC "
|
||||
" "
|
||||
" 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 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used "
|
||||
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Plugins called "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -281,13 +281,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 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -312,25 +312,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 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 120.00 "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · 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 Credits usage history ────────────────────────────────────╮ ╭─ 2 Token usage history ──────────────────────────────────────╮ "
|
||||
" │ │ │ │ "
|
||||
" │ 7d │ │ 7d · All models │ "
|
||||
" │ By product │ │ By token type │ "
|
||||
" │ 431.00 credits │ │ 4,377,500 tokens │ "
|
||||
" │ │ │ │ "
|
||||
" │ 150 │ │ 1,000,000 731,000 │ "
|
||||
" │ │ │ ▃▃▃▃▃ ▇▇▇▇ │ "
|
||||
" │ █████ │ │ ▂▂▂▂ █████ ▆▆▆▆ ████ █████ │ "
|
||||
" │ 75 ▇▇▇▇▇ ▄▄▄▄▄ █████ 64.00 │ │ 500,000 ████ ▆▆▆▆ █████ ████ ████ █████ │ "
|
||||
" │ ▅▅▅▅▅ █████ █████ █████ ▇▇▇▇▇▇ │ │ ████ ████ █████ ████ ████ █████ │ "
|
||||
" │ ▇▇▇▇▇ █████ █████ █████ █████ ██████ │ │ ▁▁▁▁ ████ ████ █████ ████ ████ █████ │ "
|
||||
" │ 0 █████ █████ █████ █████ █████ ██████ │ │ ████ ████ ████ █████ ████ ████ █████ │ "
|
||||
" │ │ │ 731,000 │ "
|
||||
" │ 150 ▁▁▁▁▁ ▃▃▃▃▃ 64.00 │ │ 1,000,000 ▁▁▁▁ ▄▄▄▄▄ ▃▃▃▃ ▅▅▅▅ ▃▃▃▃▃ │ "
|
||||
" │ 0 ▄▄▄▄▄ ▆▆▆▆▆ █████ █████ █████ ▆▆▆▆▆▆ │ │ ▂▂▂▂ ████ ████ █████ ████ ████ █████ │ "
|
||||
" │ 0 ───────────────────────────────────────────────────────── │ │ 0 ─────────────────────────────────────────────────── │ "
|
||||
" │ ▲ │ │ ▲ │ "
|
||||
" │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ │ Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 │ "
|
||||
@@ -343,18 +368,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 │ "
|
||||
@@ -368,14 +388,25 @@ expression: "screens.join(\"\\n\")"
|
||||
" │ │ │ │ "
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" ╭─ ▸ 5 Top chats ────────────────────────────────────────────────╮ "
|
||||
" │ │ "
|
||||
" │ 30d active · lifetime credits │ "
|
||||
" │ │ "
|
||||
" │ Q3 planning analysis 120.00 │ "
|
||||
" │ Refactor billing usage 113.00 │ "
|
||||
" │ Other usage 109.00 │ "
|
||||
" │ Launch readiness review 85.00 │ "
|
||||
" │ Plan the week 0.00 │ "
|
||||
" │ │ "
|
||||
" │ Local chats · excludes subagents │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" ╰────────────────────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–4 section · enter/z maximize "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
" tab/1–5 section · enter/z maximize "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
---
|
||||
source: tui/src/analytics/account_layout_tests.rs
|
||||
expression: "format!(\"{dollars}\\n{narrow}\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Est. $ spent Lifetime credits "
|
||||
" › Q3 planning analysis $123.45 120.00 "
|
||||
" Refactor billing usage — 113.00 "
|
||||
" Other usage — 109.00 "
|
||||
" Launch readiness review — 85.00 "
|
||||
" Plan the week — 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" Credits usage history Token usage "
|
||||
" history Plugins called Skills used "
|
||||
" [Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by "
|
||||
" lifetime credits "
|
||||
" "
|
||||
" Chat Est. $ / credits "
|
||||
" › Q3 planning analysis "
|
||||
" $123.45 120.00 "
|
||||
" Refactor billing usage "
|
||||
" — 113.00 "
|
||||
" Other usage "
|
||||
" — 109.00 "
|
||||
" Launch readiness review "
|
||||
" — 85.00 "
|
||||
" "
|
||||
" Showing 1–4 of 5 chats "
|
||||
" Local chats · excludes archived chats "
|
||||
" and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ "
|
||||
" row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc "
|
||||
" back · q close "
|
||||
+4
-4
@@ -5,7 +5,7 @@ expression: "format!(\"{all}\\n{filtered}\")"
|
||||
" Analytics · Live account · analytics@example.test · account-a "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Token usage history By token type "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -30,13 +30,13 @@ expression: "format!(\"{all}\\n{filtered}\")"
|
||||
" ● Output 43,000 "
|
||||
" Updated Sep 2 16:00 UTC "
|
||||
" "
|
||||
" 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 · analytics@example.test · account-a "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Token usage history By token type "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -61,5 +61,5 @@ expression: "format!(\"{all}\\n{filtered}\")"
|
||||
" ● Output 43,000 "
|
||||
" Updated Sep 2 16:00 UTC "
|
||||
" "
|
||||
" 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 "
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tui/src/analytics/account_layout_tests.rs
|
||||
assertion_line: 341
|
||||
expression: "screen(&mut view, 110, 24)"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
|
||||
" "
|
||||
" ▎ Token usage history By token type "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" All models · m change model "
|
||||
" 4,377,500 tokens "
|
||||
" 731,000 "
|
||||
" 1,000,000 ▁▁▁▁▁▁▁▁▁ ▄▄▄▄▄▄▄▄▄ ▃▃▃▃▃▃▃▃ ▅▅▅▅▅▅▅▅▅ ▃▃▃▃▃▃▃▃▃ "
|
||||
" ▂▂▂▂▂▂▂▂ █████████ ████████ █████████ ████████ █████████ █████████ "
|
||||
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" ▲ "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" "
|
||||
" ──────────────────────────────────────────────────────── "
|
||||
" Sep 2 · 731,000 tokens "
|
||||
" Token type Tokens "
|
||||
" ● Cached input 602,000 "
|
||||
" ● Uncached input 86,000 "
|
||||
" ● Output 43,000 "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
---
|
||||
source: tui/src/analytics/chat_panel_tests.rs
|
||||
expression: "screens.join(\"\\n\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 19,350.04 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Model Effort Speed Credits "
|
||||
" Top model high Not reported 19,134.43 "
|
||||
" Small model high standard 215.61 "
|
||||
" Refund model high standard -0.000004 "
|
||||
" 1 zero-credit group hidden · a show all "
|
||||
" "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" Credits usage history Token usage history Plugins "
|
||||
" called Skills used [Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 19,350.04 "
|
||||
" ──────────────────────────────────────────────────── "
|
||||
" Model / effort / speed Credits "
|
||||
" Top model 19,134.43 "
|
||||
" high · Not reported "
|
||||
" Small model 215.61 "
|
||||
" high · standard "
|
||||
" Refund model -0.000004 "
|
||||
" high · standard "
|
||||
" 1 zero-credit group hidden · a show all "
|
||||
" "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" "
|
||||
" Showing 1–4 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent "
|
||||
" usage "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter "
|
||||
" details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 19,350.04 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Model Effort Speed Credits "
|
||||
" Top model high Not reported 19,134.43 "
|
||||
" Small model high standard 215.61 "
|
||||
" Zero model high standard 0.00 "
|
||||
" Refund model high standard -0.000004 "
|
||||
" 1 zero-credit group · a hide zeros "
|
||||
" "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tui/src/analytics/chat_panel_tests.rs
|
||||
assertion_line: 123
|
||||
expression: output
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" 2 chat estimates unavailable "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Create a comprehensive list of areas to investigate across the runtime and describe ho… 120.00 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Create a comprehensive list of areas to investigate across the runtime and describe how each "
|
||||
" area affects performance in a long conversation "
|
||||
" Model Effort Speed Credits "
|
||||
" GPT-5.6-Sol high standard 120.00 "
|
||||
" "
|
||||
" Chat usage unavailable — "
|
||||
" Chat usage unavailable — "
|
||||
" Launch readiness review 85.00 "
|
||||
" Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
source: tui/src/analytics/chat_panel_tests.rs
|
||||
expression: "screens.join(\"\\n\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Chat 01 120.00 "
|
||||
" Chat 02 120.00 "
|
||||
" Chat 03 120.00 "
|
||||
" Chat 04 120.00 "
|
||||
" Chat 05 120.00 "
|
||||
" Chat 06 120.00 "
|
||||
" Chat 07 120.00 "
|
||||
" Chat 08 120.00 "
|
||||
" Chat 09 120.00 "
|
||||
" "
|
||||
" Showing 1–9 of 30 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" Chat 22 120.00 "
|
||||
" Chat 23 120.00 "
|
||||
" Chat 24 120.00 "
|
||||
" Chat 25 120.00 "
|
||||
" Chat 26 120.00 "
|
||||
" Chat 27 120.00 "
|
||||
" Chat 28 120.00 "
|
||||
" Chat 29 120.00 "
|
||||
" › Chat 30 120.00 "
|
||||
" "
|
||||
" Showing 22–30 of 30 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" Chat 26 120.00 "
|
||||
" Chat 27 120.00 "
|
||||
" Chat 28 120.00 "
|
||||
" Chat 29 120.00 "
|
||||
" › Chat 30 120.00 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Model Effort Speed Credits "
|
||||
" GPT-5.6-Sol high standard 120.00 "
|
||||
" "
|
||||
" "
|
||||
" Showing 26–30 of 30 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tui/src/analytics/chat_panel_tests.rs
|
||||
assertion_line: 205
|
||||
expression: "screen(&mut view, 110, 24)"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" Q3 planning analysis 120.00 "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" › Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
---
|
||||
source: tui/src/analytics/chat_panel_tests.rs
|
||||
assertion_line: 165
|
||||
expression: "screens.join(\"\\n\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" 1 chat estimate unavailable "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Chat usage unavailable — "
|
||||
" "
|
||||
" Showing 1–1 of 1 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" │ │ │ │ "
|
||||
" │ Sep 2 · 29 calls │ │ Sep 2 · 11 uses │ "
|
||||
" │ ● Slack 10 │ │ ● Testing 4 │ "
|
||||
" │ ● Figma 10 │ │ ● Code review 3 │ "
|
||||
" │ │ │ │ "
|
||||
" │ │ │ │ "
|
||||
" ╰─────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────╯ "
|
||||
" "
|
||||
" ╭─ ▸ 5 Top chats ─────────────────────────────────╮ "
|
||||
" │ │ "
|
||||
" │ 30d active · lifetime credits │ "
|
||||
" │ │ "
|
||||
" │ Chat usage unavailable — │ "
|
||||
" │ │ "
|
||||
" │ Local chats · excludes subagents │ "
|
||||
" │ Some estimates unavailable │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" │ │ "
|
||||
" ╰─────────────────────────────────────────────────╯ "
|
||||
" tab/1–5 section · enter/z maximize "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
+3
-3
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Token usage history By token type "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +44,7 @@ Text:
|
||||
" ● Output 43,000 "
|
||||
" Updated Sep 2 16:00 UTC "
|
||||
" "
|
||||
" 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 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 27:3 52:2 83:0
|
||||
3: 0:0 2:2 27:3 52:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 23:0 105:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used "
|
||||
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Plugins called "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +44,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 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 50:3 70:2 83:0
|
||||
3: 0:0 2:2 50:3 70:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 18:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +44,7 @@ Text:
|
||||
" ● IDE extension 400.00 "
|
||||
" ● Web 360.00 "
|
||||
" "
|
||||
" 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 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:3 29:2 83:0
|
||||
3: 0:0 2:3 29:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 25:0 108:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
Text:
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 120.00 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Model Effort Speed Credits "
|
||||
" GPT-5.6-Sol high standard 120.00 "
|
||||
" "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
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 83:3 96:0
|
||||
3: 0:0
|
||||
4: 0:0 2:3 13:0
|
||||
5: 0:0 2:4 118:0
|
||||
6: 0:0 2:2 53:0
|
||||
7: 0:0
|
||||
8: 0:0 2:1 8:0 82:1 98:0
|
||||
9: 0:0 2:3 24:0 92:3 98:0
|
||||
10: 0:0 2:4 98:0
|
||||
11: 0:0 2:2 82:0 91:1 98:0
|
||||
12: 0:0 2:1 54:2 82:0
|
||||
13: 0:0
|
||||
14: 0:0
|
||||
15: 0:0
|
||||
16: 0:0
|
||||
17: 0:0
|
||||
18: 0:0
|
||||
19: 0:0 2:2 24:0
|
||||
20: 0:0 2:2 58:0
|
||||
21: 0:0
|
||||
22: 0:0
|
||||
23: 0:0
|
||||
24: 0:0
|
||||
25: 0:0
|
||||
26: 0:0
|
||||
27: 0:0
|
||||
28: 0:0
|
||||
29: 0:0
|
||||
30: 0:0
|
||||
31: 0:0
|
||||
32: 0:0
|
||||
33: 0:0
|
||||
34: 0:0
|
||||
35: 0:0
|
||||
36: 0:0
|
||||
37: 0:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
|
||||
41: 0:0 2:5 3:2 14:5 23:2 33:5 36:2 44:5 45:2 118:0
|
||||
+47
-47
@@ -7,45 +7,45 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" 574 reported messages "
|
||||
" 54 uses "
|
||||
" "
|
||||
" 150 "
|
||||
" 20 "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 103 "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ "
|
||||
" ██████████ ██████████ "
|
||||
" ██████████ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 11 "
|
||||
" ▃▃▃▃▃▃▃▃▃▃ "
|
||||
" 10 ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
|
||||
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" ▲ "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" "
|
||||
" ──────────────────────────────────────────────────────── "
|
||||
" Sep 2 · 103 messages "
|
||||
" Model Messages "
|
||||
" ● GPT-5.6-Sol 61 "
|
||||
" ● GPT-5.6-Terra 25 "
|
||||
" ● GPT-5.5 10 "
|
||||
" ● Other 7 "
|
||||
" Sep 2 · 11 uses "
|
||||
" Skill Uses "
|
||||
" ● Testing 4 "
|
||||
" ● Code review 3 "
|
||||
" ● Planning 3 "
|
||||
" ● Debugging 1 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
" 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
|
||||
1: Reset|Reset|Reset|BOLD
|
||||
@@ -61,42 +61,42 @@ 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 68:3 85:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 12:0 110:2 118:0
|
||||
5: 0:0 2:3 15:0
|
||||
6: 0:0 2:4 118:0
|
||||
7: 0:0 2:3 23:0
|
||||
7: 0:0 2:3 9:0
|
||||
8: 0:0
|
||||
9: 0:0 3:2 6:0
|
||||
9: 0:0 3:2 5:0
|
||||
10: 0:0
|
||||
11: 0:0
|
||||
12: 0:0
|
||||
13: 0:0 108:3 111:0
|
||||
13: 0:0
|
||||
14: 0:0
|
||||
15: 0:0 41:5 51:0
|
||||
16: 0:0 41:5 51:0 105:5 115:0
|
||||
17: 0:0 41:6 51:0 105:6 115:0
|
||||
18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0
|
||||
19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0
|
||||
20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0
|
||||
21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0
|
||||
22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
15: 0:0
|
||||
16: 0:0 108:3 110:0
|
||||
17: 0:0
|
||||
18: 0:0 3:2 5:0
|
||||
19: 0:0 105:5 115:0
|
||||
20: 0:0 41:5 51:0 105:5 115:0
|
||||
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
|
||||
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
|
||||
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
|
||||
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
28: 0:0 5:2 6:0 7:4 118:0
|
||||
28: 0:0 4:2 5:0 6:4 118:0
|
||||
29: 0:0 109:3 110:0
|
||||
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
31: 0:0
|
||||
32: 0:0 2:4 58:0
|
||||
33: 0:0 2:1 7:3 22:0
|
||||
34: 0:0 2:1 7:0 50:1 58:0
|
||||
33: 0:0 2:1 7:3 17:0
|
||||
34: 0:0 2:1 7:0 54:1 58:0
|
||||
35: 0:0 2:7 4:0
|
||||
36: 0:0 2:6 4:0
|
||||
37: 0:0 2:5 4:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
|
||||
+46
-46
@@ -7,45 +7,45 @@ 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 "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ▎ Messages By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" 54 uses "
|
||||
" 574 reported messages "
|
||||
" "
|
||||
" 20 "
|
||||
" 150 "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 11 "
|
||||
" ▃▃▃▃▃▃▃▃▃▃ "
|
||||
" 10 ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
|
||||
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" 103 "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ "
|
||||
" ██████████ ██████████ "
|
||||
" ██████████ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" ▲ "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" "
|
||||
" ──────────────────────────────────────────────────────── "
|
||||
" Sep 2 · 11 uses "
|
||||
" Skill Uses "
|
||||
" ● Testing 4 "
|
||||
" ● Code review 3 "
|
||||
" ● Planning 3 "
|
||||
" ● Debugging 1 "
|
||||
" Sep 2 · 103 messages "
|
||||
" Model Messages "
|
||||
" ● GPT-5.6-Sol 61 "
|
||||
" ● GPT-5.6-Terra 25 "
|
||||
" ● GPT-5.5 10 "
|
||||
" ● Other 7 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
1: Reset|Reset|Reset|BOLD
|
||||
@@ -61,42 +61,42 @@ 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 25:3 39:2 70:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 15:0
|
||||
5: 0:0 2:3 12:0 110:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
7: 0:0 2:3 9:0
|
||||
7: 0:0 2:3 23:0
|
||||
8: 0:0
|
||||
9: 0:0 3:2 5:0
|
||||
9: 0:0 3:2 6:0
|
||||
10: 0:0
|
||||
11: 0:0
|
||||
12: 0:0
|
||||
13: 0:0
|
||||
13: 0:0 108:3 111:0
|
||||
14: 0:0
|
||||
15: 0:0
|
||||
16: 0:0 108:3 110:0
|
||||
17: 0:0
|
||||
18: 0:0 3:2 5:0
|
||||
19: 0:0 105:5 115:0
|
||||
20: 0:0 41:5 51:0 105:5 115:0
|
||||
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
|
||||
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
|
||||
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
|
||||
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
15: 0:0 41:5 51:0
|
||||
16: 0:0 41:5 51:0 105:5 115:0
|
||||
17: 0:0 41:6 51:0 105:6 115:0
|
||||
18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0
|
||||
19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0
|
||||
20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0
|
||||
21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0
|
||||
22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
28: 0:0 4:2 5:0 6:4 118:0
|
||||
28: 0:0 5:2 6:0 7:4 118:0
|
||||
29: 0:0 109:3 110:0
|
||||
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
31: 0:0
|
||||
32: 0:0 2:4 58:0
|
||||
33: 0:0 2:1 7:3 17:0
|
||||
34: 0:0 2:1 7:0 54:1 58:0
|
||||
33: 0:0 2:1 7:3 22:0
|
||||
34: 0:0 2:1 7:0 50:1 58:0
|
||||
35: 0:0 2:7 4:0
|
||||
36: 0:0 2:6 4:0
|
||||
37: 0:0 2:5 4:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -61,7 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 68:3 83:0
|
||||
3: 0:0 2:2 55:3 70:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 15:0
|
||||
6: 0:0 2:4 118:0
|
||||
+3
-3
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used "
|
||||
" 1 Credits usage history [2 Token usage history] 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Token usage history By token type "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +44,7 @@ Text:
|
||||
" ● Output 43,000 "
|
||||
" Updated Sep 2 16:00 UTC "
|
||||
" "
|
||||
" 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 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 27:3 52:2 83:0
|
||||
3: 0:0 2:2 27:3 52:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 23:0 105:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used "
|
||||
" 1 Credits usage history 2 Token usage history [3 Plugins called] 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Plugins called "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +44,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 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 50:3 70:2 83:0
|
||||
3: 0:0 2:2 50:3 70:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 18:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -44,7 +44,7 @@ Text:
|
||||
" ● IDE extension 400.00 "
|
||||
" ● Web 360.00 "
|
||||
" "
|
||||
" 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 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:3 29:2 83:0
|
||||
3: 0:0 2:3 29:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 25:0 108:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
source: tui/src/analytics/styles_tests.rs
|
||||
expression: snapshot
|
||||
---
|
||||
120x42
|
||||
Text:
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 120.00 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Model Effort Speed Credits "
|
||||
" GPT-5.6-Sol high standard 120.00 "
|
||||
" "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
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 83:3 96:0
|
||||
3: 0:0
|
||||
4: 0:0 2:3 13:0
|
||||
5: 0:0 2:4 118:0
|
||||
6: 0:0 2:2 53:0
|
||||
7: 0:0
|
||||
8: 0:0 2:1 8:0 82:1 98:0
|
||||
9: 0:0 2:3 24:0 92:3 98:0
|
||||
10: 0:0 2:4 98:0
|
||||
11: 0:0 2:2 82:0 91:1 98:0
|
||||
12: 0:0 2:1 54:2 82:0
|
||||
13: 0:0
|
||||
14: 0:0
|
||||
15: 0:0
|
||||
16: 0:0
|
||||
17: 0:0
|
||||
18: 0:0
|
||||
19: 0:0 2:2 24:0
|
||||
20: 0:0 2:2 58:0
|
||||
21: 0:0
|
||||
22: 0:0
|
||||
23: 0:0
|
||||
24: 0:0
|
||||
25: 0:0
|
||||
26: 0:0
|
||||
27: 0:0
|
||||
28: 0:0
|
||||
29: 0:0
|
||||
30: 0:0
|
||||
31: 0:0
|
||||
32: 0:0
|
||||
33: 0:0
|
||||
34: 0:0
|
||||
35: 0:0
|
||||
36: 0:0
|
||||
37: 0:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
|
||||
41: 0:0 2:5 3:2 14:5 23:2 33:5 36:2 44:5 45:2 118:0
|
||||
+47
-47
@@ -7,45 +7,45 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Total usage history [2 Messages] 3 Plugins called 4 Skills used "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] 5 Top chats "
|
||||
" "
|
||||
" ▎ Messages By model "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" 574 reported messages "
|
||||
" 54 uses "
|
||||
" "
|
||||
" 150 "
|
||||
" 20 "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 103 "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ "
|
||||
" ██████████ ██████████ "
|
||||
" ██████████ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 11 "
|
||||
" ▃▃▃▃▃▃▃▃▃▃ "
|
||||
" 10 ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
|
||||
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" ▲ "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" "
|
||||
" ──────────────────────────────────────────────────────── "
|
||||
" Sep 2 · 103 messages "
|
||||
" Model Messages "
|
||||
" ● GPT-5.6-Sol 61 "
|
||||
" ● GPT-5.6-Terra 25 "
|
||||
" ● GPT-5.5 10 "
|
||||
" ● Other 7 "
|
||||
" Sep 2 · 11 uses "
|
||||
" Skill Uses "
|
||||
" ● Testing 4 "
|
||||
" ● Code review 3 "
|
||||
" ● Planning 3 "
|
||||
" ● Debugging 1 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
" 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
|
||||
1: Reset|Reset|Reset|BOLD
|
||||
@@ -61,42 +61,42 @@ 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 68:3 85:2 96:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 12:0 110:2 118:0
|
||||
5: 0:0 2:3 15:0
|
||||
6: 0:0 2:4 118:0
|
||||
7: 0:0 2:3 23:0
|
||||
7: 0:0 2:3 9:0
|
||||
8: 0:0
|
||||
9: 0:0 3:2 6:0
|
||||
9: 0:0 3:2 5:0
|
||||
10: 0:0
|
||||
11: 0:0
|
||||
12: 0:0
|
||||
13: 0:0 108:3 111:0
|
||||
13: 0:0
|
||||
14: 0:0
|
||||
15: 0:0 41:5 51:0
|
||||
16: 0:0 41:5 51:0 105:5 115:0
|
||||
17: 0:0 41:6 51:0 105:6 115:0
|
||||
18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0
|
||||
19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0
|
||||
20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0
|
||||
21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0
|
||||
22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
15: 0:0
|
||||
16: 0:0 108:3 110:0
|
||||
17: 0:0
|
||||
18: 0:0 3:2 5:0
|
||||
19: 0:0 105:5 115:0
|
||||
20: 0:0 41:5 51:0 105:5 115:0
|
||||
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
|
||||
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
|
||||
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
|
||||
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
28: 0:0 5:2 6:0 7:4 118:0
|
||||
28: 0:0 4:2 5:0 6:4 118:0
|
||||
29: 0:0 109:3 110:0
|
||||
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
31: 0:0
|
||||
32: 0:0 2:4 58:0
|
||||
33: 0:0 2:1 7:3 22:0
|
||||
34: 0:0 2:1 7:0 50:1 58:0
|
||||
33: 0:0 2:1 7:3 17:0
|
||||
34: 0:0 2:1 7:0 54:1 58:0
|
||||
35: 0:0 2:7 4:0
|
||||
36: 0:0 2:6 4:0
|
||||
37: 0:0 2:5 4:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
|
||||
+46
-46
@@ -7,45 +7,45 @@ 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 "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ▎ Messages By model "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" 54 uses "
|
||||
" 574 reported messages "
|
||||
" "
|
||||
" 20 "
|
||||
" 150 "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 11 "
|
||||
" ▃▃▃▃▃▃▃▃▃▃ "
|
||||
" 10 ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ "
|
||||
" ██████████ ▅▅▅▅▅▅▅▅▅▅ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" 103 "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ "
|
||||
" ██████████ ██████████ "
|
||||
" ██████████ ██████████ "
|
||||
" ▄▄▄▄▄▄▄▄▄▄ ██████████ ▆▆▆▆▆▆▆▆▆▆ ██████████ "
|
||||
" 75 ██████████ ██████████ ▄▄▄▄▄▄▄▄▄▄ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ▆▆▆▆▆▆▆▆▆▆ ██████████ ██████████ ██████████ ▃▃▃▃▃▃▃▃▃▃ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" 0 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ ██████████ "
|
||||
" 0 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" ▲ "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
|
||||
" "
|
||||
" ──────────────────────────────────────────────────────── "
|
||||
" Sep 2 · 11 uses "
|
||||
" Skill Uses "
|
||||
" ● Testing 4 "
|
||||
" ● Code review 3 "
|
||||
" ● Planning 3 "
|
||||
" ● Debugging 1 "
|
||||
" Sep 2 · 103 messages "
|
||||
" Model Messages "
|
||||
" ● GPT-5.6-Sol 61 "
|
||||
" ● GPT-5.6-Terra 25 "
|
||||
" ● GPT-5.5 10 "
|
||||
" ● Other 7 "
|
||||
" "
|
||||
" tab/1–4 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
" r 7/30d · g group · R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
Palette (foreground|background|underline|modifiers):
|
||||
0: Reset|Reset|Reset|NONE
|
||||
1: Reset|Reset|Reset|BOLD
|
||||
@@ -61,42 +61,42 @@ 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 25:3 39:2 70:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 15:0
|
||||
5: 0:0 2:3 12:0 110:2 118:0
|
||||
6: 0:0 2:4 118:0
|
||||
7: 0:0 2:3 9:0
|
||||
7: 0:0 2:3 23:0
|
||||
8: 0:0
|
||||
9: 0:0 3:2 5:0
|
||||
9: 0:0 3:2 6:0
|
||||
10: 0:0
|
||||
11: 0:0
|
||||
12: 0:0
|
||||
13: 0:0
|
||||
13: 0:0 108:3 111:0
|
||||
14: 0:0
|
||||
15: 0:0
|
||||
16: 0:0 108:3 110:0
|
||||
17: 0:0
|
||||
18: 0:0 3:2 5:0
|
||||
19: 0:0 105:5 115:0
|
||||
20: 0:0 41:5 51:0 105:5 115:0
|
||||
21: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 105:6 115:0
|
||||
22: 0:0 9:5 19:0 41:5 51:0 57:5 67:0 73:5 83:0 105:6 115:0
|
||||
23: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:5 83:0 89:5 99:0 105:6 115:0
|
||||
24: 0:0 9:6 19:0 25:5 35:0 41:6 51:0 57:6 67:0 73:6 83:0 89:6 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:6 35:0 41:6 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:6 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
15: 0:0 41:5 51:0
|
||||
16: 0:0 41:5 51:0 105:5 115:0
|
||||
17: 0:0 41:6 51:0 105:6 115:0
|
||||
18: 0:0 4:2 6:0 25:5 35:0 41:6 51:0 89:5 99:0 105:6 115:0
|
||||
19: 0:0 25:6 35:0 41:6 51:0 57:5 67:0 89:6 99:0 105:6 115:0
|
||||
20: 0:0 25:6 35:0 41:7 51:0 57:6 67:0 89:6 99:0 105:7 115:0
|
||||
21: 0:0 9:6 19:0 25:6 35:0 41:7 51:0 57:6 67:0 73:5 83:0 89:6 99:0 105:7 115:0
|
||||
22: 0:0 9:6 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
23: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:6 83:0 89:7 99:0 105:7 115:0
|
||||
24: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
25: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
26: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
27: 0:0 9:7 19:0 25:7 35:0 41:7 51:0 57:7 67:0 73:7 83:0 89:7 99:0 105:7 115:0
|
||||
28: 0:0 4:2 5:0 6:4 118:0
|
||||
28: 0:0 5:2 6:0 7:4 118:0
|
||||
29: 0:0 109:3 110:0
|
||||
30: 0:0 10:2 16:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
30: 0:0 11:2 17:0 26:2 32:0 42:2 48:0 58:2 64:0 74:2 80:0 91:2 96:0 107:8 112:0
|
||||
31: 0:0
|
||||
32: 0:0 2:4 58:0
|
||||
33: 0:0 2:1 7:3 17:0
|
||||
34: 0:0 2:1 7:0 54:1 58:0
|
||||
33: 0:0 2:1 7:3 22:0
|
||||
34: 0:0 2:1 7:0 50:1 58:0
|
||||
35: 0:0 2:7 4:0
|
||||
36: 0:0 2:6 4:0
|
||||
37: 0:0 2:5 4:0
|
||||
38: 0:0
|
||||
39: 0:0
|
||||
40: 0:0 2:9 9:2 20:9 21:2 34:9 37:2 44:9 49:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 24:9 33:2 43:9 46:2 54:9 55:2 118:0
|
||||
41: 0:0 2:9 3:2 12:9 13:2 22:9 23:2 34:9 43:2 53:9 56:2 64:9 65:2 118:0
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ Text:
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called [4 Skills used] "
|
||||
" 1 Total usage history 2 Messages 3 Plugins called [4 Skills used] "
|
||||
" "
|
||||
" ▎ Skills used "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -61,7 +61,7 @@ Rows (column:palette):
|
||||
0: 0:0 2:1 11:2 28:0
|
||||
1: 0:0 2:3 6:0 11:2 34:0
|
||||
2: 0:0
|
||||
3: 0:0 2:2 68:3 83:0
|
||||
3: 0:0 2:2 55:3 70:0
|
||||
4: 0:0
|
||||
5: 0:0 2:3 15:0
|
||||
6: 0:0 2:4 118:0
|
||||
@@ -47,6 +47,7 @@ fn analytics_sections_adapt_to_terminal_colors() {
|
||||
if business {
|
||||
view = fixture::view(models::AccountKind::Enterprise);
|
||||
}
|
||||
view.sections[Section::Chats].detail = Some(0);
|
||||
// Exercise the reported credit range and preserve exact amounts below the plot.
|
||||
let mut credits =
|
||||
fixture::history(/*report*/ 1, /*range*/ 0, /*group*/ 0);
|
||||
|
||||
@@ -139,6 +139,42 @@ pub(super) fn history(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn chats() -> super::chats::Chats {
|
||||
use codex_backend_client::ThreadUsage;
|
||||
use codex_backend_client::ThreadUsageBreakdownGroup;
|
||||
super::chats::Chats {
|
||||
rows: [
|
||||
("Q3 planning analysis", 120),
|
||||
("Refactor billing usage", 113),
|
||||
("Other usage", 109),
|
||||
("Launch readiness review", 85),
|
||||
("Plan the week", 0),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (title, credits))| super::chats::Chat {
|
||||
title: title.into(),
|
||||
usage: Some(ThreadUsage {
|
||||
thread_id: format!("test-{index}"),
|
||||
estimated_usage_credits_micros: credits * 1_000_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
groups: vec![ThreadUsageBreakdownGroup {
|
||||
model: Some("GPT-5.6-Sol".into()),
|
||||
reasoning_effort: Some("high".into()),
|
||||
speed: Some("standard".into()),
|
||||
estimated_usage_credits_micros: credits * 1_000_000,
|
||||
net_new_input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
total_tokens: None,
|
||||
}],
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tokens(range: usize, grouping: usize) -> super::models::AccountAnalyticsHistory {
|
||||
let response = serde_json::json!({"data_freshness_ts":"2026-09-02T16:00:00Z", "data":
|
||||
daily(Section::Usage, range).into_iter().enumerate().map(|(index, total)| serde_json::json!({
|
||||
@@ -164,7 +200,14 @@ pub(super) fn tokens(range: usize, grouping: usize) -> super::models::AccountAna
|
||||
pub(super) fn view(kind: super::models::AccountKind) -> super::AnalyticsView {
|
||||
let mut view = super::AnalyticsView::new(crate::keymap::RuntimeKeymap::defaults().list);
|
||||
view.end_date = END_DATE;
|
||||
view.account = super::data::Load::Ready(kind);
|
||||
view.account = super::data::Load::Ready(match kind {
|
||||
super::models::AccountKind::Consumer => codex_protocol::account::PlanType::Plus,
|
||||
super::models::AccountKind::Business => codex_protocol::account::PlanType::Team,
|
||||
super::models::AccountKind::Enterprise => {
|
||||
codex_protocol::account::PlanType::EnterpriseCbpUsageBased
|
||||
}
|
||||
super::models::AccountKind::Unknown => codex_protocol::account::PlanType::Unknown,
|
||||
});
|
||||
view.start_reports();
|
||||
seed_reports(&mut view);
|
||||
view
|
||||
@@ -173,6 +216,9 @@ pub(super) fn view(kind: super::models::AccountKind) -> super::AnalyticsView {
|
||||
/// Populate explicit report state after a test chooses its account, period, and grouping.
|
||||
pub(super) fn seed_reports(view: &mut super::AnalyticsView) {
|
||||
use super::data::Load;
|
||||
if view.business() {
|
||||
view.chats = Load::Ready(chats());
|
||||
}
|
||||
for section in view.visible_sections() {
|
||||
if let Some(report) = section.report() {
|
||||
let range = view.ranges[view.range_group(*section) as usize];
|
||||
|
||||
@@ -61,6 +61,7 @@ pub(super) async fn settle(view: &mut super::AnalyticsView) {
|
||||
.0
|
||||
.iter()
|
||||
.any(|state| matches!(state.history, Load::Loading(_)))
|
||||
&& !matches!(view.chats, Load::Loading(_))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -226,6 +226,18 @@ fn analytics_credit_display_retains_integer_precision() {
|
||||
]
|
||||
.map(str::to_string)
|
||||
);
|
||||
assert_eq!(
|
||||
[0, -4, 999_999_999, 19_350_041_555, i64::MIN, i64::MAX].map(data::credits),
|
||||
[
|
||||
"0.00",
|
||||
"-0.000004",
|
||||
"1,000.00",
|
||||
"19,350.04",
|
||||
"-9,223,372,036,854.78",
|
||||
"9,223,372,036,854.78"
|
||||
]
|
||||
.map(str::to_string)
|
||||
);
|
||||
assert_eq!(
|
||||
[
|
||||
12_746.441206,
|
||||
@@ -607,3 +619,57 @@ fn analytics_empty_turns_keep_geometry_and_disable_details() {
|
||||
}
|
||||
insta::assert_snapshot!(snapshots.join("\n"));
|
||||
}
|
||||
|
||||
#[path = "analytics/chat_panel_tests.rs"]
|
||||
mod chats_table;
|
||||
|
||||
#[test]
|
||||
fn analytics_details_reflow_and_keep_focus() {
|
||||
let mut view = fixture::view(models::AccountKind::Enterprise);
|
||||
view.chats = Load::Ready(fixture::chats());
|
||||
press(&mut view, KeyCode::Char('z'));
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
press(&mut view, KeyCode::Enter);
|
||||
assert!(view.zoomed);
|
||||
press(&mut view, KeyCode::Enter);
|
||||
let wide = screen(&mut view, /*width*/ 120, /*height*/ 42);
|
||||
view.follow_selection = true;
|
||||
let narrow = screen(&mut view, /*width*/ 58, /*height*/ 20);
|
||||
assert!(narrow.contains("Q3 planning analysis") && narrow.contains("GPT-5.6-Sol"));
|
||||
insta::assert_snapshot!(format!("{wide}\n{narrow}"));
|
||||
let before = (
|
||||
view.section,
|
||||
view.sections.0.each_ref().map(|state| state.cursor),
|
||||
view.sections.0.each_ref().map(|state| state.detail),
|
||||
);
|
||||
press(&mut view, KeyCode::PageDown);
|
||||
screen(&mut view, /*width*/ 58, /*height*/ 20);
|
||||
assert_eq!(
|
||||
(
|
||||
view.section,
|
||||
view.sections.0.each_ref().map(|state| state.cursor),
|
||||
view.sections.0.each_ref().map(|state| state.detail)
|
||||
),
|
||||
before
|
||||
);
|
||||
press(&mut view, KeyCode::Tab);
|
||||
press(&mut view, KeyCode::Char('5'));
|
||||
assert_eq!(
|
||||
(
|
||||
view.section,
|
||||
view.sections.0.each_ref().map(|state| state.cursor),
|
||||
view.sections.0.each_ref().map(|state| state.detail)
|
||||
),
|
||||
before
|
||||
);
|
||||
press(&mut view, KeyCode::Esc);
|
||||
assert_eq!(
|
||||
(
|
||||
view.is_done,
|
||||
view.sections.0.each_ref().map(|state| state.detail)
|
||||
),
|
||||
(false, [None; 6])
|
||||
);
|
||||
press(&mut view, KeyCode::Esc);
|
||||
assert!(view.is_done);
|
||||
}
|
||||
|
||||
+14
-14
@@ -6,6 +6,7 @@ expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))"
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
|
||||
" 5 Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ───────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -18,14 +19,13 @@ expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))"
|
||||
" 150 "
|
||||
" "
|
||||
" 109.00 "
|
||||
" ▁▁▁▁▁▁▁ "
|
||||
" ▃▃▃▃▃▃▃ "
|
||||
" ███████ "
|
||||
" ███████ "
|
||||
" ▇▇▇▇▇▇▇ ███████ "
|
||||
" 75 ███████ ███████ ███████ "
|
||||
" ▂▂▂▂▂▂▂ ███████ ███████ ███████ ▇▇▇▇▇▇▇ "
|
||||
" ███████ ███████ ███████ ███████ ███████ "
|
||||
" ▆▆▆▆▆▆▆ ███████ ███████ ███████ ███████ ███████ "
|
||||
" ▂▂▂▂▂▂▂ ███████ "
|
||||
" 75 ███████ ▄▄▄▄▄▄▄ ███████ "
|
||||
" ███████ ███████ ███████ ▄▄▄▄▄▄▄ "
|
||||
" ▇▇▇▇▇▇▇ ███████ ███████ ███████ ███████ "
|
||||
" ▄▄▄▄▄▄▄ ███████ ███████ ███████ ███████ ███████ "
|
||||
" ███████ ███████ ███████ ███████ ███████ ███████ "
|
||||
" ███████ ███████ ███████ ███████ ███████ ███████ "
|
||||
" 0 ███████ ███████ ███████ ███████ ███████ ███████ "
|
||||
@@ -47,6 +47,7 @@ expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))"
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
|
||||
" 5 Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ───────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -56,15 +57,14 @@ expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))"
|
||||
" "
|
||||
" "
|
||||
" 109.00 "
|
||||
" ▂▂▂▂▂▂▂ "
|
||||
" ▅▅▅▅▅▅▅ "
|
||||
" ███████ "
|
||||
" ███████ "
|
||||
" ▅▅▅▅▅▅▅ ███████ "
|
||||
" 75 ███████ ▄▄▄▄▄▄▄ ███████ "
|
||||
" ███████ ███████ ███████ ▂▂▂▂▂▂▂ "
|
||||
" ▃▃▃▃▃▃▃ ███████ ███████ ███████ ███████ "
|
||||
" ███████ ███████ "
|
||||
" 75 ███████ ███████ ███████ "
|
||||
" ███████ ███████ ███████ ▆▆▆▆▆▆▆ "
|
||||
" ███████ ███████ ███████ ███████ ███████ "
|
||||
" ▅▅▅▅▅▅▅ ███████ ███████ ███████ ███████ ███████ "
|
||||
" ▂▂▂▂▂▂▂ ███████ ███████ ███████ ███████ ███████ "
|
||||
" ███████ ███████ ███████ ███████ ███████ ███████ "
|
||||
" ███████ ███████ ███████ ███████ ███████ ███████ "
|
||||
" ███████ ███████ ███████ ███████ ███████ ███████ "
|
||||
@@ -81,5 +81,5 @@ expression: "format!(\"{picker}\\n{}\", screen(&mut view, 85, 40))"
|
||||
" ● IDE extension 18.00 "
|
||||
" ● Web 14.00 "
|
||||
" "
|
||||
" 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 "
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [Credits usage history] Token usage history Plugins "
|
||||
" called Skills used "
|
||||
" called Skills used Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
@@ -36,7 +36,7 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" ● IDE extension — "
|
||||
" ● Web — "
|
||||
" "
|
||||
" 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 "
|
||||
@@ -44,7 +44,7 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -77,5 +77,5 @@ expression: "snapshots.join(\"\\n\")"
|
||||
" ● IDE extension — "
|
||||
" ● Web — "
|
||||
" "
|
||||
" 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 "
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
source: tui/src/analytics_tests.rs
|
||||
expression: "format!(\"{wide}\\n{narrow}\")"
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" 1 Credits usage history 2 Token usage history 3 Plugins called 4 Skills used [5 Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 120.00 "
|
||||
" ────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Model Effort Speed Credits "
|
||||
" GPT-5.6-Sol high standard 120.00 "
|
||||
" "
|
||||
" Refactor billing usage 113.00 "
|
||||
" Other usage 109.00 "
|
||||
" Launch readiness review 85.00 "
|
||||
" Plan the week 0.00 "
|
||||
" "
|
||||
" Showing 1–5 of 5 chats "
|
||||
" Local chats · excludes archived chats and subagent usage "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
|
||||
" Analytics · Live account "
|
||||
" "
|
||||
" Credits usage history Token usage history Plugins "
|
||||
" called Skills used [Top chats] "
|
||||
" "
|
||||
" ▎ Top chats "
|
||||
" ────────────────────────────────────────────────────── "
|
||||
" Active in past 30 days · sorted by lifetime credits "
|
||||
" "
|
||||
" Chat Lifetime credits "
|
||||
" › Q3 planning analysis 120.00 "
|
||||
" ──────────────────────────────────────────────────── "
|
||||
" Model / effort / speed Credits "
|
||||
" GPT-5.6-Sol 120.00 "
|
||||
" high · standard "
|
||||
" "
|
||||
" "
|
||||
" tab/1–5 section · z dashboard · ↑/↓ row · enter "
|
||||
" details "
|
||||
" R refresh · pgup/pgdn scroll · esc back · q close "
|
||||
+2
-2
@@ -5,7 +5,7 @@ expression: output
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 2, 2026 "
|
||||
" "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used "
|
||||
" [1 Credits usage history] 2 Token usage history 3 Plugins called 4 Skills used 5 Top chats "
|
||||
" "
|
||||
" ▎ Credits usage history By product "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
@@ -32,5 +32,5 @@ expression: output
|
||||
" ● IDE extension 0.00 "
|
||||
" ● Web 0.00 "
|
||||
" "
|
||||
" 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 "
|
||||
|
||||
Reference in New Issue
Block a user