diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 59873cbfd..f72255b22 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5423,15 +5423,23 @@ impl ChatWidget { }); })] } else { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) + Self::approval_preset_actions( + preset.approval, + preset.sandbox.clone(), + name.clone(), + ) } } #[cfg(not(target_os = "windows"))] { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) + Self::approval_preset_actions( + preset.approval, + preset.sandbox.clone(), + name.clone(), + ) } } else { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) + Self::approval_preset_actions(preset.approval, preset.sandbox.clone(), name.clone()) }; items.push(SelectionItem { name, @@ -5485,6 +5493,7 @@ impl ChatWidget { fn approval_preset_actions( approval: AskForApproval, sandbox: SandboxPolicy, + label: String, ) -> Vec { vec![Box::new(move |tx| { let sandbox_clone = sandbox.clone(); @@ -5501,6 +5510,9 @@ impl ChatWidget { })); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone)); + tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_info_event(format!("Permissions updated to {label}"), None), + ))); })] } @@ -5547,6 +5559,7 @@ impl ChatWidget { preset: ApprovalPreset, return_to_permissions: bool, ) { + let selected_name = preset.label.to_string(); let approval = preset.approval; let sandbox = preset.sandbox; let mut header_children: Vec> = Vec::new(); @@ -5563,12 +5576,14 @@ impl ChatWidget { )); let header = ColumnRenderable::with(header_children); - let mut accept_actions = Self::approval_preset_actions(approval, sandbox.clone()); + let mut accept_actions = + Self::approval_preset_actions(approval, sandbox.clone(), selected_name.clone()); accept_actions.push(Box::new(|tx| { tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true)); })); - let mut accept_and_remember_actions = Self::approval_preset_actions(approval, sandbox); + let mut accept_and_remember_actions = + Self::approval_preset_actions(approval, sandbox, selected_name); accept_and_remember_actions.push(Box::new(|tx| { tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true)); tx.send(AppEvent::PersistFullAccessWarningAcknowledged); @@ -5678,7 +5693,11 @@ impl ChatWidget { })); } if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) { - accept_actions.extend(Self::approval_preset_actions(approval, sandbox)); + accept_actions.extend(Self::approval_preset_actions( + approval, + sandbox, + mode_label.to_string(), + )); } let mut accept_and_remember_actions: Vec = Vec::new(); @@ -5687,7 +5706,11 @@ impl ChatWidget { tx.send(AppEvent::PersistWorldWritableWarningAcknowledged); })); if let (Some(approval), Some(sandbox)) = (approval, sandbox) { - accept_and_remember_actions.extend(Self::approval_preset_actions(approval, sandbox)); + accept_and_remember_actions.extend(Self::approval_preset_actions( + approval, + sandbox, + mode_label.to_string(), + )); } let items = vec![ diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_after_mode_switch.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_after_mode_switch.snap new file mode 100644 index 000000000..9cc7afa1f --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_after_mode_switch.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: "lines_to_single_string(&cells[0])" +--- +• Permissions updated to Full Access diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_full_access_to_default.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_full_access_to_default.snap new file mode 100644 index 000000000..135e5b1bf --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_full_access_to_default.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: "lines_to_single_string(&cells[0])" +--- +• Permissions updated to Default diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_full_access_to_default@windows.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_full_access_to_default@windows.snap new file mode 100644 index 000000000..eb0810856 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__permissions_selection_history_full_access_to_default@windows.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: "lines_to_single_string(&cells[0])" +--- +• Permissions updated to Default (non-admin sandbox) diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 6881459b9..f069db916 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -20,6 +20,8 @@ use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::Constrained; use codex_core::config::ConstraintError; +#[cfg(target_os = "windows")] +use codex_core::config::types::WindowsSandboxModeToml; use codex_core::config_loader::RequirementSource; use codex_core::features::Feature; use codex_core::models_manager::manager::ModelsManager; @@ -5430,6 +5432,196 @@ async fn approvals_popup_navigation_skips_disabled() { ); } +#[tokio::test] +async fn permissions_selection_emits_history_cell_when_selection_changes() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + #[cfg(target_os = "windows")] + { + chat.config.notices.hide_world_writable_warning = Some(true); + chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + } + chat.config.notices.hide_full_access_warning = Some(true); + + chat.open_permissions_popup(); + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let cells = drain_insert_history(&mut rx); + assert_eq!( + cells.len(), + 1, + "expected one permissions selection history cell" + ); + let rendered = lines_to_single_string(&cells[0]); + assert!( + rendered.contains("Permissions updated to"), + "expected permissions selection history message, got: {rendered}" + ); +} + +#[tokio::test] +async fn permissions_selection_history_snapshot_after_mode_switch() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + #[cfg(target_os = "windows")] + { + chat.config.notices.hide_world_writable_warning = Some(true); + chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + } + chat.config.notices.hide_full_access_warning = Some(true); + + chat.open_permissions_popup(); + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + #[cfg(target_os = "windows")] + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let cells = drain_insert_history(&mut rx); + assert_eq!(cells.len(), 1, "expected one mode-switch history cell"); + assert_snapshot!( + "permissions_selection_history_after_mode_switch", + lines_to_single_string(&cells[0]) + ); +} + +#[tokio::test] +async fn permissions_selection_history_snapshot_full_access_to_default() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + #[cfg(target_os = "windows")] + { + chat.config.notices.hide_world_writable_warning = Some(true); + chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + } + chat.config.notices.hide_full_access_warning = Some(true); + chat.config + .permissions + .approval_policy + .set(AskForApproval::Never) + .expect("set approval policy"); + chat.config + .permissions + .sandbox_policy + .set(SandboxPolicy::DangerFullAccess) + .expect("set sandbox policy"); + + chat.open_permissions_popup(); + chat.handle_key_event(KeyEvent::from(KeyCode::Up)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let cells = drain_insert_history(&mut rx); + assert_eq!(cells.len(), 1, "expected one mode-switch history cell"); + let rendered = lines_to_single_string(&cells[0]); + #[cfg(target_os = "windows")] + insta::with_settings!({ snapshot_suffix => "windows" }, { + assert_snapshot!("permissions_selection_history_full_access_to_default", rendered); + }); + #[cfg(not(target_os = "windows"))] + assert_snapshot!( + "permissions_selection_history_full_access_to_default", + rendered + ); +} + +#[tokio::test] +async fn permissions_selection_emits_history_cell_when_current_is_selected() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + #[cfg(target_os = "windows")] + { + chat.config.notices.hide_world_writable_warning = Some(true); + chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + } + chat.config + .permissions + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); + chat.config + .permissions + .sandbox_policy + .set(SandboxPolicy::new_workspace_write_policy()) + .expect("set sandbox policy"); + + chat.open_permissions_popup(); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let cells = drain_insert_history(&mut rx); + assert_eq!( + cells.len(), + 1, + "expected history cell even when selecting current permissions" + ); + let rendered = lines_to_single_string(&cells[0]); + assert!( + rendered.contains("Permissions updated to"), + "expected permissions update history message, got: {rendered}" + ); +} + +#[tokio::test] +async fn permissions_full_access_history_cell_emitted_only_after_confirmation() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + #[cfg(target_os = "windows")] + { + chat.config.notices.hide_world_writable_warning = Some(true); + chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + } + chat.config.notices.hide_full_access_warning = None; + + chat.open_permissions_popup(); + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + #[cfg(target_os = "windows")] + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let mut open_confirmation_event = None; + let mut cells_before_confirmation = Vec::new(); + while let Ok(event) = rx.try_recv() { + match event { + AppEvent::InsertHistoryCell(cell) => { + cells_before_confirmation.push(cell.display_lines(80)); + } + AppEvent::OpenFullAccessConfirmation { + preset, + return_to_permissions, + } => { + open_confirmation_event = Some((preset, return_to_permissions)); + } + _ => {} + } + } + if cfg!(not(target_os = "windows")) { + assert!( + cells_before_confirmation.is_empty(), + "did not expect history cell before confirming full access" + ); + } + let (preset, return_to_permissions) = + open_confirmation_event.expect("expected full access confirmation event"); + chat.open_full_access_confirmation(preset, return_to_permissions); + + let popup = render_bottom_popup(&chat, 80); + assert!( + popup.contains("Enable full access?"), + "expected full access confirmation popup, got: {popup}" + ); + + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + let cells_after_confirmation = drain_insert_history(&mut rx); + let total_history_cells = cells_before_confirmation.len() + cells_after_confirmation.len(); + assert_eq!( + total_history_cells, 1, + "expected one full access history cell total" + ); + let rendered = if !cells_before_confirmation.is_empty() { + lines_to_single_string(&cells_before_confirmation[0]) + } else { + lines_to_single_string(&cells_after_confirmation[0]) + }; + assert!( + rendered.contains("Permissions updated to Full Access"), + "expected full access update history message, got: {rendered}" + ); +} + // // Snapshot test: command approval modal //