From 18fcca6ebeef156dab45fe7b29643f1f09cb008c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 14 Mar 2026 10:47:08 -0600 Subject: [PATCH] Part 2 --- codex-rs/tui/src/account_state.rs | 26 +++ codex-rs/tui/src/app.rs | 101 +++++++--- codex-rs/tui/src/app/app_server_adapter.rs | 130 +++++++++++- codex-rs/tui/src/app_event.rs | 12 ++ codex-rs/tui/src/bottom_pane/feedback_view.rs | 176 +++++++--------- codex-rs/tui/src/bottom_pane/mod.rs | 2 + codex-rs/tui/src/chatwidget.rs | 190 ++++++------------ codex-rs/tui/src/chatwidget/tests.rs | 48 ++--- codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/status/card.rs | 14 +- codex-rs/tui/src/status/helpers.rs | 25 +-- codex-rs/tui/src/status/tests.rs | 57 ++---- 12 files changed, 435 insertions(+), 347 deletions(-) create mode 100644 codex-rs/tui/src/account_state.rs diff --git a/codex-rs/tui/src/account_state.rs b/codex-rs/tui/src/account_state.rs new file mode 100644 index 0000000000..333eaae34f --- /dev/null +++ b/codex-rs/tui/src/account_state.rs @@ -0,0 +1,26 @@ +use crate::bottom_pane::FeedbackAudience; +use codex_app_server_protocol::Account; +use codex_protocol::account::PlanType; + +pub(crate) fn account_is_chatgpt(account: Option<&Account>) -> bool { + matches!(account, Some(Account::Chatgpt { .. })) +} + +pub(crate) fn account_plan_type(account: Option<&Account>) -> Option { + match account { + Some(Account::Chatgpt { plan_type, .. }) => Some(*plan_type), + Some(Account::ApiKey {}) | None => None, + } +} + +pub(crate) fn feedback_audience_from_account(account: Option<&Account>) -> FeedbackAudience { + let is_openai_employee = matches!( + account, + Some(Account::Chatgpt { email, .. }) if email.ends_with("@openai.com") + ); + if is_openai_employee { + FeedbackAudience::OpenAiEmployee + } else { + FeedbackAudience::External + } +} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 25b4c66c5d..924db6cd90 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1,3 +1,4 @@ +use crate::account_state::feedback_audience_from_account; use crate::app_backtrack::BacktrackState; use crate::app_event::AppEvent; use crate::app_event::ExitMode; @@ -10,6 +11,8 @@ use crate::bottom_pane::FeedbackAudience; use crate::bottom_pane::McpServerElicitationFormRequest; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionViewParams; +use crate::bottom_pane::feedback_upload_classification; +use crate::bottom_pane::feedback_upload_success_lines; use crate::bottom_pane::popup_consts::standard_popup_hint_line; use crate::chatwidget::ChatWidget; use crate::chatwidget::ExternalEditorState; @@ -42,7 +45,6 @@ use codex_ansi_escape::ansi_escape_line; use codex_app_server_client::InProcessAppServerClient; use codex_app_server_protocol::ConfigLayerSource; use codex_app_server_protocol::RequestId; -use codex_core::AuthManager; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; @@ -661,7 +663,6 @@ pub(crate) struct App { pub(crate) session_telemetry: SessionTelemetry, pub(crate) app_event_tx: AppEventSender, pub(crate) chat_widget: ChatWidget, - pub(crate) auth_manager: Arc, available_models: Vec, available_collaboration_modes: Vec, /// Config is stored here so we can recreate ChatWidgets as needed. @@ -774,7 +775,7 @@ impl App { // Fork/resume bootstraps here don't carry any prefilled message content. initial_user_message: None, enhanced_keys_supported: self.enhanced_keys_supported, - auth_manager: self.auth_manager.clone(), + account: self.chat_widget.account().cloned(), available_models: self.available_models.clone(), available_collaboration_modes: self.available_collaboration_modes.clone(), feedback: self.feedback.clone(), @@ -1802,7 +1803,7 @@ impl App { // New sessions start without prefilled message content. initial_user_message: None, enhanced_keys_supported: self.enhanced_keys_supported, - auth_manager: self.auth_manager.clone(), + account: self.chat_widget.account().cloned(), available_models: self.available_models.clone(), available_collaboration_modes: self.available_collaboration_modes.clone(), feedback: self.feedback.clone(), @@ -1962,7 +1963,6 @@ impl App { let harness_overrides = normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?; - let auth_manager = app_server.auth_manager(); let available_models = App::list_models_via_app_server(&app_server) .await .map_err(|err| color_eyre::eyre::eyre!(err))?; @@ -2019,14 +2019,7 @@ impl App { codex_app_server_protocol::Account::ApiKey {} => TelemetryAuthMode::ApiKey, codex_app_server_protocol::Account::Chatgpt { .. } => TelemetryAuthMode::Chatgpt, }); - let feedback_audience = if account_email - .as_deref() - .is_some_and(|email| email.ends_with("@openai.com")) - { - FeedbackAudience::OpenAiEmployee - } else { - FeedbackAudience::External - }; + let feedback_audience = feedback_audience_from_account(account.as_ref()); let session_telemetry = SessionTelemetry::new( ThreadId::new(), model.as_str(), @@ -2071,7 +2064,7 @@ impl App { Vec::new(), ), enhanced_keys_supported, - auth_manager: auth_manager.clone(), + account: account.clone(), available_models: available_models.clone(), available_collaboration_modes: available_collaboration_modes.clone(), feedback: feedback.clone(), @@ -2095,7 +2088,6 @@ impl App { session_telemetry: session_telemetry.clone(), app_event_tx, chat_widget, - auth_manager: auth_manager.clone(), available_models, available_collaboration_modes, config, @@ -2710,7 +2702,15 @@ impl App { self.open_url_in_browser(url); } AppEvent::RefreshConnectors { force_refetch } => { - self.chat_widget.refresh_connectors(force_refetch); + let thread_id = self + .active_thread_id + .or(self.chat_widget.thread_id()) + .map(|thread_id| thread_id.to_string()); + let result = Self::list_apps_via_app_server(app_server, thread_id, force_refetch) + .await + .map(|connectors| crate::app_event::ConnectorsSnapshot { connectors }) + .map_err(|err| format!("Failed to load apps: {err}")); + self.chat_widget.on_connectors_loaded(result, true); } AppEvent::StartFileSearch(query) => { self.file_search.on_user_query(query); @@ -2721,9 +2721,70 @@ impl App { AppEvent::RateLimitSnapshotFetched(snapshot) => { self.chat_widget.on_rate_limit_snapshot(Some(snapshot)); } + AppEvent::RefreshRateLimits => { + match Self::read_account_rate_limits_via_app_server(app_server).await { + Ok(response) => { + self.chat_widget.on_rate_limit_snapshot(None); + if let Some(rate_limits_by_limit_id) = response.rate_limits_by_limit_id { + for snapshot in rate_limits_by_limit_id.into_values() { + self.chat_widget.on_rate_limit_snapshot(Some( + app_server_adapter::rate_limit_snapshot_from_api(snapshot), + )); + } + } else { + self.chat_widget.on_rate_limit_snapshot(Some( + app_server_adapter::rate_limit_snapshot_from_api( + response.rate_limits, + ), + )); + } + } + Err(err) => { + tracing::debug!(error = %err, "failed to refresh rate limits via app-server"); + } + } + } AppEvent::ConnectorsLoaded { result, is_final } => { self.chat_widget.on_connectors_loaded(result, is_final); } + AppEvent::UploadFeedback { + category, + thread_id, + note, + include_logs, + rollout_path, + feedback_audience, + } => { + match Self::upload_feedback_via_app_server( + app_server, + self.next_app_server_request_id(), + feedback_upload_classification(category).to_string(), + note, + thread_id.map(|thread_id| thread_id.to_string()), + include_logs, + include_logs.then(|| rollout_path.into_iter().collect()), + ) + .await + { + Ok(thread_id) => { + self.chat_widget + .add_to_history(history_cell::PlainHistoryCell::new( + feedback_upload_success_lines( + category, + include_logs, + &thread_id, + feedback_audience, + ), + )); + } + Err(err) => { + self.chat_widget + .add_to_history(history_cell::new_error_event(format!( + "Failed to upload feedback: {err}" + ))); + } + } + } AppEvent::UpdateReasoningEffort(effort) => { self.on_update_reasoning_effort(effort); self.refresh_status_line(); @@ -6341,9 +6402,6 @@ smart_approvals = true let available_collaboration_modes = thread_manager .get_models_manager() .list_collaboration_modes(); - let auth_manager = codex_core::test_support::auth_manager_from_auth( - CodexAuth::from_api_key("Test API Key"), - ); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let session_telemetry = test_session_telemetry(&config, model.as_str()); @@ -6352,7 +6410,6 @@ smart_approvals = true session_telemetry, app_event_tx, chat_widget, - auth_manager, available_models, available_collaboration_modes, config, @@ -6417,9 +6474,6 @@ smart_approvals = true let available_collaboration_modes = thread_manager .get_models_manager() .list_collaboration_modes(); - let auth_manager = codex_core::test_support::auth_manager_from_auth( - CodexAuth::from_api_key("Test API Key"), - ); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let session_telemetry = test_session_telemetry(&config, model.as_str()); @@ -6429,7 +6483,6 @@ smart_approvals = true session_telemetry, app_event_tx, chat_widget, - auth_manager, available_models, available_collaboration_modes, config, diff --git a/codex-rs/tui/src/app/app_server_adapter.rs b/codex-rs/tui/src/app/app_server_adapter.rs index b3c1a5e58e..a2706687e0 100644 --- a/codex-rs/tui/src/app/app_server_adapter.rs +++ b/codex-rs/tui/src/app/app_server_adapter.rs @@ -4,13 +4,18 @@ use std::path::PathBuf; use super::App; use codex_app_server_client::InProcessAppServerClient; use codex_app_server_client::InProcessServerEvent; +use codex_app_server_protocol::AppsListParams; +use codex_app_server_protocol::AppsListResponse; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::CollaborationModeListParams; use codex_app_server_protocol::CollaborationModeListResponse; use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; +use codex_app_server_protocol::FeedbackUploadParams; +use codex_app_server_protocol::FeedbackUploadResponse; use codex_app_server_protocol::FileChangeApprovalDecision; use codex_app_server_protocol::FileChangeRequestApprovalResponse; use codex_app_server_protocol::GetAccountParams; +use codex_app_server_protocol::GetAccountRateLimitsResponse; use codex_app_server_protocol::GetAccountResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::JSONRPCNotification; @@ -136,6 +141,69 @@ impl App { Ok(response.account) } + pub(super) async fn read_account_rate_limits_via_app_server( + app_server_client: &InProcessAppServerClient, + ) -> Result { + send_request_with_response( + app_server_client, + ClientRequest::GetAccountRateLimits { + request_id: RequestId::Integer(0), + params: None, + }, + "account/rateLimits/read", + ) + .await + } + + pub(super) async fn list_apps_via_app_server( + app_server_client: &InProcessAppServerClient, + thread_id: Option, + force_refetch: bool, + ) -> Result, String> { + let response: AppsListResponse = send_request_with_response( + app_server_client, + ClientRequest::AppsList { + request_id: RequestId::Integer(0), + params: AppsListParams { + cursor: None, + limit: None, + thread_id, + force_refetch, + }, + }, + "app/list", + ) + .await?; + Ok(response.data) + } + + pub(super) async fn upload_feedback_via_app_server( + app_server_client: &InProcessAppServerClient, + request_id: RequestId, + classification: String, + reason: Option, + thread_id: Option, + include_logs: bool, + extra_log_files: Option>, + ) -> Result { + let response: FeedbackUploadResponse = send_request_with_response( + app_server_client, + ClientRequest::FeedbackUpload { + request_id, + params: FeedbackUploadParams { + classification, + reason, + thread_id, + include_logs, + extra_log_files, + }, + }, + "feedback/upload", + ) + .await?; + Ok(response.thread_id) + } + pub(super) fn next_app_server_request_id(&mut self) -> RequestId { let request_id = self.next_app_server_request_id; self.next_app_server_request_id = self.next_app_server_request_id.saturating_add(1); @@ -718,7 +786,7 @@ impl App { async fn handle_server_notification( &mut self, - _app_server_client: &InProcessAppServerClient, + app_server_client: &InProcessAppServerClient, notification: ServerNotification, ) { match notification { @@ -762,6 +830,33 @@ impl App { } } } + ServerNotification::AccountUpdated(_) => { + match Self::read_account_via_app_server(app_server_client).await { + Ok(account) => { + self.feedback_audience = + crate::account_state::feedback_audience_from_account(account.as_ref()); + self.chat_widget.set_account(account); + self.refresh_status_line(); + } + Err(err) => { + tracing::warn!(error = %err, "failed to refresh account via app-server"); + } + } + } + ServerNotification::AccountRateLimitsUpdated(payload) => { + self.chat_widget + .on_rate_limit_snapshot(Some(rate_limit_snapshot_from_api( + payload.rate_limits, + ))); + } + ServerNotification::AppListUpdated(payload) => { + self.chat_widget.on_connectors_loaded( + Ok(crate::app_event::ConnectorsSnapshot { + connectors: payload.data, + }), + true, + ); + } _ => {} } } @@ -1175,6 +1270,39 @@ fn collaboration_mode_mask_from_api( } } +pub(super) fn rate_limit_snapshot_from_api( + snapshot: codex_app_server_protocol::RateLimitSnapshot, +) -> codex_protocol::protocol::RateLimitSnapshot { + codex_protocol::protocol::RateLimitSnapshot { + limit_id: snapshot.limit_id, + limit_name: snapshot.limit_name, + primary: snapshot.primary.map(rate_limit_window_from_api), + secondary: snapshot.secondary.map(rate_limit_window_from_api), + credits: snapshot.credits.map(credits_snapshot_from_api), + plan_type: snapshot.plan_type, + } +} + +fn rate_limit_window_from_api( + window: codex_app_server_protocol::RateLimitWindow, +) -> codex_protocol::protocol::RateLimitWindow { + codex_protocol::protocol::RateLimitWindow { + used_percent: f64::from(window.used_percent), + window_minutes: window.window_duration_mins, + resets_at: window.resets_at, + } +} + +fn credits_snapshot_from_api( + credits: codex_app_server_protocol::CreditsSnapshot, +) -> codex_protocol::protocol::CreditsSnapshot { + codex_protocol::protocol::CreditsSnapshot { + has_credits: credits.has_credits, + unlimited: credits.unlimited, + balance: credits.balance, + } +} + struct DecodedLegacyNotification { conversation_id: Option, event: Event, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index e2ed046690..9d5e765373 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -131,6 +131,9 @@ pub(crate) enum AppEvent { /// Result of refreshing rate limits RateLimitSnapshotFetched(RateLimitSnapshot), + /// Request a fresh account rate-limit read via app-server. + RefreshRateLimits, + /// Result of prefetching connectors. ConnectorsLoaded { result: Result, @@ -432,6 +435,15 @@ pub(crate) enum AppEvent { include_logs: bool, }, + UploadFeedback { + category: FeedbackCategory, + thread_id: Option, + note: Option, + include_logs: bool, + rollout_path: Option, + feedback_audience: crate::bottom_pane::FeedbackAudience, + }, + /// Open the upload consent popup for feedback after selecting a category. OpenFeedbackConsent { category: FeedbackCategory, diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index f09d88f1da..e087c9e08b 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -19,9 +19,8 @@ use ratatui::widgets::Widget; use crate::app_event::AppEvent; use crate::app_event::FeedbackCategory; use crate::app_event_sender::AppEventSender; -use crate::history_cell; use crate::render::renderable::Renderable; -use codex_protocol::protocol::SessionSource; +use codex_protocol::ThreadId; use super::CancellationEvent; use super::bottom_pane_view::BottomPaneView; @@ -48,7 +47,7 @@ pub(crate) enum FeedbackAudience { /// both logs and rollout with classification + metadata. pub(crate) struct FeedbackNoteView { category: FeedbackCategory, - snapshot: codex_feedback::FeedbackSnapshot, + thread_id: Option, rollout_path: Option, app_event_tx: AppEventSender, include_logs: bool, @@ -63,7 +62,7 @@ pub(crate) struct FeedbackNoteView { impl FeedbackNoteView { pub(crate) fn new( category: FeedbackCategory, - snapshot: codex_feedback::FeedbackSnapshot, + thread_id: Option, rollout_path: Option, app_event_tx: AppEventSender, include_logs: bool, @@ -71,7 +70,7 @@ impl FeedbackNoteView { ) -> Self { Self { category, - snapshot, + thread_id, rollout_path, app_event_tx, include_logs, @@ -84,90 +83,14 @@ impl FeedbackNoteView { fn submit(&mut self) { let note = self.textarea.text().trim().to_string(); - let reason_opt = if note.is_empty() { - None - } else { - Some(note.as_str()) - }; - let attachment_paths = if self.include_logs { - self.rollout_path.iter().cloned().collect::>() - } else { - Vec::new() - }; - let classification = feedback_classification(self.category); - - let mut thread_id = self.snapshot.thread_id.clone(); - - let result = self.snapshot.upload_feedback( - classification, - reason_opt, - self.include_logs, - &attachment_paths, - Some(SessionSource::Cli), - None, - ); - - match result { - Ok(()) => { - let prefix = if self.include_logs { - "• Feedback uploaded." - } else { - "• Feedback recorded (no logs)." - }; - let issue_url = - issue_url_for_category(self.category, &thread_id, self.feedback_audience); - let mut lines = vec![Line::from(match issue_url.as_ref() { - Some(_) if self.feedback_audience == FeedbackAudience::OpenAiEmployee => { - format!("{prefix} Please report this in #codex-feedback:") - } - Some(_) => format!("{prefix} Please open an issue using the following URL:"), - None => format!("{prefix} Thanks for the feedback!"), - })]; - match issue_url { - Some(url) if self.feedback_audience == FeedbackAudience::OpenAiEmployee => { - lines.extend([ - "".into(), - Line::from(vec![" ".into(), url.cyan().underlined()]), - "".into(), - Line::from(" Share this and add some info about your problem:"), - Line::from(vec![ - " ".into(), - format!("https://go/codex-feedback/{thread_id}").bold(), - ]), - ]); - } - Some(url) => { - lines.extend([ - "".into(), - Line::from(vec![" ".into(), url.cyan().underlined()]), - "".into(), - Line::from(vec![ - " Or mention your thread ID ".into(), - std::mem::take(&mut thread_id).bold(), - " in an existing issue.".into(), - ]), - ]); - } - None => { - lines.extend([ - "".into(), - Line::from(vec![ - " Thread ID: ".into(), - std::mem::take(&mut thread_id).bold(), - ]), - ]); - } - } - self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( - history_cell::PlainHistoryCell::new(lines), - ))); - } - Err(e) => { - self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( - history_cell::new_error_event(format!("Failed to upload feedback: {e}")), - ))); - } - } + self.app_event_tx.send(AppEvent::UploadFeedback { + category: self.category, + thread_id: self.thread_id, + note: (!note.is_empty()).then_some(note), + include_logs: self.include_logs, + rollout_path: self.rollout_path.clone(), + feedback_audience: self.feedback_audience, + }); self.complete = true; } } @@ -392,6 +315,64 @@ fn feedback_classification(category: FeedbackCategory) -> &'static str { } } +pub(crate) fn feedback_upload_classification(category: FeedbackCategory) -> &'static str { + feedback_classification(category) +} + +pub(crate) fn feedback_upload_success_lines( + category: FeedbackCategory, + include_logs: bool, + thread_id: &str, + feedback_audience: FeedbackAudience, +) -> Vec> { + let prefix = if include_logs { + "• Feedback uploaded." + } else { + "• Feedback recorded (no logs)." + }; + let issue_url = issue_url_for_category(category, thread_id, feedback_audience); + let mut lines = vec![Line::from(match issue_url.as_ref() { + Some(_) if feedback_audience == FeedbackAudience::OpenAiEmployee => { + format!("{prefix} Please report this in #codex-feedback:") + } + Some(_) => format!("{prefix} Please open an issue using the following URL:"), + None => format!("{prefix} Thanks for the feedback!"), + })]; + match issue_url { + Some(url) if feedback_audience == FeedbackAudience::OpenAiEmployee => { + lines.extend([ + "".into(), + Line::from(vec![" ".into(), url.cyan().underlined()]), + "".into(), + Line::from(" Share this and add some info about your problem:"), + Line::from(vec![ + " ".into(), + format!("https://go/codex-feedback/{thread_id}").bold(), + ]), + ]); + } + Some(url) => { + lines.extend([ + "".into(), + Line::from(vec![" ".into(), url.cyan().underlined()]), + "".into(), + Line::from(vec![ + " Or mention your thread ID ".into(), + thread_id.to_string().bold(), + " in an existing issue.".into(), + ]), + ]); + } + None => { + lines.extend([ + "".into(), + Line::from(vec![" Thread ID: ".into(), thread_id.to_string().bold()]), + ]); + } + } + lines +} + fn issue_url_for_category( category: FeedbackCategory, thread_id: &str, @@ -628,15 +609,7 @@ mod tests { fn make_view(category: FeedbackCategory) -> FeedbackNoteView { let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); let tx = AppEventSender::new(tx_raw); - let snapshot = codex_feedback::CodexFeedback::new().snapshot(None); - FeedbackNoteView::new( - category, - snapshot, - None, - tx, - true, - FeedbackAudience::External, - ) + FeedbackNoteView::new(category, None, None, tx, true, FeedbackAudience::External) } #[test] @@ -678,7 +651,7 @@ mod tests { fn feedback_view_with_connectivity_diagnostics() { let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); let tx = AppEventSender::new(tx_raw); - let diagnostics = FeedbackDiagnostics::new(vec![ + let _diagnostics = FeedbackDiagnostics::new(vec![ FeedbackDiagnostic { headline: "Proxy environment variables are set and may affect connectivity." .to_string(), @@ -689,12 +662,9 @@ mod tests { details: vec!["OPENAI_BASE_URL = https://example.com/v1".to_string()], }, ]); - let snapshot = codex_feedback::CodexFeedback::new() - .snapshot(None) - .with_feedback_diagnostics(diagnostics); let view = FeedbackNoteView::new( FeedbackCategory::Bug, - snapshot, + None, None, tx, false, diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index d15b03021e..8916eb7a52 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -94,7 +94,9 @@ mod feedback_view; pub(crate) use feedback_view::FeedbackAudience; pub(crate) use feedback_view::feedback_disabled_params; pub(crate) use feedback_view::feedback_selection_params; +pub(crate) use feedback_view::feedback_upload_classification; pub(crate) use feedback_view::feedback_upload_consent_params; +pub(crate) use feedback_view::feedback_upload_success_lines; pub(crate) use skills_toggle_view::SkillsToggleItem; pub(crate) use skills_toggle_view::SkillsToggleView; pub(crate) use status_line_setup::StatusLineItem; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2b0d52837e..9c4f1b871f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -51,7 +51,6 @@ use crate::status::rate_limit_snapshot_display_for_limit; use crate::text_formatting::proper_join; use crate::version::CODEX_CLI_VERSION; use codex_app_server_protocol::ConfigLayerSource; -use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; use codex_core::config::Config; use codex_core::config::Constrained; @@ -211,6 +210,9 @@ fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyB } } +use crate::account_state::account_is_chatgpt; +use crate::account_state::account_plan_type; +use crate::account_state::feedback_audience_from_account; use crate::app_event::AppEvent; use crate::app_event::ConnectorsSnapshot; use crate::app_event::ExitMode; @@ -292,8 +294,7 @@ use crate::streaming::controller::PlanStreamController; use crate::streaming::controller::StreamController; use chrono::Local; -use codex_core::AuthManager; -use codex_core::CodexAuth; +use codex_app_server_protocol::Account; use codex_core::ThreadManager; use codex_file_search::FileMatch; use codex_protocol::openai_models::InputModality; @@ -474,7 +475,7 @@ pub(crate) struct ChatWidgetInit { pub(crate) app_event_tx: AppEventSender, pub(crate) initial_user_message: Option, pub(crate) enhanced_keys_supported: bool, - pub(crate) auth_manager: Arc, + pub(crate) account: Option, pub(crate) available_models: Vec, pub(crate) available_collaboration_modes: Vec, pub(crate) feedback: codex_feedback::CodexFeedback, @@ -653,7 +654,7 @@ pub(crate) struct ChatWidget { current_collaboration_mode: CollaborationMode, /// The currently active collaboration mask, if any. active_collaboration_mask: Option, - auth_manager: Arc, + account: Option, available_models: Vec, available_collaboration_modes: Vec, session_telemetry: SessionTelemetry, @@ -1412,9 +1413,7 @@ impl ChatWidget { event, self.show_welcome_banner, startup_tooltip_override, - self.auth_manager - .auth_cached() - .and_then(|auth| auth.account_plan_type()), + account_plan_type(self.account.as_ref()), show_fast_status, ); self.apply_session_info_cell(session_info_cell); @@ -1497,27 +1496,57 @@ impl ChatWidget { self.bottom_pane.set_skills(skills); } + pub(crate) fn account(&self) -> Option<&Account> { + self.account.as_ref() + } + + pub(crate) fn set_account(&mut self, account: Option) { + let connectors_were_enabled = self.connectors_enabled(); + let should_prefetch_rate_limits = self.should_prefetch_rate_limits(); + let account_changed = self.account != account; + + self.feedback_audience = feedback_audience_from_account(account.as_ref()); + self.plan_type = account_plan_type(account.as_ref()).or(self.plan_type); + self.account = account; + + let connectors_enabled = self.connectors_enabled(); + self.bottom_pane.set_connectors_enabled(connectors_enabled); + self.sync_fast_command_enabled(); + + if connectors_enabled { + if account_changed || !connectors_were_enabled { + self.refresh_connectors(account_changed); + } + } else { + self.connectors_cache = ConnectorsCacheState::default(); + self.connectors_partial_snapshot = None; + self.bottom_pane.set_connectors_snapshot(None); + } + + if self.should_prefetch_rate_limits() { + if account_changed || !should_prefetch_rate_limits { + self.prefetch_rate_limits(); + } + } else { + self.stop_rate_limit_poller(); + self.on_rate_limit_snapshot(None); + } + + self.request_redraw(); + } + pub(crate) fn open_feedback_note( &mut self, category: crate::app_event::FeedbackCategory, include_logs: bool, ) { - if let Some(chatgpt_user_id) = self - .auth_manager - .auth_cached() - .and_then(|auth| auth.get_chatgpt_user_id()) - { - tracing::info!(target: "feedback_tags", chatgpt_user_id); - } - let snapshot = self.feedback.snapshot(self.thread_id); - self.show_feedback_note(category, include_logs, snapshot); + self.show_feedback_note(category, include_logs); } fn show_feedback_note( &mut self, category: crate::app_event::FeedbackCategory, include_logs: bool, - snapshot: codex_feedback::FeedbackSnapshot, ) { let rollout = if include_logs { self.current_rollout_path.clone() @@ -1526,7 +1555,7 @@ impl ChatWidget { }; let view = crate::bottom_pane::FeedbackNoteView::new( category, - snapshot, + self.thread_id, rollout, self.app_event_tx.clone(), include_logs, @@ -1543,13 +1572,6 @@ impl ChatWidget { } pub(crate) fn open_feedback_consent(&mut self, category: crate::app_event::FeedbackCategory) { - if let Some(chatgpt_user_id) = self - .auth_manager - .auth_cached() - .and_then(|auth| auth.get_chatgpt_user_id()) - { - tracing::info!(target: "feedback_tags", chatgpt_user_id); - } let snapshot = self.feedback.snapshot(self.thread_id); let params = crate::bottom_pane::feedback_upload_consent_params( self.app_event_tx.clone(), @@ -3520,7 +3542,7 @@ impl ChatWidget { app_event_tx, initial_user_message, enhanced_keys_supported, - auth_manager, + account, available_models, available_collaboration_modes, feedback, @@ -3589,7 +3611,7 @@ impl ChatWidget { skills_initial_state: None, current_collaboration_mode, active_collaboration_mask, - auth_manager, + account, available_models, available_collaboration_modes, session_telemetry, @@ -3712,7 +3734,7 @@ impl ChatWidget { app_event_tx, initial_user_message, enhanced_keys_supported, - auth_manager, + account, available_models, available_collaboration_modes, feedback, @@ -3780,7 +3802,7 @@ impl ChatWidget { skills_initial_state: None, current_collaboration_mode, active_collaboration_mask, - auth_manager, + account, available_models, available_collaboration_modes, session_telemetry, @@ -3895,7 +3917,7 @@ impl ChatWidget { app_event_tx, initial_user_message, enhanced_keys_supported, - auth_manager, + account, available_models, available_collaboration_modes, feedback, @@ -3963,7 +3985,7 @@ impl ChatWidget { skills_initial_state: None, current_collaboration_mode, active_collaboration_mask, - auth_manager, + account, available_models, available_collaboration_modes, session_telemetry, @@ -5697,7 +5719,7 @@ impl ChatWidget { .collect(); self.add_to_history(crate::status::new_status_output_with_rate_limits( &self.config, - self.auth_manager.as_ref(), + self.account.as_ref(), token_info, total_usage, &self.thread_id, @@ -6027,61 +6049,8 @@ impl ChatWidget { if !matches!(self.connectors_cache, ConnectorsCacheState::Ready(_)) { self.connectors_cache = ConnectorsCacheState::Loading; } - - let config = self.config.clone(); - let app_event_tx = self.app_event_tx.clone(); - tokio::spawn(async move { - let accessible_result = - match connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status( - &config, - force_refetch, - ) - .await - { - Ok(connectors) => connectors, - Err(err) => { - app_event_tx.send(AppEvent::ConnectorsLoaded { - result: Err(format!("Failed to load apps: {err}")), - is_final: true, - }); - return; - } - }; - let should_schedule_force_refetch = - !force_refetch && !accessible_result.codex_apps_ready; - let accessible_connectors = accessible_result.connectors; - - app_event_tx.send(AppEvent::ConnectorsLoaded { - result: Ok(ConnectorsSnapshot { - connectors: accessible_connectors.clone(), - }), - is_final: false, - }); - - let result: Result = async { - let all_connectors = - connectors::list_all_connectors_with_options(&config, force_refetch).await?; - let connectors = connectors::merge_connectors_with_accessible( - all_connectors, - accessible_connectors, - true, - ); - Ok(ConnectorsSnapshot { connectors }) - } - .await - .map_err(|err: anyhow::Error| format!("Failed to load apps: {err}")); - - app_event_tx.send(AppEvent::ConnectorsLoaded { - result, - is_final: true, - }); - - if should_schedule_force_refetch { - app_event_tx.send(AppEvent::RefreshConnectors { - force_refetch: true, - }); - } - }); + self.app_event_tx + .send(AppEvent::RefreshConnectors { force_refetch }); } fn prefetch_rate_limits(&mut self) { @@ -6091,21 +6060,13 @@ impl ChatWidget { return; } - let base_url = self.config.chatgpt_base_url.clone(); let app_event_tx = self.app_event_tx.clone(); - let auth_manager = Arc::clone(&self.auth_manager); let handle = tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(60)); loop { - if let Some(auth) = auth_manager.auth().await - && auth.is_chatgpt_auth() - { - for snapshot in fetch_rate_limits(base_url.clone(), auth).await { - app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot)); - } - } + app_event_tx.send(AppEvent::RefreshRateLimits); interval.tick().await; } }); @@ -6114,14 +6075,7 @@ impl ChatWidget { } fn should_prefetch_rate_limits(&self) -> bool { - if !self.config.model_provider.requires_openai_auth { - return false; - } - - self.auth_manager - .auth_cached() - .as_ref() - .is_some_and(CodexAuth::is_chatgpt_auth) + self.config.model_provider.requires_openai_auth && account_is_chatgpt(self.account.as_ref()) } fn lower_cost_preset(&self) -> Option { @@ -7957,11 +7911,7 @@ impl ChatWidget { ) -> bool { model == FAST_STATUS_MODEL && matches!(service_tier, Some(ServiceTier::Fast)) - && self - .auth_manager - .auth_cached() - .as_ref() - .is_some_and(CodexAuth::is_chatgpt_auth) + && account_is_chatgpt(self.account.as_ref()) } fn fast_mode_enabled(&self) -> bool { @@ -8268,9 +8218,7 @@ impl ChatWidget { } fn connectors_enabled(&self) -> bool { - self.config - .features - .apps_enabled_cached(Some(self.auth_manager.as_ref())) + self.config.features.enabled(Feature::Apps) && account_is_chatgpt(self.account.as_ref()) } fn connectors_for_mentions(&self) -> Option<&[connectors::AppInfo]> { @@ -9421,22 +9369,6 @@ fn hook_event_label(event_name: codex_protocol::protocol::HookEventName) -> &'st } } -async fn fetch_rate_limits(base_url: String, auth: CodexAuth) -> Vec { - match BackendClient::from_auth(base_url, &auth) { - Ok(client) => match client.get_rate_limits_many().await { - Ok(snapshots) => snapshots, - Err(err) => { - debug!(error = ?err, "failed to fetch rate limits from /usage"); - Vec::new() - } - }, - Err(err) => { - debug!(error = ?err, "failed to construct backend client for rate limits"); - Vec::new() - } - } -} - #[cfg(test)] pub(crate) fn show_review_commit_picker_with_entries( chat: &mut ChatWidget, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 2093b92463..13c64a001d 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1752,15 +1752,13 @@ async fn helpers_are_available_and_do_not_panic() { cfg.model_provider.clone(), ), ); - let auth_manager = - codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let init = ChatWidgetInit { config: cfg, frame_requester: FrameRequester::test_dummy(), app_event_tx: tx, initial_user_message: None, enhanced_keys_supported: false, - auth_manager, + account: Some(codex_app_server_protocol::Account::ApiKey {}), available_models: thread_manager .get_models_manager() .try_list_models() @@ -1844,7 +1842,7 @@ async fn make_chatwidget_manual( let codex_home = cfg.codex_home.clone(); let models_manager = Arc::new(ModelsManager::new( codex_home, - auth_manager.clone(), + auth_manager, None, CollaborationModesConfig::default(), )); @@ -1871,7 +1869,7 @@ async fn make_chatwidget_manual( config: cfg, current_collaboration_mode, active_collaboration_mask, - auth_manager, + account: Some(codex_app_server_protocol::Account::ApiKey {}), available_models, available_collaboration_modes, session_telemetry, @@ -1988,12 +1986,16 @@ fn assert_no_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) { } pub(crate) fn set_chatgpt_auth(chat: &mut ChatWidget) { - chat.auth_manager = codex_core::test_support::auth_manager_from_auth( + let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); + chat.set_account(Some(codex_app_server_protocol::Account::Chatgpt { + email: "test@example.com".to_string(), + plan_type: codex_protocol::account::PlanType::Plus, + })); let models_manager = Arc::new(ModelsManager::new( chat.config.codex_home.clone(), - chat.auth_manager.clone(), + auth_manager, None, CollaborationModesConfig::default(), )); @@ -2347,9 +2349,7 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() { #[tokio::test] async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() { let (mut chat, _, _) = make_chatwidget_manual(Some(NUDGE_MODEL_SLUG)).await; - chat.auth_manager = codex_core::test_support::auth_manager_from_auth( - CodexAuth::create_dummy_chatgpt_auth_for_testing(), - ); + set_chatgpt_auth(&mut chat); chat.on_rate_limit_snapshot(Some(snapshot(95.0))); @@ -2361,9 +2361,8 @@ async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() { #[tokio::test] async fn rate_limit_switch_prompt_skips_non_codex_limit() { - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); + set_chatgpt_auth(&mut chat); chat.on_rate_limit_snapshot(Some(RateLimitSnapshot { limit_id: Some("codex_other".to_string()), @@ -2386,9 +2385,8 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() { #[tokio::test] async fn rate_limit_switch_prompt_shows_once_per_session() { - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); + set_chatgpt_auth(&mut chat); chat.on_rate_limit_snapshot(Some(snapshot(90.0))); assert!( @@ -2410,9 +2408,8 @@ async fn rate_limit_switch_prompt_shows_once_per_session() { #[tokio::test] async fn rate_limit_switch_prompt_respects_hidden_notice() { - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); + set_chatgpt_auth(&mut chat); chat.config.notices.hide_rate_limit_model_nudge = Some(true); chat.on_rate_limit_snapshot(Some(snapshot(95.0))); @@ -2425,9 +2422,8 @@ async fn rate_limit_switch_prompt_respects_hidden_notice() { #[tokio::test] async fn rate_limit_switch_prompt_defers_until_task_complete() { - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); + set_chatgpt_auth(&mut chat); chat.bottom_pane.set_task_running(true); chat.on_rate_limit_snapshot(Some(snapshot(90.0))); @@ -2447,9 +2443,7 @@ async fn rate_limit_switch_prompt_defers_until_task_complete() { #[tokio::test] async fn rate_limit_switch_prompt_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = codex_core::test_support::auth_manager_from_auth( - CodexAuth::create_dummy_chatgpt_auth_for_testing(), - ); + set_chatgpt_auth(&mut chat); chat.on_rate_limit_snapshot(Some(snapshot(92.0))); chat.maybe_show_pending_rate_limit_prompt(); @@ -3250,9 +3244,7 @@ async fn plan_implementation_popup_shows_after_new_plan_follows_steer() { #[tokio::test] async fn plan_implementation_popup_skips_when_rate_limit_prompt_pending() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = codex_core::test_support::auth_manager_from_auth( - CodexAuth::create_dummy_chatgpt_auth_for_testing(), - ); + set_chatgpt_auth(&mut chat); chat.set_feature_enabled(Feature::CollaborationModes, true); let plan_mask = collaboration_modes::mask_for_kind(&chat.available_collaboration_modes, ModeKind::Plan) @@ -5633,15 +5625,13 @@ async fn collaboration_modes_defaults_to_code_on_startup() { cfg.model_provider.clone(), ), ); - let auth_manager = - codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let init = ChatWidgetInit { config: cfg, frame_requester: FrameRequester::test_dummy(), app_event_tx: AppEventSender::new(unbounded_channel::().0), initial_user_message: None, enhanced_keys_supported: false, - auth_manager, + account: Some(codex_app_server_protocol::Account::ApiKey {}), available_models: thread_manager .get_models_manager() .try_list_models() @@ -5689,15 +5679,13 @@ async fn experimental_mode_plan_is_ignored_on_startup() { cfg.model_provider.clone(), ), ); - let auth_manager = - codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let init = ChatWidgetInit { config: cfg, frame_requester: FrameRequester::test_dummy(), app_event_tx: AppEventSender::new(unbounded_channel::().0), initial_user_message: None, enhanced_keys_supported: false, - auth_manager, + account: Some(codex_app_server_protocol::Account::ApiKey {}), available_models: thread_manager .get_models_manager() .try_list_models() diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 43230eb2d4..bdc529ebf0 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -65,6 +65,7 @@ use tracing_subscriber::EnvFilter; use tracing_subscriber::prelude::*; use uuid::Uuid; +mod account_state; mod additional_dirs; mod app; mod app_backtrack; diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index a7546230bf..0518f99866 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -42,7 +42,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::adaptive_wrap_lines; -use codex_core::AuthManager; +use codex_app_server_protocol::Account; #[derive(Debug, Clone)] struct StatusContextWindowData { @@ -80,7 +80,7 @@ struct StatusHistoryCell { #[allow(clippy::too_many_arguments)] pub(crate) fn new_status_output( config: &Config, - auth_manager: &AuthManager, + account: Option<&Account>, token_info: Option<&TokenUsageInfo>, total_usage: &TokenUsage, session_id: &Option, @@ -96,7 +96,7 @@ pub(crate) fn new_status_output( let snapshots = rate_limits.map(std::slice::from_ref).unwrap_or_default(); new_status_output_with_rate_limits( config, - auth_manager, + account, token_info, total_usage, session_id, @@ -114,7 +114,7 @@ pub(crate) fn new_status_output( #[allow(clippy::too_many_arguments)] pub(crate) fn new_status_output_with_rate_limits( config: &Config, - auth_manager: &AuthManager, + account: Option<&Account>, token_info: Option<&TokenUsageInfo>, total_usage: &TokenUsage, session_id: &Option, @@ -130,7 +130,7 @@ pub(crate) fn new_status_output_with_rate_limits( let command = PlainHistoryCell::new(vec!["/status".magenta().into()]); let card = StatusHistoryCell::new( config, - auth_manager, + account, token_info, total_usage, session_id, @@ -151,7 +151,7 @@ impl StatusHistoryCell { #[allow(clippy::too_many_arguments)] fn new( config: &Config, - auth_manager: &AuthManager, + account: Option<&Account>, token_info: Option<&TokenUsageInfo>, total_usage: &TokenUsage, session_id: &Option, @@ -227,7 +227,7 @@ impl StatusHistoryCell { }; let agents_summary = compose_agents_summary(config); let model_provider = format_model_provider(config); - let account = compose_account_display(auth_manager, plan_type); + let account = compose_account_display(account, plan_type); let session_id = session_id.as_ref().map(std::string::ToString::to_string); let forked_from = forked_from.map(|id| id.to_string()); let default_usage = TokenUsage::default(); diff --git a/codex-rs/tui/src/status/helpers.rs b/codex-rs/tui/src/status/helpers.rs index c819405412..8a661be625 100644 --- a/codex-rs/tui/src/status/helpers.rs +++ b/codex-rs/tui/src/status/helpers.rs @@ -2,8 +2,7 @@ use crate::exec_command::relativize_to_home; use crate::text_formatting; use chrono::DateTime; use chrono::Local; -use codex_core::AuthManager; -use codex_core::auth::AuthMode as CoreAuthMode; +use codex_app_server_protocol::Account; use codex_core::config::Config; use codex_core::project_doc::discover_project_doc_paths; use codex_protocol::account::PlanType; @@ -85,20 +84,22 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String { } pub(crate) fn compose_account_display( - auth_manager: &AuthManager, - plan: Option, + account: Option<&Account>, + fallback_plan: Option, ) -> Option { - let auth = auth_manager.auth_cached()?; - - match auth.auth_mode() { - CoreAuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey), - CoreAuthMode::Chatgpt => { - let email = auth.get_account_email(); - let plan = plan + match account { + Some(Account::ApiKey {}) => Some(StatusAccountDisplay::ApiKey), + Some(Account::Chatgpt { email, plan_type }) => { + let plan = fallback_plan + .or(Some(*plan_type)) .map(|plan_type| title_case(format!("{plan_type:?}").as_str())) .or_else(|| Some("Unknown".to_string())); - Some(StatusAccountDisplay::ChatGpt { email, plan }) + Some(StatusAccountDisplay::ChatGpt { + email: Some(email.clone()), + plan, + }) } + None => None, } } diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 16ab60bd72..a18e899fe5 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -4,7 +4,6 @@ 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::ConfigBuilder; use codex_protocol::ThreadId; @@ -31,14 +30,6 @@ async 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 token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo { let context_window = codex_core::test_support::construct_model_info_offline(model_slug, config).context_window; @@ -111,7 +102,6 @@ async 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, @@ -148,7 +138,7 @@ async fn status_snapshot_includes_reasoning_details() { let reasoning_effort_override = Some(Some(ReasoningEffort::High)); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -195,7 +185,6 @@ async fn status_permissions_non_default_workspace_write_is_custom() { .expect("set sandbox policy"); config.cwd = PathBuf::from("/workspace/tests"); - let auth_manager = test_auth_manager(&config); let usage = TokenUsage::default(); let captured_at = chrono::Local .with_ymd_and_hms(2024, 1, 2, 3, 4, 5) @@ -205,7 +194,7 @@ async fn status_permissions_non_default_workspace_write_is_custom() { let composite = new_status_output( &config, - &auth_manager, + None, None, &usage, &None, @@ -244,7 +233,6 @@ async fn status_snapshot_includes_forked_from() { 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, @@ -267,7 +255,7 @@ async fn status_snapshot_includes_forked_from() { let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &Some(session_id), @@ -298,7 +286,6 @@ async 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, @@ -329,7 +316,7 @@ async fn status_snapshot_includes_monthly_limit() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -356,7 +343,6 @@ async fn status_snapshot_includes_monthly_limit() { async fn status_snapshot_shows_unlimited_credits() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home).await; - let auth_manager = test_auth_manager(&config); let usage = TokenUsage::default(); let captured_at = chrono::Local .with_ymd_and_hms(2024, 2, 3, 4, 5, 6) @@ -379,7 +365,7 @@ async fn status_snapshot_shows_unlimited_credits() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -405,7 +391,6 @@ async fn status_snapshot_shows_unlimited_credits() { async fn status_snapshot_shows_positive_credits() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home).await; - let auth_manager = test_auth_manager(&config); let usage = TokenUsage::default(); let captured_at = chrono::Local .with_ymd_and_hms(2024, 3, 4, 5, 6, 7) @@ -428,7 +413,7 @@ async fn status_snapshot_shows_positive_credits() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -454,7 +439,6 @@ async fn status_snapshot_shows_positive_credits() { async fn status_snapshot_hides_zero_credits() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home).await; - let auth_manager = test_auth_manager(&config); let usage = TokenUsage::default(); let captured_at = chrono::Local .with_ymd_and_hms(2024, 4, 5, 6, 7, 8) @@ -477,7 +461,7 @@ async fn status_snapshot_hides_zero_credits() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -501,7 +485,6 @@ async fn status_snapshot_hides_zero_credits() { async fn status_snapshot_hides_when_has_no_credits_flag() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home).await; - let auth_manager = test_auth_manager(&config); let usage = TokenUsage::default(); let captured_at = chrono::Local .with_ymd_and_hms(2024, 5, 6, 7, 8, 9) @@ -524,7 +507,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -551,7 +534,6 @@ async fn status_card_token_usage_excludes_cached_tokens() { config.model = Some("gpt-5.1-codex-max".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, @@ -569,7 +551,7 @@ async fn status_card_token_usage_excludes_cached_tokens() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -599,7 +581,6 @@ async fn status_snapshot_truncates_in_narrow_terminal() { config.model_reasoning_summary = Some(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, @@ -631,7 +612,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() { let reasoning_effort_override = Some(Some(ReasoningEffort::High)); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -662,7 +643,6 @@ async fn status_snapshot_shows_missing_limits_message() { config.model = Some("gpt-5.1-codex-max".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, @@ -680,7 +660,7 @@ async fn status_snapshot_shows_missing_limits_message() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -710,7 +690,6 @@ async fn status_snapshot_includes_credits_and_limits() { config.model = Some("gpt-5.1-codex".to_string()); config.cwd = PathBuf::from("/workspace/tests"); - let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 1_500, cached_input_tokens: 100, @@ -749,7 +728,7 @@ async fn status_snapshot_includes_credits_and_limits() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -779,7 +758,6 @@ async fn status_snapshot_shows_empty_limits_message() { config.model = Some("gpt-5.1-codex-max".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, @@ -806,7 +784,7 @@ async fn status_snapshot_shows_empty_limits_message() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -836,7 +814,6 @@ async fn status_snapshot_shows_stale_limits_message() { config.model = Some("gpt-5.1-codex-max".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, @@ -872,7 +849,7 @@ async fn status_snapshot_shows_stale_limits_message() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -902,7 +879,6 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() { config.model = Some("gpt-5.1-codex".to_string()); config.cwd = PathBuf::from("/workspace/tests"); - let auth_manager = test_auth_manager(&config); let usage = TokenUsage { input_tokens: 900, cached_input_tokens: 200, @@ -942,7 +918,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() { let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &usage, &None, @@ -971,7 +947,6 @@ async fn status_context_window_uses_last_usage() { let mut config = test_config(&temp_home).await; 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, @@ -1000,7 +975,7 @@ async fn status_context_window_uses_last_usage() { }; let composite = new_status_output( &config, - &auth_manager, + None, Some(&token_info), &total_usage, &None,