From d9118c04bf32209d09466ec4f241d243ba6b3195 Mon Sep 17 00:00:00 2001 From: Thomas Stokes Date: Sun, 2 Nov 2025 00:33:13 +0800 Subject: [PATCH 1/7] Parse the Azure OpenAI rate limit message (#5956) Fixes #4161 Currently Codex uses a regex to parse the "Please try again in 1.898s" OpenAI-style rate limit message, so that it can wait the correct duration before retrying. Azure OpenAI returns a different error that looks like "Rate limit exceeded. Try again in 35 seconds." This PR extends the regex and parsing code to match in a more fuzzy manner, handling anything matching the pattern "try again in \\". --- codex-rs/core/src/client.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 2085c39f02..9dfa3a1316 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -931,8 +931,10 @@ async fn stream_from_fixture( fn rate_limit_regex() -> &'static Regex { static RE: OnceLock = OnceLock::new(); + // Match both OpenAI-style messages like "Please try again in 1.898s" + // and Azure OpenAI-style messages like "Try again in 35 seconds". #[expect(clippy::unwrap_used)] - RE.get_or_init(|| Regex::new(r"Please try again in (\d+(?:\.\d+)?)(s|ms)").unwrap()) + RE.get_or_init(|| Regex::new(r"(?i)try again in\s*(\d+(?:\.\d+)?)\s*(s|ms|seconds?)").unwrap()) } fn try_parse_retry_after(err: &Error) -> Option { @@ -940,7 +942,8 @@ fn try_parse_retry_after(err: &Error) -> Option { return None; } - // parse the Please try again in 1.898s format using regex + // parse retry hints like "try again in 1.898s" or + // "Try again in 35 seconds" using regex let re = rate_limit_regex(); if let Some(message) = &err.message && let Some(captures) = re.captures(message) @@ -950,9 +953,9 @@ fn try_parse_retry_after(err: &Error) -> Option { if let (Some(value), Some(unit)) = (seconds, unit) { let value = value.as_str().parse::().ok()?; - let unit = unit.as_str(); + let unit = unit.as_str().to_ascii_lowercase(); - if unit == "s" { + if unit == "s" || unit.starts_with("second") { return Some(Duration::from_secs_f64(value)); } else if unit == "ms" { return Some(Duration::from_millis(value as u64)); @@ -1427,6 +1430,19 @@ mod tests { assert_eq!(delay, Some(Duration::from_secs_f64(1.898))); } + #[test] + fn test_try_parse_retry_after_azure() { + let err = Error { + r#type: None, + message: Some("Rate limit exceeded. Try again in 35 seconds.".to_string()), + code: Some("rate_limit_exceeded".to_string()), + plan_type: None, + resets_at: None, + }; + let delay = try_parse_retry_after(&err); + assert_eq!(delay, Some(Duration::from_secs(35))); + } + #[test] fn error_response_deserializes_schema_known_plan_type_and_serializes_back() { use crate::token_data::KnownPlan; From d5853d9c47b1badad183f62622745cf47e6ff0f4 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 1 Nov 2025 16:52:23 -0500 Subject: [PATCH 2/7] Changes to sandbox command assessment feature based on initial experiment feedback (#6091) * Removed sandbox risk categories; feedback indicates that these are not that useful and "less is more" * Tweaked the assessment prompt to generate terser answers * Fixed bug in orchestrator that prevents this feature from being exposed in the extension --- codex-rs/core/src/sandboxing/assessment.rs | 28 ++++--------------- codex-rs/core/src/tools/orchestrator.rs | 11 +++++++- .../templates/sandboxing/assessment_prompt.md | 11 +++----- codex-rs/otel/src/otel_event_manager.rs | 12 -------- codex-rs/protocol/src/approvals.rs | 28 ------------------- codex-rs/protocol/src/protocol.rs | 1 - .../tui/src/bottom_pane/approval_overlay.rs | 27 +----------------- 7 files changed, 20 insertions(+), 98 deletions(-) diff --git a/codex-rs/core/src/sandboxing/assessment.rs b/codex-rs/core/src/sandboxing/assessment.rs index c7310c1f13..31e76777bb 100644 --- a/codex-rs/core/src/sandboxing/assessment.rs +++ b/codex-rs/core/src/sandboxing/assessment.rs @@ -25,16 +25,6 @@ use tracing::warn; const SANDBOX_ASSESSMENT_TIMEOUT: Duration = Duration::from_secs(5); -const SANDBOX_RISK_CATEGORY_VALUES: &[&str] = &[ - "data_deletion", - "data_exfiltration", - "privilege_escalation", - "system_modification", - "network_access", - "resource_exhaustion", - "compliance", -]; - #[derive(Template)] #[template(path = "sandboxing/assessment_prompt.md", escape = "none")] struct SandboxAssessmentPromptTemplate<'a> { @@ -176,27 +166,26 @@ pub(crate) async fn assess_command( call_id, "success", Some(assessment.risk_level), - &assessment.risk_categories, duration, ); return Some(assessment); } Err(err) => { warn!("failed to parse sandbox assessment JSON: {err}"); - parent_otel.sandbox_assessment(call_id, "parse_error", None, &[], duration); + parent_otel.sandbox_assessment(call_id, "parse_error", None, duration); } }, Ok(Ok(None)) => { warn!("sandbox assessment response did not include any message"); - parent_otel.sandbox_assessment(call_id, "no_output", None, &[], duration); + parent_otel.sandbox_assessment(call_id, "no_output", None, duration); } Ok(Err(err)) => { warn!("sandbox assessment failed: {err}"); - parent_otel.sandbox_assessment(call_id, "model_error", None, &[], duration); + parent_otel.sandbox_assessment(call_id, "model_error", None, duration); } Err(_) => { warn!("sandbox assessment timed out"); - parent_otel.sandbox_assessment(call_id, "timeout", None, &[], duration); + parent_otel.sandbox_assessment(call_id, "timeout", None, duration); } } @@ -229,7 +218,7 @@ fn sandbox_roots_for_prompt(policy: &SandboxPolicy, cwd: &Path) -> Vec fn sandbox_assessment_schema() -> serde_json::Value { json!({ "type": "object", - "required": ["description", "risk_level", "risk_categories"], + "required": ["description", "risk_level"], "properties": { "description": { "type": "string", @@ -240,13 +229,6 @@ fn sandbox_assessment_schema() -> serde_json::Value { "type": "string", "enum": ["low", "medium", "high"] }, - "risk_categories": { - "type": "array", - "items": { - "type": "string", - "enum": SANDBOX_RISK_CATEGORY_VALUES - } - } }, "additionalProperties": false }) diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index f334c4ef55..878e48e8be 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -54,12 +54,21 @@ impl ToolOrchestrator { let mut already_approved = false; if needs_initial_approval { + let mut risk = None; + + if let Some(metadata) = req.sandbox_retry_data() { + risk = tool_ctx + .session + .assess_sandbox_command(turn_ctx, &tool_ctx.call_id, &metadata.command, None) + .await; + } + let approval_ctx = ApprovalCtx { session: tool_ctx.session, turn: turn_ctx, call_id: &tool_ctx.call_id, retry_reason: None, - risk: None, + risk, }; let decision = tool.start_approval_async(req, approval_ctx).await; diff --git a/codex-rs/core/templates/sandboxing/assessment_prompt.md b/codex-rs/core/templates/sandboxing/assessment_prompt.md index 7e5664a0ad..dc59f47d7c 100644 --- a/codex-rs/core/templates/sandboxing/assessment_prompt.md +++ b/codex-rs/core/templates/sandboxing/assessment_prompt.md @@ -1,13 +1,10 @@ -You are a security analyst evaluating shell commands that were blocked by a sandbox. Given the provided metadata, summarize the command's likely intent and assess the risk. Return strictly valid JSON with the keys: -- description (concise summary, at most two sentences) +You are a security analyst evaluating shell commands that were blocked by a sandbox. Given the provided metadata, summarize the command's likely intent and assess the risk to help the user decide whether to approve command execution. Return strictly valid JSON with the keys: +- description (concise summary of command intent and potential effects, no more than one sentence, use present tense) - risk_level ("low", "medium", or "high") -- risk_categories (optional array of zero or more category strings) Risk level examples: -- low: read-only inspections, listing files, printing configuration -- medium: modifying project files, installing dependencies, fetching artifacts from trusted sources +- low: read-only inspections, listing files, printing configuration, fetching artifacts from trusted sources +- medium: modifying project files, installing dependencies - high: deleting or overwriting data, exfiltrating secrets, escalating privileges, or disabling security controls -Recognized risk_categories: data_deletion, data_exfiltration, privilege_escalation, system_modification, network_access, resource_exhaustion, compliance. -Use multiple categories when appropriate. If information is insufficient, choose the most cautious risk level supported by the evidence. Respond with JSON only, without markdown code fences or extra commentary. diff --git a/codex-rs/otel/src/otel_event_manager.rs b/codex-rs/otel/src/otel_event_manager.rs index 4006df17d9..5d9cbd4997 100644 --- a/codex-rs/otel/src/otel_event_manager.rs +++ b/codex-rs/otel/src/otel_event_manager.rs @@ -8,7 +8,6 @@ use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; -use codex_protocol::protocol::SandboxRiskCategory; use codex_protocol::protocol::SandboxRiskLevel; use codex_protocol::user_input::UserInput; use eventsource_stream::Event as StreamEvent; @@ -373,19 +372,9 @@ impl OtelEventManager { call_id: &str, status: &str, risk_level: Option, - risk_categories: &[SandboxRiskCategory], duration: Duration, ) { let level = risk_level.map(|level| level.as_str()); - let categories = if risk_categories.is_empty() { - String::new() - } else { - risk_categories - .iter() - .map(SandboxRiskCategory::as_str) - .collect::>() - .join(", ") - }; tracing::event!( tracing::Level::INFO, @@ -402,7 +391,6 @@ impl OtelEventManager { call_id = %call_id, status = %status, risk_level = level, - risk_categories = categories, duration_ms = %duration.as_millis(), ); } diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index d608dba639..3227ddd1a3 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -16,24 +16,10 @@ pub enum SandboxRiskLevel { High, } -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -pub enum SandboxRiskCategory { - DataDeletion, - DataExfiltration, - PrivilegeEscalation, - SystemModification, - NetworkAccess, - ResourceExhaustion, - Compliance, -} - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct SandboxCommandAssessment { pub description: String, pub risk_level: SandboxRiskLevel, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub risk_categories: Vec, } impl SandboxRiskLevel { @@ -46,20 +32,6 @@ impl SandboxRiskLevel { } } -impl SandboxRiskCategory { - pub fn as_str(&self) -> &'static str { - match self { - Self::DataDeletion => "data_deletion", - Self::DataExfiltration => "data_exfiltration", - Self::PrivilegeEscalation => "privilege_escalation", - Self::SystemModification => "system_modification", - Self::NetworkAccess => "network_access", - Self::ResourceExhaustion => "resource_exhaustion", - Self::Compliance => "compliance", - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ExecApprovalRequestEvent { /// Identifier for the associated exec call, if available. diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 413a3aeaa2..c30ba42b56 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -37,7 +37,6 @@ use ts_rs::TS; pub use crate::approvals::ApplyPatchApprovalRequestEvent; pub use crate::approvals::ExecApprovalRequestEvent; pub use crate::approvals::SandboxCommandAssessment; -pub use crate::approvals::SandboxRiskCategory; pub use crate::approvals::SandboxRiskLevel; /// Open/close tags for special user-input blocks. Used across crates to avoid diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index ba36870005..140b5386c4 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -20,7 +20,6 @@ use codex_core::protocol::FileChange; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; use codex_core::protocol::SandboxCommandAssessment; -use codex_core::protocol::SandboxRiskCategory; use codex_core::protocol::SandboxRiskLevel; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -356,35 +355,11 @@ fn render_risk_lines(risk: &SandboxCommandAssessment) -> Vec> { ])); } - let mut spans: Vec> = vec!["Risk: ".into(), level_span]; - if !risk.risk_categories.is_empty() { - spans.push(" (".into()); - for (idx, category) in risk.risk_categories.iter().enumerate() { - if idx > 0 { - spans.push(", ".into()); - } - spans.push(risk_category_label(*category).into()); - } - spans.push(")".into()); - } - - lines.push(Line::from(spans)); + lines.push(vec!["Risk: ".into(), level_span].into()); lines.push(Line::from("")); lines } -fn risk_category_label(category: SandboxRiskCategory) -> &'static str { - match category { - SandboxRiskCategory::DataDeletion => "data deletion", - SandboxRiskCategory::DataExfiltration => "data exfiltration", - SandboxRiskCategory::PrivilegeEscalation => "privilege escalation", - SandboxRiskCategory::SystemModification => "system modification", - SandboxRiskCategory::NetworkAccess => "network access", - SandboxRiskCategory::ResourceExhaustion => "resource exhaustion", - SandboxRiskCategory::Compliance => "compliance", - } -} - #[derive(Clone)] enum ApprovalVariant { Exec { id: String, command: Vec }, From 0c7efa0cfdf9152265a1b6aa21ebba6e0af5be58 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 2 Nov 2025 18:33:09 -0600 Subject: [PATCH 3/7] Fix incorrect "deprecated" message about experimental config key (#6131) When I enable `experimental_sandbox_command_assessment`, I get an incorrect deprecation warning: "experimental_sandbox_command_assessment is deprecated. Use experimental_sandbox_command_assessment instead." This PR fixes this error. --- codex-rs/core/src/features/legacy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/features/legacy.rs b/codex-rs/core/src/features/legacy.rs index acf96a15b6..de3c007bac 100644 --- a/codex-rs/core/src/features/legacy.rs +++ b/codex-rs/core/src/features/legacy.rs @@ -123,7 +123,7 @@ fn set_if_some( if let Some(enabled) = maybe_value { set_feature(features, feature, enabled); log_alias(alias_key, feature); - features.record_legacy_usage_force(alias_key, feature); + features.record_legacy_usage(alias_key, feature); } } From 5fcf923c19ea0ef62164602528ec483850738d76 Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Sun, 2 Nov 2025 23:19:08 -0500 Subject: [PATCH 4/7] fix: pasting api key stray character (#4903) When signing in with an API key, pasting (with command+v on mac) adds a stray `v` character to the end of the api key. demo video (where I'm pasting in `sk-something-super-secret`) https://github.com/user-attachments/assets/b2b34b5f-c7e4-4760-9657-c35686dd8bb8 --- codex-rs/tui/src/onboarding/auth.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 56527ac8ef..06e1c629e3 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -10,6 +10,7 @@ use codex_login::ShutdownHandle; use codex_login::run_login_server; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; @@ -428,7 +429,9 @@ impl AuthModeWidget { should_request_frame = true; } KeyCode::Char(c) - if !key_event.modifiers.contains(KeyModifiers::CONTROL) + if key_event.kind == KeyEventKind::Press + && !key_event.modifiers.contains(KeyModifiers::SUPER) + && !key_event.modifiers.contains(KeyModifiers::CONTROL) && !key_event.modifiers.contains(KeyModifiers::ALT) => { if state.prepopulated_from_env { From f5945d7c0322bfdd701275a1083f9f2f1cafe27b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 20:38:45 -0800 Subject: [PATCH 5/7] chore(deps): bump actions/upload-artifact from 4 to 5 (#6137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
Release notes

Sourced from actions/upload-artifact's releases.

v5.0.0

What's Changed

BREAKING CHANGE: this update supports Node v24.x. This is not a breaking change per-se but we're treating it as such.

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v5.0.0

v4.6.2

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.2

v4.6.1

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.1

v4.6.0

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.0

v4.5.0

What's Changed

New Contributors

... (truncated)

Commits
  • 330a01c Merge pull request #734 from actions/danwkennedy/prepare-5.0.0
  • 03f2824 Update github.dep.yml
  • 905a1ec Prepare v5.0.0
  • 2d9f9cd Merge pull request #725 from patrikpolyak/patch-1
  • 9687587 Merge branch 'main' into patch-1
  • 2848b2c Merge pull request #727 from danwkennedy/patch-1
  • 9b51177 Spell out the first use of GHES
  • cd231ca Update GHES guidance to include reference to Node 20 version
  • de65e23 Merge pull request #712 from actions/nebuk89-patch-1
  • 8747d8c Update README.md
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/rust-release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fa732a492..38773bb9f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT" - name: Upload staged npm package artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: codex-npm-staging path: ${{ steps.stage_npm_package.outputs.pack_output }} diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 5beadd55c4..26afdaf552 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -350,7 +350,7 @@ jobs: fi fi - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: ${{ matrix.target }} # Upload the per-binary .zst files as well as the new .tar.gz From dccce34d84b39bab304f9949b72db3e3d4bc5850 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 2 Nov 2025 23:41:05 -0600 Subject: [PATCH 6/7] Fix "archive conversation" on Windows (#6124) Addresses issue https://github.com/openai/codex/issues/3582 where an "archive conversation" command in the extension fails on Windows. The problem is that the `archive_conversation` api server call is not canonicalizing the path to the rollout path when performing its check to verify that the rollout path is in the sessions directory. This causes it to fail 100% of the time on Windows. Testing: I was able to repro the error on Windows 100% prior to this change. After the change, I'm no longer able to repro. --- .../app-server/src/codex_message_processor.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 99310e198f..7cd60eaeda 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1172,9 +1172,23 @@ impl CodexMessageProcessor { // Verify that the rollout path is in the sessions directory or else // a malicious client could specify an arbitrary path. let rollout_folder = self.config.codex_home.join(codex_core::SESSIONS_SUBDIR); + let canonical_sessions_dir = match tokio::fs::canonicalize(&rollout_folder).await { + Ok(path) => path, + Err(err) => { + let error = JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!( + "failed to archive conversation: unable to resolve sessions directory: {err}" + ), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + }; let canonical_rollout_path = tokio::fs::canonicalize(&rollout_path).await; let canonical_rollout_path = if let Ok(path) = canonical_rollout_path - && path.starts_with(&rollout_folder) + && path.starts_with(&canonical_sessions_dir) { path } else { From a1ee10b43815a10da3ac09111d793d3ded1b0dc7 Mon Sep 17 00:00:00 2001 From: Vinh Nguyen <1097578+vinhnx@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:44:59 +0700 Subject: [PATCH 7/7] fix: improve usage URLs in status card and snapshots (#6111) Hi OpenAI Codex team, currently "Visit chatgpt.com/codex/settings/usage for up-to-date information on rate limits and credits" message in status card and error messages. For now, without the "https://" prefix, the link cannot be clicked directly from most terminals or chat interfaces. Screenshot 2025-11-02 at 22 47 06 --- The fix is intent to improve this issue: - It makes the link clickable in terminals that support it, hence better accessibility - It follows standard URL formatting practices - It maintains consistency with other links in the application (like the existing "https://openai.com/chatgpt/pricing" links) Thank you! --- codex-rs/core/src/error.rs | 10 +++++----- codex-rs/tui/src/status/card.rs | 4 +++- ..._tests__status_snapshot_includes_monthly_limit.snap | 2 +- ...ts__status_snapshot_includes_reasoning_details.snap | 2 +- ...ts__status_snapshot_shows_empty_limits_message.snap | 2 +- ...__status_snapshot_shows_missing_limits_message.snap | 2 +- ...ts__status_snapshot_shows_stale_limits_message.snap | 2 +- ...__status_snapshot_truncates_in_narrow_terminal.snap | 5 +++-- 8 files changed, 16 insertions(+), 13 deletions(-) diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index be1daf483a..6468327543 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -255,7 +255,7 @@ impl std::fmt::Display for UsageLimitReachedError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let message = match self.plan_type.as_ref() { Some(PlanType::Known(KnownPlan::Plus)) => format!( - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits{}", + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", retry_suffix_after_or(self.resets_at.as_ref()) ), Some(PlanType::Known(KnownPlan::Team)) | Some(PlanType::Known(KnownPlan::Business)) => { @@ -269,7 +269,7 @@ impl std::fmt::Display for UsageLimitReachedError { .to_string() } Some(PlanType::Known(KnownPlan::Pro)) => format!( - "You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits{}", + "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", retry_suffix_after_or(self.resets_at.as_ref()) ), Some(PlanType::Known(KnownPlan::Enterprise)) @@ -460,7 +460,7 @@ mod tests { }; assert_eq!( err.to_string(), - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits or try again later." + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again later." ); } @@ -613,7 +613,7 @@ mod tests { rate_limits: Some(rate_limit_snapshot()), }; let expected = format!( - "You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." + "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." ); assert_eq!(err.to_string(), expected); }); @@ -647,7 +647,7 @@ mod tests { rate_limits: Some(rate_limit_snapshot()), }; let expected = format!( - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." ); assert_eq!(err.to_string(), expected); }); diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index fdf6629a0a..11b723fef9 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -313,7 +313,9 @@ impl HistoryCell for StatusHistoryCell { let note_first_line = Line::from(vec![ Span::from("Visit ").cyan(), - "chatgpt.com/codex/settings/usage".cyan().underlined(), + "https://chatgpt.com/codex/settings/usage" + .cyan() + .underlined(), Span::from(" for up-to-date").cyan(), ]); let note_second_line = Line::from(vec![ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap index cf45f3f810..85e6356c34 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap @@ -7,7 +7,7 @@ expression: sanitized ╭────────────────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ -│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ │ information on rate limits and credits │ │ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap index e38a1c7c1b..0b7b74d745 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap @@ -7,7 +7,7 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ -│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ │ information on rate limits and credits │ │ │ │ Model: gpt-5-codex (reasoning high, summaries detailed) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap index 82ed8fc0b9..17862db238 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap @@ -7,7 +7,7 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ -│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ │ information on rate limits and credits │ │ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap index 82ed8fc0b9..17862db238 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap @@ -7,7 +7,7 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ -│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ │ information on rate limits and credits │ │ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap index bd18719700..2fc0d88744 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap @@ -7,7 +7,7 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ -│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │ │ information on rate limits and credits │ │ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap index 64e9271df7..d86e43a458 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap @@ -1,5 +1,6 @@ --- source: tui/src/status/tests.rs +assertion_line: 257 expression: sanitized --- /status @@ -7,8 +8,8 @@ expression: sanitized ╭────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ -│ Visit chatgpt.com/codex/settings/usage for │ -│ up-to-date │ +│ Visit https://chatgpt.com/codex/settings/ │ +│ usage for up-to-date │ │ information on rate limits and credits │ │ │ │ Model: gpt-5-codex (reasoning │