From 25a2e15ec5cc58becdec269651386663fc7a4694 Mon Sep 17 00:00:00 2001
From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com>
Date: Thu, 2 Oct 2025 14:41:29 -0700
Subject: [PATCH] tui: tweaks to dialog display (#4622)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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.
---
.../tui/src/bottom_pane/approval_overlay.rs | 86 ++++++++++---------
codex-rs/tui/src/bottom_pane/command_popup.rs | 1 +
.../tui/src/bottom_pane/file_search_popup.rs | 1 +
.../src/bottom_pane/list_selection_view.rs | 38 ++++----
.../src/bottom_pane/selection_popup_common.rs | 9 ++
codex-rs/tui/src/chatwidget.rs | 30 +++----
...hatwidget__tests__approval_modal_exec.snap | 12 +--
..._tests__approval_modal_exec_no_reason.snap | 24 +++---
...atwidget__tests__approval_modal_patch.snap | 30 +++----
...dget__tests__exec_approval_modal_exec.snap | 33 ++++---
...sts__status_widget_and_approval_modal.snap | 12 +--
codex-rs/tui/src/chatwidget/tests.rs | 32 +++++--
codex-rs/tui/src/diff_render.rs | 1 +
13 files changed, 159 insertions(+), 150 deletions(-)
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));
}