execpolicy tui flow (#7543)

## Updating the `execpolicy` TUI flow

In the TUI, when going through the command approval flow, codex will now
ask the user if they would like to whitelist the FIRST unmatched command
among a chain of commands.

For example, let's say the agent wants to run `apple | pear` with an
empty `execpolicy`

Neither apple nor pear will match to an `execpolicy` rule. Thus, when
prompting the user, codex tui will ask the user if they would like to
whitelist `apple`.

If the agent wants to run `apple | pear` again, they would be prompted
again because pear is still unknown. when prompted, the user will now be
asked if they'd like to whitelist `pear`.

Here's a demo video of this flow:


https://github.com/user-attachments/assets/fd160717-f6cb-46b0-9f4a-f0a974d4e710

This PR also removed the `allow for this session` option from the TUI.
This commit is contained in:
zhao-oai
2025-12-04 02:58:13 -05:00
committed by GitHub
parent 871f44f385
commit 87666695ba
8 changed files with 120 additions and 79 deletions

View File

@@ -8,6 +8,7 @@ use crate::render::renderable::Renderable;
use crate::render::renderable::RenderableItem;
use crate::tui::FrameRequester;
use bottom_pane_view::BottomPaneView;
use codex_core::features::Features;
use codex_core::skills::model::SkillMetadata;
use codex_file_search::FileMatch;
use crossterm::event::KeyCode;
@@ -409,7 +410,7 @@ impl BottomPane {
}
/// Called when the agent requests user approval.
pub fn push_approval_request(&mut self, request: ApprovalRequest) {
pub fn push_approval_request(&mut self, request: ApprovalRequest, features: &Features) {
let request = if let Some(view) = self.view_stack.last_mut() {
match view.try_consume_approval_request(request) {
Some(request) => request,
@@ -423,7 +424,7 @@ impl BottomPane {
};
// Otherwise create a new approval modal overlay.
let modal = ApprovalOverlay::new(request, self.app_event_tx.clone());
let modal = ApprovalOverlay::new(request, self.app_event_tx.clone(), features.clone());
self.pause_status_timer_for_modal();
self.push_view(Box::new(modal));
}
@@ -578,6 +579,7 @@ mod tests {
fn ctrl_c_on_modal_consumes_and_shows_quit_hint() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let features = Features::with_defaults();
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: FrameRequester::test_dummy(),
@@ -588,7 +590,7 @@ mod tests {
animations_enabled: true,
skills: Some(Vec::new()),
});
pane.push_approval_request(exec_request());
pane.push_approval_request(exec_request(), &features);
assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c());
assert!(pane.ctrl_c_quit_hint_visible());
assert_eq!(CancellationEvent::NotHandled, pane.on_ctrl_c());
@@ -600,6 +602,7 @@ mod tests {
fn overlay_not_shown_above_approval_modal() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let features = Features::with_defaults();
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: FrameRequester::test_dummy(),
@@ -612,7 +615,7 @@ mod tests {
});
// Create an approval modal (active view).
pane.push_approval_request(exec_request());
pane.push_approval_request(exec_request(), &features);
// Render and verify the top row does not include an overlay.
let area = Rect::new(0, 0, 60, 6);
@@ -633,6 +636,7 @@ mod tests {
fn composer_shown_after_denied_while_task_running() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let features = Features::with_defaults();
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: FrameRequester::test_dummy(),
@@ -648,7 +652,7 @@ mod tests {
pane.set_task_running(true);
// Push an approval modal (e.g., command approval) which should hide the status view.
pane.push_approval_request(exec_request());
pane.push_approval_request(exec_request(), &features);
// Simulate pressing 'n' (No) on the modal.
use crossterm::event::KeyCode;