mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Revert "Option to Notify Workspace Owner When Usage Limit is Reached" (#17391)
Reverts openai/codex#16969 #sev3-2026-04-10-accountscheckversion-500s-for-openai-workspace-7300
This commit is contained in:
@@ -538,9 +538,6 @@ impl ThreadEventStore {
|
||||
ThreadBufferedEvent::Request(_)
|
||||
| ThreadBufferedEvent::Notification(ServerNotification::HookStarted(_))
|
||||
| ThreadBufferedEvent::Notification(ServerNotification::HookCompleted(_))
|
||||
| ThreadBufferedEvent::Notification(
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(_),
|
||||
)
|
||||
| ThreadBufferedEvent::FeedbackSubmission(_)
|
||||
)
|
||||
}
|
||||
@@ -1100,8 +1097,6 @@ impl App {
|
||||
feedback: self.feedback.clone(),
|
||||
is_first_run: false,
|
||||
status_account_display: self.chat_widget.status_account_display().cloned(),
|
||||
initial_workspace_role: self.chat_widget.current_workspace_role(),
|
||||
initial_is_workspace_owner: self.chat_widget.current_is_workspace_owner(),
|
||||
initial_plan_type: self.chat_widget.current_plan_type(),
|
||||
model: Some(self.chat_widget.current_model().to_string()),
|
||||
startup_tooltip_override: None,
|
||||
@@ -2432,10 +2427,6 @@ impl App {
|
||||
.await?;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommandView::SendAddCreditsNudgeEmail => {
|
||||
app_server.thread_add_credits_nudge_email(thread_id).await?;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommandView::ReloadUserConfig => {
|
||||
app_server.reload_user_config().await?;
|
||||
Ok(true)
|
||||
@@ -3663,8 +3654,6 @@ impl App {
|
||||
let has_chatgpt_account = bootstrap.has_chatgpt_account;
|
||||
let requires_openai_auth = bootstrap.requires_openai_auth;
|
||||
let status_account_display = bootstrap.status_account_display.clone();
|
||||
let initial_workspace_role = bootstrap.workspace_role;
|
||||
let initial_is_workspace_owner = bootstrap.is_workspace_owner;
|
||||
let initial_plan_type = bootstrap.plan_type;
|
||||
let session_telemetry = SessionTelemetry::new(
|
||||
ThreadId::new(),
|
||||
@@ -3714,8 +3703,6 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
status_account_display: status_account_display.clone(),
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model: Some(model.clone()),
|
||||
startup_tooltip_override,
|
||||
@@ -3750,8 +3737,6 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
status_account_display: status_account_display.clone(),
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model: config.model.clone(),
|
||||
startup_tooltip_override: None,
|
||||
@@ -3791,8 +3776,6 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
status_account_display: status_account_display.clone(),
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model: config.model.clone(),
|
||||
startup_tooltip_override: None,
|
||||
@@ -4577,23 +4560,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
},
|
||||
AppEvent::NotifyWorkspaceOwner => {
|
||||
if self.active_thread_id.is_some() {
|
||||
self.chat_widget.start_notify_workspace_owner();
|
||||
if let Err(err) = self
|
||||
.submit_active_thread_op(app_server, Op::SendAddCreditsNudgeEmail.into())
|
||||
.await
|
||||
{
|
||||
self.chat_widget
|
||||
.finish_notify_workspace_owner(Err(err.to_string()));
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
self.chat_widget.finish_notify_workspace_owner(Err(
|
||||
"no active thread is available".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
AppEvent::ConnectorsLoaded { result, is_final } => {
|
||||
self.chat_widget.on_connectors_loaded(result, is_final);
|
||||
}
|
||||
@@ -6374,8 +6340,6 @@ mod tests {
|
||||
use crate::multi_agents::AgentPickerThreadEntry;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AdditionalFileSystemPermissions;
|
||||
use codex_app_server_protocol::AdditionalNetworkPermissions;
|
||||
use codex_app_server_protocol::AdditionalPermissionProfile;
|
||||
@@ -6763,8 +6727,6 @@ mod tests {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: false,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(model),
|
||||
startup_tooltip_override: None,
|
||||
@@ -9668,29 +9630,6 @@ guardian_approval = true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_event_store_rebase_preserves_add_credits_nudge_email_completion() {
|
||||
let thread_id = ThreadId::new();
|
||||
let mut store = ThreadEventStore::new(/*capacity*/ 8);
|
||||
let notification =
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(AddCreditsNudgeEmailNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
result: AddCreditsNudgeEmailResult::Sent,
|
||||
});
|
||||
store.push_notification(notification.clone());
|
||||
|
||||
store.rebase_buffer_after_session_refresh();
|
||||
|
||||
let snapshot = store.snapshot();
|
||||
let [ThreadBufferedEvent::Notification(actual)] = snapshot.events.as_slice() else {
|
||||
panic!("expected add-credits nudge email completion notification");
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(actual).expect("notification should serialize"),
|
||||
serde_json::to_value(notification).expect("notification should serialize"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_feedback_upload_params_includes_thread_id_and_rollout_path() {
|
||||
let thread_id = ThreadId::new();
|
||||
@@ -10752,8 +10691,6 @@ guardian_approval = true
|
||||
feedback: app.feedback.clone(),
|
||||
is_first_run: false,
|
||||
status_account_display: app.chat_widget.status_account_display().cloned(),
|
||||
initial_workspace_role: app.chat_widget.current_workspace_role(),
|
||||
initial_is_workspace_owner: app.chat_widget.current_is_workspace_owner(),
|
||||
initial_plan_type: app.chat_widget.current_plan_type(),
|
||||
model: Some(app.chat_widget.current_model().to_string()),
|
||||
startup_tooltip_override: None,
|
||||
|
||||
@@ -176,8 +176,6 @@ impl App {
|
||||
notification.auth_mode,
|
||||
notification.plan_type,
|
||||
),
|
||||
notification.workspace_role,
|
||||
notification.is_workspace_owner,
|
||||
notification.plan_type,
|
||||
matches!(
|
||||
notification.auth_mode,
|
||||
@@ -402,9 +400,6 @@ fn server_notification_thread_target(
|
||||
ServerNotification::ThreadRealtimeClosed(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::SkillsChanged(_)
|
||||
| ServerNotification::McpServerStatusUpdated(_)
|
||||
| ServerNotification::McpServerOauthLoginCompleted(_)
|
||||
@@ -1032,14 +1027,10 @@ fn app_server_codex_error_info_to_core(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ServerNotificationThreadTarget;
|
||||
use super::command_execution_started_event;
|
||||
use super::server_notification_thread_events;
|
||||
use super::server_notification_thread_target;
|
||||
use super::thread_snapshot_events;
|
||||
use super::turn_snapshot_events;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AgentMessageDeltaNotification;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::CommandAction;
|
||||
@@ -1070,19 +1061,6 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn add_credits_nudge_email_completion_targets_thread() {
|
||||
let thread_id = ThreadId::new();
|
||||
let target = server_notification_thread_target(
|
||||
&ServerNotification::AddCreditsNudgeEmailCompleted(AddCreditsNudgeEmailNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
result: AddCreditsNudgeEmailResult::Sent,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridges_completed_agent_messages_from_server_notifications() {
|
||||
let thread_id = "019cee8c-b993-7e33-88c0-014d4e62612d".to_string();
|
||||
|
||||
@@ -98,7 +98,6 @@ pub(crate) enum AppCommandView<'a> {
|
||||
SetThreadName {
|
||||
name: &'a str,
|
||||
},
|
||||
SendAddCreditsNudgeEmail,
|
||||
Shutdown,
|
||||
ThreadRollback {
|
||||
num_turns: u32,
|
||||
@@ -392,7 +391,6 @@ impl AppCommand {
|
||||
num_turns: *num_turns,
|
||||
},
|
||||
Op::Review { review_request } => AppCommandView::Review { review_request },
|
||||
Op::SendAddCreditsNudgeEmail => AppCommandView::SendAddCreditsNudgeEmail,
|
||||
op => AppCommandView::Other(op),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,10 +170,6 @@ pub(crate) enum AppEvent {
|
||||
result: Result<Vec<RateLimitSnapshot>, String>,
|
||||
},
|
||||
|
||||
/// Ask Codex to notify the current workspace owner that credits need to be
|
||||
/// added.
|
||||
NotifyWorkspaceOwner,
|
||||
|
||||
/// Result of prefetching connectors.
|
||||
ConnectorsLoaded {
|
||||
result: Result<ConnectorsSnapshot, String>,
|
||||
|
||||
@@ -24,8 +24,6 @@ use codex_app_server_protocol::ReviewStartResponse;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailResponse;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse;
|
||||
use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
@@ -105,8 +103,6 @@ pub(crate) struct AppServerBootstrap {
|
||||
pub(crate) account_email: Option<String>,
|
||||
pub(crate) auth_mode: Option<TelemetryAuthMode>,
|
||||
pub(crate) status_account_display: Option<StatusAccountDisplay>,
|
||||
pub(crate) workspace_role: Option<codex_app_server_protocol::WorkspaceRole>,
|
||||
pub(crate) is_workspace_owner: Option<bool>,
|
||||
pub(crate) plan_type: Option<codex_protocol::account::PlanType>,
|
||||
/// Whether the configured model provider needs OpenAI-style auth. Combined
|
||||
/// with `has_chatgpt_account` to decide if a startup rate-limit prefetch
|
||||
@@ -221,8 +217,6 @@ impl AppServerSession {
|
||||
account_email,
|
||||
auth_mode,
|
||||
status_account_display,
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
plan_type,
|
||||
feedback_audience,
|
||||
has_chatgpt_account,
|
||||
@@ -232,8 +226,6 @@ impl AppServerSession {
|
||||
Some(TelemetryAuthMode::ApiKey),
|
||||
Some(StatusAccountDisplay::ApiKey),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
FeedbackAudience::External,
|
||||
false,
|
||||
),
|
||||
@@ -250,30 +242,17 @@ impl AppServerSession {
|
||||
email: Some(email),
|
||||
plan: Some(plan_type_display_name(plan_type)),
|
||||
}),
|
||||
account.workspace_role,
|
||||
account.is_workspace_owner,
|
||||
Some(plan_type),
|
||||
feedback_audience,
|
||||
true,
|
||||
)
|
||||
}
|
||||
None => (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
FeedbackAudience::External,
|
||||
false,
|
||||
),
|
||||
None => (None, None, None, None, FeedbackAudience::External, false),
|
||||
};
|
||||
Ok(AppServerBootstrap {
|
||||
account_email,
|
||||
auth_mode,
|
||||
status_account_display,
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
plan_type,
|
||||
requires_openai_auth: account.requires_openai_auth,
|
||||
default_model,
|
||||
@@ -581,24 +560,6 @@ impl AppServerSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_add_credits_nudge_email(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<()> {
|
||||
let request_id = self.next_request_id();
|
||||
let _: ThreadAddCreditsNudgeEmailResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadAddCreditsNudgeEmail {
|
||||
request_id,
|
||||
params: ThreadAddCreditsNudgeEmailParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/addCreditsNudgeEmail failed in TUI")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_background_terminals_clean(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
@@ -1173,9 +1134,6 @@ pub(crate) fn app_server_rate_limit_snapshot_to_core(
|
||||
primary: snapshot.primary.map(app_server_rate_limit_window_to_core),
|
||||
secondary: snapshot.secondary.map(app_server_rate_limit_window_to_core),
|
||||
credits: snapshot.credits.map(app_server_credits_snapshot_to_core),
|
||||
spend_control: snapshot
|
||||
.spend_control
|
||||
.map(app_server_spend_control_snapshot_to_core),
|
||||
plan_type: snapshot.plan_type,
|
||||
}
|
||||
}
|
||||
@@ -1200,14 +1158,6 @@ fn app_server_credits_snapshot_to_core(
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_spend_control_snapshot_to_core(
|
||||
snapshot: codex_app_server_protocol::SpendControlSnapshot,
|
||||
) -> codex_protocol::protocol::SpendControlSnapshot {
|
||||
codex_protocol::protocol::SpendControlSnapshot {
|
||||
reached: snapshot.reached,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -648,23 +648,7 @@ impl BottomPaneView for ListSelectionView {
|
||||
&& !modifiers.contains(KeyModifiers::CONTROL)
|
||||
&& !modifiers.contains(KeyModifiers::ALT) =>
|
||||
{
|
||||
if let Some((visible_idx, _)) = self
|
||||
.filtered_indices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, actual_idx)| {
|
||||
self.items.get(**actual_idx).is_some_and(|item| {
|
||||
item.display_shortcut
|
||||
.as_ref()
|
||||
.is_some_and(|shortcut| shortcut.is_press(key_event))
|
||||
&& item.disabled_reason.is_none()
|
||||
&& !item.is_disabled
|
||||
})
|
||||
})
|
||||
{
|
||||
self.state.selected_idx = Some(visible_idx);
|
||||
self.accept();
|
||||
} else if let Some(idx) = c
|
||||
if let Some(idx) = c
|
||||
.to_digit(10)
|
||||
.map(|d| d as usize)
|
||||
.and_then(|d| d.checked_sub(1))
|
||||
|
||||
@@ -67,7 +67,6 @@ use crate::terminal_title::clear_terminal_title;
|
||||
use crate::terminal_title::set_terminal_title;
|
||||
use crate::text_formatting::proper_join;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult as AppServerAddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::CollabAgentState as AppServerCollabAgentState;
|
||||
@@ -92,7 +91,6 @@ use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::WorkspaceRole as AppServerWorkspaceRole;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::Notifications;
|
||||
@@ -135,7 +133,6 @@ use codex_protocol::models::local_image_label_text;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg;
|
||||
use codex_protocol::plan_tool::StepStatus as UpdatePlanItemStatus;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailStatus;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::AgentMessageDeltaEvent;
|
||||
#[cfg(test)]
|
||||
@@ -195,7 +192,6 @@ use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata;
|
||||
use codex_protocol::protocol::SpendControlSnapshot;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::StreamErrorEvent;
|
||||
use codex_protocol::protocol::TerminalInteractionEvent;
|
||||
@@ -561,8 +557,6 @@ pub(crate) struct ChatWidgetInit {
|
||||
pub(crate) feedback: codex_feedback::CodexFeedback,
|
||||
pub(crate) is_first_run: bool,
|
||||
pub(crate) status_account_display: Option<StatusAccountDisplay>,
|
||||
pub(crate) initial_workspace_role: Option<AppServerWorkspaceRole>,
|
||||
pub(crate) initial_is_workspace_owner: Option<bool>,
|
||||
pub(crate) initial_plan_type: Option<PlanType>,
|
||||
pub(crate) model: Option<String>,
|
||||
pub(crate) startup_tooltip_override: Option<String>,
|
||||
@@ -609,32 +603,6 @@ enum RateLimitErrorKind {
|
||||
Generic,
|
||||
}
|
||||
|
||||
pub(crate) const CODEX_USAGE_SETTINGS_URL: &str = "https://chatgpt.com/codex/settings/usage";
|
||||
const WORKSPACE_OWNER_NOTIFICATION_PROMPT: &str =
|
||||
"Your workspace is out of credits. Request more from your workspace owner? [y/N]";
|
||||
const WORKSPACE_OWNER_NOTIFICATION_TITLE: &str = "Request more credits from your workspace owner?";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum UsageBasedWorkspaceRateLimitState {
|
||||
OwnerCreditsDepleted,
|
||||
OwnerSpendCapReached,
|
||||
MemberCreditsDepleted,
|
||||
MemberSpendCapReached,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum UsageBasedWorkspaceBlockKind {
|
||||
CreditsDepleted,
|
||||
SpendCapReached,
|
||||
}
|
||||
|
||||
fn is_usage_based_workspace_plan(plan_type: PlanType) -> bool {
|
||||
matches!(
|
||||
plan_type,
|
||||
PlanType::SelfServeBusinessUsageBased | PlanType::EnterpriseCbpUsageBased
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn core_rate_limit_error_kind(info: &CoreCodexErrorInfo) -> Option<RateLimitErrorKind> {
|
||||
match info {
|
||||
@@ -790,8 +758,6 @@ pub(crate) struct ChatWidget {
|
||||
/// The currently active collaboration mask, if any.
|
||||
active_collaboration_mask: Option<CollaborationModeMask>,
|
||||
has_chatgpt_account: bool,
|
||||
workspace_role: Option<AppServerWorkspaceRole>,
|
||||
is_workspace_owner: Option<bool>,
|
||||
model_catalog: Arc<ModelCatalog>,
|
||||
session_telemetry: SessionTelemetry,
|
||||
session_header: SessionHeader,
|
||||
@@ -802,8 +768,6 @@ pub(crate) struct ChatWidget {
|
||||
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
|
||||
next_status_refresh_request_id: u64,
|
||||
plan_type: Option<PlanType>,
|
||||
notify_workspace_owner_in_flight: bool,
|
||||
pending_workspace_owner_notification_prompt: bool,
|
||||
rate_limit_warnings: RateLimitWarningState,
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState,
|
||||
adaptive_chunking: AdaptiveChunkingPolicy,
|
||||
@@ -2721,15 +2685,6 @@ impl ChatWidget {
|
||||
balance: credits.balance.clone(),
|
||||
});
|
||||
}
|
||||
if snapshot.spend_control.is_none() {
|
||||
snapshot.spend_control = self
|
||||
.rate_limit_snapshots_by_limit_id
|
||||
.get(&limit_id)
|
||||
.and_then(|display| display.spend_control.as_ref())
|
||||
.map(|spend_control| SpendControlSnapshot {
|
||||
reached: spend_control.reached,
|
||||
});
|
||||
}
|
||||
|
||||
self.plan_type = snapshot.plan_type.or(self.plan_type);
|
||||
|
||||
@@ -2798,7 +2753,6 @@ impl ChatWidget {
|
||||
} else {
|
||||
self.rate_limit_snapshots_by_limit_id.clear();
|
||||
}
|
||||
self.maybe_show_pending_workspace_owner_notification_prompt();
|
||||
self.refresh_status_line();
|
||||
}
|
||||
/// Finalize any active exec as failed and stop/clear agent-turn UI state.
|
||||
@@ -2841,34 +2795,15 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_error(&mut self, message: String) {
|
||||
self.on_error_with_hint(message, /*hint*/ None);
|
||||
}
|
||||
|
||||
fn on_error_with_hint(&mut self, message: String, hint: Option<String>) {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
self.finalize_turn();
|
||||
self.add_to_history(history_cell::new_error_event_with_hint(message, hint));
|
||||
self.add_to_history(history_cell::new_error_event(message));
|
||||
self.request_redraw();
|
||||
|
||||
// After an error ends the turn, try sending the next queued input.
|
||||
self.maybe_send_next_queued_input();
|
||||
}
|
||||
|
||||
fn on_rate_limit_error(&mut self, message: String) {
|
||||
if self.should_prefetch_rate_limits() {
|
||||
self.request_rate_limit_refresh();
|
||||
}
|
||||
let guidance = self.usage_based_workspace_rate_limit_error_guidance();
|
||||
let should_prompt_workspace_owner = self.should_prompt_workspace_owner_notification();
|
||||
let hint = guidance.as_ref().and_then(|(_, hint)| hint.clone());
|
||||
let message = guidance.map(|(message, _)| message).unwrap_or(message);
|
||||
self.on_error_with_hint(message, hint);
|
||||
if should_prompt_workspace_owner {
|
||||
self.pending_workspace_owner_notification_prompt = true;
|
||||
self.maybe_show_pending_workspace_owner_notification_prompt();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_non_retry_error(
|
||||
&mut self,
|
||||
message: String,
|
||||
@@ -2885,7 +2820,7 @@ impl ChatWidget {
|
||||
match info {
|
||||
RateLimitErrorKind::ServerOverloaded => self.on_server_overloaded_error(message),
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_rate_limit_error(message);
|
||||
self.on_error(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -4778,8 +4713,6 @@ impl ChatWidget {
|
||||
feedback,
|
||||
is_first_run,
|
||||
status_account_display,
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model,
|
||||
startup_tooltip_override,
|
||||
@@ -4841,8 +4774,6 @@ impl ChatWidget {
|
||||
current_collaboration_mode,
|
||||
active_collaboration_mask,
|
||||
has_chatgpt_account,
|
||||
workspace_role: initial_workspace_role,
|
||||
is_workspace_owner: initial_is_workspace_owner,
|
||||
model_catalog,
|
||||
session_telemetry,
|
||||
session_header: SessionHeader::new(header_model),
|
||||
@@ -4853,8 +4784,6 @@ impl ChatWidget {
|
||||
refreshing_status_outputs: Vec::new(),
|
||||
next_status_refresh_request_id: 0,
|
||||
plan_type: initial_plan_type,
|
||||
notify_workspace_owner_in_flight: false,
|
||||
pending_workspace_owner_notification_prompt: false,
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
||||
adaptive_chunking: AdaptiveChunkingPolicy::default(),
|
||||
@@ -5495,7 +5424,9 @@ impl ChatWidget {
|
||||
}
|
||||
SlashCommand::Status => {
|
||||
if self.should_prefetch_rate_limits() {
|
||||
let request_id = self.next_rate_limit_refresh_request_id();
|
||||
let request_id = self.next_status_refresh_request_id;
|
||||
self.next_status_refresh_request_id =
|
||||
self.next_status_refresh_request_id.wrapping_add(1);
|
||||
self.add_status_output(/*refreshing_rate_limits*/ true, Some(request_id));
|
||||
self.app_event_tx.send(AppEvent::RefreshRateLimits {
|
||||
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
||||
@@ -6756,18 +6687,6 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(notification) => {
|
||||
let result = match notification.result {
|
||||
AppServerAddCreditsNudgeEmailResult::Sent => {
|
||||
Ok(AddCreditsNudgeEmailStatus::Sent)
|
||||
}
|
||||
AppServerAddCreditsNudgeEmailResult::CooldownActive => {
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive)
|
||||
}
|
||||
AppServerAddCreditsNudgeEmailResult::Failed { message } => Err(message),
|
||||
};
|
||||
self.finish_notify_workspace_owner(result);
|
||||
}
|
||||
ServerNotification::ThreadRealtimeSdp(notification) => {
|
||||
if !from_replay {
|
||||
self.on_realtime_conversation_sdp(notification.sdp);
|
||||
@@ -7183,7 +7102,7 @@ impl ChatWidget {
|
||||
self.on_server_overloaded_error(message)
|
||||
}
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_rate_limit_error(message)
|
||||
self.on_error(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -7239,9 +7158,6 @@ impl ChatWidget {
|
||||
EventMsg::GetHistoryEntryResponse(ev) => self.handle_history_entry_response(ev),
|
||||
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
|
||||
EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev),
|
||||
EventMsg::AddCreditsNudgeEmailResponse(ev) => {
|
||||
self.finish_notify_workspace_owner(ev.result);
|
||||
}
|
||||
EventMsg::SkillsUpdateAvailable => {
|
||||
self.submit_op(AppCommand::list_skills(
|
||||
Vec::new(),
|
||||
@@ -9781,268 +9697,19 @@ impl ChatWidget {
|
||||
self.plan_type
|
||||
}
|
||||
|
||||
pub(crate) fn current_is_workspace_owner(&self) -> Option<bool> {
|
||||
self.workspace_role
|
||||
.map(|role| {
|
||||
matches!(
|
||||
role,
|
||||
AppServerWorkspaceRole::AccountOwner | AppServerWorkspaceRole::AccountAdmin
|
||||
)
|
||||
})
|
||||
.or(self.is_workspace_owner)
|
||||
}
|
||||
|
||||
pub(crate) fn current_workspace_role(&self) -> Option<AppServerWorkspaceRole> {
|
||||
self.workspace_role
|
||||
}
|
||||
|
||||
pub(crate) fn has_chatgpt_account(&self) -> bool {
|
||||
self.has_chatgpt_account
|
||||
}
|
||||
|
||||
fn usage_based_workspace_rate_limit_state(&self) -> Option<UsageBasedWorkspaceRateLimitState> {
|
||||
let block_kind = self.usage_based_workspace_block_kind()?;
|
||||
let is_workspace_owner = self.current_is_workspace_owner()?;
|
||||
Some(match (is_workspace_owner, block_kind) {
|
||||
(true, UsageBasedWorkspaceBlockKind::CreditsDepleted) => {
|
||||
UsageBasedWorkspaceRateLimitState::OwnerCreditsDepleted
|
||||
}
|
||||
(true, UsageBasedWorkspaceBlockKind::SpendCapReached) => {
|
||||
UsageBasedWorkspaceRateLimitState::OwnerSpendCapReached
|
||||
}
|
||||
(false, UsageBasedWorkspaceBlockKind::CreditsDepleted) => {
|
||||
UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted
|
||||
}
|
||||
(false, UsageBasedWorkspaceBlockKind::SpendCapReached) => {
|
||||
UsageBasedWorkspaceRateLimitState::MemberSpendCapReached
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn usage_based_workspace_block_kind(&self) -> Option<UsageBasedWorkspaceBlockKind> {
|
||||
let plan_type = self.plan_type?;
|
||||
if !is_usage_based_workspace_plan(plan_type) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let codex_snapshot = self.rate_limit_snapshots_by_limit_id.get("codex")?;
|
||||
let credits_depleted = codex_snapshot
|
||||
.credits
|
||||
.as_ref()
|
||||
.is_some_and(|credits| !credits.has_credits);
|
||||
let spend_control_reached = codex_snapshot
|
||||
.spend_control
|
||||
.as_ref()
|
||||
.map(|spend_control| spend_control.reached);
|
||||
|
||||
let blocked_state = match (credits_depleted, spend_control_reached) {
|
||||
(true, _) => Some(true),
|
||||
(false, Some(true)) => Some(false),
|
||||
(false, Some(false)) => None,
|
||||
// Older snapshots do not carry spend-control state, so preserve the
|
||||
// historical interpretation for compatibility.
|
||||
(false, None) => Some(false),
|
||||
}?;
|
||||
|
||||
Some(match blocked_state {
|
||||
true => UsageBasedWorkspaceBlockKind::CreditsDepleted,
|
||||
false => UsageBasedWorkspaceBlockKind::SpendCapReached,
|
||||
})
|
||||
}
|
||||
|
||||
fn usage_based_workspace_rate_limit_error_guidance(&self) -> Option<(String, Option<String>)> {
|
||||
if let Some(state) = self.usage_based_workspace_rate_limit_state() {
|
||||
return match state {
|
||||
UsageBasedWorkspaceRateLimitState::OwnerCreditsDepleted => Some((
|
||||
"Your workspace is out of credits.".to_string(),
|
||||
Some(format!(
|
||||
"Visit {CODEX_USAGE_SETTINGS_URL} to add workspace credits and continue using Codex."
|
||||
)),
|
||||
)),
|
||||
UsageBasedWorkspaceRateLimitState::OwnerSpendCapReached => Some((
|
||||
"Your workspace has reached its spend cap.".to_string(),
|
||||
Some(format!(
|
||||
"Visit {CODEX_USAGE_SETTINGS_URL} to increase your workspace spend cap and continue using Codex."
|
||||
)),
|
||||
)),
|
||||
UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted => {
|
||||
Some((WORKSPACE_OWNER_NOTIFICATION_PROMPT.to_string(), None))
|
||||
}
|
||||
UsageBasedWorkspaceRateLimitState::MemberSpendCapReached => Some((
|
||||
"Your workspace has reached its spend cap.".to_string(),
|
||||
Some(format!(
|
||||
"Ask an admin to increase your workspace spend cap. Visit {CODEX_USAGE_SETTINGS_URL} for usage settings."
|
||||
)),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
match self.usage_based_workspace_block_kind()? {
|
||||
UsageBasedWorkspaceBlockKind::CreditsDepleted => Some((
|
||||
"Your workspace is out of credits.".to_string(),
|
||||
Some(format!(
|
||||
"If you're the workspace owner, visit {CODEX_USAGE_SETTINGS_URL} to add credits. Otherwise wait a moment while Codex refreshes your workspace status."
|
||||
)),
|
||||
)),
|
||||
UsageBasedWorkspaceBlockKind::SpendCapReached => Some((
|
||||
"Your workspace has reached its spend cap.".to_string(),
|
||||
Some(format!(
|
||||
"Ask an admin to increase the workspace spend cap, or visit {CODEX_USAGE_SETTINGS_URL} for usage settings."
|
||||
)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_rate_limit_refresh_request_id(&mut self) -> u64 {
|
||||
let request_id = self.next_status_refresh_request_id;
|
||||
self.next_status_refresh_request_id = self.next_status_refresh_request_id.wrapping_add(1);
|
||||
request_id
|
||||
}
|
||||
|
||||
fn send_rate_limit_refresh_request(&self, request_id: u64) {
|
||||
self.app_event_tx.send(AppEvent::RefreshRateLimits {
|
||||
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
||||
});
|
||||
}
|
||||
|
||||
fn request_rate_limit_refresh(&mut self) -> u64 {
|
||||
let request_id = self.next_rate_limit_refresh_request_id();
|
||||
self.send_rate_limit_refresh_request(request_id);
|
||||
request_id
|
||||
}
|
||||
|
||||
fn missing_usage_based_workspace_rate_limit_snapshot(&self) -> bool {
|
||||
self.should_prefetch_rate_limits()
|
||||
&& self.plan_type.is_some()
|
||||
&& !self.rate_limit_snapshots_by_limit_id.contains_key("codex")
|
||||
}
|
||||
|
||||
fn should_prompt_workspace_owner_notification(&self) -> bool {
|
||||
if self.notify_workspace_owner_in_flight {
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
self.usage_based_workspace_rate_limit_state(),
|
||||
Some(UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let is_workspace_owner = self.current_is_workspace_owner();
|
||||
if !self.plan_type.is_some_and(is_usage_based_workspace_plan)
|
||||
|| is_workspace_owner == Some(true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let block_kind = self.usage_based_workspace_block_kind();
|
||||
block_kind != Some(UsageBasedWorkspaceBlockKind::SpendCapReached)
|
||||
}
|
||||
|
||||
fn maybe_show_pending_workspace_owner_notification_prompt(&mut self) {
|
||||
if !self.pending_workspace_owner_notification_prompt {
|
||||
return;
|
||||
}
|
||||
if self.notify_workspace_owner_in_flight {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
return;
|
||||
}
|
||||
if !self.bottom_pane.no_modal_or_popup_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.usage_based_workspace_rate_limit_state() {
|
||||
Some(UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted) => {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
self.open_workspace_owner_notification_prompt();
|
||||
}
|
||||
Some(_) => {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
}
|
||||
None => {
|
||||
if self.usage_based_workspace_block_kind()
|
||||
== Some(UsageBasedWorkspaceBlockKind::CreditsDepleted)
|
||||
&& self.current_is_workspace_owner().is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !self.missing_usage_based_workspace_rate_limit_snapshot() {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_workspace_owner_notification_prompt(&mut self) {
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Yes".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('y'))),
|
||||
actions: vec![Box::new(|tx| tx.send(AppEvent::NotifyWorkspaceOwner))],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "No".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('n'))),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: Some(WORKSPACE_OWNER_NOTIFICATION_TITLE.to_string()),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
initial_selected_idx: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn start_notify_workspace_owner(&mut self) {
|
||||
self.notify_workspace_owner_in_flight = true;
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
}
|
||||
|
||||
pub(crate) fn finish_notify_workspace_owner(
|
||||
&mut self,
|
||||
result: Result<AddCreditsNudgeEmailStatus, String>,
|
||||
) {
|
||||
self.notify_workspace_owner_in_flight = false;
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
match result {
|
||||
Ok(AddCreditsNudgeEmailStatus::Sent) => {
|
||||
self.add_info_message("Workspace owner notified.".to_string(), /*hint*/ None);
|
||||
}
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive) => {
|
||||
self.add_info_message(
|
||||
"Workspace owner was already notified recently.".to_string(),
|
||||
/*hint*/ None,
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
self.add_error_message(
|
||||
"Could not notify your workspace owner. Please try again.".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_account_state(
|
||||
&mut self,
|
||||
status_account_display: Option<StatusAccountDisplay>,
|
||||
workspace_role: Option<AppServerWorkspaceRole>,
|
||||
is_workspace_owner: Option<bool>,
|
||||
plan_type: Option<PlanType>,
|
||||
has_chatgpt_account: bool,
|
||||
) {
|
||||
self.status_account_display = status_account_display;
|
||||
self.workspace_role = workspace_role;
|
||||
self.is_workspace_owner = is_workspace_owner;
|
||||
self.plan_type = plan_type;
|
||||
self.has_chatgpt_account = has_chatgpt_account;
|
||||
self.maybe_show_pending_workspace_owner_notification_prompt();
|
||||
self.bottom_pane
|
||||
.set_connectors_enabled(self.connectors_enabled());
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/app_server.rs
|
||||
assertion_line: 608
|
||||
expression: rendered
|
||||
---
|
||||
■ Your workspace is out of credits. Request more from your workspace owner? [y/N]
|
||||
@@ -128,7 +128,6 @@ pub(super) use codex_protocol::parse_command::ParsedCommand;
|
||||
pub(super) use codex_protocol::plan_tool::PlanItemArg;
|
||||
pub(super) use codex_protocol::plan_tool::StepStatus;
|
||||
pub(super) use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
pub(super) use codex_protocol::protocol::AddCreditsNudgeEmailStatus;
|
||||
pub(super) use codex_protocol::protocol::AgentMessageDeltaEvent;
|
||||
pub(super) use codex_protocol::protocol::AgentMessageEvent;
|
||||
pub(super) use codex_protocol::protocol::AgentReasoningDeltaEvent;
|
||||
|
||||
@@ -569,117 +569,6 @@ async fn live_app_server_server_overloaded_error_renders_warning() {
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_usage_limit_error_shows_notify_owner_hint() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded.into()),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace is out of credits."),
|
||||
"expected usage-limit error, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner confirmation popup, got {popup:?}"
|
||||
);
|
||||
assert_chatwidget_snapshot!(
|
||||
"live_app_server_usage_limit_error_shows_notify_owner_hint",
|
||||
rendered
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_usage_limit_error_shows_spend_cap_hint() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded.into()),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace has reached its spend cap."),
|
||||
"expected spend-cap error, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected spend-cap guidance instead of workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_invalid_thread_name_update_is_ignored() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -104,7 +104,6 @@ pub(super) fn snapshot(percent: f64) -> RateLimitSnapshot {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}
|
||||
}
|
||||
@@ -190,8 +189,6 @@ pub(super) async fn make_chatwidget_manual(
|
||||
current_collaboration_mode,
|
||||
active_collaboration_mask,
|
||||
has_chatgpt_account: false,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
model_catalog,
|
||||
session_telemetry,
|
||||
session_header: SessionHeader::new(resolved_model.clone()),
|
||||
@@ -202,8 +199,6 @@ pub(super) async fn make_chatwidget_manual(
|
||||
refreshing_status_outputs: Vec::new(),
|
||||
next_status_refresh_request_id: 0,
|
||||
plan_type: None,
|
||||
notify_workspace_owner_in_flight: false,
|
||||
pending_workspace_owner_notification_prompt: false,
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
||||
adaptive_chunking: crate::streaming::chunking::AdaptiveChunkingPolicy::default(),
|
||||
|
||||
@@ -1245,8 +1245,6 @@ async fn collaboration_modes_defaults_to_code_on_startup() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(resolved_model.clone()),
|
||||
startup_tooltip_override: None,
|
||||
|
||||
@@ -71,8 +71,6 @@ async fn experimental_mode_plan_is_ignored_on_startup() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(resolved_model.clone()),
|
||||
startup_tooltip_override: None,
|
||||
|
||||
@@ -130,8 +130,6 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(resolved_model),
|
||||
startup_tooltip_override: None,
|
||||
@@ -236,7 +234,6 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
|
||||
unlimited: false,
|
||||
balance: Some("17.5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
let initial_balance = chat
|
||||
@@ -256,7 +253,6 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
|
||||
@@ -295,7 +291,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
|
||||
resets_at: None,
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Plus),
|
||||
}));
|
||||
assert_eq!(chat.plan_type, Some(PlanType::Plus));
|
||||
@@ -314,7 +309,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
|
||||
resets_at: Some(234),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Pro),
|
||||
}));
|
||||
assert_eq!(chat.plan_type, Some(PlanType::Pro));
|
||||
@@ -333,7 +327,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
|
||||
resets_at: Some(567),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
assert_eq!(chat.plan_type, Some(PlanType::Pro));
|
||||
@@ -357,7 +350,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
unlimited: false,
|
||||
balance: Some("5.00".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Pro),
|
||||
}));
|
||||
|
||||
@@ -371,7 +363,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Pro),
|
||||
}));
|
||||
|
||||
@@ -396,101 +387,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
assert!(other.credits.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_usage_limit_error_shows_notify_owner_hint() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace is out of credits."),
|
||||
"expected usage-limit error, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner confirmation popup, got {popup:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_usage_limit_error_shows_spend_cap_hint_when_reached() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace has reached its spend cap."),
|
||||
"expected spend-cap hint, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected spend-cap hint instead of workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[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;
|
||||
@@ -519,7 +415,6 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
|
||||
|
||||
@@ -120,212 +120,3 @@ async fn status_command_overlapping_refreshes_update_matching_cells_only() {
|
||||
chat.finish_status_rate_limit_refresh(second_request_id);
|
||||
assert!(chat.refreshing_status_outputs.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn usage_limit_error_requests_background_rate_limit_refresh() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
|
||||
"expected usage-limit errors to trigger a background rate-limit refresh; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn usage_limit_error_opens_workspace_owner_prompt_after_rate_limits_refresh() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
|
||||
"expected usage-limit errors to request a refresh when rate limits are missing; events: {events:?}"
|
||||
);
|
||||
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner prompt after refresh, got: {popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::NotifyWorkspaceOwner));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn usage_limit_error_opens_workspace_owner_prompt_after_async_workspace_role() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
/*is_workspace_owner*/ None,
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
|
||||
"expected usage-limit errors to request a refresh when rate limits are missing; events: {events:?}"
|
||||
);
|
||||
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
!popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected no prompt before the async workspace role update, got: {popup}"
|
||||
);
|
||||
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
Some(AppServerWorkspaceRole::StandardUser),
|
||||
/*is_workspace_owner*/ None,
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner prompt after async workspace role, got: {popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::NotifyWorkspaceOwner));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_workspace_owner_success_adds_confirmation_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.start_notify_workspace_owner();
|
||||
|
||||
chat.finish_notify_workspace_owner(Ok(AddCreditsNudgeEmailStatus::Sent));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one confirmation message");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Workspace owner notified."),
|
||||
"expected success message, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!chat.notify_workspace_owner_in_flight,
|
||||
"notify-owner state should clear after success"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_workspace_owner_cooldown_adds_info_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.start_notify_workspace_owner();
|
||||
|
||||
chat.finish_notify_workspace_owner(Ok(AddCreditsNudgeEmailStatus::CooldownActive));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one cooldown message");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Workspace owner was already notified recently."),
|
||||
"expected cooldown message, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!chat.notify_workspace_owner_in_flight,
|
||||
"notify-owner state should clear after cooldown"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_workspace_owner_error_adds_retry_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.start_notify_workspace_owner();
|
||||
|
||||
chat.finish_notify_workspace_owner(Err("backend failed".to_string()));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one error message");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Could not notify your workspace owner. Please try again."),
|
||||
"expected retry message, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!chat.notify_workspace_owner_in_flight,
|
||||
"notify-owner state should clear after errors"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2166,17 +2166,10 @@ pub(crate) fn new_info_event(message: String, hint: Option<String>) -> PlainHist
|
||||
}
|
||||
|
||||
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
||||
new_error_event_with_hint(message, /*hint*/ None)
|
||||
}
|
||||
|
||||
pub(crate) fn new_error_event_with_hint(message: String, hint: Option<String>) -> PlainHistoryCell {
|
||||
// Use a hair space (U+200A) to create a subtle, near-invisible separation
|
||||
// before the text. VS16 is intentionally omitted to keep spacing tighter
|
||||
// in terminals like Ghostty.
|
||||
let mut lines: Vec<Line<'static>> = vec![vec![format!("■ {message}").red()].into()];
|
||||
if let Some(hint) = hint {
|
||||
lines.push(vec![" ".into(), hint.dark_gray()].into());
|
||||
}
|
||||
let lines: Vec<Line<'static>> = vec![vec![format!("■ {message}").red()].into()];
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
|
||||
@@ -891,9 +891,7 @@ pub async fn run_main(
|
||||
// use RUST_LOG env var, default to info for codex crates.
|
||||
let env_filter = || {
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new(
|
||||
"codex_core=info,codex_tui=info,codex_rmcp_client=info,codex_app_server=info",
|
||||
)
|
||||
EnvFilter::new("codex_core=info,codex_tui=info,codex_rmcp_client=info")
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ use chrono::Utc;
|
||||
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::SpendControlSnapshot as CoreSpendControlSnapshot;
|
||||
|
||||
const STATUS_LIMIT_BAR_SEGMENTS: usize = 20;
|
||||
const STATUS_LIMIT_BAR_FILLED: &str = "█";
|
||||
@@ -99,8 +98,6 @@ pub(crate) struct RateLimitSnapshotDisplay {
|
||||
pub secondary: Option<RateLimitWindowDisplay>,
|
||||
/// Optional credits metadata when available.
|
||||
pub credits: Option<CreditsSnapshotDisplay>,
|
||||
/// Optional spend-control metadata for usage-based workspace plans.
|
||||
pub spend_control: Option<SpendControlSnapshotDisplay>,
|
||||
}
|
||||
|
||||
/// Display-ready credits state extracted from protocol snapshots.
|
||||
@@ -114,13 +111,6 @@ pub(crate) struct CreditsSnapshotDisplay {
|
||||
pub balance: Option<String>,
|
||||
}
|
||||
|
||||
/// Display-ready spend-control state extracted from protocol snapshots.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SpendControlSnapshotDisplay {
|
||||
/// Whether a workspace spend cap is currently blocking usage.
|
||||
pub reached: bool,
|
||||
}
|
||||
|
||||
/// Converts a protocol snapshot into UI-friendly display data.
|
||||
///
|
||||
/// Pass the timestamp from the same observation point as `snapshot`; supplying a significantly
|
||||
@@ -150,10 +140,6 @@ pub(crate) fn rate_limit_snapshot_display_for_limit(
|
||||
.as_ref()
|
||||
.map(|window| RateLimitWindowDisplay::from_window(window, captured_at)),
|
||||
credits: snapshot.credits.as_ref().map(CreditsSnapshotDisplay::from),
|
||||
spend_control: snapshot
|
||||
.spend_control
|
||||
.as_ref()
|
||||
.map(SpendControlSnapshotDisplay::from),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +153,6 @@ impl From<&CoreCreditsSnapshot> for CreditsSnapshotDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CoreSpendControlSnapshot> for SpendControlSnapshotDisplay {
|
||||
fn from(value: &CoreSpendControlSnapshot) -> Self {
|
||||
Self {
|
||||
reached: value.reached,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds display rows from a snapshot and marks stale data by capture age.
|
||||
///
|
||||
/// Callers should pass `Local::now()` for `now` at render time; using a cached timestamp can make
|
||||
@@ -197,7 +175,7 @@ pub(crate) fn compose_rate_limit_data_many(
|
||||
return StatusRateLimitData::Missing;
|
||||
}
|
||||
|
||||
let mut rows = Vec::with_capacity(snapshots.len().saturating_mul(4));
|
||||
let mut rows = Vec::with_capacity(snapshots.len().saturating_mul(3));
|
||||
let mut stale = false;
|
||||
|
||||
for snapshot in snapshots {
|
||||
@@ -285,12 +263,6 @@ pub(crate) fn compose_rate_limit_data_many(
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(spend_control) = snapshot.spend_control.as_ref()
|
||||
&& let Some(row) = spend_control_status_row(spend_control)
|
||||
{
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
if let Some(credits) = snapshot.credits.as_ref()
|
||||
&& let Some(row) = credit_status_row(credits)
|
||||
{
|
||||
@@ -350,15 +322,6 @@ fn credit_status_row(credits: &CreditsSnapshotDisplay) -> Option<StatusRateLimit
|
||||
})
|
||||
}
|
||||
|
||||
fn spend_control_status_row(
|
||||
spend_control: &SpendControlSnapshotDisplay,
|
||||
) -> Option<StatusRateLimitRow> {
|
||||
spend_control.reached.then(|| StatusRateLimitRow {
|
||||
label: "Spend cap".to_string(),
|
||||
value: StatusRateLimitValue::Text("Reached".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn format_credit_balance(raw: &str) -> Option<String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -386,7 +349,6 @@ mod tests {
|
||||
use super::CreditsSnapshotDisplay;
|
||||
use super::RateLimitSnapshotDisplay;
|
||||
use super::RateLimitWindowDisplay;
|
||||
use super::SpendControlSnapshotDisplay;
|
||||
use super::StatusRateLimitData;
|
||||
use super::compose_rate_limit_data_many;
|
||||
use chrono::Local;
|
||||
@@ -413,7 +375,6 @@ mod tests {
|
||||
unlimited: false,
|
||||
balance: Some("25".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
};
|
||||
let other = RateLimitSnapshotDisplay {
|
||||
limit_name: "codex-other".to_string(),
|
||||
@@ -425,7 +386,6 @@ mod tests {
|
||||
unlimited: false,
|
||||
balance: Some("99".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
};
|
||||
|
||||
let rows = match compose_rate_limit_data_many(&[codex, other], now) {
|
||||
@@ -463,7 +423,6 @@ mod tests {
|
||||
window_minutes: None,
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
};
|
||||
|
||||
let rows = match compose_rate_limit_data_many(&[other], now) {
|
||||
@@ -480,28 +439,4 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spend_cap_reached_renders_status_row_without_windows() {
|
||||
let now = Local::now();
|
||||
let snapshot = RateLimitSnapshotDisplay {
|
||||
limit_name: "codex".to_string(),
|
||||
captured_at: now,
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshotDisplay {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(SpendControlSnapshotDisplay { reached: true }),
|
||||
};
|
||||
|
||||
let rows = match compose_rate_limit_data_many(&[snapshot], now) {
|
||||
StatusRateLimitData::Available(rows) => rows,
|
||||
other => panic!("unexpected status: {other:?}"),
|
||||
};
|
||||
let labels: Vec<String> = rows.iter().map(|row| row.label.clone()).collect();
|
||||
assert_eq!(labels, vec!["Spend cap".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
---
|
||||
source: tui/src/status/tests.rs
|
||||
assertion_line: 963
|
||||
expression: sanitized
|
||||
---
|
||||
/status
|
||||
|
||||
╭───────────────────────────────────────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v0.0.0) │
|
||||
│ │
|
||||
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
|
||||
│ information on rate limits and credits │
|
||||
│ │
|
||||
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
|
||||
│ Directory: [[workspace]] │
|
||||
│ Permissions: Custom (read-only, on-request) │
|
||||
│ Agents.md: <none> │
|
||||
│ │
|
||||
│ Token usage: 750 total (500 input + 250 output) │
|
||||
│ Context window: 100% left (750 used / 272K) │
|
||||
│ Spend cap: Reached │
|
||||
╰───────────────────────────────────────────────────────────────────────╯
|
||||
@@ -136,7 +136,6 @@ async fn status_snapshot_includes_reasoning_details() {
|
||||
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -320,7 +319,6 @@ async fn status_snapshot_includes_monthly_limit() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -372,7 +370,6 @@ async fn status_snapshot_shows_unlimited_credits() {
|
||||
unlimited: true,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -422,7 +419,6 @@ async fn status_snapshot_shows_positive_credits() {
|
||||
unlimited: false,
|
||||
balance: Some("12.5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -472,7 +468,6 @@ async fn status_snapshot_hides_zero_credits() {
|
||||
unlimited: false,
|
||||
balance: Some("0".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -520,7 +515,6 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
|
||||
unlimited: true,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -626,7 +620,6 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -740,7 +733,6 @@ async fn status_snapshot_shows_refreshing_limits_notice() {
|
||||
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_700)),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -811,7 +803,6 @@ async fn status_snapshot_includes_credits_and_limits() {
|
||||
unlimited: false,
|
||||
balance: Some("37.5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -865,69 +856,6 @@ async fn status_snapshot_shows_unavailable_limits_message() {
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
.with_ymd_and_hms(2024, 6, 7, 8, 9, 10)
|
||||
.single()
|
||||
.expect("timestamp");
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
|
||||
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
|
||||
let token_info = token_info_for(&model_slug, &config, &usage);
|
||||
let composite = new_status_output(
|
||||
&config,
|
||||
account_display.as_ref(),
|
||||
Some(&token_info),
|
||||
&usage,
|
||||
&None,
|
||||
/*thread_name*/ None,
|
||||
/*forked_from*/ None,
|
||||
Some(&rate_display),
|
||||
None,
|
||||
captured_at,
|
||||
&model_slug,
|
||||
/*collaboration_mode*/ None,
|
||||
/*reasoning_effort_override*/ None,
|
||||
);
|
||||
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
||||
if cfg!(windows) {
|
||||
for line in &mut rendered_lines {
|
||||
*line = line.replace('\\', "/");
|
||||
}
|
||||
}
|
||||
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
||||
assert_snapshot!(sanitized);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_snapshot_shows_spend_cap_reached_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = PathBuf::from("/workspace/tests").abs();
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
output_tokens: 250,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 750,
|
||||
};
|
||||
|
||||
let snapshot = RateLimitSnapshot {
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
@@ -984,7 +912,6 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
@@ -1055,7 +982,6 @@ async fn status_snapshot_shows_stale_limits_message() {
|
||||
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_800)),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -1126,7 +1052,6 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
|
||||
unlimited: false,
|
||||
balance: Some("80".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
|
||||
Reference in New Issue
Block a user