diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index a9d640600..d874435e8 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -227,6 +227,14 @@ impl CodexAuth { }) } + /// Raw plan string from the ID token (including unknown/new plan types). + pub fn raw_plan_type(&self) -> Option { + self.get_plan_type().map(|plan| match plan { + InternalPlanType::Known(k) => format!("{k:?}"), + InternalPlanType::Unknown(raw) => raw, + }) + } + /// Raw internal plan value from the ID token. /// Exposes the underlying `token_data::PlanType` without mapping it to the /// public `AccountPlanType`. Use this when downstream code needs to inspect @@ -335,7 +343,10 @@ pub fn save_auth( } /// Load CLI auth data using the configured credential store backend. -/// Returns `None` when no credentials are stored. +/// Returns `None` when no credentials are stored. This function is +/// provided only for tests. Production code should not directly load +/// from the auth.json storage. It should use the AuthManager abstraction +/// instead. pub fn load_auth_dot_json( codex_home: &Path, auth_credentials_store_mode: AuthCredentialsStoreMode, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 705582227..cc45694e6 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1709,6 +1709,7 @@ impl ChatWidget { }; self.add_to_history(crate::status::new_status_output( &self.config, + self.auth_manager.as_ref(), total_usage, context_usage, &self.conversation_id, diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 977f913d1..be10eb569 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -33,6 +33,7 @@ use super::rate_limits::format_status_limit_summary; use super::rate_limits::render_status_limit_progress_bar; use crate::wrapping::RtOptions; use crate::wrapping::word_wrap_lines; +use codex_core::AuthManager; #[derive(Debug, Clone)] struct StatusContextWindowData { @@ -65,6 +66,7 @@ struct StatusHistoryCell { pub(crate) fn new_status_output( config: &Config, + auth_manager: &AuthManager, total_usage: &TokenUsage, context_usage: Option<&TokenUsage>, session_id: &Option, @@ -74,6 +76,7 @@ pub(crate) fn new_status_output( let command = PlainHistoryCell::new(vec!["/status".magenta().into()]); let card = StatusHistoryCell::new( config, + auth_manager, total_usage, context_usage, session_id, @@ -87,6 +90,7 @@ pub(crate) fn new_status_output( impl StatusHistoryCell { fn new( config: &Config, + auth_manager: &AuthManager, total_usage: &TokenUsage, context_usage: Option<&TokenUsage>, session_id: &Option, @@ -106,7 +110,7 @@ impl StatusHistoryCell { SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(), }; let agents_summary = compose_agents_summary(config); - let account = compose_account_display(config); + let account = compose_account_display(auth_manager); let session_id = session_id.as_ref().map(std::string::ToString::to_string); let context_window = config.model_context_window.and_then(|window| { context_usage.map(|usage| StatusContextWindowData { diff --git a/codex-rs/tui/src/status/helpers.rs b/codex-rs/tui/src/status/helpers.rs index 52544b3d9..fcc252654 100644 --- a/codex-rs/tui/src/status/helpers.rs +++ b/codex-rs/tui/src/status/helpers.rs @@ -2,7 +2,8 @@ use crate::exec_command::relativize_to_home; use crate::text_formatting; use chrono::DateTime; use chrono::Local; -use codex_core::auth::load_auth_dot_json; +use codex_app_server_protocol::AuthMode; +use codex_core::AuthManager; use codex_core::config::Config; use codex_core::project_doc::discover_project_doc_paths; use std::path::Path; @@ -82,24 +83,17 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String { } } -pub(crate) fn compose_account_display(config: &Config) -> Option { - let auth = - load_auth_dot_json(&config.codex_home, config.cli_auth_credentials_store_mode).ok()??; +pub(crate) fn compose_account_display(auth_manager: &AuthManager) -> Option { + let auth = auth_manager.auth()?; - if let Some(tokens) = auth.tokens.as_ref() { - let info = &tokens.id_token; - let email = info.email.clone(); - let plan = info.get_chatgpt_plan_type().as_deref().map(title_case); - return Some(StatusAccountDisplay::ChatGpt { email, plan }); + match auth.mode { + AuthMode::ChatGPT => { + let email = auth.get_account_email(); + let plan = auth.raw_plan_type().map(|plan| title_case(plan.as_str())); + Some(StatusAccountDisplay::ChatGpt { email, plan }) + } + AuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey), } - - if let Some(key) = auth.openai_api_key - && !key.is_empty() - { - return Some(StatusAccountDisplay::ApiKey); - } - - None } pub(crate) fn format_tokens_compact(value: i64) -> String { diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 4ab4a8ea6..49a593728 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -4,6 +4,7 @@ use crate::history_cell::HistoryCell; use chrono::Duration as ChronoDuration; use chrono::TimeZone; use chrono::Utc; +use codex_core::AuthManager; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; @@ -27,6 +28,14 @@ fn test_config(temp_home: &TempDir) -> Config { .expect("load config") } +fn test_auth_manager(config: &Config) -> AuthManager { + AuthManager::new( + config.codex_home.clone(), + false, + config.cli_auth_credentials_store_mode, + ) +} + fn render_lines(lines: &[Line<'static>]) -> Vec { lines .iter() @@ -85,6 +94,7 @@ fn status_snapshot_includes_reasoning_details() { config.cwd = PathBuf::from("/workspace/tests"); + let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 1_200, cached_input_tokens: 200, @@ -113,6 +123,7 @@ fn status_snapshot_includes_reasoning_details() { let composite = new_status_output( &config, + &auth_manager, &usage, Some(&usage), &None, @@ -137,6 +148,7 @@ fn status_snapshot_includes_monthly_limit() { config.model_provider_id = "openai".to_string(); config.cwd = PathBuf::from("/workspace/tests"); + let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 800, cached_input_tokens: 0, @@ -161,6 +173,7 @@ fn status_snapshot_includes_monthly_limit() { let composite = new_status_output( &config, + &auth_manager, &usage, Some(&usage), &None, @@ -184,6 +197,7 @@ fn status_card_token_usage_excludes_cached_tokens() { config.model = "gpt-5-codex".to_string(); config.cwd = PathBuf::from("/workspace/tests"); + let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 1_200, cached_input_tokens: 200, @@ -197,7 +211,15 @@ fn status_card_token_usage_excludes_cached_tokens() { .single() .expect("timestamp"); - let composite = new_status_output(&config, &usage, Some(&usage), &None, None, now); + let composite = new_status_output( + &config, + &auth_manager, + &usage, + Some(&usage), + &None, + None, + now, + ); let rendered = render_lines(&composite.display_lines(120)); assert!( @@ -216,6 +238,7 @@ fn status_snapshot_truncates_in_narrow_terminal() { config.model_reasoning_summary = ReasoningSummary::Detailed; config.cwd = PathBuf::from("/workspace/tests"); + let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 1_200, cached_input_tokens: 200, @@ -240,6 +263,7 @@ fn status_snapshot_truncates_in_narrow_terminal() { let composite = new_status_output( &config, + &auth_manager, &usage, Some(&usage), &None, @@ -264,6 +288,7 @@ fn status_snapshot_shows_missing_limits_message() { config.model = "gpt-5-codex".to_string(); config.cwd = PathBuf::from("/workspace/tests"); + let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 500, cached_input_tokens: 0, @@ -277,7 +302,15 @@ fn status_snapshot_shows_missing_limits_message() { .single() .expect("timestamp"); - let composite = new_status_output(&config, &usage, Some(&usage), &None, None, now); + let composite = new_status_output( + &config, + &auth_manager, + &usage, + Some(&usage), + &None, + None, + now, + ); let mut rendered_lines = render_lines(&composite.display_lines(80)); if cfg!(windows) { for line in &mut rendered_lines { @@ -295,6 +328,7 @@ fn status_snapshot_shows_empty_limits_message() { config.model = "gpt-5-codex".to_string(); config.cwd = PathBuf::from("/workspace/tests"); + let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 500, cached_input_tokens: 0, @@ -315,6 +349,7 @@ fn status_snapshot_shows_empty_limits_message() { let composite = new_status_output( &config, + &auth_manager, &usage, Some(&usage), &None, @@ -338,6 +373,7 @@ fn status_snapshot_shows_stale_limits_message() { config.model = "gpt-5-codex".to_string(); config.cwd = PathBuf::from("/workspace/tests"); + let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 1_200, cached_input_tokens: 200, @@ -367,6 +403,7 @@ fn status_snapshot_shows_stale_limits_message() { let composite = new_status_output( &config, + &auth_manager, &usage, Some(&usage), &None, @@ -389,6 +426,7 @@ fn status_context_window_uses_last_usage() { let mut config = test_config(&temp_home); config.model_context_window = Some(272_000); + let auth_manager = test_auth_manager(&config); let total_usage = TokenUsage { input_tokens: 12_800, cached_input_tokens: 0, @@ -409,7 +447,15 @@ fn status_context_window_uses_last_usage() { .single() .expect("timestamp"); - let composite = new_status_output(&config, &total_usage, Some(&last_usage), &None, None, now); + let composite = new_status_output( + &config, + &auth_manager, + &total_usage, + Some(&last_usage), + &None, + None, + now, + ); let rendered_lines = render_lines(&composite.display_lines(80)); let context_line = rendered_lines .into_iter()