diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 81b360cb4..f189f5390 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5434,21 +5434,7 @@ impl ChatWidget { current_sandbox: &SandboxPolicy, preset: &ApprovalPreset, ) -> bool { - if current_approval != preset.approval { - return false; - } - matches!( - (&preset.sandbox, current_sandbox), - (SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly) - | ( - SandboxPolicy::DangerFullAccess, - SandboxPolicy::DangerFullAccess - ) - | ( - SandboxPolicy::WorkspaceWrite { .. }, - SandboxPolicy::WorkspaceWrite { .. } - ) - ) + current_approval == preset.approval && *current_sandbox == preset.sandbox } #[cfg(target_os = "windows")] diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f92dee2a9..f4a3f14e2 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -4176,7 +4176,7 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { } #[tokio::test] -async fn preset_matching_ignores_extra_writable_roots() { +async fn preset_matching_requires_exact_workspace_write_settings() { let preset = builtin_approval_presets() .into_iter() .find(|p| p.id == "auto") @@ -4189,8 +4189,8 @@ async fn preset_matching_ignores_extra_writable_roots() { }; assert!( - ChatWidget::preset_matches_current(AskForApproval::OnRequest, ¤t_sandbox, &preset), - "WorkspaceWrite with extra roots should still match the Agent preset" + !ChatWidget::preset_matches_current(AskForApproval::OnRequest, ¤t_sandbox, &preset), + "WorkspaceWrite with extra roots should not match the Default preset" ); assert!( !ChatWidget::preset_matches_current(AskForApproval::Never, ¤t_sandbox, &preset), diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index f8361d674..334a65519 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -7,6 +7,7 @@ use chrono::DateTime; use chrono::Local; use codex_core::WireApi; use codex_core::config::Config; +use codex_core::protocol::AskForApproval; use codex_core::protocol::NetworkAccess; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TokenUsage; @@ -63,8 +64,7 @@ struct StatusHistoryCell { model_name: String, model_details: Vec, directory: PathBuf, - approval: String, - sandbox: String, + permissions: String, agents_summary: String, collaboration_mode: Option, model_provider: Option, @@ -194,6 +194,10 @@ impl StatusHistoryCell { let sandbox = match config.sandbox_policy.get() { SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(), SandboxPolicy::ReadOnly => "read-only".to_string(), + SandboxPolicy::WorkspaceWrite { + network_access: true, + .. + } => "workspace-write with network access".to_string(), SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(), SandboxPolicy::ExternalSandbox { network_access } => { if matches!(network_access, NetworkAccess::Enabled) { @@ -203,6 +207,17 @@ impl StatusHistoryCell { } } }; + let permissions = if config.approval_policy.value() == AskForApproval::OnRequest + && *config.sandbox_policy.get() == SandboxPolicy::new_workspace_write_policy() + { + "Default".to_string() + } else if config.approval_policy.value() == AskForApproval::Never + && *config.sandbox_policy.get() == SandboxPolicy::DangerFullAccess + { + "Full Access".to_string() + } else { + format!("Custom ({sandbox}, {approval})") + }; let agents_summary = compose_agents_summary(config); let model_provider = format_model_provider(config); let account = compose_account_display(auth_manager, plan_type); @@ -235,8 +250,7 @@ impl StatusHistoryCell { model_name, model_details, directory: config.cwd.clone(), - approval, - sandbox, + permissions, agents_summary, collaboration_mode: collaboration_mode.map(ToString::to_string), model_provider, @@ -416,11 +430,10 @@ impl HistoryCell for StatusHistoryCell { } }); - let mut labels: Vec = - vec!["Model", "Directory", "Approval", "Sandbox", "Agents.md"] - .into_iter() - .map(str::to_string) - .collect(); + let mut labels: Vec = vec!["Model", "Directory", "Permissions", "Agents.md"] + .into_iter() + .map(str::to_string) + .collect(); let mut seen: BTreeSet = labels.iter().cloned().collect(); let thread_name = self.thread_name.as_deref().filter(|name| !name.is_empty()); @@ -483,8 +496,7 @@ impl HistoryCell for StatusHistoryCell { lines.push(formatter.line("Model provider", vec![Span::from(model_provider.clone())])); } lines.push(formatter.line("Directory", vec![Span::from(directory_value)])); - lines.push(formatter.line("Approval", vec![Span::from(self.approval.clone())])); - lines.push(formatter.line("Sandbox", vec![Span::from(self.sandbox.clone())])); + lines.push(formatter.line("Permissions", vec![Span::from(self.permissions.clone())])); lines.push(formatter.line("Agents.md", vec![Span::from(self.agents_summary.clone())])); if let Some(account_value) = account_value { diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap index dbb634bab..4c2018abf 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_cached_limits_hide_credits_without_flag.snap @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ │ │ Token usage: 1.05K total (700 input + 350 output) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap index 1707a4c5f..6cc7a8946 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_credits_and_limits.snap @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ │ │ Token usage: 2K total (1.4K input + 600 output) │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap index 18772b7d7..fa0df32ed 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_forked_from.snap @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │ │ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │ 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 3ecc4fa8e..027d935c7 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 @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ │ │ Token usage: 1.2K total (800 input + 400 output) │ 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 c22577407..9115a04d5 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 @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning high, summaries detailed) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: workspace-write │ +│ Permissions: Default │ │ Agents.md: │ │ │ │ Token usage: 1.9K total (1K input + 900 output) │ 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 f0e6b7344..6db1821d4 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 @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ │ │ Token usage: 750 total (500 input + 250 output) │ 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 f0e6b7344..6db1821d4 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 @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ │ │ Token usage: 750 total (500 input + 250 output) │ 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 a12be950b..e6043b65f 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 @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ │ │ Token usage: 1.9K total (1K input + 900 output) │ 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 02ba1adec..91057d482 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 @@ -12,8 +12,7 @@ expression: sanitized │ │ │ Model: gpt-5.1-codex-max (reasoning high, summaries de │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ +│ Permissions: Custom (read-only, on-request) │ │ Agents.md: │ │ │ │ Token usage: 1.9K total (1K input + 900 output) │ diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 37926009a..e46baf5b3 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -7,6 +7,7 @@ use chrono::Utc; use codex_core::AuthManager; use codex_core::config::Config; use codex_core::config::ConfigBuilder; +use codex_core::protocol::AskForApproval; use codex_core::protocol::CreditsSnapshot; use codex_core::protocol::RateLimitSnapshot; use codex_core::protocol::RateLimitWindow; @@ -17,6 +18,7 @@ use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ReasoningEffort; use insta::assert_snapshot; +use pretty_assertions::assert_eq; use ratatui::prelude::*; use std::path::PathBuf; use tempfile::TempDir; @@ -167,6 +169,68 @@ async fn status_snapshot_includes_reasoning_details() { assert_snapshot!(sanitized); } +#[tokio::test] +async fn status_permissions_non_default_workspace_write_is_custom() { + 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.model_provider_id = "openai".to_string(); + config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); + config + .sandbox_policy + .set(SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }) + .expect("set sandbox policy"); + config.cwd = PathBuf::from("/workspace/tests"); + + let auth_manager = test_auth_manager(&config); + let usage = TokenUsage::default(); + let captured_at = chrono::Local + .with_ymd_and_hms(2024, 1, 2, 3, 4, 5) + .single() + .expect("timestamp"); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); + + let composite = new_status_output( + &config, + &auth_manager, + None, + &usage, + &None, + None, + None, + None, + None, + captured_at, + &model_slug, + None, + None, + ); + let rendered_lines = render_lines(&composite.display_lines(80)); + let permissions_line = rendered_lines + .iter() + .find(|line| line.contains("Permissions:")) + .expect("permissions line"); + let permissions_text = permissions_line + .split("Permissions:") + .nth(1) + .map(str::trim) + .map(|text| text.trim_end_matches('│')) + .map(str::trim); + + assert_eq!( + permissions_text, + Some("Custom (workspace-write with network access, on-request)") + ); +} + #[tokio::test] async fn status_snapshot_includes_forked_from() { let temp_home = TempDir::new().expect("temp home");