Files
codex/codex-rs/tui/src/analytics/summary.rs
T
Eric Traut 46f7d99d0e Stabilize usage dashboard navigation and add keyboard help (#46863)
## Why

Wrapping navigation and contextual hints can shift report content as the displayed state changes, especially on narrow terminals.

## What changed

- Use compact report tabs, fixed control rows, and two footer hint rows to keep report geometry stable.
- Show the account plan in the `Usage` heading and move date ranges, grouping, model filters, sort controls, and update timestamps above reports.
- Add scrollable `?` help with an independent scroll position, preserving report selection, expanded details, and reading position when dismissed.
- Share control availability between keyboard actions and help, and preserve configured keybindings when they conflict with the help shortcut.

## Testing

Add regression coverage for stable tabs after request failures, help state preservation, dashboard selection scrolling, date and update-time controls, and custom keybinding precedence. Update layout snapshots for the revised navigation and narrow help view.

GitOrigin-RevId: ab89ed2fd346171362f487c64af7206d9123859a
2026-09-20 18:18:19 +00:00

77 lines
2.7 KiB
Rust

//! Account profile loading and the Summary's year-view interaction.
//! Aggregation changes reuse the loaded profile; account refresh replaces all profile data.
use super::AnalyticsView;
use super::activity_chart::TokenActivityView;
use super::client::Live;
use super::data::Load;
use super::sections::Section;
use crate::keymap::ListAction;
use codex_backend_client::AccountProfile;
pub(super) const VIEWS: [TokenActivityView; 3] = [
TokenActivityView::Daily,
TokenActivityView::Weekly,
TokenActivityView::Cumulative,
];
impl Live {
pub(super) async fn profile(&self) -> Result<Option<AccountProfile>, String> {
let session = self.session().await?;
let profile = session
.backend
.request(|client| async move { client.get_account_profile().await })
.await;
self.ensure_identity().await?;
profile.map(Some).map_err(super::client::request_error)
}
}
impl AnalyticsView {
pub(crate) fn select_summary(&mut self, view: Option<TokenActivityView>) {
self.section = Section::Summary;
self.zoomed = true;
self.show_help = false;
*self.scroll_offset_mut() = 0;
if let Some(view) = view {
self.sections[Section::Summary].group = VIEWS
.iter()
.position(|item| *item == view)
.unwrap_or_default();
}
}
pub(super) fn load_summary(&mut self) {
if let (Some((_, _, frame)), Some(live)) = (&self.connection, &self.live) {
let live = std::sync::Arc::clone(live);
self.profile = Load::start(async move { live.profile().await }, frame.clone());
}
}
pub(super) fn summary_action(&mut self, action: Option<ListAction>) {
let step = match action {
Some(ListAction::MoveUp) => Some(-1),
Some(ListAction::MoveDown) => Some(1),
Some(ListAction::PageUp) => Some(-(self.viewport_height as isize)),
Some(ListAction::PageDown) => Some(self.viewport_height as isize),
Some(ListAction::JumpTop) => {
*self.scroll_offset_mut() = 0;
Some(0)
}
Some(ListAction::JumpBottom) => {
*self.scroll_offset_mut() = usize::MAX;
Some(0)
}
Some(ListAction::Cancel) => {
self.is_done = true;
None
}
Some(ListAction::MoveLeft | ListAction::MoveRight | ListAction::Accept) | None => None,
};
if let Some(step) = step {
*self.scroll_offset_mut() = self.scroll_offset().saturating_add_signed(step);
self.follow_selection = false;
}
}
}