//! Interactive tool request surfaces for `ChatWidget`.//!//! This module owns approval, permission, elicitation, and user-input prompts//! that block on user decisions.use super::*;impl ChatWidget { pub(super) fn on_exec_approval_request(&mut self, _id: String, ev: ExecApprovalRequestEvent) { self.record_visible_turn_activity(); let ev2 = ev.clone(); self.defer_or_handle( |q| q.push_exec_approval(ev), |s| s.handle_exec_approval_now(ev2), ); } pub(super) fn on_apply_patch_approval_request( &mut self, _id: String, ev: ApplyPatchApprovalRequestEvent, ) { self.record_visible_turn_activity(); let ev2 = ev.clone(); self.defer_or_handle( |q| q.push_apply_patch_approval(ev), |s| s.handle_apply_patch_approval_now(ev2), ); } /// Handle guardian review lifecycle events for the current thread. /// /// In-progress assessments temporarily own the live status footer so the /// user can see what is being reviewed, including parallel review /// aggregation. Terminal assessments clear or update that footer state and /// render the final approved/denied history cell when guardian returns a /// decision. pub(super) fn on_guardian_assessment(&mut self, ev: GuardianAssessmentEvent) { let permission_request_summary = |subject: &str, reason: &Option<String>| { reason .as_deref() .map(str::trim) .filter(|reason| !reason.is_empty()) .map(|reason| format!("{subject}: {reason}")) .unwrap_or_else(|| subject.to_string()) }; let guardian_action_summary = |action: &GuardianAssessmentAction| match action { GuardianAssessmentAction::Command { command, .. } => Some(command.clone()), GuardianAssessmentAction::Execve { program, argv, .. } => { let command = if argv.is_empty() { vec![program.clone()] } else { argv.clone() }; shlex::try_join(command.iter().map(String::as_str)) .ok() .or_else(|| Some(command.join(" "))) } GuardianAssessmentAction::ApplyPatch { files, .. } => Some(if files.len() == 1 { format!("apply_patch touching {}", files[0].display()) } else { format!("apply_patch touching {} files", files.len()) }), GuardianAssessmentAction::NetworkAccess { target, .. } => { Some(format!("network access to {target}")) } GuardianAssessmentAction::McpToolCall { server, tool_name, connector_name, .. } => { let label = connector_name.as_deref().unwrap_or(server.as_str()); Some(format!("MCP {tool_name} on {label}")) } GuardianAssessmentAction::RequestPermissions { reason, .. } => { Some(permission_request_summary("permission request", reason)) } }; let guardian_command = |action: &GuardianAssessmentAction| match action { GuardianAssessmentAction::Command { command, .. } => shlex::split(command) .filter(|command| !command.is_empty()) .or_else(|| Some(vec![command.clone()])), GuardianAssessmentAction::Execve { program, argv, .. } => Some(if argv.is_empty() { vec![program.clone()] } else { argv.clone() }) .filter(|command| !command.is_empty()), GuardianAssessmentAction::ApplyPatch { .. } | GuardianAssessmentAction::NetworkAccess { .. } | GuardianAssessmentAction::McpToolCall { .. } | GuardianAssessmentAction::RequestPermissions { .. } => None, }; if ev.status == GuardianAssessmentStatus::InProgress && let Some(detail) = guardian_action_summary(&ev.action) { // In-progress assessments own the live footer state while the // review is pending. Parallel reviews are aggregated into one // footer summary by `PendingGuardianReviewStatus`. self.bottom_pane.ensure_status_indicator(); self.bottom_pane .set_interrupt_hint_visible(/*visible*/ true); self.status_state .pending_guardian_review_status .start_or_update(ev.id.clone(), detail); if let Some(status) = self .status_state .pending_guardian_review_status .status_indicator_state() { self.set_status( status.header, status.details, StatusDetailsCapitalization::Preserve, status.details_max_lines, ); } self.request_redraw(); return; } // Terminal assessments remove the matching pending footer entry first, // then render the final approved/denied history cell below. if self .status_state .pending_guardian_review_status .finish(&ev.id) { if let Some(status) = self .status_state .pending_guardian_review_status .status_indicator_state() { self.set_status( status.header, status.details, StatusDetailsCapitalization::Preserve, status.details_max_lines, ); } else if self.status_state.current_status.is_guardian_review() { self.set_status_header(String::from("Working")); } } else if self.status_state.pending_guardian_review_status.is_empty() && self.status_state.current_status.is_guardian_review() { self.set_status_header(String::from("Working")); } if ev.status == GuardianAssessmentStatus::Approved { let cell = if let Some(command) = guardian_command(&ev.action) { history_cell::new_approval_decision_cell( history_cell::ApprovalDecisionSubject::Command(command), crate::history_cell::ReviewDecision::Approved, history_cell::ApprovalDecisionActor::Guardian, ) } else if let Some(summary) = guardian_action_summary(&ev.action) { history_cell::new_guardian_approved_action_request(summary) } else { let summary = serde_json::to_string(&ev.action) .unwrap_or_else(|_| "<unrenderable guardian action>".to_string()); history_cell::new_guardian_approved_action_request(summary) }; self.add_boxed_history(cell); self.request_redraw(); return; } if ev.status == GuardianAssessmentStatus::TimedOut { let cell = if let Some(command) = guardian_command(&ev.action) { history_cell::new_approval_decision_cell( history_cell::ApprovalDecisionSubject::Command(command), crate::history_cell::ReviewDecision::TimedOut, history_cell::ApprovalDecisionActor::Guardian, ) } else { match &ev.action { GuardianAssessmentAction::ApplyPatch { files, .. } => { let files = files .iter() .map(|path| path.display().to_string()) .collect::<Vec<_>>(); history_cell::new_guardian_timed_out_patch_request(files) } GuardianAssessmentAction::McpToolCall { server, tool_name, .. } => history_cell::new_guardian_timed_out_action_request(format!( "codex could call MCP tool {server}.{tool_name}" )), GuardianAssessmentAction::NetworkAccess { target, .. } => { history_cell::new_guardian_timed_out_action_request(format!( "codex could access {target}" )) } GuardianAssessmentAction::RequestPermissions { reason, .. } => { history_cell::new_guardian_timed_out_action_request( permission_request_summary("codex could request permissions", reason), ) } GuardianAssessmentAction::Command { .. } => unreachable!(), GuardianAssessmentAction::Execve { .. } => unreachable!(), } }; self.add_boxed_history(cell); self.request_redraw(); return; } if ev.status != GuardianAssessmentStatus::Denied { return; } self.review.recent_auto_review_denials.push(ev.clone()); let cell = if let Some(command) = guardian_command(&ev.action) { history_cell::new_approval_decision_cell( history_cell::ApprovalDecisionSubject::Command(command), crate::history_cell::ReviewDecision::Denied, history_cell::ApprovalDecisionActor::Guardian, ) } else { match &ev.action { GuardianAssessmentAction::ApplyPatch { files, .. } => { let files = files .iter() .map(|path| path.display().to_string()) .collect::<Vec<_>>(); history_cell::new_guardian_denied_patch_request(files) } GuardianAssessmentAction::McpToolCall { server, tool_name, .. } => history_cell::new_guardian_denied_action_request(format!( "codex to call MCP tool {server}.{tool_name}" )), GuardianAssessmentAction::NetworkAccess { target, .. } => { history_cell::new_guardian_denied_action_request(format!( "codex to access {target}" )) } GuardianAssessmentAction::RequestPermissions { reason, .. } => { history_cell::new_guardian_denied_action_request(permission_request_summary( "codex to request permissions", reason, )) } GuardianAssessmentAction::Command { .. } => unreachable!(), GuardianAssessmentAction::Execve { .. } => unreachable!(), } }; self.add_boxed_history(cell); self.request_redraw(); } pub(super) fn on_elicitation_request( &mut self, request_id: AppServerRequestId, params: McpServerElicitationRequestParams, ) { self.record_visible_turn_activity(); let request_id2 = request_id.clone(); let params2 = params.clone(); self.defer_or_handle( |q| q.push_elicitation(request_id, params), |s| s.handle_elicitation_request_now(request_id2, params2), ); } pub(super) fn on_request_user_input(&mut self, ev: ToolRequestUserInputParams) { self.record_visible_turn_activity(); let ev2 = ev.clone(); self.defer_or_handle( |q| q.push_user_input(ev), |s| s.handle_request_user_input_now(ev2), ); } pub(super) fn on_request_permissions(&mut self, ev: RequestPermissionsEvent) { self.record_visible_turn_activity(); let ev2 = ev.clone(); self.defer_or_handle( |q| q.push_request_permissions(ev), |s| s.handle_request_permissions_now(ev2), ); } pub(crate) fn handle_exec_approval_now(&mut self, ev: ExecApprovalRequestEvent) { self.flush_answer_stream_with_separator(); let command = shlex::try_join(ev.command.iter().map(String::as_str)) .unwrap_or_else(|_| ev.command.join(" ")); self.notify(Notification::ExecApprovalRequested { command }); let available_decisions = ev.effective_available_decisions(); let request = ApprovalRequest::Exec { thread_id: self.thread_id.unwrap_or_default(), thread_label: None, id: ev.effective_approval_id(), command: ev.command, reason: ev.reason, available_decisions, network_approval_context: ev.network_approval_context, additional_permissions: ev.additional_permissions, }; self.bottom_pane .push_approval_request(request, &self.config.features); self.set_ambient_pet_notification( crate::pets::PetNotificationKind::Waiting, /*body*/ None, ); self.request_redraw(); } pub(crate) fn handle_apply_patch_approval_now(&mut self, ev: ApplyPatchApprovalRequestEvent) { self.flush_answer_stream_with_separator(); let request = ApprovalRequest::ApplyPatch { thread_id: self.thread_id.unwrap_or_default(), thread_label: None, id: ev.call_id, reason: ev.reason, changes: ev.changes.clone(), cwd: self.config.cwd.clone(), }; self.bottom_pane .push_approval_request(request, &self.config.features); self.set_ambient_pet_notification( crate::pets::PetNotificationKind::Waiting, /*body*/ None, ); self.request_redraw(); self.notify(Notification::EditApprovalRequested { cwd: self.config.cwd.to_path_buf(), changes: ev.changes.keys().cloned().collect(), }); } pub(crate) fn handle_elicitation_request_now( &mut self, request_id: AppServerRequestId, params: McpServerElicitationRequestParams, ) { self.flush_answer_stream_with_separator(); self.notify(Notification::ElicitationRequested { server_name: params.server_name.clone(), }); let thread_id = ThreadId::from_string(¶ms.thread_id) .unwrap_or_else(|_| self.thread_id.unwrap_or_default()); if let Some(params) = crate::bottom_pane::AppLinkViewParams::from_url_app_server_request( thread_id, ¶ms.server_name, request_id.clone(), ¶ms.request, ) { self.open_app_link_view(params); } else if let Some(request) = McpServerElicitationFormRequest::from_app_server_request( thread_id, request_id.clone(), params.clone(), ) { self.bottom_pane .push_mcp_server_elicitation_request(request); } else { match params.request { McpServerElicitationRequest::Form { message, .. } => { let request = ApprovalRequest::McpElicitation { thread_id, thread_label: None, server_name: params.server_name, request_id, message, }; self.bottom_pane .push_approval_request(request, &self.config.features); } McpServerElicitationRequest::Url { .. } => { self.app_event_tx.resolve_elicitation( thread_id, params.server_name, request_id, codex_app_server_protocol::McpServerElicitationAction::Decline, /*content*/ None, /*meta*/ None, ); } } } self.set_ambient_pet_notification( crate::pets::PetNotificationKind::Waiting, /*body*/ None, ); self.request_redraw(); } pub(crate) fn push_approval_request(&mut self, request: ApprovalRequest) { self.bottom_pane .push_approval_request(request, &self.config.features); self.set_ambient_pet_notification( crate::pets::PetNotificationKind::Waiting, /*body*/ None, ); self.request_redraw(); } pub(crate) fn push_mcp_server_elicitation_request( &mut self, request: McpServerElicitationFormRequest, ) { self.bottom_pane .push_mcp_server_elicitation_request(request); self.set_ambient_pet_notification( crate::pets::PetNotificationKind::Waiting, /*body*/ None, ); self.request_redraw(); } pub(crate) fn handle_request_user_input_now(&mut self, ev: ToolRequestUserInputParams) { self.flush_answer_stream_with_separator(); let question_count = ev.questions.len(); let summary = Notification::user_input_request_summary(&ev.questions); let title = match (question_count, summary.as_deref()) { (1, Some(summary)) => summary.to_string(), (1, None) => "Question requested".to_string(), (count, _) => format!("{count} questions requested"), }; self.notify(Notification::PlanModePrompt { title }); self.bottom_pane.push_user_input_request(ev); self.set_ambient_pet_notification( crate::pets::PetNotificationKind::Waiting, /*body*/ None, ); self.request_redraw(); } pub(crate) fn handle_request_permissions_now(&mut self, ev: RequestPermissionsEvent) { self.flush_answer_stream_with_separator(); let request = ApprovalRequest::Permissions { thread_id: self.thread_id.unwrap_or_default(), thread_label: None, call_id: ev.call_id, environment_id: ev.environment_id, reason: ev.reason, permissions: ev.permissions, }; self.bottom_pane .push_approval_request(request, &self.config.features); self.set_ambient_pet_notification( crate::pets::PetNotificationKind::Waiting, /*body*/ None, ); self.request_redraw(); }}