This commit is contained in:
Eric Traut
2026-03-14 10:47:08 -06:00
parent 709916ad10
commit 18fcca6ebe
12 changed files with 435 additions and 347 deletions
+26
View File
@@ -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<PlanType> {
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
}
}
+77 -24
View File
@@ -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<AuthManager>,
available_models: Vec<ModelPreset>,
available_collaboration_modes: Vec<CollaborationModeMask>,
/// 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,
+129 -1
View File
@@ -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<GetAccountRateLimitsResponse, String> {
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<String>,
force_refetch: bool,
) -> Result<Vec<codex_app_server_protocol::AppInfo>, 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<String>,
thread_id: Option<String>,
include_logs: bool,
extra_log_files: Option<Vec<PathBuf>>,
) -> Result<String, String> {
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<String>,
event: Event,
+12
View File
@@ -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<ConnectorsSnapshot, String>,
@@ -432,6 +435,15 @@ pub(crate) enum AppEvent {
include_logs: bool,
},
UploadFeedback {
category: FeedbackCategory,
thread_id: Option<ThreadId>,
note: Option<String>,
include_logs: bool,
rollout_path: Option<PathBuf>,
feedback_audience: crate::bottom_pane::FeedbackAudience,
},
/// Open the upload consent popup for feedback after selecting a category.
OpenFeedbackConsent {
category: FeedbackCategory,
+73 -103
View File
@@ -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<ThreadId>,
rollout_path: Option<PathBuf>,
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<ThreadId>,
rollout_path: Option<PathBuf>,
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::<Vec<_>>()
} 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<Line<'static>> {
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::<AppEvent>();
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::<AppEvent>();
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,
+2
View File
@@ -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;
+61 -129
View File
@@ -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<UserMessage>,
pub(crate) enhanced_keys_supported: bool,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) account: Option<Account>,
pub(crate) available_models: Vec<ModelPreset>,
pub(crate) available_collaboration_modes: Vec<CollaborationModeMask>,
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<CollaborationModeMask>,
auth_manager: Arc<AuthManager>,
account: Option<Account>,
available_models: Vec<ModelPreset>,
available_collaboration_modes: Vec<CollaborationModeMask>,
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<Account>) {
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<ConnectorsSnapshot, String> = 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<ModelPreset> {
@@ -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<RateLimitSnapshot> {
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,
+18 -30
View File
@@ -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<Op>) {
}
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::<AppEvent>().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::<AppEvent>().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()
+1
View File
@@ -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;
+7 -7
View File
@@ -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<ThreadId>,
@@ -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<ThreadId>,
@@ -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<ThreadId>,
@@ -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();
+13 -12
View File
@@ -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<PlanType>,
account: Option<&Account>,
fallback_plan: Option<PlanType>,
) -> Option<StatusAccountDisplay> {
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,
}
}
+16 -41
View File
@@ -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,