# PR #1691: Fix invisible commands while approving - URL: https://github.com/openai/codex/pull/1691 - Author: easong-openai - Created: 2025-07-26 19:35:50 UTC - Updated: 2025-07-27 18:23:05 UTC - Changes: +178/-4, Files changed: 2, Commits: 3 ## Description Fixes disappearing approvals and adds tests. ## Full Diff ```diff diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index bc647c683e..8099bf9289 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -3,10 +3,12 @@ use codex_core::config::Config; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::AgentReasoningDeltaEvent; +use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::ErrorEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; +use codex_core::protocol::ExecApprovalRequestEvent; use codex_core::protocol::ExecCommandBeginEvent; use codex_core::protocol::ExecCommandEndEvent; use codex_core::protocol::FileChange; @@ -474,11 +476,45 @@ impl EventProcessor for EventProcessorWithHumanOutput { println!("{}", line.style(self.dimmed)); } } - EventMsg::ExecApprovalRequest(_) => { - // Should we exit? + EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + command, + cwd, + reason, + .. + }) => { + ts_println!( + self, + "{} {} in {}", + "approval required for".style(self.magenta), + escape_command(&command).style(self.bold), + cwd.to_string_lossy(), + ); + if let Some(r) = reason { + ts_println!(self, "{r}"); + } + return CodexStatus::InitiateShutdown; } - EventMsg::ApplyPatchApprovalRequest(_) => { - // Should we exit? + EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + changes, + reason, + .. + }) => { + ts_println!( + self, + "{}:", + "approval required for apply_patch".style(self.magenta), + ); + for (path, change) in changes.iter() { + println!( + " {} {}", + format_file_change(change).style(self.cyan), + path.to_string_lossy(), + ); + } + if let Some(r) = reason { + ts_println!(self, "{r}"); + } + return CodexStatus::InitiateShutdown; } EventMsg::AgentReasoning(agent_reasoning_event) => { if self.show_agent_reasoning { @@ -538,3 +574,42 @@ fn format_file_change(change: &FileChange) -> &'static str { } => "M", } } + +#[cfg(test)] +mod tests { + use super::*; + use codex_core::config::Config; + use codex_core::config::ConfigOverrides; + use codex_core::config::ConfigToml; + use codex_core::protocol::Event; + use codex_core::protocol::EventMsg; + use codex_core::protocol::ExecApprovalRequestEvent; + + fn test_config() -> Config { + Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides::default(), + std::env::temp_dir(), + ) + .unwrap_or_else(|e| panic!("failed to load test configuration: {e}")) + } + + #[test] + fn exec_approval_request_displays_command() { + let config = test_config(); + let mut processor = EventProcessorWithHumanOutput::create_with_ansi(false, &config, None); + + let event = Event { + id: "1".into(), + msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + call_id: "c1".into(), + command: vec!["rm".into(), "-rf".into(), "/".into()], + cwd: PathBuf::from("/tmp"), + reason: None, + }), + }; + + let status = processor.process_event(event); + assert!(matches!(status, CodexStatus::InitiateShutdown)); + } +} diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index 431f85a268..c492de19e0 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -368,3 +368,102 @@ impl WidgetRef for &UserApprovalWidget<'_> { Widget::render(List::new(lines), response_chunk, buf); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_event::AppEvent; + use crate::app_event_sender::AppEventSender; + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; + use ratatui::widgets::WidgetRef; + use std::path::PathBuf; + use std::sync::mpsc::channel; + + #[test] + fn exec_command_is_visible_in_small_viewport() { + let long_reason = "This is a very long explanatory reason that would normally occupy many lines in the confirmation prompt. \ +It should not cause the actual command or the response options to be scrolled out of the visible area."; + + let (tx, _rx) = channel::(); + let app_tx = AppEventSender::new(tx); + + let cwd = PathBuf::from("/home/alice/project"); + let command = vec![ + "bash".to_string(), + "-lc".to_string(), + "echo 123 && printf 'hello'".to_string(), + ]; + + let widget = UserApprovalWidget::new( + ApprovalRequest::Exec { + id: "test-id".to_string(), + command: command.clone(), + cwd: cwd.clone(), + reason: Some(long_reason.to_string()), + }, + app_tx, + ); + + let area = Rect::new(0, 0, 50, 12); + let mut buf = Buffer::empty(area); + (&widget).render_ref(area, &mut buf); + + let mut rendered = String::new(); + for y in 0..area.height { + for x in 0..area.width { + let cell = &buf[(x, y)]; + rendered.push(cell.symbol().chars().next().unwrap_or('\0')); + } + rendered.push('\n'); + } + + assert!( + rendered.contains("echo 123 && printf 'hello'"), + "rendered buffer did not contain the command.\n--- buffer ---\n{rendered}\n----------------" + ); + assert!(rendered.contains("Yes (y)")); + } + + #[test] + fn all_options_visible_in_reasonable_viewport() { + let (tx, _rx) = channel::(); + let app_tx = AppEventSender::new(tx); + + let widget = UserApprovalWidget::new( + ApprovalRequest::Exec { + id: "test-id".to_string(), + command: vec![ + "bash".into(), + "-lc".into(), + "echo 123 && printf 'hello'".into(), + ], + cwd: PathBuf::from("/home/alice/project"), + reason: Some("short reason".into()), + }, + app_tx, + ); + + // Use a generous area to avoid truncation of either the prompt or the options. + let area = Rect::new(0, 0, 100, 30); + let mut buf = Buffer::empty(area); + (&widget).render_ref(area, &mut buf); + + let mut rendered = String::new(); + for y in 0..area.height { + for x in 0..area.width { + let cell = &buf[(x, y)]; + rendered.push(cell.symbol().chars().next().unwrap_or('\0')); + } + rendered.push('\n'); + } + + for opt in super::SELECT_OPTIONS { + assert!( + rendered.contains(opt.label), + "expected option label to be visible: {}\n--- buffer ---\n{rendered}\n----------------", + opt.label + ); + } + } +} ``` ## Review Comments ### codex-rs/exec/src/event_processor_with_human_output.rs - Created: 2025-07-27 16:37:04 UTC | Link: https://github.com/openai/codex/pull/1691#discussion_r2234053080 ```diff @@ -474,11 +476,45 @@ impl EventProcessor for EventProcessorWithHumanOutput { println!("{}", line.style(self.dimmed)); } } - EventMsg::ExecApprovalRequest(_) => { - // Should we exit? + EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { ``` > Please change this to what it was before: the `codex exec` command is hardcoded to _never_ ask for approvals: > > https://github.com/openai/codex/blob/5a0079fea2d325d2638e2b1857cba0871fba6402/codex-rs/exec/src/lib.rs#L106-L108 ### codex-rs/tui/src/user_approval_widget.rs - Created: 2025-07-27 16:38:24 UTC | Link: https://github.com/openai/codex/pull/1691#discussion_r2234053504 ```diff @@ -368,3 +368,102 @@ impl WidgetRef for &UserApprovalWidget<'_> { Widget::render(List::new(lines), response_chunk, buf); } } + +#[cfg(test)] ``` > I see tests in this PR, but no logic to change behavior to "fix invisible commands," so I'm confused.