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:
@@ -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'))],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 "
|
|
||||||
" "
|
|
||||||
|
|||||||
@@ -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 "
|
|
||||||
" "
|
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -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?'"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user