diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index d85e5db5..7f52b11b 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -12,6 +12,7 @@ use crate::diff_render::DiffSummary; use crate::exec_command::strip_bash_lc_and_escape; use crate::history_cell; use crate::key_hint; +use crate::key_hint::KeyBinding; use crate::render::highlight::highlight_bash_to_lines; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; @@ -94,8 +95,14 @@ impl ApprovalOverlay { header: Box, ) -> (Vec, SelectionViewParams) { let (options, title) = match &variant { - ApprovalVariant::Exec { .. } => (exec_options(), "Allow command?".to_string()), - ApprovalVariant::ApplyPatch { .. } => (patch_options(), "Apply changes?".to_string()), + ApprovalVariant::Exec { .. } => ( + exec_options(), + "Would you like to run the following command?".to_string(), + ), + ApprovalVariant::ApplyPatch { .. } => ( + patch_options(), + "Would you like to make the following edits?".to_string(), + ), }; let header = Box::new(ColumnRenderable::new([ @@ -108,11 +115,9 @@ impl ApprovalOverlay { .iter() .map(|opt| SelectionItem { name: opt.label.clone(), - description: Some(opt.description.clone()), - is_current: false, - actions: Vec::new(), + display_shortcut: opt.display_shortcut, dismiss_on_select: false, - search_value: None, + ..Default::default() }) .collect(); @@ -197,28 +202,18 @@ impl ApprovalOverlay { false } } - KeyEvent { - kind: KeyEventKind::Press, - code: KeyCode::Char(c), - modifiers, - .. - } if !modifiers.contains(KeyModifiers::CONTROL) - && !modifiers.contains(KeyModifiers::ALT) => - { - let lower = c.to_ascii_lowercase(); - match self + e => { + if let Some(idx) = self .options .iter() - .position(|opt| opt.shortcut.map(|s| s == lower).unwrap_or(false)) + .position(|opt| opt.shortcuts().any(|s| s.is_press(*e))) { - Some(idx) => { - self.apply_selection(idx); - true - } - None => false, + self.apply_selection(idx); + true + } else { + false } } - _ => false, } } } @@ -299,7 +294,7 @@ impl From for ApprovalRequestState { if let Some(reason) = reason && !reason.is_empty() { - header.push(reason.italic().into()); + header.push(Line::from(vec!["Reason: ".into(), reason.italic()])); header.push(Line::from("")); } let full_cmd = strip_bash_lc_and_escape(&command); @@ -347,31 +342,38 @@ enum ApprovalVariant { #[derive(Clone)] struct ApprovalOption { label: String, - description: String, decision: ReviewDecision, - shortcut: Option, + display_shortcut: Option, + additional_shortcuts: Vec, +} + +impl ApprovalOption { + fn shortcuts(&self) -> impl Iterator + '_ { + self.display_shortcut + .into_iter() + .chain(self.additional_shortcuts.iter().copied()) + } } fn exec_options() -> Vec { vec![ ApprovalOption { - label: "Approve and run now".to_string(), - description: "Run this command one time".to_string(), + label: "Yes, proceed".to_string(), decision: ReviewDecision::Approved, - shortcut: Some('y'), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], }, ApprovalOption { - label: "Always approve this session".to_string(), - description: "Automatically approve this command for the rest of the session" - .to_string(), + label: "Yes, and don't ask again for this command".to_string(), decision: ReviewDecision::ApprovedForSession, - shortcut: Some('a'), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))], }, ApprovalOption { - label: "Cancel".to_string(), - description: "Do not run the command".to_string(), + label: "No, and tell Codex what to do differently".to_string(), decision: ReviewDecision::Abort, - shortcut: Some('n'), + display_shortcut: Some(key_hint::plain(KeyCode::Esc)), + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], }, ] } @@ -379,16 +381,16 @@ fn exec_options() -> Vec { fn patch_options() -> Vec { vec![ ApprovalOption { - label: "Approve".to_string(), - description: "Apply the proposed changes".to_string(), + label: "Yes, proceed".to_string(), decision: ReviewDecision::Approved, - shortcut: Some('y'), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], }, ApprovalOption { - label: "Cancel".to_string(), - description: "Do not apply the changes".to_string(), + label: "No, and tell Codex what to do differently".to_string(), decision: ReviewDecision::Abort, - shortcut: Some('n'), + display_shortcut: Some(key_hint::plain(KeyCode::Esc)), + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], }, ] } diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index b597f9ec..1842a999 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -173,6 +173,7 @@ impl CommandPopup { name, match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), is_current: false, + display_shortcut: None, description: Some(description), } }) diff --git a/codex-rs/tui/src/bottom_pane/file_search_popup.rs b/codex-rs/tui/src/bottom_pane/file_search_popup.rs index 6ba8599d..708b0047 100644 --- a/codex-rs/tui/src/bottom_pane/file_search_popup.rs +++ b/codex-rs/tui/src/bottom_pane/file_search_popup.rs @@ -130,6 +130,7 @@ impl WidgetRef for &FileSearchPopup { .as_ref() .map(|v| v.iter().map(|&i| i as usize).collect()), is_current: false, + display_shortcut: None, description: None, }) .collect() diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index cb3de212..9f0ce3df 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -1,6 +1,7 @@ use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; +use itertools::Itertools as _; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; use ratatui::layout::Layout; @@ -13,6 +14,7 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; use crate::app_event_sender::AppEventSender; +use crate::key_hint::KeyBinding; use crate::render::Insets; use crate::render::RectExt as _; use crate::render::renderable::ColumnRenderable; @@ -31,8 +33,10 @@ use super::selection_popup_common::render_rows; /// One selectable item in the generic selection list. pub(crate) type SelectionAction = Box; +#[derive(Default)] pub(crate) struct SelectionItem { pub name: String, + pub display_shortcut: Option, pub description: Option, pub is_current: bool, pub actions: Vec, @@ -135,18 +139,10 @@ impl ListSelectionView { self.filtered_indices = self .items .iter() - .enumerate() - .filter_map(|(idx, item)| { - let matches = if let Some(search_value) = &item.search_value { - search_value.to_lowercase().contains(&query_lower) - } else { - let mut matches = item.name.to_lowercase().contains(&query_lower); - if !matches && let Some(desc) = &item.description { - matches = desc.to_lowercase().contains(&query_lower); - } - matches - }; - matches.then_some(idx) + .positions(|item| { + item.search_value + .as_ref() + .is_some_and(|v| v.to_lowercase().contains(&query_lower)) }) .collect(); } else { @@ -200,6 +196,7 @@ impl ListSelectionView { }; GenericDisplayRow { name: display_name, + display_shortcut: item.display_shortcut, match_indices: None, is_current: item.is_current, description: item.description.clone(), @@ -329,7 +326,8 @@ impl Renderable for ListSelectionView { let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, width); - let mut height = self.header.desired_height(width); + // Subtract 4 for the padding on the left and right of the header. + let mut height = self.header.desired_height(width.saturating_sub(4)); height = height.saturating_add(rows_height + 3); if self.is_searchable { height = height.saturating_add(1); @@ -355,7 +353,10 @@ impl Renderable for ListSelectionView { .style(user_message_style(terminal_palette::default_bg())) .render(content_area, buf); - let header_height = self.header.desired_height(content_area.width); + let header_height = self + .header + // Subtract 4 for the padding on the left and right of the header. + .desired_height(content_area.width.saturating_sub(4)); let rows = self.build_rows(); let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, content_area.width); @@ -438,17 +439,15 @@ mod tests { name: "Read Only".to_string(), description: Some("Codex can read files".to_string()), is_current: true, - actions: vec![], dismiss_on_select: true, - search_value: None, + ..Default::default() }, SelectionItem { name: "Full Access".to_string(), description: Some("Codex can edit files".to_string()), is_current: false, - actions: vec![], dismiss_on_select: true, - search_value: None, + ..Default::default() }, ]; ListSelectionView::new( @@ -510,9 +509,8 @@ mod tests { name: "Read Only".to_string(), description: Some("Codex can read files".to_string()), is_current: false, - actions: vec![], dismiss_on_select: true, - search_value: None, + ..Default::default() }]; let mut view = ListSelectionView::new( SelectionViewParams { diff --git a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs index fd523532..4246adf4 100644 --- a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -9,11 +9,14 @@ use ratatui::text::Span; use ratatui::widgets::Widget; use unicode_width::UnicodeWidthChar; +use crate::key_hint::KeyBinding; + use super::scroll_state::ScrollState; /// A generic representation of a display row for selection popups. pub(crate) struct GenericDisplayRow { pub name: String, + pub display_shortcut: Option, pub match_indices: Option>, // indices to bold (char positions) pub is_current: bool, pub description: Option, // optional grey text after the name @@ -92,6 +95,10 @@ fn build_full_line(row: &GenericDisplayRow, desc_col: usize) -> Line<'static> { let this_name_width = Line::from(name_spans.clone()).width(); let mut full_spans: Vec = name_spans; + if let Some(display_shortcut) = row.display_shortcut { + full_spans.push(" ".into()); + full_spans.push(display_shortcut.into()); + } if let Some(desc) = row.description.as_ref() { let gap = desc_col.saturating_sub(this_name_width); if gap > 0 { @@ -155,6 +162,7 @@ pub(crate) fn render_rows( let GenericDisplayRow { name, match_indices, + display_shortcut, is_current: _is_current, description, } = row; @@ -163,6 +171,7 @@ pub(crate) fn render_rows( &GenericDisplayRow { name: name.clone(), match_indices: match_indices.clone(), + display_shortcut: *display_shortcut, is_current: *_is_current, description: description.clone(), }, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c03caca8..1292aafd 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1630,7 +1630,7 @@ impl ChatWidget { is_current, actions, dismiss_on_select: true, - search_value: None, + ..Default::default() }); } @@ -1749,7 +1749,7 @@ impl ChatWidget { is_current: is_current_model && choice.stored == highlight_choice, actions, dismiss_on_select: true, - search_value: None, + ..Default::default() }); } @@ -1793,7 +1793,7 @@ impl ChatWidget { is_current, actions, dismiss_on_select: true, - search_value: None, + ..Default::default() }); } @@ -1917,7 +1917,6 @@ impl ChatWidget { items.push(SelectionItem { name: "Review against a base branch".to_string(), description: Some("(PR Style)".into()), - is_current: false, actions: vec![Box::new({ let cwd = self.config.cwd.clone(); move |tx| { @@ -1925,13 +1924,11 @@ impl ChatWidget { } })], dismiss_on_select: false, - search_value: None, + ..Default::default() }); items.push(SelectionItem { name: "Review uncommitted changes".to_string(), - description: None, - is_current: false, actions: vec![Box::new( move |tx: &AppEventSender| { tx.send(AppEvent::CodexOp(Op::Review { @@ -1943,14 +1940,12 @@ impl ChatWidget { }, )], dismiss_on_select: true, - search_value: None, + ..Default::default() }); // New: Review a specific commit (opens commit picker) items.push(SelectionItem { name: "Review a commit".to_string(), - description: None, - is_current: false, actions: vec![Box::new({ let cwd = self.config.cwd.clone(); move |tx| { @@ -1958,18 +1953,16 @@ impl ChatWidget { } })], dismiss_on_select: false, - search_value: None, + ..Default::default() }); items.push(SelectionItem { name: "Custom review instructions".to_string(), - description: None, - is_current: false, actions: vec![Box::new(move |tx| { tx.send(AppEvent::OpenReviewCustomPrompt); })], dismiss_on_select: false, - search_value: None, + ..Default::default() }); self.bottom_pane.show_selection_view(SelectionViewParams { @@ -1991,8 +1984,6 @@ impl ChatWidget { let branch = option.clone(); items.push(SelectionItem { name: format!("{current_branch} -> {branch}"), - description: None, - is_current: false, actions: vec![Box::new(move |tx3: &AppEventSender| { tx3.send(AppEvent::CodexOp(Op::Review { review_request: ReviewRequest { @@ -2005,6 +1996,7 @@ impl ChatWidget { })], dismiss_on_select: true, search_value: Some(option), + ..Default::default() }); } @@ -2030,8 +2022,6 @@ impl ChatWidget { items.push(SelectionItem { name: subject.clone(), - description: None, - is_current: false, actions: vec![Box::new(move |tx3: &AppEventSender| { let hint = format!("commit {short}"); let prompt = format!( @@ -2046,6 +2036,7 @@ impl ChatWidget { })], dismiss_on_select: true, search_value: Some(search_val), + ..Default::default() }); } @@ -2255,8 +2246,6 @@ pub(crate) fn show_review_commit_picker_with_entries( items.push(SelectionItem { name: subject.clone(), - description: None, - is_current: false, actions: vec![Box::new(move |tx3: &AppEventSender| { let hint = format!("commit {short}"); let prompt = format!( @@ -2271,6 +2260,7 @@ pub(crate) fn show_review_commit_picker_with_entries( })], dismiss_on_select: true, search_value: Some(search_val), + ..Default::default() }); } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap index 558728bd..d0990fa9 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap @@ -2,15 +2,15 @@ source: tui/src/chatwidget/tests.rs expression: terminal.backend().vt100().screen().contents() --- - Allow command? + Would you like to run the following command? - this is a test reason such as one that would be produced by the model + Reason: this is a test reason such as one that would be produced by the + model $ echo hello world -› 1. Approve and run now Run this command one time - 2. Always approve this session Automatically approve this command for the - rest of the session - 3. Cancel Do not run the command +› 1. Yes, proceed + 2. Yes, and don't ask again for this command + 3. No, and tell Codex what to do differently esc Press enter to confirm or esc to cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap index 53348af1..3a557bf6 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap @@ -1,17 +1,13 @@ --- source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +expression: terminal.backend().vt100().screen().contents() --- -" " -" " -" Allow command? " -" " -" $ echo hello world " -" " -"› 1. Approve and run now Run this command one time " -" 2. Always approve this session Automatically approve this command for the " -" rest of the session " -" 3. Cancel Do not run the command " -" " -" Press enter to confirm or esc to cancel " -" " + Would you like to run the following command? + + $ echo hello world + +› 1. Yes, proceed + 2. Yes, and don't ask again for this command + 3. No, and tell Codex what to do differently esc + + Press enter to confirm or esc to cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap index 64fbada3..ab88ffaf 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap @@ -1,19 +1,17 @@ --- source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +expression: terminal.backend().vt100().screen().contents() --- -" " -" " -" Apply changes? " -" " -" README.md (+2 -0) " -" 1 +hello " -" 2 +world " -" " -" The model wants to apply changes " -" " -"› 1. Approve Apply the proposed changes " -" 2. Cancel Do not apply the changes " -" " -" Press enter to confirm or esc to cancel " -" " + Would you like to make the following edits? + + README.md (+2 -0) + + 1 +hello + 2 +world + + The model wants to apply changes + +› 1. Yes, proceed + 2. No, and tell Codex what to do differently esc + + Press enter to confirm or esc to cancel diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap index 8b7c5838..f52a0f38 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap @@ -7,16 +7,16 @@ Buffer { content: [ " ", " ", - " Allow command? ", + " Would you like to run the following command? ", " ", - " this is a test reason such as one that would be produced by the model ", + " Reason: this is a test reason such as one that would be produced by the ", + " model ", " ", " $ echo hello world ", " ", - "› 1. Approve and run now Run this command one time ", - " 2. Always approve this session Automatically approve this command for the ", - " rest of the session ", - " 3. Cancel Do not run the command ", + "› 1. Yes, proceed ", + " 2. Yes, and don't ask again for this command ", + " 3. No, and tell Codex what to do differently esc ", " ", " Press enter to confirm or esc to cancel ", " ", @@ -24,18 +24,15 @@ Buffer { styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 16, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, - x: 71, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 0, y: 8, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD, - x: 34, y: 8, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD | DIM, - x: 59, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 34, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 76, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 34, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 53, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 34, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 56, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 46, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 10, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, + x: 73, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, + x: 7, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 9, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD, + x: 17, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 47, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 50, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 0, y: 14, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, ] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap index 96b51f7a..d1951cd0 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap @@ -4,16 +4,16 @@ expression: terminal.backend() --- " " " " -" Allow command? " +" Would you like to run the following command? " " " -" this is a test reason such as one that would be produced by the model " +" Reason: this is a test reason such as one that would be produced by the " +" model " " " " $ echo 'hello world' " " " -"› 1. Approve and run now Run this command one time " -" 2. Always approve this session Automatically approve this command for the " -" rest of the session " -" 3. Cancel Do not run the command " +"› 1. Yes, proceed " +" 2. Yes, and don't ask again for this command " +" 3. No, and tell Codex what to do differently esc " " " " Press enter to confirm or esc to cancel " " " diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 64b4c72b..b61d3d2e 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1236,6 +1236,14 @@ fn approval_modal_exec_snapshot() { terminal .draw(|f| f.render_widget_ref(&chat, f.area())) .expect("draw approval modal"); + assert!( + terminal + .backend() + .vt100() + .screen() + .contents() + .contains("echo hello world") + ); assert_snapshot!( "approval_modal_exec", terminal.backend().vt100().screen().contents() @@ -1261,12 +1269,16 @@ fn approval_modal_exec_without_reason_snapshot() { }); let height = chat.desired_height(80); - let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height)) - .expect("create terminal"); + let mut terminal = + ratatui::Terminal::new(VT100Backend::new(80, height)).expect("create terminal"); + terminal.set_viewport_area(Rect::new(0, 0, 80, height)); terminal .draw(|f| f.render_widget_ref(&chat, f.area())) .expect("draw approval modal (no reason)"); - assert_snapshot!("approval_modal_exec_no_reason", terminal.backend()); + assert_snapshot!( + "approval_modal_exec_no_reason", + terminal.backend().vt100().screen().contents() + ); } // Snapshot test: patch approval modal @@ -1296,12 +1308,16 @@ fn approval_modal_patch_snapshot() { // Render at the widget's desired height and snapshot. let height = chat.desired_height(80); - let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height)) - .expect("create terminal"); + let mut terminal = + ratatui::Terminal::new(VT100Backend::new(80, height)).expect("create terminal"); + terminal.set_viewport_area(Rect::new(0, 0, 80, height)); terminal .draw(|f| f.render_widget_ref(&chat, f.area())) .expect("draw patch approval modal"); - assert_snapshot!("approval_modal_patch", terminal.backend()); + assert_snapshot!( + "approval_modal_patch", + terminal.backend().vt100().screen().contents() + ); } #[test] @@ -1831,14 +1847,14 @@ fn apply_patch_untrusted_shows_approval_modal() { for x in 0..area.width { row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' ')); } - if row.contains("Apply changes?") { + if row.contains("Would you like to make the following edits?") { contains_title = true; break; } } assert!( contains_title, - "expected approval modal to be visible with title 'Apply changes?'" + "expected approval modal to be visible with title 'Would you like to make the following edits?'" ); } diff --git a/codex-rs/tui/src/diff_render.rs b/codex-rs/tui/src/diff_render.rs index 6f133fee..b7fa5be5 100644 --- a/codex-rs/tui/src/diff_render.rs +++ b/codex-rs/tui/src/diff_render.rs @@ -64,6 +64,7 @@ impl From for Box { path.push_span(" "); path.extend(render_line_count_summary(row.added, row.removed)); rows.push(Box::new(path)); + rows.push(Box::new(RtLine::from(""))); rows.push(Box::new(row.change)); }