From 11d49d925935f4f0fcb152392da9f9b97542f3a0 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Fri, 13 Mar 2026 23:20:36 -0700 Subject: [PATCH] tui: queue smart approvals updates Co-authored-by: Codex --- codex-rs/tui/src/app.rs | 63 ++++-- codex-rs/tui/src/app_event.rs | 7 + codex-rs/tui/src/chatwidget.rs | 194 +++++++++++++++--- ..._chatwidget__tests__slash_help_output.snap | 12 +- ...get__tests__slash_help_output@windows.snap | 12 +- ...dget__tests__slash_help_search_output.snap | 12 +- ...sts__slash_help_search_output@windows.snap | 12 +- codex-rs/tui/src/chatwidget/tests.rs | 49 ++++- codex-rs/tui/src/slash_command.rs | 2 +- 9 files changed, 296 insertions(+), 67 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index d7697b6fa5..d609dd8fed 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -2802,12 +2802,14 @@ impl App { } AppEvent::OpenWorldWritableWarningConfirmation { preset, + approvals_reviewer, sample_paths, extra_count, failed_scan, } => { self.chat_widget.open_world_writable_warning_confirmation( preset, + approvals_reviewer, sample_paths, extra_count, failed_scan, @@ -2827,10 +2829,17 @@ impl App { self.launch_external_editor(tui).await; } } - AppEvent::OpenWindowsSandboxEnablePrompt { preset } => { - self.chat_widget.open_windows_sandbox_enable_prompt(preset); + AppEvent::OpenWindowsSandboxEnablePrompt { + preset, + approvals_reviewer, + } => { + self.chat_widget + .open_windows_sandbox_enable_prompt(preset, approvals_reviewer); } - AppEvent::OpenWindowsSandboxFallbackPrompt { preset } => { + AppEvent::OpenWindowsSandboxFallbackPrompt { + preset, + approvals_reviewer, + } => { self.session_telemetry.counter( "codex.windows_sandbox.fallback_prompt_shown", 1, @@ -2845,9 +2854,12 @@ impl App { ); } self.chat_widget - .open_windows_sandbox_fallback_prompt(preset); + .open_windows_sandbox_fallback_prompt(preset, approvals_reviewer); } - AppEvent::BeginWindowsSandboxElevatedSetup { preset } => { + AppEvent::BeginWindowsSandboxElevatedSetup { + preset, + approvals_reviewer, + } => { #[cfg(target_os = "windows")] { let policy = preset.sandbox.clone(); @@ -2865,6 +2877,7 @@ impl App { tx.send(AppEvent::EnableWindowsSandboxForAgentMode { preset, mode: WindowsSandboxEnableMode::Elevated, + approvals_reviewer, }); return Ok(AppRunControl::Continue); } @@ -2890,6 +2903,7 @@ impl App { AppEvent::EnableWindowsSandboxForAgentMode { preset: preset.clone(), mode: WindowsSandboxEnableMode::Elevated, + approvals_reviewer, } } Err(err) => { @@ -2921,7 +2935,10 @@ impl App { error = %err, "failed to run elevated Windows sandbox setup" ); - AppEvent::OpenWindowsSandboxFallbackPrompt { preset } + AppEvent::OpenWindowsSandboxFallbackPrompt { + preset, + approvals_reviewer, + } } }; tx.send(event); @@ -2929,10 +2946,13 @@ impl App { } #[cfg(not(target_os = "windows"))] { - let _ = preset; + let _ = (preset, approvals_reviewer); } } - AppEvent::BeginWindowsSandboxLegacySetup { preset } => { + AppEvent::BeginWindowsSandboxLegacySetup { + preset, + approvals_reviewer, + } => { #[cfg(target_os = "windows")] { let policy = preset.sandbox.clone(); @@ -2955,6 +2975,7 @@ impl App { let event = match result { Ok(()) => AppEvent::WindowsSandboxLegacySetupCompleted { preset, + approvals_reviewer, error: None, }, Err(err) => { @@ -2964,6 +2985,7 @@ impl App { ); AppEvent::WindowsSandboxLegacySetupCompleted { preset: preset_for_error, + approvals_reviewer, error: Some(err.to_string()), } } @@ -2973,10 +2995,14 @@ impl App { } #[cfg(not(target_os = "windows"))] { - let _ = preset; + let _ = (preset, approvals_reviewer); } } - AppEvent::WindowsSandboxLegacySetupCompleted { preset, error } => { + AppEvent::WindowsSandboxLegacySetupCompleted { + preset, + approvals_reviewer, + error, + } => { #[cfg(target_os = "windows")] match error { None => { @@ -2984,6 +3010,7 @@ impl App { .send(AppEvent::EnableWindowsSandboxForAgentMode { preset, mode: WindowsSandboxEnableMode::Legacy, + approvals_reviewer, }); } Some(err) => { @@ -2994,7 +3021,7 @@ impl App { } #[cfg(not(target_os = "windows"))] { - let _ = (preset, error); + let _ = (preset, approvals_reviewer, error); } } AppEvent::BeginWindowsSandboxGrantReadRoot { path } => { @@ -3054,7 +3081,11 @@ impl App { )); } }, - AppEvent::EnableWindowsSandboxForAgentMode { preset, mode } => { + AppEvent::EnableWindowsSandboxForAgentMode { + preset, + mode, + approvals_reviewer, + } => { #[cfg(target_os = "windows")] { self.chat_widget.clear_windows_sandbox_setup_status(); @@ -3110,6 +3141,7 @@ impl App { self.app_event_tx.send( AppEvent::OpenWorldWritableWarningConfirmation { preset: Some(preset.clone()), + approvals_reviewer: Some(approvals_reviewer), sample_paths, extra_count, failed_scan, @@ -3120,7 +3152,7 @@ impl App { Op::OverrideTurnContext { cwd: None, approval_policy: Some(preset.approval), - approvals_reviewer: Some(self.config.approvals_reviewer), + approvals_reviewer: Some(approvals_reviewer), sandbox_policy: Some(preset.sandbox.clone()), windows_sandbox_level: Some(windows_sandbox_level), model: None, @@ -3135,6 +3167,8 @@ impl App { .send(AppEvent::UpdateAskForApprovalPolicy(preset.approval)); self.app_event_tx .send(AppEvent::UpdateSandboxPolicy(preset.sandbox.clone())); + self.app_event_tx + .send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer)); let _ = mode; self.chat_widget.add_plain_history_lines(vec![ Line::from(vec!["• ".dim(), "Sandbox ready".into()]), @@ -3159,7 +3193,7 @@ impl App { } #[cfg(not(target_os = "windows"))] { - let _ = (preset, mode); + let _ = (preset, mode, approvals_reviewer); } } AppEvent::PersistModelSelection { model, effort } => { @@ -4239,6 +4273,7 @@ impl App { // Scan failed: warn without examples. tx.send(AppEvent::OpenWorldWritableWarningConfirmation { preset: None, + approvals_reviewer: None, sample_paths: Vec::new(), extra_count: 0usize, failed_scan: true, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 2f8da68070..fe57765705 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -268,6 +268,7 @@ pub(crate) enum AppEvent { #[cfg_attr(not(target_os = "windows"), allow(dead_code))] OpenWorldWritableWarningConfirmation { preset: Option, + approvals_reviewer: Option, /// Up to 3 sample world-writable directories to display in the warning. sample_paths: Vec, /// If there are more than `sample_paths`, this carries the remaining count. @@ -280,30 +281,35 @@ pub(crate) enum AppEvent { #[cfg_attr(not(target_os = "windows"), allow(dead_code))] OpenWindowsSandboxEnablePrompt { preset: ApprovalPreset, + approvals_reviewer: ApprovalsReviewer, }, /// Open the Windows sandbox fallback prompt after declining or failing elevation. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] OpenWindowsSandboxFallbackPrompt { preset: ApprovalPreset, + approvals_reviewer: ApprovalsReviewer, }, /// Begin the elevated Windows sandbox setup flow. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] BeginWindowsSandboxElevatedSetup { preset: ApprovalPreset, + approvals_reviewer: ApprovalsReviewer, }, /// Begin the non-elevated Windows sandbox setup flow. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] BeginWindowsSandboxLegacySetup { preset: ApprovalPreset, + approvals_reviewer: ApprovalsReviewer, }, /// Result of the non-elevated Windows sandbox setup flow. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] WindowsSandboxLegacySetupCompleted { preset: ApprovalPreset, + approvals_reviewer: ApprovalsReviewer, error: Option, }, @@ -325,6 +331,7 @@ pub(crate) enum AppEvent { EnableWindowsSandboxForAgentMode { preset: ApprovalPreset, mode: WindowsSandboxEnableMode, + approvals_reviewer: ApprovalsReviewer, }, /// Update the Windows sandbox feature mode without changing approval presets. diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 5a9044f283..97c35b45a4 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4537,7 +4537,10 @@ impl ChatWidget { &[], ); self.app_event_tx - .send(AppEvent::BeginWindowsSandboxElevatedSetup { preset }); + .send(AppEvent::BeginWindowsSandboxElevatedSetup { + preset, + approvals_reviewer: self.config.approvals_reviewer, + }); } #[cfg(not(target_os = "windows"))] { @@ -4803,7 +4806,7 @@ impl ChatWidget { SlashCommand::Approvals | SlashCommand::Permissions => { let args = match SlashCommandInvocation::parse_args( &args_message.text, - "Usage: /approvals [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]", + "Usage: /approvals [--smart-approvals] [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]", ) { Ok(args) => args, Err(message) => { @@ -4818,7 +4821,7 @@ impl ChatWidget { return self.restore_invalid_inline_slash_command( cmd, args_message, - "Usage: /approvals [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]".to_string(), + "Usage: /approvals [--smart-approvals] [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]".to_string(), ); } let preset_id = args[0].as_str(); @@ -4837,9 +4840,11 @@ impl ChatWidget { let mut remember_full_access = false; let mut confirm_world_writable = false; let mut remember_world_writable = false; + let mut smart_approvals = false; let mut windows_sandbox_mode = None; for token in args.iter().skip(1) { match token.as_str() { + "--smart-approvals" => smart_approvals = true, "--confirm-full-access" => confirm_full_access = true, "--remember-full-access" => remember_full_access = true, "--confirm-world-writable" => confirm_world_writable = true, @@ -4859,6 +4864,25 @@ impl ChatWidget { } } } + if smart_approvals && preset.id != "auto" { + return self.restore_invalid_inline_slash_command( + cmd, + args_message, + "Smart Approvals is only available for Default permissions.".to_string(), + ); + } + if smart_approvals && !self.config.features.enabled(Feature::GuardianApproval) { + return self.restore_invalid_inline_slash_command( + cmd, + args_message, + "Smart Approvals is not enabled in this session.".to_string(), + ); + } + let approvals_reviewer = if smart_approvals { + ApprovalsReviewer::GuardianSubagent + } else { + ApprovalsReviewer::User + }; #[cfg(not(target_os = "windows"))] let _ = windows_sandbox_mode; @@ -4889,6 +4913,11 @@ impl ChatWidget { }; #[cfg(not(target_os = "windows"))] let label = preset.label.to_string(); + let label = if smart_approvals { + "Smart Approvals".to_string() + } else { + label + }; if preset.id == "full-access" && !confirm_full_access @@ -4921,12 +4950,19 @@ impl ChatWidget { }; match mode { WindowsSandboxEnableMode::Elevated => { - self.app_event_tx - .send(AppEvent::BeginWindowsSandboxElevatedSetup { preset }); + self.app_event_tx.send( + AppEvent::BeginWindowsSandboxElevatedSetup { + preset, + approvals_reviewer, + }, + ); } WindowsSandboxEnableMode::Legacy => { self.app_event_tx - .send(AppEvent::BeginWindowsSandboxLegacySetup { preset }); + .send(AppEvent::BeginWindowsSandboxLegacySetup { + preset, + approvals_reviewer, + }); } } return QueueReplayControl::Stop; @@ -4966,7 +5002,7 @@ impl ChatWidget { .send(AppEvent::CodexOp(Op::OverrideTurnContext { cwd: None, approval_policy: Some(preset.approval), - approvals_reviewer: Some(ApprovalsReviewer::User), + approvals_reviewer: Some(approvals_reviewer), sandbox_policy: Some(sandbox.clone()), windows_sandbox_level: None, model: None, @@ -4981,7 +5017,7 @@ impl ChatWidget { self.app_event_tx .send(AppEvent::UpdateSandboxPolicy(sandbox)); self.app_event_tx - .send(AppEvent::UpdateApprovalsReviewer(ApprovalsReviewer::User)); + .send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer)); self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( history_cell::new_info_event(format!("Permissions updated to {label}"), None), ))); @@ -5666,12 +5702,23 @@ impl ChatWidget { } } - fn approval_preset_draft(preset_id: &str, flags: &[&str]) -> UserMessage { + fn approval_preset_draft_for_reviewer( + preset_id: &str, + approvals_reviewer: ApprovalsReviewer, + flags: &[&str], + ) -> UserMessage { let mut args = vec![preset_id.to_string()]; + if approvals_reviewer == ApprovalsReviewer::GuardianSubagent { + args.push("--smart-approvals".to_string()); + } args.extend(flags.iter().map(|flag| (*flag).to_string())); SlashCommandInvocation::with_args(SlashCommand::Approvals, args).into_user_message() } + fn approval_preset_draft(preset_id: &str, flags: &[&str]) -> UserMessage { + Self::approval_preset_draft_for_reviewer(preset_id, ApprovalsReviewer::User, flags) + } + #[cfg(all(not(target_os = "linux"), feature = "voice-input"))] fn settings_device_draft(kind: RealtimeAudioDeviceKind, name: Option<&str>) -> UserMessage { let kind_name = match kind { @@ -8464,6 +8511,7 @@ impl ChatWidget { vec![Box::new(move |tx| { tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { preset: preset_clone.clone(), + approvals_reviewer: ApprovalsReviewer::User, }); })] } @@ -8474,6 +8522,7 @@ impl ChatWidget { vec![Box::new(move |tx| { tx.send(AppEvent::OpenWorldWritableWarningConfirmation { preset: Some(preset_clone.clone()), + approvals_reviewer: Some(ApprovalsReviewer::User), sample_paths: sample_paths.clone(), extra_count, failed_scan, @@ -8504,6 +8553,63 @@ impl ChatWidget { if guardian_approval_enabled { let guardian_preset = preset.clone(); + let guardian_actions: Vec = { + #[cfg(target_os = "windows")] + { + if WindowsSandboxLevel::from_config(&self.config) + == WindowsSandboxLevel::Disabled + { + let preset_clone = guardian_preset.clone(); + if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && codex_core::windows_sandbox::sandbox_setup_is_complete( + self.config.codex_home.as_path(), + ) + { + Self::approval_preset_actions_for_reviewer( + guardian_preset.id, + ApprovalsReviewer::GuardianSubagent, + &["--enable-windows-sandbox=elevated"], + ) + } else { + vec![Box::new(move |tx| { + tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { + preset: preset_clone.clone(), + approvals_reviewer: ApprovalsReviewer::GuardianSubagent, + }); + })] + } + } else if let Some((sample_paths, extra_count, failed_scan)) = + self.world_writable_warning_details() + { + let preset_clone = guardian_preset.clone(); + vec![Box::new(move |tx| { + tx.send(AppEvent::OpenWorldWritableWarningConfirmation { + preset: Some(preset_clone.clone()), + approvals_reviewer: Some( + ApprovalsReviewer::GuardianSubagent, + ), + sample_paths: sample_paths.clone(), + extra_count, + failed_scan, + }); + })] + } else { + Self::approval_preset_actions_for_reviewer( + guardian_preset.id, + ApprovalsReviewer::GuardianSubagent, + &[], + ) + } + } + #[cfg(not(target_os = "windows"))] + { + Self::approval_preset_actions_for_reviewer( + guardian_preset.id, + ApprovalsReviewer::GuardianSubagent, + &[], + ) + } + }; items.push(SelectionItem { name: "Guardian Approvals".to_string(), description: Some( @@ -8516,11 +8622,7 @@ impl ChatWidget { current_sandbox, &preset, ), - actions: Self::approval_preset_actions_for_reviewer( - guardian_preset.id, - ApprovalsReviewer::GuardianSubagent, - &[], - ), + actions: guardian_actions, dismiss_on_select: true, disabled_reason: approval_disabled_reason .or_else(|| guardian_disabled_reason(true)), @@ -8593,6 +8695,18 @@ impl ChatWidget { })] } + fn approval_preset_actions_for_reviewer( + preset_id: &'static str, + approvals_reviewer: ApprovalsReviewer, + flags: &'static [&'static str], + ) -> Vec { + vec![Box::new(move |tx| { + tx.send(AppEvent::HandleSlashCommandDraft( + Self::approval_preset_draft_for_reviewer(preset_id, approvals_reviewer, flags), + )); + })] + } + fn preset_matches_current( current_approval: AskForApproval, current_sandbox: &SandboxPolicy, @@ -8727,6 +8841,7 @@ impl ChatWidget { pub(crate) fn open_world_writable_warning_confirmation( &mut self, preset: Option, + approvals_reviewer: Option, sample_paths: Vec, extra_count: usize, failed_scan: bool, @@ -8776,12 +8891,19 @@ impl ChatWidget { // so downstream policy-change hooks don't re-trigger the warning. let accept_actions = preset .as_ref() - .map(|preset| Self::approval_preset_actions(preset.id, &["--confirm-world-writable"])) + .map(|preset| { + Self::approval_preset_actions_for_reviewer( + preset.id, + approvals_reviewer.unwrap_or(ApprovalsReviewer::User), + &["--confirm-world-writable"], + ) + }) .unwrap_or_default(); let accept_and_remember_actions: Vec = if let Some(preset) = preset.as_ref() { - Self::approval_preset_actions( + Self::approval_preset_actions_for_reviewer( preset.id, + approvals_reviewer.unwrap_or(ApprovalsReviewer::User), &["--confirm-world-writable", "--remember-world-writable"], ) } else { @@ -8820,6 +8942,7 @@ impl ChatWidget { pub(crate) fn open_world_writable_warning_confirmation( &mut self, _preset: Option, + _approvals_reviewer: Option, _sample_paths: Vec, _extra_count: usize, _failed_scan: bool, @@ -8827,7 +8950,11 @@ impl ChatWidget { } #[cfg(target_os = "windows")] - pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) { + pub(crate) fn open_windows_sandbox_enable_prompt( + &mut self, + preset: ApprovalPreset, + approvals_reviewer: ApprovalsReviewer, + ) { use ratatui_macros::line; if !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED { @@ -8850,6 +8977,7 @@ impl ChatWidget { actions: vec![Box::new(move |tx| { tx.send(AppEvent::BeginWindowsSandboxLegacySetup { preset: preset_clone.clone(), + approvals_reviewer, }); })], dismiss_on_select: true, @@ -8898,8 +9026,9 @@ impl ChatWidget { actions: vec![Box::new(move |tx| { accept_otel.counter("codex.windows_sandbox.elevated_prompt_accept", 1, &[]); tx.send(AppEvent::HandleSlashCommandDraft( - Self::approval_preset_draft( + Self::approval_preset_draft_for_reviewer( preset.id, + approvals_reviewer, &["--enable-windows-sandbox=elevated"], ), )); @@ -8913,8 +9042,9 @@ impl ChatWidget { actions: vec![Box::new(move |tx| { legacy_otel.counter("codex.windows_sandbox.elevated_prompt_use_legacy", 1, &[]); tx.send(AppEvent::HandleSlashCommandDraft( - Self::approval_preset_draft( + Self::approval_preset_draft_for_reviewer( legacy_preset.id, + approvals_reviewer, &["--enable-windows-sandbox=legacy"], ), )); @@ -8944,10 +9074,19 @@ impl ChatWidget { } #[cfg(not(target_os = "windows"))] - pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {} + pub(crate) fn open_windows_sandbox_enable_prompt( + &mut self, + _preset: ApprovalPreset, + _approvals_reviewer: ApprovalsReviewer, + ) { + } #[cfg(target_os = "windows")] - pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, preset: ApprovalPreset) { + pub(crate) fn open_windows_sandbox_fallback_prompt( + &mut self, + preset: ApprovalPreset, + approvals_reviewer: ApprovalsReviewer, + ) { use ratatui_macros::line; let mut lines = Vec::new(); @@ -8978,8 +9117,9 @@ impl ChatWidget { move |tx| { otel.counter("codex.windows_sandbox.fallback_retry_elevated", 1, &[]); tx.send(AppEvent::HandleSlashCommandDraft( - Self::approval_preset_draft( + Self::approval_preset_draft_for_reviewer( preset.id, + approvals_reviewer, &["--enable-windows-sandbox=elevated"], ), )); @@ -8997,8 +9137,9 @@ impl ChatWidget { move |tx| { otel.counter("codex.windows_sandbox.fallback_use_legacy", 1, &[]); tx.send(AppEvent::HandleSlashCommandDraft( - Self::approval_preset_draft( + Self::approval_preset_draft_for_reviewer( preset.id, + approvals_reviewer, &["--enable-windows-sandbox=legacy"], ), )); @@ -9029,7 +9170,12 @@ impl ChatWidget { } #[cfg(not(target_os = "windows"))] - pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, _preset: ApprovalPreset) {} + pub(crate) fn open_windows_sandbox_fallback_prompt( + &mut self, + _preset: ApprovalPreset, + _approvals_reviewer: ApprovalsReviewer, + ) { + } #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap index 8060910fb3..80b4e3f851 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap @@ -29,17 +29,17 @@ expression: popup choose what Codex is allowed to do Usage: /approvals - /approvals [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /approvals [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /permissions choose what Codex is allowed to do Usage: /permissions - /permissions [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /permissions [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /experimental toggle experimental features diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output@windows.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output@windows.snap index 0684e830f0..6ca9e87aea 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output@windows.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output@windows.snap @@ -29,17 +29,17 @@ expression: popup choose what Codex is allowed to do Usage: /approvals - /approvals [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /approvals [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /permissions choose what Codex is allowed to do Usage: /permissions - /permissions [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /permissions [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /setup-default-sandbox set up elevated agent sandbox diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap index 6ba1dcd2f5..92c04aba5e 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap @@ -29,17 +29,17 @@ expression: searching choose what Codex is allowed to do Usage: /approvals - /approvals [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /approvals [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /permissions choose what Codex is allowed to do Usage: /permissions - /permissions [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /permissions [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /experimental toggle experimental features diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output@windows.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output@windows.snap index 0bb7606c1a..7720b5a8a0 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output@windows.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output@windows.snap @@ -29,17 +29,17 @@ expression: searching choose what Codex is allowed to do Usage: /approvals - /approvals [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /approvals [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /permissions choose what Codex is allowed to do Usage: /permissions - /permissions [--confirm-full-access] [--remember-full-access] - [--confirm-world-writable] [--remember-world-writable] [--enable-windows- - sandbox=elevated|legacy] + /permissions [--smart-approvals] [--confirm-full-access] + [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] + [--enable-windows-sandbox=elevated|legacy] /setup-default-sandbox set up elevated agent sandbox diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 7df8130e6d..0549aff6ed 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -9185,7 +9185,10 @@ async fn approvals_elevated_inline_args_start_windows_sandbox_setup() { assert_matches!( rx.try_recv(), - Ok(AppEvent::BeginWindowsSandboxElevatedSetup { preset }) if preset.id == "auto" + Ok(AppEvent::BeginWindowsSandboxElevatedSetup { + preset, + approvals_reviewer: ApprovalsReviewer::User, + }) if preset.id == "auto" ); } @@ -9201,7 +9204,31 @@ async fn approvals_legacy_inline_args_start_windows_sandbox_setup() { assert_matches!( rx.try_recv(), - Ok(AppEvent::BeginWindowsSandboxLegacySetup { preset }) if preset.id == "auto" + Ok(AppEvent::BeginWindowsSandboxLegacySetup { + preset, + approvals_reviewer: ApprovalsReviewer::User, + }) if preset.id == "auto" + ); +} + +#[cfg(target_os = "windows")] +#[tokio::test] +async fn approvals_smart_inline_args_preserve_guardian_reviewer_through_windows_setup() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + chat.set_feature_enabled(Feature::GuardianApproval, true); + chat.set_windows_sandbox_mode(None); + chat.handle_serialized_slash_command(ChatWidget::approval_preset_draft_for_reviewer( + "auto", + ApprovalsReviewer::GuardianSubagent, + &["--enable-windows-sandbox=elevated"], + )); + + assert_matches!( + rx.try_recv(), + Ok(AppEvent::BeginWindowsSandboxElevatedSetup { + preset, + approvals_reviewer: ApprovalsReviewer::GuardianSubagent, + }) if preset.id == "auto" ); } @@ -9583,7 +9610,7 @@ async fn permissions_selection_can_disable_guardian_approvals() { } #[tokio::test] -async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context() { +async fn permissions_selection_emits_smart_approvals_draft_before_replay() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; #[cfg(target_os = "windows")] { @@ -9623,12 +9650,26 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context ); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + let draft = match rx.try_recv() { + Ok(AppEvent::HandleSlashCommandDraft(draft)) => draft, + other => panic!("expected serialized smart approvals draft, got {other:?}"), + }; + assert_eq!( + draft, + ChatWidget::approval_preset_draft_for_reviewer( + "auto", + ApprovalsReviewer::GuardianSubagent, + &[], + ) + ); + chat.handle_serialized_slash_command(draft); + let op = std::iter::from_fn(|| rx.try_recv().ok()) .find_map(|event| match event { AppEvent::CodexOp(op @ Op::OverrideTurnContext { .. }) => Some(op), _ => None, }) - .expect("expected OverrideTurnContext op"); + .expect("expected OverrideTurnContext op after replay"); assert_eq!( op, diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index b9104f002e..a1db2e4146 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -187,7 +187,7 @@ impl SlashCommand { SlashCommand::Fast => &["", ""], SlashCommand::Approvals | SlashCommand::Permissions => &[ "", - " [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]", + " [--smart-approvals] [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]", ], SlashCommand::ElevateSandbox => &[""], SlashCommand::SandboxReadRoot => &[""],