tui: tweaks to dialog display (#4622)

- prefix command approval reasons with "Reason:"
- show keyboard shortcuts for some ListSelectionItems
- remove "description" lines for approval options, and make the labels
more verbose
- add a spacer line in diff display after the path

and some other minor refactors that go along with the above.

<img width="859" height="508" alt="Screenshot 2025-10-02 at 1 24 50 PM"
src="https://github.com/user-attachments/assets/4fa7ecaf-3d3a-406a-bb4d-23e30ce3e5cf"
/>
This commit is contained in:
Jeremy Rose
2025-10-02 14:41:29 -07:00
committed by GitHub
parent 62cc8a4b8d
commit 25a2e15ec5
13 changed files with 159 additions and 150 deletions

View File

@@ -12,6 +12,7 @@ use crate::diff_render::DiffSummary;
use crate::exec_command::strip_bash_lc_and_escape; use crate::exec_command::strip_bash_lc_and_escape;
use crate::history_cell; use crate::history_cell;
use crate::key_hint; use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::render::highlight::highlight_bash_to_lines; use crate::render::highlight::highlight_bash_to_lines;
use crate::render::renderable::ColumnRenderable; use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable; use crate::render::renderable::Renderable;
@@ -94,8 +95,14 @@ impl ApprovalOverlay {
header: Box<dyn Renderable>, header: Box<dyn Renderable>,
) -> (Vec<ApprovalOption>, SelectionViewParams) { ) -> (Vec<ApprovalOption>, SelectionViewParams) {
let (options, title) = match &variant { let (options, title) = match &variant {
ApprovalVariant::Exec { .. } => (exec_options(), "Allow command?".to_string()), ApprovalVariant::Exec { .. } => (
ApprovalVariant::ApplyPatch { .. } => (patch_options(), "Apply changes?".to_string()), 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([ let header = Box::new(ColumnRenderable::new([
@@ -108,11 +115,9 @@ impl ApprovalOverlay {
.iter() .iter()
.map(|opt| SelectionItem { .map(|opt| SelectionItem {
name: opt.label.clone(), name: opt.label.clone(),
description: Some(opt.description.clone()), display_shortcut: opt.display_shortcut,
is_current: false,
actions: Vec::new(),
dismiss_on_select: false, dismiss_on_select: false,
search_value: None, ..Default::default()
}) })
.collect(); .collect();
@@ -197,28 +202,18 @@ impl ApprovalOverlay {
false false
} }
} }
KeyEvent { e => {
kind: KeyEventKind::Press, if let Some(idx) = self
code: KeyCode::Char(c),
modifiers,
..
} if !modifiers.contains(KeyModifiers::CONTROL)
&& !modifiers.contains(KeyModifiers::ALT) =>
{
let lower = c.to_ascii_lowercase();
match self
.options .options
.iter() .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);
self.apply_selection(idx); true
true } else {
} false
None => false,
} }
} }
_ => false,
} }
} }
} }
@@ -299,7 +294,7 @@ impl From<ApprovalRequest> for ApprovalRequestState {
if let Some(reason) = reason if let Some(reason) = reason
&& !reason.is_empty() && !reason.is_empty()
{ {
header.push(reason.italic().into()); header.push(Line::from(vec!["Reason: ".into(), reason.italic()]));
header.push(Line::from("")); header.push(Line::from(""));
} }
let full_cmd = strip_bash_lc_and_escape(&command); let full_cmd = strip_bash_lc_and_escape(&command);
@@ -347,31 +342,38 @@ enum ApprovalVariant {
#[derive(Clone)] #[derive(Clone)]
struct ApprovalOption { struct ApprovalOption {
label: String, label: String,
description: String,
decision: ReviewDecision, decision: ReviewDecision,
shortcut: Option<char>, display_shortcut: Option<KeyBinding>,
additional_shortcuts: Vec<KeyBinding>,
}
impl ApprovalOption {
fn shortcuts(&self) -> impl Iterator<Item = KeyBinding> + '_ {
self.display_shortcut
.into_iter()
.chain(self.additional_shortcuts.iter().copied())
}
} }
fn exec_options() -> Vec<ApprovalOption> { fn exec_options() -> Vec<ApprovalOption> {
vec![ vec![
ApprovalOption { ApprovalOption {
label: "Approve and run now".to_string(), label: "Yes, proceed".to_string(),
description: "Run this command one time".to_string(),
decision: ReviewDecision::Approved, decision: ReviewDecision::Approved,
shortcut: Some('y'), display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
}, },
ApprovalOption { ApprovalOption {
label: "Always approve this session".to_string(), label: "Yes, and don't ask again for this command".to_string(),
description: "Automatically approve this command for the rest of the session"
.to_string(),
decision: ReviewDecision::ApprovedForSession, decision: ReviewDecision::ApprovedForSession,
shortcut: Some('a'), display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))],
}, },
ApprovalOption { ApprovalOption {
label: "Cancel".to_string(), label: "No, and tell Codex what to do differently".to_string(),
description: "Do not run the command".to_string(),
decision: ReviewDecision::Abort, 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<ApprovalOption> {
fn patch_options() -> Vec<ApprovalOption> { fn patch_options() -> Vec<ApprovalOption> {
vec![ vec![
ApprovalOption { ApprovalOption {
label: "Approve".to_string(), label: "Yes, proceed".to_string(),
description: "Apply the proposed changes".to_string(),
decision: ReviewDecision::Approved, decision: ReviewDecision::Approved,
shortcut: Some('y'), display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
}, },
ApprovalOption { ApprovalOption {
label: "Cancel".to_string(), label: "No, and tell Codex what to do differently".to_string(),
description: "Do not apply the changes".to_string(),
decision: ReviewDecision::Abort, decision: ReviewDecision::Abort,
shortcut: Some('n'), display_shortcut: Some(key_hint::plain(KeyCode::Esc)),
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
}, },
] ]
} }

View File

@@ -173,6 +173,7 @@ impl CommandPopup {
name, name,
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false, is_current: false,
display_shortcut: None,
description: Some(description), description: Some(description),
} }
}) })

