mirror of
https://github.com/openai/codex.git
synced 2026-04-28 10:21:06 +03:00
9.3 KiB
9.3 KiB
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 --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::<AppEvent>();
+ 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::<AppEvent>();
+ 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
@@ -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 execcommand is hardcoded to never ask for approvals:
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
@@ -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.