diff --git a/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs b/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs index 1e3ad6591244..0c36c403becc 100644 --- a/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs +++ b/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs @@ -166,7 +166,9 @@ pub(crate) struct McpServerElicitationFormRequest { thread_id: ThreadId, server_name: String, request_id: McpRequestId, + is_high_risk: bool, message: String, + subtitle: Option, approval_display_params: Vec, response_mode: McpServerElicitationResponseMode, fields: Vec, @@ -264,6 +266,20 @@ impl McpServerElicitationFormRequest { message: String, requested_schema: Value, ) -> Option { + let is_high_risk = meta + .as_ref() + .and_then(Value::as_object) + .and_then(|meta| meta.get("riskLevel")) + .and_then(Value::as_str) + == Some("high"); + let subtitle = meta + .as_ref() + .and_then(Value::as_object) + .and_then(|meta| meta.get("subtitle")) + .and_then(Value::as_str) + .map(str::trim) + .filter(|subtitle| !subtitle.is_empty()) + .map(str::to_string); let tool_suggestion = parse_tool_suggestion_request(meta.as_ref()); let is_tool_approval = meta .as_ref() @@ -367,7 +383,9 @@ impl McpServerElicitationFormRequest { thread_id, server_name, request_id, + is_high_risk, message, + subtitle, approval_display_params, response_mode, fields, @@ -946,25 +964,42 @@ impl McpServerElicitationOverlay { .collect() } - fn wrapped_prompt_lines(&self, width: u16) -> Vec { - textwrap::wrap(&self.current_prompt_text(), width.max(1) as usize) - .into_iter() - .map(|line| line.to_string()) - .collect() - } - - fn current_prompt_text(&self) -> String { + fn prompt_lines(&self, width: u16, answered: bool) -> Vec> { + let wrap_width = width.max(1) as usize; + let mut lines = Vec::new(); let request_message = format_tool_approval_display_message( &self.request.message, &self.request.approval_display_params, ); - let Some(field) = self.current_field() else { - return request_message; - }; - let mut sections = Vec::new(); if !request_message.trim().is_empty() { - sections.push(request_message); + for (idx, line) in textwrap::wrap(&request_message, wrap_width) + .into_iter() + .enumerate() + { + let text = if self.request.is_high_risk && idx == 0 { + format!("⚠ {line}") + } else { + line.to_string() + }; + let line = if self.request.is_high_risk { + Line::from(text).red() + } else if answered { + Line::from(text) + } else { + Line::from(text).cyan() + }; + lines.push(line); + } + } + if let Some(subtitle) = self.request.subtitle.as_deref() { + for line in textwrap::wrap(subtitle, wrap_width) { + lines.push(Line::from(line.to_string())); + } } + + let Some(field) = self.current_field() else { + return lines; + }; let field_prompt = if field.label.trim().is_empty() || field.prompt.trim().is_empty() || field.label == field.prompt @@ -978,9 +1013,19 @@ impl McpServerElicitationOverlay { format!("{}\n{}", field.label, field.prompt) }; if !field_prompt.trim().is_empty() { - sections.push(field_prompt); + if !lines.is_empty() { + lines.push(Line::from(String::new())); + } + for line in textwrap::wrap(&field_prompt, wrap_width) { + let line = if answered { + Line::from(line.to_string()) + } else { + Line::from(line.to_string()).cyan() + }; + lines.push(line); + } } - sections.join("\n\n") + lines } fn footer_tips(&self) -> Vec { @@ -1268,17 +1313,12 @@ impl McpServerElicitationOverlay { return; } let answered = self.is_current_field_answered(); - for (offset, line) in self.wrapped_prompt_lines(area.width).iter().enumerate() { + for (offset, line) in self.prompt_lines(area.width, answered).iter().enumerate() { let y = area.y.saturating_add(offset as u16); if y >= area.y + area.height { break; } - let line = if answered { - Line::from(line.clone()) - } else { - Line::from(line.clone()).cyan() - }; - Paragraph::new(line).render( + Paragraph::new(line.clone()).render( Rect { x: area.x, y, @@ -1371,8 +1411,9 @@ impl Renderable for McpServerElicitationOverlay { let outer = Rect::new(0, 0, width, u16::MAX); let inner = menu_surface_inset(outer); let inner_width = inner.width.max(1); + let answered = self.is_current_field_answered(); let height = 1u16 - .saturating_add(self.wrapped_prompt_lines(inner_width).len() as u16) + .saturating_add(self.prompt_lines(inner_width, answered).len() as u16) .saturating_add(self.input_height(inner_width)) .saturating_add(self.footer_tip_lines(inner_width).len() as u16) .saturating_add(menu_surface_padding_height()); @@ -1387,7 +1428,7 @@ impl Renderable for McpServerElicitationOverlay { if content_area.width == 0 || content_area.height == 0 { return; } - let prompt_lines = self.wrapped_prompt_lines(content_area.width); + let prompt_lines = self.prompt_lines(content_area.width, self.is_current_field_answered()); let footer_lines = self.footer_tip_lines(content_area.width); let mut remaining = content_area.height; @@ -1461,7 +1502,7 @@ impl Renderable for McpServerElicitationOverlay { if content_area.width == 0 || content_area.height == 0 { return None; } - let prompt_lines = self.wrapped_prompt_lines(content_area.width); + let prompt_lines = self.prompt_lines(content_area.width, self.is_current_field_answered()); let footer_lines = self.footer_tip_lines(content_area.width); let mut remaining = content_area.height; remaining = remaining.saturating_sub(u16::from(remaining > 0)); @@ -1813,7 +1854,9 @@ mod tests { thread_id, server_name: "server-1".to_string(), request_id: McpRequestId::String("request-1".to_string()), + is_high_risk: false, message: "Allow this request?".to_string(), + subtitle: None, approval_display_params: Vec::new(), response_mode: McpServerElicitationResponseMode::FormContent, fields: vec![McpServerElicitationField { @@ -1879,7 +1922,9 @@ mod tests { thread_id, server_name: "server-1".to_string(), request_id: McpRequestId::String("request-1".to_string()), + is_high_risk: false, message: "Allow this request?".to_string(), + subtitle: None, approval_display_params: Vec::new(), response_mode: McpServerElicitationResponseMode::ApprovalAction, fields: vec![McpServerElicitationField { @@ -1936,7 +1981,9 @@ mod tests { thread_id, server_name: "server-1".to_string(), request_id: McpRequestId::String("request-1".to_string()), + is_high_risk: false, message: "Allow this request?".to_string(), + subtitle: None, approval_display_params: Vec::new(), response_mode: McpServerElicitationResponseMode::ApprovalAction, fields: vec![McpServerElicitationField {