Codex Handbook
tui/src/bottom_pane/approval_overlay.rs 2387 lines
//! Approval modal rendering and decision routing for high-risk operations.//!//! This module converts agent approval requests (exec/apply-patch/MCP//! elicitation) into a list-selection view with action-specific options and//! shortcuts. It owns two important contracts://!//! 1. Selection always emits an explicit decision event back to the app.//! 2. MCP elicitation keeps `Esc` mapped to `Cancel`, even with custom//!    keybindings, so dismissal never silently becomes "continue without info".//!//! This module does not evaluate whether an action is safe to run; it only//! presents choices and routes user decisions.use std::collections::HashMap;use std::path::PathBuf;use crate::app::app_server_requests::ResolvedAppServerRequest;#[cfg(test)]use crate::app_command::AppCommand as Op;use crate::app_event::AppEvent;use crate::app_event_sender::AppEventSender;use crate::bottom_pane::BottomPaneView;use crate::bottom_pane::CancellationEvent;use crate::bottom_pane::list_selection_view::ListSelectionView;use crate::bottom_pane::list_selection_view::SelectionItem;use crate::bottom_pane::list_selection_view::SelectionViewParams;use crate::bottom_pane::popup_consts::accept_cancel_hint_line;use crate::diff_model::FileChange;use crate::exec_command::strip_bash_lc_and_escape;use crate::history_cell;use crate::history_cell::ReviewDecision;use crate::key_hint;use crate::key_hint::KeyBinding;use crate::key_hint::KeyBindingListExt;use crate::keymap::ApprovalKeymap;use crate::keymap::ListKeymap;use crate::keymap::primary_binding;use crate::render::highlight::highlight_bash_to_lines;use crate::render::renderable::ColumnRenderable;use crate::render::renderable::Renderable;use codex_app_server_protocol::AdditionalPermissionProfile;use codex_app_server_protocol::CommandExecutionApprovalDecision;use codex_app_server_protocol::FileChangeApprovalDecision;use codex_app_server_protocol::FileSystemAccessMode;use codex_app_server_protocol::FileSystemPath;use codex_app_server_protocol::FileSystemSandboxEntry;use codex_app_server_protocol::FileSystemSpecialPath;use codex_app_server_protocol::McpServerElicitationAction;use codex_app_server_protocol::NetworkApprovalContext;use codex_app_server_protocol::NetworkApprovalProtocol;use codex_app_server_protocol::NetworkPolicyRuleAction;use codex_app_server_protocol::RequestId;use codex_features::Features;use codex_protocol::ThreadId;use codex_protocol::request_permissions::PermissionGrantScope;use codex_protocol::request_permissions::RequestPermissionProfile;use codex_utils_absolute_path::AbsolutePathBuf;use crossterm::event::KeyCode;use crossterm::event::KeyEvent;use crossterm::event::KeyEventKind;use crossterm::event::KeyModifiers;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::text::Span;use ratatui::widgets::Paragraph;use ratatui::widgets::Wrap;/// Request coming from the agent that needs user approval.#[derive(Clone, Debug)]pub(crate) enum ApprovalRequest {    Exec {        thread_id: ThreadId,        thread_label: Option<String>,        id: String,        command: Vec<String>,        reason: Option<String>,        available_decisions: Vec<CommandExecutionApprovalDecision>,        network_approval_context: Option<NetworkApprovalContext>,        additional_permissions: Option<AdditionalPermissionProfile>,    },    Permissions {        thread_id: ThreadId,        thread_label: Option<String>,        call_id: String,        environment_id: Option<String>,        reason: Option<String>,        permissions: RequestPermissionProfile,    },    ApplyPatch {        thread_id: ThreadId,        thread_label: Option<String>,        id: String,        reason: Option<String>,        cwd: AbsolutePathBuf,        changes: HashMap<PathBuf, FileChange>,    },    McpElicitation {        thread_id: ThreadId,        thread_label: Option<String>,        server_name: String,        request_id: RequestId,        message: String,    },}impl ApprovalRequest {    fn thread_id(&self) -> ThreadId {        match self {            ApprovalRequest::Exec { thread_id, .. }            | ApprovalRequest::Permissions { thread_id, .. }            | ApprovalRequest::ApplyPatch { thread_id, .. }            | ApprovalRequest::McpElicitation { thread_id, .. } => *thread_id,        }    }    fn thread_label(&self) -> Option<&str> {        match self {            ApprovalRequest::Exec { thread_label, .. }            | ApprovalRequest::Permissions { thread_label, .. }            | ApprovalRequest::ApplyPatch { thread_label, .. }            | ApprovalRequest::McpElicitation { thread_label, .. } => thread_label.as_deref(),        }    }    pub(super) fn matches_resolved_request(&self, request: &ResolvedAppServerRequest) -> bool {        match (self, request) {            (                ApprovalRequest::Exec { id, .. },                ResolvedAppServerRequest::ExecApproval { id: resolved_id },            ) => id == resolved_id,            (                ApprovalRequest::Permissions { call_id, .. },                ResolvedAppServerRequest::PermissionsApproval { id },            ) => call_id == id,            (                ApprovalRequest::ApplyPatch { id, .. },                ResolvedAppServerRequest::FileChangeApproval { id: resolved_id },            ) => id == resolved_id,            (                ApprovalRequest::McpElicitation {                    server_name,                    request_id,                    ..                },                ResolvedAppServerRequest::McpElicitation {                    server_name: resolved_server_name,                    request_id: resolved_request_id,                },            ) => server_name == resolved_server_name && request_id == resolved_request_id,            _ => false,        }    }}/// Modal overlay asking the user to approve or deny one or more requests.pub(crate) struct ApprovalOverlay {    current_request: Option<ApprovalRequest>,    queue: Vec<ApprovalRequest>,    app_event_tx: AppEventSender,    list: ListSelectionView,    options: Vec<ApprovalOption>,    current_complete: bool,    done: bool,    features: Features,    approval_keymap: ApprovalKeymap,    list_keymap: ListKeymap,}impl ApprovalOverlay {    pub fn new(        request: ApprovalRequest,        app_event_tx: AppEventSender,        features: Features,        approval_keymap: ApprovalKeymap,        list_keymap: ListKeymap,    ) -> Self {        let mut view = Self {            current_request: None,            queue: Vec::new(),            app_event_tx: app_event_tx.clone(),            list: ListSelectionView::new(Default::default(), app_event_tx, list_keymap.clone()),            options: Vec::new(),            current_complete: false,            done: false,            features,            approval_keymap,            list_keymap,        };        view.set_current(request);        view    }    pub fn enqueue_request(&mut self, req: ApprovalRequest) {        self.queue.push(req);    }    fn dismiss_resolved_request(&mut self, request: &ResolvedAppServerRequest) -> bool {        let queue_len = self.queue.len();        self.queue            .retain(|queued_request| !queued_request.matches_resolved_request(request));        if self            .current_request            .as_ref()            .is_some_and(|current_request| current_request.matches_resolved_request(request))        {            self.current_complete = true;            self.advance_queue();            return true;        }        self.queue.len() != queue_len    }    fn set_current(&mut self, request: ApprovalRequest) {        self.current_complete = false;        let header = build_header(&request);        let (options, params) = Self::build_options(            &request,            header,            &self.features,            &self.approval_keymap,            &self.list_keymap,        );        self.current_request = Some(request);        self.options = options;        self.list =            ListSelectionView::new(params, self.app_event_tx.clone(), self.list_keymap.clone());    }    fn build_options(        request: &ApprovalRequest,        header: Box<dyn Renderable>,        _features: &Features,        approval_keymap: &ApprovalKeymap,        list_keymap: &ListKeymap,    ) -> (Vec<ApprovalOption>, SelectionViewParams) {        let (options, title) = match request {            ApprovalRequest::Exec {                available_decisions,                network_approval_context,                additional_permissions,                ..            } => (                exec_options(                    available_decisions,                    network_approval_context.as_ref(),                    additional_permissions.as_ref(),                    approval_keymap,                ),                network_approval_context.as_ref().map_or_else(                    || "Would you like to run the following command?".to_string(),                    |network_approval_context| {                        format!(                            "Do you want to approve network access to \"{}\"?",                            network_approval_context.host                        )                    },                ),            ),            ApprovalRequest::Permissions { .. } => (                permissions_options(approval_keymap),                "Would you like to grant these permissions?".to_string(),            ),            ApprovalRequest::ApplyPatch { .. } => (                patch_options(approval_keymap),                "Would you like to make the following edits?".to_string(),            ),            ApprovalRequest::McpElicitation { server_name, .. } => (                elicitation_options(approval_keymap),                format!("{server_name} needs your approval."),            ),        };        let header = Box::new(ColumnRenderable::with([            Line::from(title.bold()).into(),            Line::from("").into(),            header,        ]));        let items = options            .iter()            .map(|opt| SelectionItem {                name: opt.label.clone(),                display_shortcut: opt.shortcuts.first().copied(),                dismiss_on_select: false,                ..Default::default()            })            .collect();        let params = SelectionViewParams {            footer_hint: Some(approval_footer_hint(request, approval_keymap, list_keymap)),            items,            header,            ..Default::default()        };        (options, params)    }    fn apply_selection(&mut self, actual_idx: usize) {        if self.current_complete {            return;        }        let Some(option) = self.options.get(actual_idx) else {            return;        };        if let Some(request) = self.current_request.as_ref() {            match (request, &option.decision) {                (                    ApprovalRequest::Exec { id, command, .. },                    ApprovalDecision::Command(decision),                ) => {                    self.handle_exec_decision(id, command, decision.clone());                }                (                    ApprovalRequest::Permissions {                        call_id,                        permissions,                        ..                    },                    ApprovalDecision::Permissions(decision),                ) => self.handle_permissions_decision(call_id, permissions, *decision),                (                    ApprovalRequest::ApplyPatch { id, .. },                    ApprovalDecision::FileChange(decision),                ) => {                    self.handle_patch_decision(id, decision.clone());                }                (                    ApprovalRequest::McpElicitation {                        server_name,                        request_id,                        ..                    },                    ApprovalDecision::McpElicitation(decision),                ) => {                    self.handle_elicitation_decision(server_name, request_id, *decision);                }                _ => {}            }        }        self.current_complete = true;        self.advance_queue();    }    fn handle_exec_decision(        &self,        id: &str,        command: &[String],        decision: CommandExecutionApprovalDecision,    ) {        let Some(request) = self.current_request.as_ref() else {            return;        };        if request.thread_label().is_none() {            let subject = match request {                ApprovalRequest::Exec {                    network_approval_context: Some(network_approval_context),                    ..                } => history_cell::ApprovalDecisionSubject::NetworkAccess {                    target: network_approval_target(network_approval_context, command),                },                _ => {                    if let Some(target) = network_approval_command_target(command) {                        history_cell::ApprovalDecisionSubject::NetworkAccess {                            target: target.to_string(),                        }                    } else {                        history_cell::ApprovalDecisionSubject::Command(command.to_vec())                    }                }            };            let cell = history_cell::new_approval_decision_cell(                subject,                command_decision_to_review_decision(&decision),                history_cell::ApprovalDecisionActor::User,            );            self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));        }        let thread_id = request.thread_id();        self.app_event_tx            .exec_approval(thread_id, id.to_string(), decision);    }    fn handle_permissions_decision(        &self,        call_id: &str,        permissions: &RequestPermissionProfile,        decision: PermissionsDecision,    ) {        let Some(request) = self.current_request.as_ref() else {            return;        };        let granted_permissions = match decision {            PermissionsDecision::GrantForTurn            | PermissionsDecision::GrantForTurnWithStrictAutoReview            | PermissionsDecision::GrantForSession => permissions.clone(),            PermissionsDecision::Deny => Default::default(),        };        let scope = if matches!(decision, PermissionsDecision::GrantForSession) {            PermissionGrantScope::Session        } else {            PermissionGrantScope::Turn        };        let strict_auto_review = matches!(            decision,            PermissionsDecision::GrantForTurnWithStrictAutoReview        );        if request.thread_label().is_none() {            let message = if granted_permissions.is_empty() {                "You did not grant additional permissions"            } else if strict_auto_review {                "You granted additional permissions with strict auto review"            } else if matches!(scope, PermissionGrantScope::Session) {                "You granted additional permissions for this session"            } else {                "You granted additional permissions"            };            self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(                crate::history_cell::PlainHistoryCell::new(vec![message.into()]),            )));        }        let thread_id = request.thread_id();        self.app_event_tx.request_permissions_response(            thread_id,            call_id.to_string(),            codex_protocol::request_permissions::RequestPermissionsResponse {                permissions: granted_permissions,                scope,                strict_auto_review,            },        );    }    fn handle_patch_decision(&self, id: &str, decision: FileChangeApprovalDecision) {        let Some(thread_id) = self            .current_request            .as_ref()            .map(ApprovalRequest::thread_id)        else {            return;        };        self.app_event_tx            .patch_approval(thread_id, id.to_string(), decision);    }    fn handle_elicitation_decision(        &self,        server_name: &str,        request_id: &RequestId,        decision: McpServerElicitationAction,    ) {        let Some(thread_id) = self            .current_request            .as_ref()            .map(ApprovalRequest::thread_id)        else {            return;        };        self.app_event_tx.resolve_elicitation(            thread_id,            server_name.to_string(),            request_id.clone(),            decision,            /*content*/ None,            /*meta*/ None,        );    }    fn advance_queue(&mut self) {        if let Some(next) = self.queue.pop() {            self.set_current(next);        } else {            self.done = true;        }    }    fn cancel_current_request(&mut self) {        if self.done {            return;        }        if !self.current_complete            && let Some(request) = self.current_request.as_ref()        {            match request {                ApprovalRequest::Exec { id, command, .. } => {                    self.handle_exec_decision(                        id,                        command,                        CommandExecutionApprovalDecision::Cancel,                    );                }                ApprovalRequest::Permissions {                    call_id,                    permissions,                    ..                } => {                    self.handle_permissions_decision(                        call_id,                        permissions,                        PermissionsDecision::Deny,                    );                }                ApprovalRequest::ApplyPatch { id, .. } => {                    self.handle_patch_decision(id, FileChangeApprovalDecision::Cancel);                }                ApprovalRequest::McpElicitation {                    server_name,                    request_id,                    ..                } => {                    self.handle_elicitation_decision(                        server_name,                        request_id,                        McpServerElicitationAction::Cancel,                    );                }            }        }        self.queue.clear();        self.done = true;    }    /// Apply approval-specific shortcuts before delegating to list navigation.    ///    /// `open_fullscreen` is handled here because it is orthogonal to list item    /// selection and should work regardless of current highlighted row.    fn try_handle_shortcut(&mut self, key_event: &KeyEvent) -> bool {        if key_event.kind == KeyEventKind::Press            && self.approval_keymap.open_fullscreen.is_pressed(*key_event)            && let Some(request) = self.current_request.as_ref()        {            self.app_event_tx                .send(AppEvent::FullScreenApprovalRequest(request.clone()));            return true;        }        if key_event.kind == KeyEventKind::Press            && self.approval_keymap.open_thread.is_pressed(*key_event)            && let Some(request) = self.current_request.as_ref()            && request.thread_label().is_some()        {            self.app_event_tx                .send(AppEvent::SelectAgentThread(request.thread_id()));            return true;        }        if self.list_keymap.cancel.is_pressed(*key_event) {            self.cancel_current_request();            return true;        }        if let Some(idx) = self            .options            .iter()            .position(|opt| opt.shortcuts.iter().any(|s| s.is_press(*key_event)))        {            self.apply_selection(idx);            true        } else {            false        }    }}impl BottomPaneView for ApprovalOverlay {    fn handle_key_event(&mut self, key_event: KeyEvent) {        if self.try_handle_shortcut(&key_event) {            return;        }        self.list.handle_key_event(key_event);        if let Some(idx) = self.list.take_last_selected_index() {            self.apply_selection(idx);        }    }    fn on_ctrl_c(&mut self) -> CancellationEvent {        self.cancel_current_request();        CancellationEvent::Handled    }    fn is_complete(&self) -> bool {        self.done    }    fn try_consume_approval_request(        &mut self,        request: ApprovalRequest,    ) -> Option<ApprovalRequest> {        self.enqueue_request(request);        None    }    fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) -> bool {        self.dismiss_resolved_request(request)    }    fn terminal_title_requires_action(&self) -> bool {        true    }}impl Renderable for ApprovalOverlay {    fn desired_height(&self, width: u16) -> u16 {        self.list.desired_height(width)    }    fn render(&self, area: Rect, buf: &mut Buffer) {        self.list.render(area, buf);    }    fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {        self.list.cursor_pos(area)    }}fn approval_footer_hint(    request: &ApprovalRequest,    approval_keymap: &ApprovalKeymap,    list_keymap: &ListKeymap,) -> Line<'static> {    let mut spans = accept_cancel_hint_line(        primary_binding(&list_keymap.accept),        "to confirm",        primary_binding(&list_keymap.cancel),        "to cancel",    )    .spans;    if request.thread_label().is_some()        && let Some(open_thread) = primary_binding(&approval_keymap.open_thread)    {        if !spans.is_empty() {            spans.push(" or ".into());        } else {            spans.push("Press ".into());        }        spans.extend([open_thread.into(), " to open thread".into()]);    }    Line::from(spans)}fn network_approval_target(    network_approval_context: &NetworkApprovalContext,    command: &[String],) -> String {    if let Some(target) = network_approval_command_target(command) {        return target.to_string();    }    let scheme = match network_approval_context.protocol {        NetworkApprovalProtocol::Http => "http",        NetworkApprovalProtocol::Https => "https",        NetworkApprovalProtocol::Socks5Tcp => "socks5-tcp",        NetworkApprovalProtocol::Socks5Udp => "socks5-udp",    };    format!("{scheme}://{}", network_approval_context.host)}fn network_approval_command_target(command: &[String]) -> Option<&str> {    match command {        [program, target] if program == "network-access" && !target.is_empty() => {            Some(target.as_str())        }        [command] => command            .strip_prefix("network-access ")            .filter(|target| !target.is_empty()),        _ => None,    }}fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {    match request {        ApprovalRequest::Exec {            thread_label,            reason,            command,            network_approval_context,            additional_permissions,            ..        } => {            let mut header: Vec<Line<'static>> = Vec::new();            if let Some(thread_label) = thread_label {                header.push(Line::from(vec![                    "Thread: ".into(),                    thread_label.clone().bold(),                ]));                header.push(Line::from(""));            }            if let Some(reason) = reason {                header.push(Line::from(vec!["Reason: ".into(), reason.clone().italic()]));                header.push(Line::from(""));            }            if let Some(additional_permissions) = additional_permissions                && let Some(rule_line) = format_additional_permissions_rule(additional_permissions)            {                header.push(Line::from(vec![                    "Permission rule: ".into(),                    rule_line.cyan(),                ]));                header.push(Line::from(""));            }            let full_cmd = strip_bash_lc_and_escape(command);            let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd);            if let Some(first) = full_cmd_lines.first_mut() {                first.spans.insert(0, Span::from("$ "));            }            if network_approval_context.is_none() {                header.extend(full_cmd_lines);            }            Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))        }        ApprovalRequest::Permissions {            thread_label,            environment_id,            reason,            permissions,            ..        } => {            let mut header: Vec<Line<'static>> = Vec::new();            if let Some(thread_label) = thread_label {                header.push(Line::from(vec![                    "Thread: ".into(),                    thread_label.clone().bold(),                ]));                header.push(Line::from(""));            }            if let Some(environment_id) = environment_id {                header.push(Line::from(vec![                    "Environment: ".into(),                    environment_id.clone().bold(),                ]));                header.push(Line::from(""));            }            if let Some(reason) = reason {                header.push(Line::from(vec!["Reason: ".into(), reason.clone().italic()]));                header.push(Line::from(""));            }            if let Some(rule_line) = format_requested_permissions_rule(permissions) {                header.push(Line::from(vec![                    "Permission rule: ".into(),                    rule_line.cyan(),                ]));            }            Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))        }        ApprovalRequest::ApplyPatch {            thread_label,            reason,            ..        } => {            let mut header: Vec<Box<dyn Renderable>> = Vec::new();            if let Some(thread_label) = thread_label {                header.push(Box::new(Line::from(vec![                    "Thread: ".into(),                    thread_label.clone().bold(),                ])));            }            if let Some(reason) = reason                && !reason.is_empty()            {                if !header.is_empty() {                    header.push(Box::new(Line::from("")));                }                header.push(Box::new(                    Paragraph::new(Line::from_iter([                        "Reason: ".into(),                        reason.clone().italic(),                    ]))                    .wrap(Wrap { trim: false }),                ));            }            Box::new(ColumnRenderable::with(header))        }        ApprovalRequest::McpElicitation {            thread_label,            server_name,            message,            ..        } => {            let mut lines = Vec::new();            if let Some(thread_label) = thread_label {                lines.push(Line::from(vec![                    "Thread: ".into(),                    thread_label.clone().bold(),                ]));                lines.push(Line::from(""));            }            lines.extend([                Line::from(vec!["Server: ".into(), server_name.clone().bold()]),                Line::from(""),                Line::from(message.clone()),            ]);            let header = Paragraph::new(lines).wrap(Wrap { trim: false });            Box::new(header)        }    }}#[derive(Clone)]enum ApprovalDecision {    Command(CommandExecutionApprovalDecision),    FileChange(FileChangeApprovalDecision),    Permissions(PermissionsDecision),    McpElicitation(McpServerElicitationAction),}#[derive(Clone, Copy)]enum PermissionsDecision {    GrantForTurn,    GrantForTurnWithStrictAutoReview,    GrantForSession,    Deny,}#[derive(Clone)]struct ApprovalOption {    label: String,    decision: ApprovalDecision,    shortcuts: Vec<KeyBinding>,}fn command_decision_to_review_decision(    decision: &CommandExecutionApprovalDecision,) -> ReviewDecision {    match decision {        CommandExecutionApprovalDecision::Accept => ReviewDecision::Approved,        CommandExecutionApprovalDecision::AcceptForSession => ReviewDecision::ApprovedForSession,        CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {            execpolicy_amendment,        } => ReviewDecision::ApprovedExecpolicyAmendment {            proposed_execpolicy_amendment: execpolicy_amendment.clone().into_core(),        },        CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {            network_policy_amendment,        } => ReviewDecision::NetworkPolicyAmendment {            network_policy_amendment: network_policy_amendment.clone().into_core(),        },        CommandExecutionApprovalDecision::Decline => ReviewDecision::Denied,        CommandExecutionApprovalDecision::Cancel => ReviewDecision::Abort,    }}fn exec_options(    available_decisions: &[CommandExecutionApprovalDecision],    network_approval_context: Option<&NetworkApprovalContext>,    additional_permissions: Option<&AdditionalPermissionProfile>,    keymap: &ApprovalKeymap,) -> Vec<ApprovalOption> {    available_decisions        .iter()        .filter_map(|decision| match decision {            CommandExecutionApprovalDecision::Accept => Some(ApprovalOption {                label: if network_approval_context.is_some() {                    "Yes, just this once".to_string()                } else {                    "Yes, proceed".to_string()                },                decision: ApprovalDecision::Command(CommandExecutionApprovalDecision::Accept),                shortcuts: keymap.approve.clone(),            }),            CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {                execpolicy_amendment,            } => {                let rendered_prefix = strip_bash_lc_and_escape(&execpolicy_amendment.command);                if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') {                    return None;                }                Some(ApprovalOption {                    label: format!(                        "Yes, and don't ask again for commands that start with `{rendered_prefix}`"                    ),                    decision: ApprovalDecision::Command(                        CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {                            execpolicy_amendment: execpolicy_amendment.clone(),                        },                    ),                    shortcuts: keymap.approve_for_prefix.clone(),                })            }            CommandExecutionApprovalDecision::AcceptForSession => Some(ApprovalOption {                label: if network_approval_context.is_some() {                    "Yes, and allow this host for this conversation".to_string()                } else if additional_permissions.is_some() {                    "Yes, and allow these permissions for this session".to_string()                } else {                    "Yes, and don't ask again for this command in this session".to_string()                },                decision: ApprovalDecision::Command(                    CommandExecutionApprovalDecision::AcceptForSession,                ),                shortcuts: keymap.approve_for_session.clone(),            }),            CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                network_policy_amendment,            } => {                let (label, shortcuts) = match network_policy_amendment.action {                    NetworkPolicyRuleAction::Allow => (                        "Yes, and allow this host in the future".to_string(),                        keymap.approve_for_prefix.clone(),                    ),                    NetworkPolicyRuleAction::Deny => (                        "No, and block this host in the future".to_string(),                        keymap.deny.clone(),                    ),                };                Some(ApprovalOption {                    label,                    decision: ApprovalDecision::Command(                        CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                            network_policy_amendment: network_policy_amendment.clone(),                        },                    ),                    shortcuts,                })            }            CommandExecutionApprovalDecision::Decline => Some(ApprovalOption {                label: "No, continue without running it".to_string(),                decision: ApprovalDecision::Command(CommandExecutionApprovalDecision::Decline),                shortcuts: keymap.deny.clone(),            }),            CommandExecutionApprovalDecision::Cancel => Some(ApprovalOption {                label: "No, and tell Codex what to do differently".to_string(),                decision: ApprovalDecision::Command(CommandExecutionApprovalDecision::Cancel),                shortcuts: keymap.decline.clone(),            }),        })        .collect()}pub(crate) fn format_additional_permissions_rule(    additional_permissions: &AdditionalPermissionProfile,) -> Option<String> {    let mut parts = Vec::new();    if additional_permissions        .network        .as_ref()        .and_then(|network| network.enabled)        .unwrap_or(false)    {        parts.push("network".to_string());    }    if let Some(file_system) = additional_permissions.file_system.as_ref() {        let reads = format_file_system_entry_paths(            file_system                .entries                .iter()                .flatten()                .filter(|entry| entry.access == FileSystemAccessMode::Read),        );        if !reads.is_empty() {            parts.push(format!("read {reads}"));        }        let writes = format_file_system_entry_paths(            file_system                .entries                .iter()                .flatten()                .filter(|entry| entry.access == FileSystemAccessMode::Write),        );        if !writes.is_empty() {            parts.push(format!("write {writes}"));        }        let denied_reads = format_file_system_entry_paths(            file_system                .entries                .iter()                .flatten()                .filter(|entry| entry.access == FileSystemAccessMode::Deny),        );        if !denied_reads.is_empty() {            parts.push(format!("deny read {denied_reads}"));        }    }    if parts.is_empty() {        None    } else {        Some(parts.join("; "))    }}pub(crate) fn format_requested_permissions_rule(    permissions: &RequestPermissionProfile,) -> Option<String> {    let permissions =        crate::app_server_approval_conversions::granted_permission_profile_from_request(            permissions.clone(),        );    format_additional_permissions_rule(&AdditionalPermissionProfile {        network: permissions.network,        file_system: permissions.file_system,    })}fn format_file_system_entry_paths<'a>(    entries: impl Iterator<Item = &'a FileSystemSandboxEntry>,) -> String {    entries        .map(|entry| match &entry.path {            FileSystemPath::Path { path } => format!("`{path}`"),            FileSystemPath::GlobPattern { pattern } => format!("glob `{pattern}`"),            FileSystemPath::Special { value } => format!("`{}`", special_path_label(value)),        })        .collect::<Vec<_>>()        .join(", ")}fn special_path_label(value: &FileSystemSpecialPath) -> String {    match value {        FileSystemSpecialPath::Root => ":root".to_string(),        FileSystemSpecialPath::Minimal => ":minimal".to_string(),        FileSystemSpecialPath::ProjectRoots { subpath } => path_label(":workspace_roots", subpath),        FileSystemSpecialPath::Tmpdir => ":tmpdir".to_string(),        FileSystemSpecialPath::SlashTmp => "/tmp".to_string(),        FileSystemSpecialPath::Unknown { path, subpath } => path_label(path, subpath),    }}fn path_label(base: &str, subpath: &Option<PathBuf>) -> String {    match subpath {        Some(subpath) => format!("{base}/{}", subpath.display()),        None => base.to_string(),    }}fn patch_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption> {    vec![        ApprovalOption {            label: "Yes, proceed".to_string(),            decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::Accept),            shortcuts: keymap.approve.clone(),        },        ApprovalOption {            label: "Yes, and don't ask again for these files".to_string(),            decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::AcceptForSession),            shortcuts: keymap.approve_for_session.clone(),        },        ApprovalOption {            label: "No, and tell Codex what to do differently".to_string(),            decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::Cancel),            shortcuts: keymap.decline.clone(),        },    ]}fn permissions_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption> {    let deny_shortcuts = keymap        .deny        .iter()        .copied()        .filter(|shortcut| shortcut.parts() != (KeyCode::Esc, KeyModifiers::NONE))        .collect();    vec![        ApprovalOption {            label: "Yes, grant these permissions for this turn".to_string(),            decision: ApprovalDecision::Permissions(PermissionsDecision::GrantForTurn),            shortcuts: keymap.approve.clone(),        },        ApprovalOption {            label: "Yes, grant for this turn with strict auto review".to_string(),            decision: ApprovalDecision::Permissions(                PermissionsDecision::GrantForTurnWithStrictAutoReview,            ),            shortcuts: vec![key_hint::plain(KeyCode::Char('r'))],        },        ApprovalOption {            label: "Yes, grant these permissions for this session".to_string(),            decision: ApprovalDecision::Permissions(PermissionsDecision::GrantForSession),            shortcuts: keymap.approve_for_session.clone(),        },        ApprovalOption {            label: "No, continue without permissions".to_string(),            decision: ApprovalDecision::Permissions(PermissionsDecision::Deny),            shortcuts: deny_shortcuts,        },    ]}/// Build MCP elicitation options with stable cancellation semantics.////// `Esc` is always treated as cancel for elicitation prompts, even if users/// customize `decline`/`cancel` bindings. We keep this as a hard contract so/// dismissal remains a safe abort path and never silently maps to "continue/// without requested info." Any decline/cancel overlap is removed from the/// decline option in elicitation mode to preserve this invariant.fn elicitation_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption> {    let mut cancel_shortcuts = vec![key_hint::plain(KeyCode::Esc)];    for shortcut in &keymap.cancel {        if !cancel_shortcuts.contains(shortcut) {            cancel_shortcuts.push(*shortcut);        }    }    let decline_shortcuts: Vec<KeyBinding> = keymap        .decline        .iter()        .copied()        .filter(|shortcut| !cancel_shortcuts.contains(shortcut))        .collect();    vec![        ApprovalOption {            label: "Yes, provide the requested info".to_string(),            decision: ApprovalDecision::McpElicitation(McpServerElicitationAction::Accept),            shortcuts: keymap.approve.clone(),        },        ApprovalOption {            label: "No, but continue without it".to_string(),            decision: ApprovalDecision::McpElicitation(McpServerElicitationAction::Decline),            shortcuts: decline_shortcuts,        },        ApprovalOption {            label: "Cancel this request".to_string(),            decision: ApprovalDecision::McpElicitation(McpServerElicitationAction::Cancel),            shortcuts: cancel_shortcuts,        },    ]}#[cfg(test)]mod tests {    use super::*;    use crate::app_event::AppEvent;    use codex_app_server_protocol::AdditionalFileSystemPermissions;    use codex_app_server_protocol::AdditionalNetworkPermissions;    use codex_app_server_protocol::ExecPolicyAmendment;    use codex_app_server_protocol::NetworkApprovalProtocol;    use codex_app_server_protocol::NetworkPolicyAmendment;    use codex_protocol::models::FileSystemPermissions;    use codex_protocol::models::NetworkPermissions;    use codex_utils_absolute_path::AbsolutePathBuf;    use crossterm::event::KeyModifiers;    use insta::assert_snapshot;    use pretty_assertions::assert_eq;    use tokio::sync::mpsc::unbounded_channel;    fn absolute_path(path: &str) -> AbsolutePathBuf {        AbsolutePathBuf::from_absolute_path(path).expect("absolute path")    }    fn render_overlay_lines(view: &ApprovalOverlay, width: u16) -> String {        let height = view.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        view.render(Rect::new(0, 0, width, height), &mut buf);        (0..buf.area.height)            .map(|row| {                (0..buf.area.width)                    .map(|col| buf[(col, row)].symbol().to_string())                    .collect::<String>()                    .trim_end()                    .to_string()            })            .collect::<Vec<_>>()            .join("\n")    }    fn render_history_cell_lines(        cell: &dyn crate::history_cell::HistoryCell,        width: u16,    ) -> Vec<String> {        cell.display_lines(width)            .iter()            .map(|line| {                line.spans                    .iter()                    .map(|span| span.content.as_ref())                    .collect::<String>()            })            .collect()    }    fn normalize_snapshot_paths(rendered: String) -> String {        [            (absolute_path("/tmp/readme.txt"), "/tmp/readme.txt"),            (absolute_path("/tmp/out.txt"), "/tmp/out.txt"),        ]        .into_iter()        .fold(rendered, |rendered, (path, normalized)| {            rendered.replace(&path.display().to_string(), normalized)        })    }    fn make_overlay(        request: ApprovalRequest,        app_event_tx: AppEventSender,        features: Features,    ) -> ApprovalOverlay {        let keymap = crate::keymap::RuntimeKeymap::defaults();        make_overlay_with_keymap(            request,            app_event_tx,            features,            keymap.approval,            keymap.list,        )    }    fn make_overlay_with_keymap(        request: ApprovalRequest,        app_event_tx: AppEventSender,        features: Features,        approval_keymap: ApprovalKeymap,        list_keymap: ListKeymap,    ) -> ApprovalOverlay {        ApprovalOverlay::new(            request,            app_event_tx,            features,            approval_keymap,            list_keymap,        )    }    fn make_exec_request() -> ApprovalRequest {        ApprovalRequest::Exec {            thread_id: ThreadId::new(),            thread_label: None,            id: "test".to_string(),            command: vec!["echo".to_string(), "hi".to_string()],            reason: Some("reason".to_string()),            available_decisions: vec![                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::Cancel,            ],            network_approval_context: None,            additional_permissions: None,        }    }    fn make_permissions_request() -> ApprovalRequest {        ApprovalRequest::Permissions {            thread_id: ThreadId::new(),            thread_label: None,            call_id: "test".to_string(),            environment_id: None,            reason: Some("need workspace access".to_string()),            permissions: RequestPermissionProfile {                network: Some(NetworkPermissions {                    enabled: Some(true),                }),                file_system: Some(FileSystemPermissions::from_read_write_roots(                    Some(vec![absolute_path("/tmp/readme.txt")]),                    Some(vec![absolute_path("/tmp/out.txt")]),                )),            },        }    }    fn make_elicitation_request() -> ApprovalRequest {        ApprovalRequest::McpElicitation {            thread_id: ThreadId::new(),            thread_label: None,            server_name: "test-server".to_string(),            request_id: RequestId::String("request-1".to_string()),            message: "Need more information".to_string(),        }    }    #[test]    fn ctrl_c_aborts_and_clears_queue() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(make_exec_request(), tx, Features::with_defaults());        view.enqueue_request(make_exec_request());        assert_eq!(CancellationEvent::Handled, view.on_ctrl_c());        assert!(view.queue.is_empty());        assert!(view.is_complete());    }    #[test]    fn configured_list_cancel_aborts_exec_approval() {        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut keymap = crate::keymap::RuntimeKeymap::defaults();        keymap.list.cancel = vec![key_hint::plain(KeyCode::Char('q'))];        let mut view = make_overlay_with_keymap(            make_exec_request(),            tx,            Features::with_defaults(),            keymap.approval,            keymap.list,        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE));        assert!(view.is_complete());        let mut decision = None;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ExecApproval { decision: d, .. },                ..            } = ev            {                decision = Some(d);                break;            }        }        assert_eq!(decision, Some(CommandExecutionApprovalDecision::Cancel));    }    #[test]    fn configured_list_cancel_cancels_mcp_elicitation() {        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut keymap = crate::keymap::RuntimeKeymap::defaults();        keymap.list.cancel = vec![key_hint::plain(KeyCode::Char('q'))];        let mut view = make_overlay_with_keymap(            make_elicitation_request(),            tx,            Features::with_defaults(),            keymap.approval,            keymap.list,        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE));        assert!(view.is_complete());        let mut decision = None;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ResolveElicitation { decision: d, .. },                ..            } = ev            {                decision = Some(d);                break;            }        }        assert_eq!(decision, Some(McpServerElicitationAction::Cancel));    }    #[test]    fn shortcut_triggers_selection() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(make_exec_request(), tx, Features::with_defaults());        assert!(!view.is_complete());        view.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));        // We expect at least one thread-scoped approval op message in the queue.        let mut saw_op = false;        while let Ok(ev) = rx.try_recv() {            if matches!(ev, AppEvent::SubmitThreadOp { .. }) {                saw_op = true;                break;            }        }        assert!(saw_op, "expected approval decision to emit an op");    }    #[test]    fn deny_shortcut_submits_denied_exec_decision() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(            ApprovalRequest::Exec {                thread_id: ThreadId::new(),                thread_label: None,                id: "test".to_string(),                command: vec!["echo".to_string(), "hi".to_string()],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::Decline,                ],                network_approval_context: None,                additional_permissions: None,            },            tx,            Features::with_defaults(),        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        let mut saw_denied = false;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ExecApproval { decision, .. },                ..            } = ev            {                assert_eq!(decision, CommandExecutionApprovalDecision::Decline);                saw_denied = true;                break;            }        }        assert!(saw_denied, "expected deny shortcut to emit denied decision");    }    #[test]    fn network_deny_shortcut_submits_policy_deny_decision() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let amendment = NetworkPolicyAmendment {            host: "example.com".to_string(),            action: NetworkPolicyRuleAction::Deny,        };        let mut view = make_overlay(            ApprovalRequest::Exec {                thread_id: ThreadId::new(),                thread_label: None,                id: "test".to_string(),                command: vec!["curl".to_string(), "https://example.com".to_string()],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                        network_policy_amendment: amendment.clone(),                    },                ],                network_approval_context: Some(NetworkApprovalContext {                    host: "example.com".to_string(),                    protocol: NetworkApprovalProtocol::Https,                }),                additional_permissions: None,            },            tx,            Features::with_defaults(),        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        let mut saw_deny = false;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ExecApproval { decision, .. },                ..            } = ev            {                assert_eq!(                    decision,                    CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                        network_policy_amendment: amendment                    }                );                saw_deny = true;                break;            }        }        assert!(            saw_deny,            "expected deny shortcut to emit network policy deny decision"        );    }    #[test]    fn resolved_request_dismisses_overlay_without_emitting_abort() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(make_exec_request(), tx, Features::with_defaults());        assert!(            view.dismiss_app_server_request(&ResolvedAppServerRequest::ExecApproval {                id: "test".to_string(),            })        );        assert!(            view.is_complete(),            "resolved request should close the overlay"        );        assert!(            rx.try_recv().is_err(),            "dismissing a stale request should not emit an approval op"        );    }    #[test]    fn o_opens_source_thread_for_cross_thread_approval() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let thread_id = ThreadId::new();        let mut view = make_overlay(            ApprovalRequest::Exec {                thread_id,                thread_label: Some("Robie [explorer]".to_string()),                id: "test".to_string(),                command: vec!["echo".to_string(), "hi".to_string()],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::Cancel,                ],                network_approval_context: None,                additional_permissions: None,            },            tx,            Features::with_defaults(),        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE));        let event = rx.try_recv().expect("expected select-agent-thread event");        assert_eq!(            matches!(event, AppEvent::SelectAgentThread(id) if id == thread_id),            true        );    }    #[test]    fn configured_open_thread_shortcut_opens_source_thread() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let thread_id = ThreadId::new();        let mut keymap = crate::keymap::RuntimeKeymap::defaults();        keymap.approval.open_thread = vec![key_hint::plain(KeyCode::Char('x'))];        let mut view = make_overlay_with_keymap(            ApprovalRequest::Exec {                thread_id,                thread_label: Some("Robie [explorer]".to_string()),                id: "test".to_string(),                command: vec!["echo".to_string(), "hi".to_string()],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::Cancel,                ],                network_approval_context: None,                additional_permissions: None,            },            tx,            Features::with_defaults(),            keymap.approval,            keymap.list,        );        view.handle_key_event(KeyEvent::new(            KeyCode::Char('o'),            /*modifiers*/ KeyModifiers::NONE,        ));        assert!(rx.try_recv().is_err());        view.handle_key_event(KeyEvent::new(            KeyCode::Char('x'),            /*modifiers*/ KeyModifiers::NONE,        ));        let event = rx.try_recv().expect("expected select-agent-thread event");        assert!(matches!(event, AppEvent::SelectAgentThread(id) if id == thread_id));    }    #[test]    fn cross_thread_footer_hint_mentions_o_shortcut() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let view = make_overlay(            ApprovalRequest::Exec {                thread_id: ThreadId::new(),                thread_label: Some("Robie [explorer]".to_string()),                id: "test".to_string(),                command: vec!["echo".to_string(), "hi".to_string()],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::Cancel,                ],                network_approval_context: None,                additional_permissions: None,            },            tx,            Features::with_defaults(),        );        assert_snapshot!(            "approval_overlay_cross_thread_prompt",            render_overlay_lines(&view, /*width*/ 80)        );    }    #[test]    fn exec_prefix_option_emits_execpolicy_amendment() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(            ApprovalRequest::Exec {                thread_id: ThreadId::new(),                thread_label: None,                id: "test".to_string(),                command: vec!["echo".to_string()],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {                        execpolicy_amendment: ExecPolicyAmendment {                            command: vec!["echo".to_string()],                        },                    },                    CommandExecutionApprovalDecision::Cancel,                ],                network_approval_context: None,                additional_permissions: None,            },            tx,            Features::with_defaults(),        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE));        let mut saw_op = false;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ExecApproval { decision, .. },                ..            } = ev            {                assert_eq!(                    decision,                    CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {                        execpolicy_amendment: ExecPolicyAmendment {                            command: vec!["echo".to_string()],                        }                    }                );                saw_op = true;                break;            }        }        assert!(            saw_op,            "expected approval decision to emit an op with command prefix"        );    }    #[test]    fn network_deny_forever_shortcut_is_not_bound() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(            ApprovalRequest::Exec {                thread_id: ThreadId::new(),                thread_label: None,                id: "test".to_string(),                command: vec!["curl".to_string(), "https://example.com".to_string()],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::AcceptForSession,                    CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                        network_policy_amendment: NetworkPolicyAmendment {                            host: "example.com".to_string(),                            action: NetworkPolicyRuleAction::Allow,                        },                    },                    CommandExecutionApprovalDecision::Cancel,                ],                network_approval_context: Some(NetworkApprovalContext {                    host: "example.com".to_string(),                    protocol: NetworkApprovalProtocol::Https,                }),                additional_permissions: None,            },            tx,            Features::with_defaults(),        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        assert!(            rx.try_recv().is_err(),            "unexpected approval event emitted for hidden network deny shortcut"        );    }    #[test]    fn header_includes_command_snippet() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let command = vec!["echo".into(), "hello".into(), "world".into()];        let exec_request = ApprovalRequest::Exec {            thread_id: ThreadId::new(),            thread_label: None,            id: "test".into(),            command,            reason: None,            available_decisions: vec![                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::Cancel,            ],            network_approval_context: None,            additional_permissions: None,        };        let view = make_overlay(exec_request, tx, Features::with_defaults());        let mut buf = Buffer::empty(Rect::new(0, 0, 80, view.desired_height(/*width*/ 80)));        view.render(            Rect::new(0, 0, 80, view.desired_height(/*width*/ 80)),            &mut buf,        );        let rendered: Vec<String> = (0..buf.area.height)            .map(|row| {                (0..buf.area.width)                    .map(|col| buf[(col, row)].symbol().to_string())                    .collect()            })            .collect();        assert!(            rendered                .iter()                .any(|line| line.contains("echo hello world")),            "expected header to include command snippet, got {rendered:?}"        );    }    #[test]    fn network_exec_options_use_expected_labels_and_hide_execpolicy_amendment() {        let network_context = NetworkApprovalContext {            host: "example.com".to_string(),            protocol: NetworkApprovalProtocol::Https,        };        let keymap = crate::keymap::RuntimeKeymap::defaults();        let options = exec_options(            &[                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::AcceptForSession,                CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                    network_policy_amendment: NetworkPolicyAmendment {                        host: "example.com".to_string(),                        action: NetworkPolicyRuleAction::Allow,                    },                },                CommandExecutionApprovalDecision::Cancel,            ],            Some(&network_context),            /*additional_permissions*/ None,            &keymap.approval,        );        let labels: Vec<String> = options.into_iter().map(|option| option.label).collect();        assert_eq!(            labels,            vec![                "Yes, just this once".to_string(),                "Yes, and allow this host for this conversation".to_string(),                "Yes, and allow this host in the future".to_string(),                "No, and tell Codex what to do differently".to_string(),            ]        );    }    #[test]    fn generic_exec_options_can_offer_allow_for_session() {        let keymap = crate::keymap::RuntimeKeymap::defaults();        let options = exec_options(            &[                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::AcceptForSession,                CommandExecutionApprovalDecision::Cancel,            ],            /*network_approval_context*/ None,            /*additional_permissions*/ None,            &keymap.approval,        );        let labels: Vec<String> = options.into_iter().map(|option| option.label).collect();        assert_eq!(            labels,            vec![                "Yes, proceed".to_string(),                "Yes, and don't ask again for this command in this session".to_string(),                "No, and tell Codex what to do differently".to_string(),            ]        );    }    #[test]    fn additional_permissions_exec_options_hide_execpolicy_amendment() {        let keymap = crate::keymap::RuntimeKeymap::defaults();        let additional_permissions = AdditionalPermissionProfile {            network: None,            file_system: Some(                FileSystemPermissions::from_read_write_roots(                    Some(vec![absolute_path("/tmp/readme.txt")]),                    Some(vec![absolute_path("/tmp/out.txt")]),                )                .into(),            ),        };        let options = exec_options(            &[                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::Cancel,            ],            /*network_approval_context*/ None,            Some(&additional_permissions),            &keymap.approval,        );        let labels: Vec<String> = options.into_iter().map(|option| option.label).collect();        assert_eq!(            labels,            vec![                "Yes, proceed".to_string(),                "No, and tell Codex what to do differently".to_string(),            ]        );    }    #[test]    fn permissions_options_use_expected_labels() {        let keymap = crate::keymap::RuntimeKeymap::defaults();        let labels: Vec<String> = permissions_options(&keymap.approval)            .into_iter()            .map(|option| option.label)            .collect();        assert_eq!(            labels,            vec![                "Yes, grant these permissions for this turn".to_string(),                "Yes, grant for this turn with strict auto review".to_string(),                "Yes, grant these permissions for this session".to_string(),                "No, continue without permissions".to_string(),            ]        );    }    #[test]    fn additional_permissions_rule_shows_non_path_file_system_entries() {        let additional_permissions = AdditionalPermissionProfile {            network: None,            file_system: Some(AdditionalFileSystemPermissions {                read: None,                write: None,                entries: Some(vec![                    FileSystemSandboxEntry {                        path: FileSystemPath::Special {                            value: FileSystemSpecialPath::Root,                        },                        access: FileSystemAccessMode::Write,                    },                    FileSystemSandboxEntry {                        path: FileSystemPath::GlobPattern {                            pattern: "**/*.env".to_string(),                        },                        access: FileSystemAccessMode::Deny,                    },                ]),                glob_scan_max_depth: None,            }),        };        assert_eq!(            format_additional_permissions_rule(&additional_permissions),            Some("write `:root`; deny read glob `**/*.env`".to_string())        );    }    #[test]    fn additional_permissions_rule_uses_workspace_roots_label() {        let additional_permissions = AdditionalPermissionProfile {            network: None,            file_system: Some(AdditionalFileSystemPermissions {                read: None,                write: None,                entries: Some(vec![FileSystemSandboxEntry {                    path: FileSystemPath::Special {                        value: FileSystemSpecialPath::ProjectRoots {                            subpath: Some(".git".into()),                        },                    },                    access: FileSystemAccessMode::Read,                }]),                glob_scan_max_depth: None,            }),        };        assert_eq!(            format_additional_permissions_rule(&additional_permissions),            Some("read `:workspace_roots/.git`".to_string())        );    }    #[test]    fn permissions_session_shortcut_submits_session_scope() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(make_permissions_request(), tx, Features::with_defaults());        view.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));        let mut saw_op = false;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::RequestPermissionsResponse { response, .. },                ..            } = ev            {                assert_eq!(response.scope, PermissionGrantScope::Session);                saw_op = true;                break;            }        }        assert!(            saw_op,            "expected permission approval decision to emit a session-scoped response"        );    }    #[test]    fn permissions_deny_shortcut_uses_deny_keymap() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut keymap = crate::keymap::RuntimeKeymap::defaults();        keymap.approval.deny = vec![key_hint::plain(KeyCode::Char('x'))];        keymap.approval.decline = Vec::new();        let mut view = make_overlay_with_keymap(            make_permissions_request(),            tx,            Features::with_defaults(),            keymap.approval,            keymap.list,        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));        let mut saw_op = false;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::RequestPermissionsResponse { response, .. },                ..            } = ev            {                assert!(response.permissions.is_empty());                assert_eq!(response.scope, PermissionGrantScope::Turn);                assert!(!response.strict_auto_review);                saw_op = true;                break;            }        }        assert!(            saw_op,            "expected permission deny shortcut to emit an empty permission response"        );    }    #[test]    fn permissions_strict_auto_review_shortcut_submits_turn_scope_with_strict_review() {        let (tx, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut view = make_overlay(make_permissions_request(), tx, Features::with_defaults());        view.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));        let mut saw_op = false;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::RequestPermissionsResponse { response, .. },                ..            } = ev            {                assert_eq!(response.scope, PermissionGrantScope::Turn);                assert!(response.strict_auto_review);                saw_op = true;                break;            }        }        assert!(            saw_op,            "expected permission approval decision to emit a strict auto review response"        );    }    #[test]    fn additional_permissions_prompt_shows_permission_rule_line() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let exec_request = ApprovalRequest::Exec {            thread_id: ThreadId::new(),            thread_label: None,            id: "test".into(),            command: vec!["cat".into(), "/tmp/readme.txt".into()],            reason: None,            available_decisions: vec![                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::Cancel,            ],            network_approval_context: None,            additional_permissions: Some(AdditionalPermissionProfile {                network: Some(AdditionalNetworkPermissions {                    enabled: Some(true),                }),                file_system: Some(                    FileSystemPermissions::from_read_write_roots(                        Some(vec![absolute_path("/tmp/readme.txt")]),                        Some(vec![absolute_path("/tmp/out.txt")]),                    )                    .into(),                ),            }),        };        let view = make_overlay(exec_request, tx, Features::with_defaults());        let mut buf = Buffer::empty(Rect::new(0, 0, 100, view.desired_height(/*width*/ 100)));        view.render(            Rect::new(0, 0, 100, view.desired_height(/*width*/ 100)),            &mut buf,        );        let rendered: Vec<String> = (0..buf.area.height)            .map(|row| {                (0..buf.area.width)                    .map(|col| buf[(col, row)].symbol().to_string())                    .collect()            })            .collect();        assert!(            rendered                .iter()                .any(|line| line.contains("Permission rule:")),            "expected permission-rule line, got {rendered:?}"        );        assert!(            rendered.iter().any(|line| line.contains("network;")),            "expected network permission text, got {rendered:?}"        );    }    #[test]    fn additional_permissions_prompt_snapshot() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let exec_request = ApprovalRequest::Exec {            thread_id: ThreadId::new(),            thread_label: None,            id: "test".into(),            command: vec!["cat".into(), "/tmp/readme.txt".into()],            reason: Some("need filesystem access".into()),            available_decisions: vec![                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::Cancel,            ],            network_approval_context: None,            additional_permissions: Some(AdditionalPermissionProfile {                network: Some(AdditionalNetworkPermissions {                    enabled: Some(true),                }),                file_system: Some(                    FileSystemPermissions::from_read_write_roots(                        Some(vec![absolute_path("/tmp/readme.txt")]),                        Some(vec![absolute_path("/tmp/out.txt")]),                    )                    .into(),                ),            }),        };        let view = make_overlay(exec_request, tx, Features::with_defaults());        assert_snapshot!(            "approval_overlay_additional_permissions_prompt",            normalize_snapshot_paths(render_overlay_lines(&view, /*width*/ 120))        );    }    #[test]    fn permissions_prompt_snapshot() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let view = make_overlay(make_permissions_request(), tx, Features::with_defaults());        assert_snapshot!(            "approval_overlay_permissions_prompt",            normalize_snapshot_paths(render_overlay_lines(&view, /*width*/ 120))        );    }    #[test]    fn apply_patch_prompt_with_thread_label_omits_command_line() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let mut changes = HashMap::new();        changes.insert(            PathBuf::from("bug1.txt"),            FileChange::Add {                content: "one\ntwo\nthree\n".to_string(),            },        );        let request = ApprovalRequest::ApplyPatch {            thread_id: ThreadId::new(),            thread_label: Some("Banach [worker]".to_string()),            id: "test".to_string(),            reason: None,            cwd: absolute_path("/tmp"),            changes,        };        let keymap = crate::keymap::RuntimeKeymap::defaults();        let view = ApprovalOverlay::new(            request,            tx,            Features::with_defaults(),            keymap.approval,            keymap.list,        );        let rendered = render_overlay_lines(&view, /*width*/ 120);        assert!(rendered.contains("Thread: Banach [worker]"));        assert!(rendered.contains("o to open thread"));        assert!(!rendered.contains("$ apply_patch"));    }    #[test]    fn network_exec_prompt_title_includes_host() {        let (tx, _rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx);        let exec_request = ApprovalRequest::Exec {            thread_id: ThreadId::new(),            thread_label: None,            id: "test".into(),            command: vec!["curl".into(), "https://example.com".into()],            reason: Some("network request blocked".into()),            available_decisions: vec![                CommandExecutionApprovalDecision::Accept,                CommandExecutionApprovalDecision::AcceptForSession,                CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                    network_policy_amendment: NetworkPolicyAmendment {                        host: "example.com".to_string(),                        action: NetworkPolicyRuleAction::Allow,                    },                },                CommandExecutionApprovalDecision::Cancel,            ],            network_approval_context: Some(NetworkApprovalContext {                host: "example.com".to_string(),                protocol: NetworkApprovalProtocol::Https,            }),            additional_permissions: None,        };        let view = make_overlay(exec_request, tx, Features::with_defaults());        let mut buf = Buffer::empty(Rect::new(0, 0, 100, view.desired_height(/*width*/ 100)));        view.render(            Rect::new(0, 0, 100, view.desired_height(/*width*/ 100)),            &mut buf,        );        assert_snapshot!("network_exec_prompt", format!("{buf:?}"));        let rendered: Vec<String> = (0..buf.area.height)            .map(|row| {                (0..buf.area.width)                    .map(|col| buf[(col, row)].symbol().to_string())                    .collect()            })            .collect();        assert!(            rendered.iter().any(|line| {                line.contains("Do you want to approve network access to \"example.com\"?")            }),            "expected network title to include host, got {rendered:?}"        );        assert!(            !rendered.iter().any(|line| line.contains("$ curl")),            "network prompt should not show command line, got {rendered:?}"        );        assert!(            !rendered.iter().any(|line| line.contains("don't ask again")),            "network prompt should not show execpolicy option, got {rendered:?}"        );    }    #[test]    fn ctrl_shift_a_opens_fullscreen() {        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut view = make_overlay(make_exec_request(), tx, Features::with_defaults());        view.handle_key_event(KeyEvent::new(            KeyCode::Char('a'),            KeyModifiers::CONTROL | KeyModifiers::SHIFT,        ));        let mut saw_fullscreen = false;        while let Ok(ev) = rx.try_recv() {            if matches!(ev, AppEvent::FullScreenApprovalRequest(_)) {                saw_fullscreen = true;                break;            }        }        assert!(saw_fullscreen, "expected ctrl+shift+a to open fullscreen");    }    #[test]    fn exec_history_cell_wraps_with_two_space_indent() {        let command = vec![            "/bin/zsh".into(),            "-lc".into(),            "git add tui/src/render/mod.rs tui/src/render/renderable.rs".into(),        ];        let cell = history_cell::new_approval_decision_cell(            history_cell::ApprovalDecisionSubject::Command(command),            ReviewDecision::Approved,            history_cell::ApprovalDecisionActor::User,        );        let lines = cell.display_lines(/*width*/ 28);        let rendered: Vec<String> = lines            .iter()            .map(|line| {                line.spans                    .iter()                    .map(|span| span.content.as_ref())                    .collect::<String>()            })            .collect();        let expected = vec![            "✔ You approved codex to run".to_string(),            "  git add tui/src/render/".to_string(),            "  mod.rs tui/src/render/".to_string(),            "  renderable.rs this time".to_string(),        ];        assert_eq!(rendered, expected);    }    #[test]    fn exec_history_cell_does_not_render_blank_action_for_empty_command() {        let approved = history_cell::new_approval_decision_cell(            history_cell::ApprovalDecisionSubject::Command(Vec::new()),            ReviewDecision::Approved,            history_cell::ApprovalDecisionActor::User,        );        assert_eq!(            render_history_cell_lines(approved.as_ref(), /*width*/ 80),            vec!["✔ You approved this request this time".to_string()]        );        let approved_for_session = history_cell::new_approval_decision_cell(            history_cell::ApprovalDecisionSubject::Command(Vec::new()),            ReviewDecision::ApprovedForSession,            history_cell::ApprovalDecisionActor::User,        );        assert_eq!(            render_history_cell_lines(approved_for_session.as_ref(), /*width*/ 80),            vec!["✔ You approved this request every time this session".to_string()]        );    }    #[test]    fn network_access_command_history_uses_target_without_structured_context() {        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut view = make_overlay(            ApprovalRequest::Exec {                thread_id: ThreadId::new(),                thread_label: None,                id: "test".into(),                command: vec![                    "network-access".to_string(),                    "https://example.com:8443".to_string(),                ],                reason: None,                available_decisions: vec![                    CommandExecutionApprovalDecision::Accept,                    CommandExecutionApprovalDecision::Cancel,                ],                network_approval_context: None,                additional_permissions: None,            },            tx,            Features::with_defaults(),        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));        let mut decision = None;        while let Ok(event) = rx.try_recv() {            if let AppEvent::InsertHistoryCell(cell) = event {                decision = Some(cell);                break;            }        }        let decision = decision.expect("expected decision cell in history");        assert_eq!(            render_history_cell_lines(decision.as_ref(), /*width*/ 80),            vec![                "✔ You approved codex network access to https://example.com:8443 this time"                    .to_string(),            ]        );    }    #[test]    fn esc_cancels_mcp_elicitation() {        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut view = make_overlay(make_elicitation_request(), tx, Features::with_defaults());        view.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        let mut decision = None;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ResolveElicitation { decision: d, .. },                ..            } = ev            {                decision = Some(d);                break;            }        }        assert_eq!(decision, Some(McpServerElicitationAction::Cancel));    }    #[test]    fn esc_still_cancels_elicitation_with_custom_overlap() {        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut keymap = crate::keymap::RuntimeKeymap::defaults();        keymap.approval.decline = vec![            key_hint::plain(KeyCode::Esc),            key_hint::plain(KeyCode::Char('n')),        ];        keymap.approval.cancel = vec![key_hint::plain(KeyCode::Char('x'))];        let mut view = make_overlay_with_keymap(            make_elicitation_request(),            tx,            Features::with_defaults(),            keymap.approval,            keymap.list,        );        view.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        let mut esc_decision = None;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ResolveElicitation { decision, .. },                ..            } = ev            {                esc_decision = Some(decision);                break;            }        }        assert_eq!(esc_decision, Some(McpServerElicitationAction::Cancel));        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut keymap = crate::keymap::RuntimeKeymap::defaults();        keymap.approval.decline = vec![            key_hint::plain(KeyCode::Esc),            key_hint::plain(KeyCode::Char('n')),        ];        keymap.approval.cancel = vec![key_hint::plain(KeyCode::Char('x'))];        let mut view = make_overlay_with_keymap(            make_elicitation_request(),            tx,            Features::with_defaults(),            keymap.approval,            keymap.list,        );        view.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));        let mut n_decision = None;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ResolveElicitation { decision, .. },                ..            } = ev            {                n_decision = Some(decision);                break;            }        }        assert_eq!(n_decision, Some(McpServerElicitationAction::Decline));    }    #[test]    fn enter_sets_last_selected_index_without_dismissing() {        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let mut view = make_overlay(make_exec_request(), tx, Features::with_defaults());        view.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));        assert!(            view.is_complete(),            "exec approval should complete without queued requests"        );        let mut decision = None;        while let Ok(ev) = rx.try_recv() {            if let AppEvent::SubmitThreadOp {                op: Op::ExecApproval { decision: d, .. },                ..            } = ev            {                decision = Some(d);                break;            }        }        assert_eq!(decision, Some(CommandExecutionApprovalDecision::Accept));    }}