use ratatui::widgets::{ListState, TableState};
use crate::config::ResolvedRetryConfig;
use crate::sessions::{SessionMeta, SessionSummary, SessionTranscriptMessage};
use std::collections::HashMap;
use super::Language;
use super::model::{Snapshot, filtered_requests_len};
use super::types::{Focus, Overlay, Page, StatsFocus};
#[derive(Debug, Clone)]
pub(in crate::tui) struct RecentCodexRow {
pub(in crate::tui) root: String,
pub(in crate::tui) branch: Option<String>,
pub(in crate::tui) session_id: String,
pub(in crate::tui) cwd: Option<String>,
pub(in crate::tui) mtime_ms: u64,
}
#[derive(Debug)]
pub(in crate::tui) struct UiState {
pub(in crate::tui) service_name: &'static str,
pub(in crate::tui) port: u16,
pub(in crate::tui) language: Language,
pub(in crate::tui) refresh_ms: u64,
pub(in crate::tui) page: Page,
pub(in crate::tui) focus: Focus,
pub(in crate::tui) overlay: Overlay,
pub(in crate::tui) selected_config_idx: usize,
pub(in crate::tui) selected_session_idx: usize,
pub(in crate::tui) selected_session_id: Option<String>,
pub(in crate::tui) selected_request_idx: usize,
pub(in crate::tui) selected_request_page_idx: usize,
pub(in crate::tui) request_page_errors_only: bool,
pub(in crate::tui) request_page_scope_session: bool,
pub(in crate::tui) selected_sessions_page_idx: usize,
pub(in crate::tui) sessions_page_active_only: bool,
pub(in crate::tui) sessions_page_errors_only: bool,
pub(in crate::tui) sessions_page_overrides_only: bool,
pub(in crate::tui) effort_menu_idx: usize,
pub(in crate::tui) provider_menu_idx: usize,
pub(in crate::tui) stats_focus: StatsFocus,
pub(in crate::tui) stats_days: usize,
pub(in crate::tui) stats_errors_only: bool,
pub(in crate::tui) selected_stats_config_idx: usize,
pub(in crate::tui) selected_stats_provider_idx: usize,
pub(in crate::tui) needs_snapshot_refresh: bool,
pub(in crate::tui) toast: Option<(String, std::time::Instant)>,
pub(in crate::tui) codex_history_sessions: Vec<SessionSummary>,
pub(in crate::tui) codex_history_error: Option<String>,
pub(in crate::tui) codex_history_loaded_at_ms: Option<u64>,
pub(in crate::tui) needs_codex_history_refresh: bool,
pub(in crate::tui) selected_codex_history_idx: usize,
pub(in crate::tui) codex_recent_rows: Vec<RecentCodexRow>,
pub(in crate::tui) codex_recent_error: Option<String>,
pub(in crate::tui) codex_recent_loaded_at_ms: Option<u64>,
pub(in crate::tui) needs_codex_recent_refresh: bool,
pub(in crate::tui) codex_recent_window_idx: usize,
pub(in crate::tui) codex_recent_selected_idx: usize,
pub(in crate::tui) codex_recent_selected_id: Option<String>,
pub(in crate::tui) codex_recent_raw_cwd: bool,
pub(in crate::tui) codex_recent_branch_cache: HashMap<String, Option<String>>,
pub(in crate::tui) session_transcript_meta: Option<SessionMeta>,
pub(in crate::tui) session_transcript_sid: Option<String>,
pub(in crate::tui) session_transcript_file: Option<String>,
pub(in crate::tui) session_transcript_tail: Option<usize>,
pub(in crate::tui) session_transcript_messages: Vec<SessionTranscriptMessage>,
pub(in crate::tui) session_transcript_scroll: u16,
pub(in crate::tui) session_transcript_error: Option<String>,
pub(in crate::tui) pending_overwrite_from_codex_confirm_at: Option<std::time::Instant>,
pub(in crate::tui) last_runtime_config_loaded_at_ms: Option<u64>,
pub(in crate::tui) last_runtime_config_source_mtime_ms: Option<u64>,
pub(in crate::tui) last_runtime_retry: Option<ResolvedRetryConfig>,
pub(in crate::tui) last_runtime_config_refresh_at: Option<std::time::Instant>,
pub(in crate::tui) should_exit: bool,
pub(in crate::tui) configs_table: TableState,
pub(in crate::tui) sessions_table: TableState,
pub(in crate::tui) requests_table: TableState,
pub(in crate::tui) request_page_table: TableState,
pub(in crate::tui) sessions_page_table: TableState,
pub(in crate::tui) codex_history_table: TableState,
pub(in crate::tui) codex_recent_table: TableState,
pub(in crate::tui) stats_configs_table: TableState,
pub(in crate::tui) stats_providers_table: TableState,
pub(in crate::tui) menu_list: ListState,
pub(in crate::tui) config_info_scroll: u16,
}
impl Default for UiState {
fn default() -> Self {
Self {
service_name: "codex",
port: 3211,
language: Language::En,
refresh_ms: 500,
page: Page::Dashboard,
focus: Focus::Sessions,
overlay: Overlay::None,
selected_config_idx: 0,
selected_session_idx: 0,
selected_session_id: None,
selected_request_idx: 0,
selected_request_page_idx: 0,
request_page_errors_only: false,
request_page_scope_session: false,
selected_sessions_page_idx: 0,
sessions_page_active_only: false,
sessions_page_errors_only: false,
sessions_page_overrides_only: false,
effort_menu_idx: 0,
provider_menu_idx: 0,
stats_focus: StatsFocus::Configs,
stats_days: 21,
stats_errors_only: false,
selected_stats_config_idx: 0,
selected_stats_provider_idx: 0,
needs_snapshot_refresh: false,
toast: None,
codex_history_sessions: Vec::new(),
codex_history_error: None,
codex_history_loaded_at_ms: None,
needs_codex_history_refresh: false,
selected_codex_history_idx: 0,
codex_recent_rows: Vec::new(),
codex_recent_error: None,
codex_recent_loaded_at_ms: None,
needs_codex_recent_refresh: false,
codex_recent_window_idx: 1,
codex_recent_selected_idx: 0,
codex_recent_selected_id: None,
codex_recent_raw_cwd: false,
codex_recent_branch_cache: HashMap::new(),
session_transcript_meta: None,
session_transcript_sid: None,
session_transcript_file: None,
session_transcript_tail: Some(80),
session_transcript_messages: Vec::new(),
session_transcript_scroll: 0,
session_transcript_error: None,
pending_overwrite_from_codex_confirm_at: None,
last_runtime_config_loaded_at_ms: None,
last_runtime_config_source_mtime_ms: None,
last_runtime_retry: None,
last_runtime_config_refresh_at: None,
should_exit: false,
configs_table: TableState::default(),
sessions_table: TableState::default(),
requests_table: TableState::default(),
request_page_table: TableState::default(),
sessions_page_table: TableState::default(),
codex_history_table: TableState::default(),
codex_recent_table: TableState::default(),
stats_configs_table: TableState::default(),
stats_providers_table: TableState::default(),
menu_list: ListState::default(),
config_info_scroll: 0,
}
}
}
impl UiState {
pub(in crate::tui) fn clamp_selection(&mut self, snapshot: &Snapshot, providers_len: usize) {
if providers_len == 0 {
self.selected_config_idx = 0;
self.configs_table.select(None);
} else {
self.selected_config_idx = self.selected_config_idx.min(providers_len - 1);
self.configs_table.select(Some(self.selected_config_idx));
}
if snapshot.rows.is_empty() {
self.selected_session_idx = 0;
self.selected_session_id = None;
self.sessions_table.select(None);
self.selected_request_idx = 0;
self.requests_table.select(None);
return;
}
if let Some(sid) = self.selected_session_id.clone()
&& let Some(idx) = snapshot
.rows
.iter()
.position(|r| r.session_id.as_deref() == Some(sid.as_str()))
{
self.selected_session_idx = idx;
} else {
self.selected_session_idx = self.selected_session_idx.min(snapshot.rows.len() - 1);
self.selected_session_id = snapshot.rows[self.selected_session_idx].session_id.clone();
}
self.sessions_table.select(Some(self.selected_session_idx));
let req_len = filtered_requests_len(snapshot, self.selected_session_idx);
if req_len == 0 {
self.selected_request_idx = 0;
self.requests_table.select(None);
} else {
self.selected_request_idx = self.selected_request_idx.min(req_len - 1);
self.requests_table.select(Some(self.selected_request_idx));
}
let stats_configs_len = snapshot.usage_rollup.by_config.len();
if stats_configs_len == 0 {
self.selected_stats_config_idx = 0;
self.stats_configs_table.select(None);
} else {
self.selected_stats_config_idx =
self.selected_stats_config_idx.min(stats_configs_len - 1);
self.stats_configs_table
.select(Some(self.selected_stats_config_idx));
}
let stats_providers_len = snapshot.usage_rollup.by_provider.len();
if stats_providers_len == 0 {
self.selected_stats_provider_idx = 0;
self.stats_providers_table.select(None);
} else {
self.selected_stats_provider_idx = self
.selected_stats_provider_idx
.min(stats_providers_len - 1);
self.stats_providers_table
.select(Some(self.selected_stats_provider_idx));
}
}
}
pub(in crate::tui) fn adjust_table_selection(
table: &mut TableState,
delta: i32,
len: usize,
) -> Option<usize> {
if len == 0 {
table.select(None);
return None;
}
let cur = table.selected().unwrap_or(0);
let next = if delta.is_negative() {
cur.saturating_sub(delta.unsigned_abs() as usize)
} else {
(cur + delta as usize).min(len - 1)
};
table.select(Some(next));
Some(next)
}