View File

@@ -130,6 +130,7 @@ impl WidgetRef for &FileSearchPopup {
.as_ref() .as_ref()
.map(|v| v.iter().map(|&i| i as usize).collect()), .map(|v| v.iter().map(|&i| i as usize).collect()),
is_current: false, is_current: false,
display_shortcut: None,
description: None, description: None,
}) })
.collect() .collect()

View File

@@ -1,6 +1,7 @@
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers; use crossterm::event::KeyModifiers;
use itertools::Itertools as _;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Constraint; use ratatui::layout::Constraint;
use ratatui::layout::Layout; use ratatui::layout::Layout;
@@ -13,6 +14,7 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget; use ratatui::widgets::Widget;
use crate::app_event_sender::AppEventSender; use crate::app_event_sender::AppEventSender;
use crate::key_hint::KeyBinding;
use crate::render::Insets; use crate::render::Insets;
use crate::render::RectExt as _; use crate::render::RectExt as _;
use crate::render::renderable::ColumnRenderable; use crate::render::renderable::ColumnRenderable;
@@ -31,8 +33,10 @@ use super::selection_popup_common::render_rows;
/// One selectable item in the generic selection list. /// One selectable item in the generic selection list.
pub(crate) type SelectionAction = Box<dyn Fn(&AppEventSender) + Send + Sync>; pub(crate) type SelectionAction = Box<dyn Fn(&AppEventSender) + Send + Sync>;
#[derive(Default)]
pub(crate) struct SelectionItem { pub(crate) struct SelectionItem {
pub name: String, pub name: String,
pub display_shortcut: Option<KeyBinding>,
pub description: Option<String>, pub description: Option<String>,
pub is_current: bool, pub is_current: bool,
pub actions: Vec<SelectionAction>, pub actions: Vec<SelectionAction>,
@@ -135,18 +139,10 @@ impl ListSelectionView {
self.filtered_indices = self self.filtered_indices = self
.items .items
.iter() .iter()
.enumerate() .positions(|item| {
.filter_map(|(idx, item)| { item.search_value
let matches = if let Some(search_value) = &item.search_value { .as_ref()
search_value.to_lowercase().contains(&query_lower) .is_some_and(|v| v.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)
}) })
.collect(); .collect();
} else { } else {
@@ -200,6 +196,7 @@ impl ListSelectionView {
}; };
GenericDisplayRow { GenericDisplayRow {
name: display_name, name: display_name,
display_shortcut: item.display_shortcut,
match_indices: None, match_indices: None,
is_current: item.is_current, is_current: item.is_current,
description: item.description.clone(), 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 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); height = height.saturating_add(rows_height + 3);
if self.is_searchable { if self.is_searchable {
height = height.saturating_add(1); height = height.saturating_add(1);
@@ -355,7 +353,10 @@ impl Renderable for ListSelectionView {
.style(user_message_style(terminal_palette::default_bg())) .style(user_message_style(terminal_palette::default_bg()))
.render(content_area, buf); .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 = self.build_rows();
let rows_height = let rows_height =
measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, content_area.width); measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, content_area.width);
@@ -438,17 +439,15 @@ mod tests {
name: "Read Only".to_string(), name: "Read Only".to_string(),
description: Some("Codex can read files".to_string()), description: Some("Codex can read files".to_string()),
is_current: true, is_current: true,
actions: vec![],
dismiss_on_select: true, dismiss_on_select: true,
search_value: None, ..Default::default()
}, },
SelectionItem { SelectionItem {
name: "Full Access".to_string(), name: "Full Access".to_string(),
description: Some("Codex can edit files".to_string()), description: Some("Codex can edit files".to_string()),
is_current: false, is_current: false,
actions: vec![],
dismiss_on_select: true, dismiss_on_select: true,
search_value: None, ..Default::default()
}, },
]; ];
ListSelectionView::new( ListSelectionView::new(
@@ -510,9 +509,8 @@ mod tests {
name: "Read Only".to_string(), name: "Read Only".to_string(),
description: Some("Codex can read files".to_string()), description: Some("Codex can read files".to_string()),
is_current: false, is_current: false,
actions: vec![],
dismiss_on_select: true, dismiss_on_select: true,
search_value: None, ..Default::default()
}]; }];
let mut view = ListSelectionView::new( let mut view = ListSelectionView::new(
SelectionViewParams { SelectionViewParams {

View File

@@ -9,11 +9,14 @@ use ratatui::text::Span;
use ratatui::widgets::Widget; use ratatui::widgets::Widget;
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
use crate::key_hint::KeyBinding;
use super::scroll_state::ScrollState; use super::scroll_state::ScrollState;
/// A generic representation of a display row for selection popups. /// A generic representation of a display row for selection popups.
pub(crate) struct GenericDisplayRow { pub(crate) struct GenericDisplayRow {
pub name: String, pub name: String,
pub display_shortcut: Option<KeyBinding>,
pub match_indices: Option<Vec<usize>>, // indices to bold (char positions) pub match_indices: Option<Vec<usize>>, // indices to bold (char positions)
pub is_current: bool, pub is_current: bool,
pub description: Option<String>, // optional grey text after the name pub description: Option<String>, // 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 this_name_width = Line::from(name_spans.clone()).width();
let mut full_spans: Vec<Span> = name_spans; let mut full_spans: Vec<Span> = 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() { if let Some(desc) = row.description.as_ref() {
let gap = desc_col.saturating_sub(this_name_width); let gap = desc_col.saturating_sub(this_name_width);
if gap > 0 { if gap > 0 {
@@ -155,6 +162,7 @@ pub(crate) fn render_rows(
let GenericDisplayRow { let GenericDisplayRow {
name, name,
match_indices, match_indices,
display_shortcut,
is_current: _is_current, is_current: _is_current,
description, description,
} = row; } = row;
@@ -163,6 +171,7 @@ pub(crate) fn render_rows(
&GenericDisplayRow { &GenericDisplayRow {
name: name.clone(), name: name.clone(),
match_indices: match_indices.clone(), match_indices: match_indices.clone(),
display_shortcut: *display_shortcut,
is_current: *_is_current, is_current: *_is_current,
description: description.clone(), description: description.clone(),
}, },

View File

@@ -1630,7 +1630,7 @@ impl ChatWidget {
is_current, is_current,
actions, actions,
dismiss_on_select: true, dismiss_on_select: true,
search_value: None, ..Default::default()
}); });
} }
@@ -1749,7 +1749,7 @@ impl ChatWidget {
is_current: is_current_model && choice.stored == highlight_choice, is_current: is_current_model && choice.stored == highlight_choice,
actions, actions,
dismiss_on_select: true, dismiss_on_select: true,
search_value: None, ..Default::default()
}); });
} }
@@ -1793,7 +1793,7 @@ impl ChatWidget {
is_current, is_current,
actions, actions,
dismiss_on_select: true, dismiss_on_select: true,
search_value: None, ..Default::default()
}); });
} }
@@ -1917,7 +1917,6 @@ impl ChatWidget {
items.push(SelectionItem { items.push(SelectionItem {
name: "Review against a base branch".to_string(), name: "Review against a base branch".to_string(),
description: Some("(PR Style)".into()), description: Some("(PR Style)".into()),
is_current: false,
actions: vec![Box::new({ actions: vec![Box::new({
let cwd = self.config.cwd.clone(); let cwd = self.config.cwd.clone();
move |tx| { move |tx| {
@@ -1925,13 +1924,11 @@ impl ChatWidget {
} }
})], })],
dismiss_on_select: false, dismiss_on_select: false,
search_value: None, ..Default::default()
}); });
items.push(SelectionItem { items.push(SelectionItem {
name: "Review uncommitted changes".to_string(), name: "Review uncommitted changes".to_string(),
description: None,
is_current: false,
actions: vec![Box::new( actions: vec![Box::new(
move |tx: &AppEventSender| { move |tx: &AppEventSender| {
tx.send(AppEvent::CodexOp(Op::Review { tx.send(AppEvent::CodexOp(Op::Review {
@@ -1943,14 +1940,12 @@ impl ChatWidget {
}, },
)], )],
dismiss_on_select: true, dismiss_on_select: true,
search_value: None, ..Default::default()
}); });
// New: Review a specific commit (opens commit picker) // New: Review a specific commit (opens commit picker)
items.push(SelectionItem { items.push(SelectionItem {
name: "Review a commit".to_string(), name: "Review a commit".to_string(),
description: None,
is_current: false,
actions: vec![Box::new({ actions: vec![Box::new({
let cwd = self.config.cwd.clone(); let cwd = self.config.cwd.clone();
move |tx| { move |tx| {
@@ -1958,18 +1953,16 @@ impl ChatWidget {
} }
})], })],
dismiss_on_select: false, dismiss_on_select: false,
search_value: None, ..Default::default()
}); });
items.push(SelectionItem { items.push(SelectionItem {
name: "Custom review instructions".to_string(), name: "Custom review instructions".to_string(),
description: None,
is_current: false,
actions: vec![Box::new(move |tx| { actions: vec![Box::new(move |tx| {
tx.send(AppEvent::OpenReviewCustomPrompt); tx.send(AppEvent::OpenReviewCustomPrompt);
})], })],
dismiss_on_select: false, dismiss_on_select: false,
search_value: None, ..Default::default()
}); });
self.bottom_pane.show_selection_view(SelectionViewParams { self.bottom_pane.show_selection_view(SelectionViewParams {
@@ -1991,8 +1984,6 @@ impl ChatWidget {
let branch = option.clone(); let branch = option.clone();
items.push(SelectionItem { items.push(SelectionItem {
name: format!("{current_branch} -> {branch}"), name: format!("{current_branch} -> {branch}"),
description: None,
is_current: false,
actions: vec![Box::new(move |tx3: &AppEventSender| { actions: vec![Box::new(move |tx3: &AppEventSender| {
tx3.send(AppEvent::CodexOp(Op::Review { tx3.send(AppEvent::CodexOp(Op::Review {
review_request: ReviewRequest { review_request: ReviewRequest {
@@ -2005,6 +1996,7 @@ impl ChatWidget {
})], })],
dismiss_on_select: true, dismiss_on_select: true,
search_value: Some(option), search_value: Some(option),
..Default::default()
}); });
} }
@@ -2030,8 +2022,6 @@ impl ChatWidget {
items.push(SelectionItem { items.push(SelectionItem {
name: subject.clone(), name: subject.clone(),
description: None,
is_current: false,
actions: vec![Box::new(move |tx3: &AppEventSender| { actions: vec![Box::new(move |tx3: &AppEventSender| {
let hint = format!("commit {short}"); let hint = format!("commit {short}");
let prompt = format!( let prompt = format!(
@@ -2046,6 +2036,7 @@ impl ChatWidget {
})], })],
dismiss_on_select: true, dismiss_on_select: true,
search_value: Some(search_val), search_value: Some(search_val),
..Default::default()
}); });
} }
@@ -2255,8 +2246,6 @@ pub(crate) fn show_review_commit_picker_with_entries(
items.push(SelectionItem { items.push(SelectionItem {
name: subject.clone(), name: subject.clone(),
description: None,
is_current: false,
actions: vec![Box::new(move |tx3: &AppEventSender| { actions: vec![Box::new(move |tx3: &AppEventSender| {
let hint = format!("commit {short}"); let hint = format!("commit {short}");
let prompt = format!( let prompt = format!(
@@ -2271,6 +2260,7 @@ pub(crate) fn show_review_commit_picker_with_entries(
})], })],
dismiss_on_select: true, dismiss_on_select: true,
search_value: Some(search_val), search_value: Some(search_val),
..Default::default()
}); });
} }

View File

@@ -2,15 +2,15 @@
source: tui/src/chatwidget/tests.rs source: tui/src/chatwidget/tests.rs
expression: terminal.backend().vt100().screen().contents() 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 $ echo hello world
1. Approve and run now Run this command one time 1. Yes, proceed
2. Always approve this session Automatically approve this command for the 2. Yes, and don't ask again for this command
rest of the session 3. No, and tell Codex what to do differently esc
3. Cancel Do not run the command
Press enter to confirm or esc to cancel Press enter to confirm or esc to cancel

View File

@@ -1,17 +1,13 @@
--- ---
source: tui/src/chatwidget/tests.rs source: tui/src/chatwidget/tests.rs
expression: terminal.backend() expression: terminal.backend().vt100().screen().contents()
--- ---
" " Would you like to run the following command?
" "
" Allow command? " $ echo hello world
" "
" $ echo hello world " 1. Yes, proceed
" " 2. Yes, and don't ask again for this command
" 1. Approve and run now Run this command one time " 3. No, and tell Codex what to do differently esc
" 2. Always approve this session Automatically approve this command for the "
" rest of the session " Press enter to confirm or esc to cancel
" 3. Cancel Do not run the command "
" "
" Press enter to confirm or esc to cancel "
" "

View File

@@ -1,19 +1,17 @@
--- ---
source: tui/src/chatwidget/tests.rs source: tui/src/chatwidget/tests.rs
expression: terminal.backend() expression: terminal.backend().vt100().screen().contents()
--- ---
" " Would you like to make the following edits?
" "
" Apply changes? " README.md (+2 -0)
" "
" README.md (+2 -0) " 1 +hello
" 1 +hello " 2 +world
" 2 +world "
" " The model wants to apply changes
" The model wants to apply changes "
" " 1. Yes, proceed
" 1. Approve Apply the proposed changes " 2. No, and tell Codex what to do differently esc
" 2. Cancel Do not apply the changes "
" " Press enter to confirm or esc to cancel
" Press enter to confirm or esc to cancel "
" "

View File

@@ -7,16 +7,16 @@ Buffer {
content: [ 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 ", " $ echo hello world ",
" ", " ",
" 1. Approve and run now Run this command one time ", " 1. Yes, proceed ",
" 2. Always approve this session Automatically approve this command for the ", " 2. Yes, and don't ask again for this command ",
" rest of the session ", " 3. No, and tell Codex what to do differently esc ",
" 3. Cancel Do not run the command ",
" ", " ",
" Press enter to confirm or esc to cancel ", " Press enter to confirm or esc to cancel ",
" ", " ",
@@ -24,18 +24,15 @@ Buffer {
styles: [ styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, 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: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
x: 16, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 46, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 2, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, x: 10, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC,
x: 71, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 73, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 8, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD, x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC,
x: 34, y: 8, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD | DIM, x: 7, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 59, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 9, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
x: 34, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 17, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 76, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 47, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 34, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 50, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
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: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 0, y: 14, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 14, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
] ]

View File

@@ -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' " " $ echo 'hello world' "
" " " "
" 1. Approve and run now Run this command one time " " 1. Yes, proceed "
" 2. Always approve this session Automatically approve this command for the " " 2. Yes, and don't ask again for this command "
" rest of the session " " 3. No, and tell Codex what to do differently esc "
" 3. Cancel Do not run the command "
" " " "
" Press enter to confirm or esc to cancel " " Press enter to confirm or esc to cancel "
" " " "

View File

@@ -1236,6 +1236,14 @@ fn approval_modal_exec_snapshot() {
terminal terminal
.draw(|f| f.render_widget_ref(&chat, f.area())) .draw(|f| f.render_widget_ref(&chat, f.area()))
.expect("draw approval modal"); .expect("draw approval modal");
assert!(
terminal
.backend()
.vt100()
.screen()
.contents()
.contains("echo hello world")
);
assert_snapshot!( assert_snapshot!(
"approval_modal_exec", "approval_modal_exec",
terminal.backend().vt100().screen().contents() terminal.backend().vt100().screen().contents()
@@ -1261,12 +1269,16 @@ fn approval_modal_exec_without_reason_snapshot() {
}); });
let height = chat.desired_height(80); let height = chat.desired_height(80);
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height)) let mut terminal =
.expect("create terminal"); ratatui::Terminal::new(VT100Backend::new(80, height)).expect("create terminal");
terminal.set_viewport_area(Rect::new(0, 0, 80, height));
terminal terminal
.draw(|f| f.render_widget_ref(&chat, f.area())) .draw(|f| f.render_widget_ref(&chat, f.area()))
.expect("draw approval modal (no reason)"); .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 // Snapshot test: patch approval modal
@@ -1296,12 +1308,16 @@ fn approval_modal_patch_snapshot() {
// Render at the widget's desired height and snapshot. // Render at the widget's desired height and snapshot.
let height = chat.desired_height(80); let height = chat.desired_height(80);
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height)) let mut terminal =
.expect("create terminal"); ratatui::Terminal::new(VT100Backend::new(80, height)).expect("create terminal");
terminal.set_viewport_area(Rect::new(0, 0, 80, height));
terminal terminal
.draw(|f| f.render_widget_ref(&chat, f.area())) .draw(|f| f.render_widget_ref(&chat, f.area()))
.expect("draw patch approval modal"); .expect("draw patch approval modal");
assert_snapshot!("approval_modal_patch", terminal.backend()); assert_snapshot!(
"approval_modal_patch",
terminal.backend().vt100().screen().contents()
);
} }
#[test] #[test]
@@ -1831,14 +1847,14 @@ fn apply_patch_untrusted_shows_approval_modal() {
for x in 0..area.width { for x in 0..area.width {
row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' ')); 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; contains_title = true;
break; break;
} }
} }
assert!( assert!(
contains_title, 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?'"
); );
} }

View File

@@ -64,6 +64,7 @@ impl From<DiffSummary> for Box<dyn Renderable> {
path.push_span(" "); path.push_span(" ");
path.extend(render_line_count_summary(row.added, row.removed)); path.extend(render_line_count_summary(row.added, row.removed));
rows.push(Box::new(path)); rows.push(Box::new(path));
rows.push(Box::new(RtLine::from("")));
rows.push(Box::new(row.change)); rows.push(Box::new(row.change));
} }