mirror of
https://github.com/openai/codex.git
synced 2026-11-06 08:31:30 +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
@@ -538,6 +538,9 @@
|
|||||||
},
|
},
|
||||||
"workspace_dependencies": {
|
"workspace_dependencies": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"workspace_owner_usage_nudge": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -2510,6 +2513,9 @@
|
|||||||
},
|
},
|
||||||
"workspace_dependencies": {
|
"workspace_dependencies": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"workspace_owner_usage_nudge": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ pub enum Feature {
|
|||||||
TuiAppServer,
|
TuiAppServer,
|
||||||
/// Prevent idle system sleep while a turn is actively running.
|
/// Prevent idle system sleep while a turn is actively running.
|
||||||
PreventIdleSleep,
|
PreventIdleSleep,
|
||||||
|
/// Enable workspace-specific owner nudge copy and prompts in the TUI.
|
||||||
|
WorkspaceOwnerUsageNudge,
|
||||||
/// Legacy rollout flag for Responses API WebSocket transport experiments.
|
/// Legacy rollout flag for Responses API WebSocket transport experiments.
|
||||||
ResponsesWebsockets,
|
ResponsesWebsockets,
|
||||||
/// Legacy rollout flag for Responses API WebSocket transport v2 experiments.
|
/// Legacy rollout flag for Responses API WebSocket transport v2 experiments.
|
||||||
@@ -968,6 +970,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
},
|
},
|
||||||
default_enabled: false,
|
default_enabled: false,
|
||||||
},
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::WorkspaceOwnerUsageNudge,
|
||||||
|
key: "workspace_owner_usage_nudge",
|
||||||
|
stage: Stage::UnderDevelopment,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
id: Feature::ResponsesWebsockets,
|
id: Feature::ResponsesWebsockets,
|
||||||
key: "responses_websockets",
|
key: "responses_websockets",
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ use crate::version::CODEX_CLI_VERSION;
|
|||||||
use codex_ansi_escape::ansi_escape_line;
|
use codex_ansi_escape::ansi_escape_line;
|
||||||
use codex_app_server_client::AppServerRequestHandle;
|
use codex_app_server_client::AppServerRequestHandle;
|
||||||
use codex_app_server_client::TypedRequestError;
|
use codex_app_server_client::TypedRequestError;
|
||||||
|
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||||
use codex_app_server_protocol::ClientRequest;
|
use codex_app_server_protocol::ClientRequest;
|
||||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||||
use codex_app_server_protocol::ConfigLayerSource;
|
use codex_app_server_protocol::ConfigLayerSource;
|
||||||
@@ -96,6 +97,7 @@ use codex_app_server_protocol::PluginReadResponse;
|
|||||||
use codex_app_server_protocol::PluginUninstallParams;
|
use codex_app_server_protocol::PluginUninstallParams;
|
||||||
use codex_app_server_protocol::PluginUninstallResponse;
|
use codex_app_server_protocol::PluginUninstallResponse;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
|
use codex_app_server_protocol::SendAddCreditsNudgeEmailParams;
|
||||||
use codex_app_server_protocol::ServerNotification;
|
use codex_app_server_protocol::ServerNotification;
|
||||||
use codex_app_server_protocol::ServerRequest;
|
use codex_app_server_protocol::ServerRequest;
|
||||||
use codex_app_server_protocol::SkillsListParams;
|
use codex_app_server_protocol::SkillsListParams;
|
||||||
@@ -2086,6 +2088,21 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_add_credits_nudge_email(
|
||||||
|
&mut self,
|
||||||
|
app_server: &AppServerSession,
|
||||||
|
credit_type: AddCreditsNudgeCreditType,
|
||||||
|
) {
|
||||||
|
let request_handle = app_server.request_handle();
|
||||||
|
let app_event_tx = self.app_event_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = send_add_credits_nudge_email(request_handle, credit_type)
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string());
|
||||||
|
app_event_tx.send(AppEvent::AddCreditsNudgeEmailFinished { result });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Starts the initial skills refresh without delaying the first interactive frame.
|
/// Starts the initial skills refresh without delaying the first interactive frame.
|
||||||
///
|
///
|
||||||
/// Startup only needs skill metadata to populate skill mentions and the skills UI; the prompt can be
|
/// Startup only needs skill metadata to populate skill mentions and the skills UI; the prompt can be
|
||||||
@@ -4973,6 +4990,18 @@ impl App {
|
|||||||
AppEvent::RefreshRateLimits { origin } => {
|
AppEvent::RefreshRateLimits { origin } => {
|
||||||
self.refresh_rate_limits(app_server, origin);
|
self.refresh_rate_limits(app_server, origin);
|
||||||
}
|
}
|
||||||
|
AppEvent::SendAddCreditsNudgeEmail { credit_type } => {
|
||||||
|
if self
|
||||||
|
.chat_widget
|
||||||
|
.start_add_credits_nudge_email_request(credit_type)
|
||||||
|
{
|
||||||
|
self.send_add_credits_nudge_email(app_server, credit_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppEvent::AddCreditsNudgeEmailFinished { result } => {
|
||||||
|
self.chat_widget
|
||||||
|
.finish_add_credits_nudge_email_request(result);
|
||||||
|
}
|
||||||
AppEvent::RateLimitsLoaded { origin, result } => match result {
|
AppEvent::RateLimitsLoaded { origin, result } => match result {
|
||||||
Ok(snapshots) => {
|
Ok(snapshots) => {
|
||||||
for snapshot in snapshots {
|
for snapshot in snapshots {
|
||||||
@@ -6685,6 +6714,22 @@ async fn fetch_account_rate_limits(
|
|||||||
Ok(app_server_rate_limit_snapshots_to_core(response))
|
Ok(app_server_rate_limit_snapshots_to_core(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_add_credits_nudge_email(
|
||||||
|
request_handle: AppServerRequestHandle,
|
||||||
|
credit_type: AddCreditsNudgeCreditType,
|
||||||
|
) -> Result<codex_app_server_protocol::AddCreditsNudgeEmailStatus> {
|
||||||
|
let request_id = RequestId::String(format!("add-credits-nudge-{}", Uuid::new_v4()));
|
||||||
|
let response: codex_app_server_protocol::SendAddCreditsNudgeEmailResponse = request_handle
|
||||||
|
.request_typed(ClientRequest::SendAddCreditsNudgeEmail {
|
||||||
|
request_id,
|
||||||
|
params: SendAddCreditsNudgeEmailParams { credit_type },
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.wrap_err("account/sendAddCreditsNudgeEmail failed in TUI")?;
|
||||||
|
|
||||||
|
Ok(response.status)
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_skills_list(
|
async fn fetch_skills_list(
|
||||||
request_handle: AppServerRequestHandle,
|
request_handle: AppServerRequestHandle,
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||||
|
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||||
use codex_app_server_protocol::AppInfo;
|
use codex_app_server_protocol::AppInfo;
|
||||||
use codex_app_server_protocol::McpServerStatus;
|
use codex_app_server_protocol::McpServerStatus;
|
||||||
use codex_app_server_protocol::PluginInstallResponse;
|
use codex_app_server_protocol::PluginInstallResponse;
|
||||||
@@ -190,6 +192,16 @@ pub(crate) enum AppEvent {
|
|||||||
result: Result<Vec<RateLimitSnapshot>, String>,
|
result: Result<Vec<RateLimitSnapshot>, String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Send a user-confirmed request to notify the workspace owner.
|
||||||
|
SendAddCreditsNudgeEmail {
|
||||||
|
credit_type: AddCreditsNudgeCreditType,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Result of notifying the workspace owner.
|
||||||
|
AddCreditsNudgeEmailFinished {
|
||||||
|
result: Result<AddCreditsNudgeEmailStatus, String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Result of prefetching connectors.
|
/// Result of prefetching connectors.
|
||||||
ConnectorsLoaded {
|
ConnectorsLoaded {
|
||||||
result: Result<ConnectorsSnapshot, String>,
|
result: Result<ConnectorsSnapshot, String>,
|
||||||
|
|||||||
@@ -842,6 +842,16 @@ impl BottomPaneView for ListSelectionView {
|
|||||||
&& !modifiers.contains(KeyModifiers::CONTROL)
|
&& !modifiers.contains(KeyModifiers::CONTROL)
|
||||||
&& !modifiers.contains(KeyModifiers::ALT) =>
|
&& !modifiers.contains(KeyModifiers::ALT) =>
|
||||||
{
|
{
|
||||||
|
if let Some(idx) = self.items.iter().position(|item| {
|
||||||
|
item.display_shortcut
|
||||||
|
.is_some_and(|shortcut| shortcut.is_press(key_event))
|
||||||
|
&& item.disabled_reason.is_none()
|
||||||
|
&& !item.is_disabled
|
||||||
|
}) {
|
||||||
|
self.state.selected_idx = Some(idx);
|
||||||
|
self.accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(idx) = c
|
if let Some(idx) = c
|
||||||
.to_digit(10)
|
.to_digit(10)
|
||||||
.map(|d| d as usize)
|
.map(|d| d as usize)
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ use crate::terminal_title::clear_terminal_title;
|
|||||||
use crate::terminal_title::set_terminal_title;
|
use crate::terminal_title::set_terminal_title;
|
||||||
use crate::text_formatting::proper_join;
|
use crate::text_formatting::proper_join;
|
||||||
use crate::version::CODEX_CLI_VERSION;
|
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::AppInfo;
|
||||||
use codex_app_server_protocol::AppSummary;
|
use codex_app_server_protocol::AppSummary;
|
||||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
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::McpToolCallEndEvent;
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||||
|
use codex_protocol::protocol::RateLimitReachedType;
|
||||||
use codex_protocol::protocol::RateLimitSnapshot;
|
use codex_protocol::protocol::RateLimitSnapshot;
|
||||||
use codex_protocol::protocol::ReviewRequest;
|
use codex_protocol::protocol::ReviewRequest;
|
||||||
use codex_protocol::protocol::ReviewTarget;
|
use codex_protocol::protocol::ReviewTarget;
|
||||||
@@ -781,8 +784,10 @@ pub(crate) struct ChatWidget {
|
|||||||
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
|
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
|
||||||
next_status_refresh_request_id: u64,
|
next_status_refresh_request_id: u64,
|
||||||
plan_type: Option<PlanType>,
|
plan_type: Option<PlanType>,
|
||||||
|
codex_rate_limit_reached_type: Option<RateLimitReachedType>,
|
||||||
rate_limit_warnings: RateLimitWarningState,
|
rate_limit_warnings: RateLimitWarningState,
|
||||||
rate_limit_switch_prompt: RateLimitSwitchPromptState,
|
rate_limit_switch_prompt: RateLimitSwitchPromptState,
|
||||||
|
add_credits_nudge_email_in_flight: Option<AddCreditsNudgeCreditType>,
|
||||||
adaptive_chunking: AdaptiveChunkingPolicy,
|
adaptive_chunking: AdaptiveChunkingPolicy,
|
||||||
// Stream lifecycle controller
|
// Stream lifecycle controller
|
||||||
stream_controller: Option<StreamController>,
|
stream_controller: Option<StreamController>,
|
||||||
@@ -2857,6 +2862,11 @@ impl ChatWidget {
|
|||||||
self.plan_type = snapshot.plan_type.or(self.plan_type);
|
self.plan_type = snapshot.plan_type.or(self.plan_type);
|
||||||
|
|
||||||
let is_codex_limit = limit_id.eq_ignore_ascii_case("codex");
|
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 {
|
let warnings = if is_codex_limit {
|
||||||
self.rate_limit_warnings.take_warnings(
|
self.rate_limit_warnings.take_warnings(
|
||||||
snapshot
|
snapshot
|
||||||
@@ -2920,6 +2930,7 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.rate_limit_snapshots_by_limit_id.clear();
|
self.rate_limit_snapshots_by_limit_id.clear();
|
||||||
|
self.codex_rate_limit_reached_type = None;
|
||||||
}
|
}
|
||||||
self.refresh_status_line();
|
self.refresh_status_line();
|
||||||
}
|
}
|
||||||
@@ -2973,6 +2984,62 @@ impl ChatWidget {
|
|||||||
self.maybe_send_next_queued_input();
|
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(
|
fn handle_non_retry_error(
|
||||||
&mut self,
|
&mut self,
|
||||||
message: String,
|
message: String,
|
||||||
@@ -2989,7 +3056,7 @@ impl ChatWidget {
|
|||||||
match info {
|
match info {
|
||||||
RateLimitErrorKind::ServerOverloaded => self.on_server_overloaded_error(message),
|
RateLimitErrorKind::ServerOverloaded => self.on_server_overloaded_error(message),
|
||||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||||
self.on_error(message)
|
self.on_rate_limit_error(info, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -4996,8 +5063,10 @@ impl ChatWidget {
|
|||||||
refreshing_status_outputs: Vec::new(),
|
refreshing_status_outputs: Vec::new(),
|
||||||
next_status_refresh_request_id: 0,
|
next_status_refresh_request_id: 0,
|
||||||
plan_type: initial_plan_type,
|
plan_type: initial_plan_type,
|
||||||
|
codex_rate_limit_reached_type: None,
|
||||||
rate_limit_warnings: RateLimitWarningState::default(),
|
rate_limit_warnings: RateLimitWarningState::default(),
|
||||||
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
||||||
|
add_credits_nudge_email_in_flight: None,
|
||||||
adaptive_chunking: AdaptiveChunkingPolicy::default(),
|
adaptive_chunking: AdaptiveChunkingPolicy::default(),
|
||||||
stream_controller: None,
|
stream_controller: None,
|
||||||
plan_stream_controller: None,
|
plan_stream_controller: None,
|
||||||
@@ -6942,7 +7011,7 @@ impl ChatWidget {
|
|||||||
self.on_server_overloaded_error(message)
|
self.on_server_overloaded_error(message)
|
||||||
}
|
}
|
||||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||||
self.on_error(message)
|
self.on_rate_limit_error(kind, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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"
|
/// Open a popup to choose a quick auto model. Selecting "All models"
|
||||||
/// opens the full picker with every available preset.
|
/// opens the full picker with every available preset.
|
||||||
pub(crate) fn open_model_popup(&mut self) {
|
pub(crate) fn open_model_popup(&mut self) {
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||||
|
expression: popup
|
||||||
|
---
|
||||||
|
You've reached your workspace credit limit
|
||||||
|
Your workspace is out of credits. Ask your workspace owner to add more. Notify owner?
|
||||||
|
|
||||||
|
1. Yes (y)
|
||||||
|
› 2. No (default) (n)
|
||||||
|
|
||||||
|
Press enter to confirm or esc to go back
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||||
|
expression: popup
|
||||||
|
---
|
||||||
|
Usage limit reached
|
||||||
|
Request a limit increase from your owner to continue using codex. Request increase?
|
||||||
|
|
||||||
|
1. Yes (y)
|
||||||
|
› 2. No (default) (n)
|
||||||
|
|
||||||
|
Press enter to confirm or esc to go back
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||||
|
expression: "rendered_cases.join(\"\\n---\\n\")"
|
||||||
|
---
|
||||||
|
• Workspace owner notified.
|
||||||
|
|
||||||
|
---
|
||||||
|
• Workspace owner was already notified recently.
|
||||||
|
|
||||||
|
---
|
||||||
|
• Could not notify your workspace owner. Please try again.
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||||
|
expression: "rendered_cases.join(\"\\n---\\n\")"
|
||||||
|
---
|
||||||
|
■ You're out of credits. Your workspace is out of credits. Add credits to continue using Codex.
|
||||||
|
|
||||||
|
---
|
||||||
|
■ Usage limit reached. You've reached your usage limit. Increase your limits to continue using codex.
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||||
|
expression: "rendered_cases.join(\"\\n---\\n\")"
|
||||||
|
---
|
||||||
|
• Limit increase requested.
|
||||||
|
|
||||||
|
---
|
||||||
|
• A limit increase was already requested recently.
|
||||||
|
|
||||||
|
---
|
||||||
|
• Could not request a limit increase. Please try again.
|
||||||
@@ -33,6 +33,8 @@ pub(super) use crate::test_support::test_path_buf;
|
|||||||
pub(super) use crate::test_support::test_path_display;
|
pub(super) use crate::test_support::test_path_display;
|
||||||
pub(super) use crate::tui::FrameRequester;
|
pub(super) use crate::tui::FrameRequester;
|
||||||
pub(super) use assert_matches::assert_matches;
|
pub(super) use assert_matches::assert_matches;
|
||||||
|
pub(super) use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||||
|
pub(super) use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||||
pub(super) use codex_app_server_protocol::AdditionalFileSystemPermissions as AppServerAdditionalFileSystemPermissions;
|
pub(super) use codex_app_server_protocol::AdditionalFileSystemPermissions as AppServerAdditionalFileSystemPermissions;
|
||||||
pub(super) use codex_app_server_protocol::AdditionalNetworkPermissions as AppServerAdditionalNetworkPermissions;
|
pub(super) use codex_app_server_protocol::AdditionalNetworkPermissions as AppServerAdditionalNetworkPermissions;
|
||||||
pub(super) use codex_app_server_protocol::AdditionalPermissionProfile as AppServerAdditionalPermissionProfile;
|
pub(super) use codex_app_server_protocol::AdditionalPermissionProfile as AppServerAdditionalPermissionProfile;
|
||||||
@@ -170,6 +172,8 @@ pub(super) use codex_protocol::protocol::Op;
|
|||||||
pub(super) use codex_protocol::protocol::PatchApplyBeginEvent;
|
pub(super) use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||||
pub(super) use codex_protocol::protocol::PatchApplyEndEvent;
|
pub(super) use codex_protocol::protocol::PatchApplyEndEvent;
|
||||||
pub(super) use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
|
pub(super) use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
|
||||||
|
pub(super) use codex_protocol::protocol::RateLimitReachedType;
|
||||||
|
pub(super) use codex_protocol::protocol::RateLimitSnapshot;
|
||||||
pub(super) use codex_protocol::protocol::RateLimitWindow;
|
pub(super) use codex_protocol::protocol::RateLimitWindow;
|
||||||
pub(super) use codex_protocol::protocol::ReadOnlyAccess;
|
pub(super) use codex_protocol::protocol::ReadOnlyAccess;
|
||||||
pub(super) use codex_protocol::protocol::RealtimeConversationClosedEvent;
|
pub(super) use codex_protocol::protocol::RealtimeConversationClosedEvent;
|
||||||
|
|||||||
@@ -201,8 +201,10 @@ pub(super) async fn make_chatwidget_manual(
|
|||||||
refreshing_status_outputs: Vec::new(),
|
refreshing_status_outputs: Vec::new(),
|
||||||
next_status_refresh_request_id: 0,
|
next_status_refresh_request_id: 0,
|
||||||
plan_type: None,
|
plan_type: None,
|
||||||
|
codex_rate_limit_reached_type: None,
|
||||||
rate_limit_warnings: RateLimitWarningState::default(),
|
rate_limit_warnings: RateLimitWarningState::default(),
|
||||||
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
||||||
|
add_credits_nudge_email_in_flight: None,
|
||||||
adaptive_chunking: crate::streaming::chunking::AdaptiveChunkingPolicy::default(),
|
adaptive_chunking: crate::streaming::chunking::AdaptiveChunkingPolicy::default(),
|
||||||
stream_controller: None,
|
stream_controller: None,
|
||||||
plan_stream_controller: None,
|
plan_stream_controller: None,
|
||||||
|
|||||||
@@ -501,6 +501,370 @@ async fn rate_limit_switch_prompt_popup_snapshot() {
|
|||||||
assert_chatwidget_snapshot!("rate_limit_switch_prompt_popup", popup);
|
assert_chatwidget_snapshot!("rate_limit_switch_prompt_popup", popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_owner_usage_nudge_flag_disabled_keeps_generic_rate_limit_error() {
|
||||||
|
{
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type =
|
||||||
|
Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let rendered = drain_insert_history(&mut rx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|lines| lines_to_single_string(&lines))
|
||||||
|
.collect::<String>();
|
||||||
|
assert!(
|
||||||
|
rendered.contains("Usage limit reached."),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type =
|
||||||
|
Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||||
|
assert!(
|
||||||
|
!popup.contains("Request a limit increase from your owner"),
|
||||||
|
"popup: {popup}"
|
||||||
|
);
|
||||||
|
assert_no_owner_nudge_or_rate_limit_refresh(&mut rx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable_workspace_owner_usage_nudge(chat: &mut ChatWidget) {
|
||||||
|
chat.set_feature_enabled(Feature::WorkspaceOwnerUsageNudge, /*enabled*/ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_member_credits_depleted_prompts_and_sends_credits() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type = Some(RateLimitReachedType::WorkspaceMemberCreditsDepleted);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::Generic,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 90);
|
||||||
|
assert_chatwidget_snapshot!("workspace_member_credits_depleted_prompt", popup);
|
||||||
|
|
||||||
|
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||||
|
let event = next_send_add_credits_nudge_email_event(&mut rx);
|
||||||
|
assert_eq!(event, AddCreditsNudgeCreditType::Credits);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_member_usage_limit_prompts_and_sends_usage_limit() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type = Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||||
|
assert_chatwidget_snapshot!("workspace_member_usage_limit_prompt", popup);
|
||||||
|
|
||||||
|
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||||
|
let event = next_send_add_credits_nudge_email_event(&mut rx);
|
||||||
|
assert_eq!(event, AddCreditsNudgeCreditType::UsageLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn header_rate_limit_snapshot_preserves_member_limit_type_for_error_prompt() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut usage_limits = snapshot(/*percent*/ 100.0);
|
||||||
|
usage_limits.rate_limit_reached_type =
|
||||||
|
Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached);
|
||||||
|
chat.on_rate_limit_snapshot(Some(usage_limits));
|
||||||
|
|
||||||
|
// Turn-failure snapshots are derived from response headers and do not carry
|
||||||
|
// the backend-classified reached type. They arrive before the Error event.
|
||||||
|
let mut header_limits = snapshot(/*percent*/ 100.0);
|
||||||
|
header_limits.rate_limit_reached_type = None;
|
||||||
|
chat.on_rate_limit_snapshot(Some(header_limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||||
|
assert!(
|
||||||
|
popup.contains("Request a limit increase from your owner"),
|
||||||
|
"popup: {popup}"
|
||||||
|
);
|
||||||
|
|
||||||
|
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||||
|
let event = next_send_add_credits_nudge_email_event(&mut rx);
|
||||||
|
assert_eq!(event, AddCreditsNudgeCreditType::UsageLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn usage_limit_error_remaps_stale_member_credits_state_to_usage_limit_prompt() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type = Some(RateLimitReachedType::WorkspaceMemberCreditsDepleted);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||||
|
assert!(
|
||||||
|
popup.contains("Request a limit increase from your owner"),
|
||||||
|
"popup: {popup}"
|
||||||
|
);
|
||||||
|
|
||||||
|
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||||
|
let event = next_send_add_credits_nudge_email_event(&mut rx);
|
||||||
|
assert_eq!(event, AddCreditsNudgeCreditType::UsageLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_owner_limit_states_do_not_prompt_for_owner_nudge() {
|
||||||
|
for (limit_type, error_kind) in [
|
||||||
|
(
|
||||||
|
RateLimitReachedType::WorkspaceOwnerCreditsDepleted,
|
||||||
|
RateLimitErrorKind::Generic,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RateLimitReachedType::WorkspaceOwnerUsageLimitReached,
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RateLimitReachedType::RateLimitReached,
|
||||||
|
RateLimitErrorKind::Generic,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type = Some(limit_type);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(error_kind, "Usage limit reached.".to_string());
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 90);
|
||||||
|
assert!(!popup.contains("workspace owner"));
|
||||||
|
assert_no_owner_nudge_or_rate_limit_refresh(&mut rx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_owner_limit_states_render_state_specific_messages() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
RateLimitReachedType::WorkspaceOwnerCreditsDepleted,
|
||||||
|
RateLimitErrorKind::Generic,
|
||||||
|
"You're out of credits. Your workspace is out of credits. Add credits to continue using Codex.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RateLimitReachedType::WorkspaceOwnerUsageLimitReached,
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached. You've reached your usage limit. Increase your limits to continue using codex.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut rendered_cases = Vec::new();
|
||||||
|
for (limit_type, error_kind, expected) in cases {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type = Some(limit_type);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(error_kind, "Usage limit reached.".to_string());
|
||||||
|
let rendered = drain_insert_history(&mut rx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|lines| lines_to_single_string(&lines))
|
||||||
|
.collect::<String>();
|
||||||
|
assert!(rendered.contains(expected), "rendered: {rendered}");
|
||||||
|
rendered_cases.push(rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_chatwidget_snapshot!(
|
||||||
|
"workspace_owner_limit_state_messages",
|
||||||
|
rendered_cases.join("\n---\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn missing_rate_limit_reached_type_does_not_prompt_or_refresh() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
chat.on_rate_limit_snapshot(Some(snapshot(/*percent*/ 100.0)));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 90);
|
||||||
|
assert!(!popup.contains("workspace owner"));
|
||||||
|
assert_no_owner_nudge_or_rate_limit_refresh(&mut rx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_owner_nudge_default_no_dismisses_without_sending() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type = Some(RateLimitReachedType::WorkspaceMemberCreditsDepleted);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::Generic,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
|
|
||||||
|
assert_no_owner_nudge_or_rate_limit_refresh(&mut rx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_owner_nudge_reappears_after_dismissing_no() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
let mut limits = snapshot(/*percent*/ 100.0);
|
||||||
|
limits.rate_limit_reached_type = Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached);
|
||||||
|
chat.on_rate_limit_snapshot(Some(limits));
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
|
assert_no_owner_nudge_or_rate_limit_refresh(&mut rx);
|
||||||
|
|
||||||
|
chat.on_rate_limit_error(
|
||||||
|
RateLimitErrorKind::UsageLimit,
|
||||||
|
"Usage limit reached.".to_string(),
|
||||||
|
);
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||||
|
assert!(
|
||||||
|
popup.contains("Request a limit increase from your owner"),
|
||||||
|
"popup: {popup}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_owner_credits_nudge_completion_renders_feedback() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
Ok(AddCreditsNudgeEmailStatus::Sent),
|
||||||
|
"Workspace owner notified.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ok(AddCreditsNudgeEmailStatus::CooldownActive),
|
||||||
|
"Workspace owner was already notified recently.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Err("request failed".to_string()),
|
||||||
|
"Could not notify your workspace owner. Please try again.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut rendered_cases = Vec::new();
|
||||||
|
for (result, expected) in cases {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
chat.start_add_credits_nudge_email_request(AddCreditsNudgeCreditType::Credits);
|
||||||
|
chat.finish_add_credits_nudge_email_request(result);
|
||||||
|
let rendered = drain_insert_history(&mut rx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|lines| lines_to_single_string(&lines))
|
||||||
|
.collect::<String>();
|
||||||
|
assert!(rendered.contains(expected), "rendered: {rendered}");
|
||||||
|
rendered_cases.push(rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_chatwidget_snapshot!(
|
||||||
|
"workspace_owner_credits_nudge_completion_feedback",
|
||||||
|
rendered_cases.join("\n---\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_owner_usage_limit_nudge_completion_renders_feedback() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
Ok(AddCreditsNudgeEmailStatus::Sent),
|
||||||
|
"Limit increase requested.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ok(AddCreditsNudgeEmailStatus::CooldownActive),
|
||||||
|
"A limit increase was already requested recently.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Err("request failed".to_string()),
|
||||||
|
"Could not request a limit increase. Please try again.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut rendered_cases = Vec::new();
|
||||||
|
for (result, expected) in cases {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
enable_workspace_owner_usage_nudge(&mut chat);
|
||||||
|
chat.start_add_credits_nudge_email_request(AddCreditsNudgeCreditType::UsageLimit);
|
||||||
|
chat.finish_add_credits_nudge_email_request(result);
|
||||||
|
let rendered = drain_insert_history(&mut rx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|lines| lines_to_single_string(&lines))
|
||||||
|
.collect::<String>();
|
||||||
|
assert!(rendered.contains(expected), "rendered: {rendered}");
|
||||||
|
rendered_cases.push(rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_chatwidget_snapshot!(
|
||||||
|
"workspace_owner_usage_limit_nudge_completion_feedback",
|
||||||
|
rendered_cases.join("\n---\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_send_add_credits_nudge_email_event(
|
||||||
|
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||||
|
) -> AddCreditsNudgeCreditType {
|
||||||
|
while let Ok(event) = rx.try_recv() {
|
||||||
|
if let AppEvent::SendAddCreditsNudgeEmail { credit_type } = event {
|
||||||
|
return credit_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("expected SendAddCreditsNudgeEmail app event");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_no_owner_nudge_or_rate_limit_refresh(
|
||||||
|
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||||
|
) {
|
||||||
|
while let Ok(event) = rx.try_recv() {
|
||||||
|
assert!(
|
||||||
|
!matches!(
|
||||||
|
event,
|
||||||
|
AppEvent::SendAddCreditsNudgeEmail { .. } | AppEvent::RefreshRateLimits { .. }
|
||||||
|
),
|
||||||
|
"unexpected event: {event:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn streaming_final_answer_keeps_task_running_state() {
|
async fn streaming_final_answer_keeps_task_running_state() {
|
||||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
|||||||
Reference in new issue
Block a user