mirror of
https://github.com/openai/codex.git
synced 2026-11-11 06:31:37 +00:00
[codex] Add workspace owner usage nudge UI (#18221)
## Summary Third PR in the split from #17956. Stacked on #18220. - shows workspace-owner/member-specific rate-limit messages behind `workspace_owner_usage_nudge` - prompts workspace members to notify the owner or request a usage-limit increase - sends the confirmed nudge through the app-server API and renders completion feedback - adds focused TUI snapshot coverage for prompts and completion states - feature gate ## Validation - `cargo test -p codex-backend-client` - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-app-server rate_limits` - `cargo test -p codex-tui workspace_` - `cargo test -p codex-tui status_` - `just fmt` - `just fix -p codex-backend-client` - `just fix -p codex-app-server-protocol` - `just fix -p codex-app-server` - `just fix -p codex-tui`
This commit is contained in:
1 parent
ab65fbbdd6
commit
3c75f9b4dd
14 files changed
+673
-2
No files matched your search
@@ -78,6 +78,8 @@ 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::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
@@ -192,6 +194,7 @@ use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::RateLimitReachedType;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
@@ -781,8 +784,10 @@ pub(crate) struct ChatWidget {
|
||||
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
|
||||
next_status_refresh_request_id: u64,
|
||||
plan_type: Option<PlanType>,
|
||||
codex_rate_limit_reached_type: Option<RateLimitReachedType>,
|
||||
rate_limit_warnings: RateLimitWarningState,
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState,
|
||||
add_credits_nudge_email_in_flight: Option<AddCreditsNudgeCreditType>,
|
||||
adaptive_chunking: AdaptiveChunkingPolicy,
|
||||
// Stream lifecycle controller
|
||||
stream_controller: Option<StreamController>,
|
||||
@@ -2857,6 +2862,11 @@ impl ChatWidget {
|
||||
self.plan_type = snapshot.plan_type.or(self.plan_type);
|
||||
|
||||
let is_codex_limit = limit_id.eq_ignore_ascii_case("codex");
|
||||
if is_codex_limit
|
||||
&& let Some(rate_limit_reached_type) = snapshot.rate_limit_reached_type
|
||||
{
|
||||
self.codex_rate_limit_reached_type = Some(rate_limit_reached_type);
|
||||
}
|
||||
let warnings = if is_codex_limit {
|
||||
self.rate_limit_warnings.take_warnings(
|
||||
snapshot
|
||||
@@ -2920,6 +2930,7 @@ impl ChatWidget {
|
||||
}
|
||||
} else {
|
||||
self.rate_limit_snapshots_by_limit_id.clear();
|
||||
self.codex_rate_limit_reached_type = None;
|
||||
}
|
||||
self.refresh_status_line();
|
||||
}
|
||||
@@ -2973,6 +2984,62 @@ impl ChatWidget {
|
||||
self.maybe_send_next_queued_input();
|
||||
}
|
||||
|
||||
fn workspace_owner_usage_nudge_enabled(&self) -> bool {
|
||||
self.config
|
||||
.features
|
||||
.enabled(Feature::WorkspaceOwnerUsageNudge)
|
||||
}
|
||||
|
||||
fn on_rate_limit_error(&mut self, error_kind: RateLimitErrorKind, message: String) {
|
||||
if !self.workspace_owner_usage_nudge_enabled() {
|
||||
self.on_error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
let rate_limit_reached_type = self.codex_rate_limit_reached_type.map(|kind| {
|
||||
if matches!(error_kind, RateLimitErrorKind::UsageLimit) {
|
||||
match kind {
|
||||
RateLimitReachedType::WorkspaceOwnerCreditsDepleted => {
|
||||
RateLimitReachedType::WorkspaceOwnerUsageLimitReached
|
||||
}
|
||||
RateLimitReachedType::WorkspaceMemberCreditsDepleted => {
|
||||
RateLimitReachedType::WorkspaceMemberUsageLimitReached
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
self.codex_rate_limit_reached_type = rate_limit_reached_type;
|
||||
|
||||
match rate_limit_reached_type {
|
||||
Some(RateLimitReachedType::WorkspaceOwnerCreditsDepleted) => {
|
||||
self.on_error(
|
||||
"You're out of credits. Your workspace is out of credits. Add credits to continue using Codex."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Some(RateLimitReachedType::WorkspaceOwnerUsageLimitReached) => {
|
||||
self.on_error(
|
||||
"Usage limit reached. You've reached your usage limit. Increase your limits to continue using codex."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Some(RateLimitReachedType::WorkspaceMemberCreditsDepleted) => {
|
||||
self.on_error(message);
|
||||
self.open_workspace_owner_nudge_prompt(AddCreditsNudgeCreditType::Credits);
|
||||
}
|
||||
Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached) => {
|
||||
self.on_error(message);
|
||||
self.open_workspace_owner_nudge_prompt(AddCreditsNudgeCreditType::UsageLimit);
|
||||
}
|
||||
Some(RateLimitReachedType::RateLimitReached) | None => {
|
||||
self.on_error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_non_retry_error(
|
||||
&mut self,
|
||||
message: String,
|
||||
@@ -2989,7 +3056,7 @@ impl ChatWidget {
|
||||
match info {
|
||||
RateLimitErrorKind::ServerOverloaded => self.on_server_overloaded_error(message),
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_error(message)
|
||||
self.on_rate_limit_error(info, message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -4996,8 +5063,10 @@ impl ChatWidget {
|
||||
refreshing_status_outputs: Vec::new(),
|
||||
next_status_refresh_request_id: 0,
|
||||
plan_type: initial_plan_type,
|
||||
codex_rate_limit_reached_type: None,
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
||||
add_credits_nudge_email_in_flight: None,
|
||||
adaptive_chunking: AdaptiveChunkingPolicy::default(),
|
||||
stream_controller: None,
|
||||
plan_stream_controller: None,
|
||||
@@ -6942,7 +7011,7 @@ impl ChatWidget {
|
||||
self.on_server_overloaded_error(message)
|
||||
}
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_error(message)
|
||||
self.on_rate_limit_error(kind, message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -7794,6 +7863,105 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
fn open_workspace_owner_nudge_prompt(&mut self, credit_type: AddCreditsNudgeCreditType) {
|
||||
if !self.workspace_owner_usage_nudge_enabled()
|
||||
|| self.add_credits_nudge_email_in_flight.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let (title, prompt) = match credit_type {
|
||||
AddCreditsNudgeCreditType::Credits => (
|
||||
"You've reached your workspace credit limit",
|
||||
"Your workspace is out of credits. Ask your workspace owner to add more. Notify owner?",
|
||||
),
|
||||
AddCreditsNudgeCreditType::UsageLimit => (
|
||||
"Usage limit reached",
|
||||
"Request a limit increase from your owner to continue using codex. Request increase?",
|
||||
),
|
||||
};
|
||||
let send_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::SendAddCreditsNudgeEmail { credit_type });
|
||||
})];
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Yes".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('y'))),
|
||||
actions: send_actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "No".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('n'))),
|
||||
is_default: true,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: Some(title.to_string()),
|
||||
subtitle: Some(prompt.to_string()),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
initial_selected_idx: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn start_add_credits_nudge_email_request(
|
||||
&mut self,
|
||||
credit_type: AddCreditsNudgeCreditType,
|
||||
) -> bool {
|
||||
if !self.workspace_owner_usage_nudge_enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.add_credits_nudge_email_in_flight = Some(credit_type);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn finish_add_credits_nudge_email_request(
|
||||
&mut self,
|
||||
result: Result<AddCreditsNudgeEmailStatus, String>,
|
||||
) {
|
||||
let credit_type = self
|
||||
.add_credits_nudge_email_in_flight
|
||||
.take()
|
||||
.unwrap_or(AddCreditsNudgeCreditType::Credits);
|
||||
if !self.workspace_owner_usage_nudge_enabled() {
|
||||
return;
|
||||
}
|
||||
let message = match (credit_type, result) {
|
||||
(AddCreditsNudgeCreditType::Credits, Ok(AddCreditsNudgeEmailStatus::Sent)) => {
|
||||
"Workspace owner notified."
|
||||
}
|
||||
(
|
||||
AddCreditsNudgeCreditType::Credits,
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive),
|
||||
) => "Workspace owner was already notified recently.",
|
||||
(AddCreditsNudgeCreditType::Credits, Err(_)) => {
|
||||
"Could not notify your workspace owner. Please try again."
|
||||
}
|
||||
(AddCreditsNudgeCreditType::UsageLimit, Ok(AddCreditsNudgeEmailStatus::Sent)) => {
|
||||
"Limit increase requested."
|
||||
}
|
||||
(
|
||||
AddCreditsNudgeCreditType::UsageLimit,
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive),
|
||||
) => "A limit increase was already requested recently.",
|
||||
(AddCreditsNudgeCreditType::UsageLimit, Err(_)) => {
|
||||
"Could not request a limit increase. Please try again."
|
||||
}
|
||||
};
|
||||
self.add_to_history(history_cell::new_info_event(
|
||||
message.to_string(),
|
||||
/*hint*/ None,
|
||||
));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Open a popup to choose a quick auto model. Selecting "All models"
|
||||
/// opens the full picker with every available preset.
|
||||
pub(crate) fn open_model_popup(&mut self) {
|
||||
|
||||
Reference in new issue
Block a user