mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
improvements, danger widget
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::chatwidget::ChatWidget;
|
||||
use crate::command_utils::strip_surrounding_quotes;
|
||||
use crate::danger_warning_screen::DangerWarningOutcome;
|
||||
use crate::danger_warning_screen::DangerWarningScreen;
|
||||
use crate::file_search::FileSearchManager;
|
||||
use crate::get_git_diff::get_git_diff;
|
||||
use crate::git_warning_screen::GitWarningOutcome;
|
||||
@@ -16,8 +19,12 @@ use crossterm::SynchronizedUpdate;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use crossterm::execute as ct_execute;
|
||||
use crossterm::terminal::EnterAlternateScreen;
|
||||
use crossterm::terminal::LeaveAlternateScreen;
|
||||
use crossterm::terminal::supports_keyboard_enhancement;
|
||||
use ratatui::layout::Offset;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Backend;
|
||||
use ratatui::text::Line;
|
||||
use std::path::PathBuf;
|
||||
@@ -43,29 +50,19 @@ enum AppState<'a> {
|
||||
},
|
||||
/// The start-up warning that recommends running codex inside a Git repo.
|
||||
GitWarning { screen: GitWarningScreen },
|
||||
/// Full‑screen warning when switching to the fully‑unsafe execution mode.
|
||||
DangerWarning {
|
||||
screen: DangerWarningScreen,
|
||||
/// Retain the chat widget so background events can still be processed.
|
||||
widget: Box<ChatWidget<'a>>,
|
||||
pending_approval: codex_core::protocol::AskForApproval,
|
||||
pending_sandbox: codex_core::protocol::SandboxPolicy,
|
||||
},
|
||||
}
|
||||
|
||||
/// Strip a single pair of surrounding quotes from the provided string if present.
|
||||
/// Supports straight and common curly quotes: '…', "…", ‘…’, “…”.
|
||||
pub fn strip_surrounding_quotes(s: &str) -> &str {
|
||||
// Opening/closing pairs (note curly quotes differ on each side)
|
||||
const QUOTE_PAIRS: &[(char, char)] = &[('"', '"'), ('\'', '\''), ('“', '”'), ('‘', '’')];
|
||||
|
||||
let t = s.trim();
|
||||
if t.len() < 2 {
|
||||
return t;
|
||||
}
|
||||
|
||||
for &(open, close) in QUOTE_PAIRS {
|
||||
if t.starts_with(open) && t.ends_with(close) {
|
||||
let start = open.len_utf8();
|
||||
let end = t.len() - close.len_utf8();
|
||||
return &t[start..end];
|
||||
}
|
||||
}
|
||||
|
||||
t
|
||||
}
|
||||
// strip_surrounding_quotes moved to command_utils.rs
|
||||
|
||||
pub(crate) struct App<'a> {
|
||||
app_event_tx: AppEventSender,
|
||||
@@ -87,6 +84,13 @@ pub(crate) struct App<'a> {
|
||||
chat_args: Option<ChatWidgetArgs>,
|
||||
|
||||
enhanced_keys_supported: bool,
|
||||
/// One-shot flag to resync viewport and cursor after leaving the
|
||||
/// alternate-screen Danger warning so the composer stays at the bottom.
|
||||
fixup_viewport_after_danger: bool,
|
||||
/// If set, defer opening the DangerWarning screen until after the next
|
||||
/// redraw so any selection popups are cleared from the normal screen.
|
||||
pending_show_danger: Option<(codex_core::protocol::AskForApproval, codex_core::protocol::SandboxPolicy)>,
|
||||
last_bottom_pane_area: Option<Rect>,
|
||||
}
|
||||
|
||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||
@@ -102,6 +106,34 @@ struct ChatWidgetArgs {
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
/// Handle `/model <arg>` from the slash command dispatcher.
|
||||
fn handle_model_command(&mut self, args: &str) {
|
||||
let arg = args.trim();
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
let normalized = strip_surrounding_quotes(arg).trim().to_string();
|
||||
if !normalized.is_empty() {
|
||||
widget.update_model_and_reconfigure(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle `/approvals <preset>` from the slash command dispatcher.
|
||||
fn handle_approvals_command(&mut self, args: &str) {
|
||||
let arg = args.trim();
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
let normalized = strip_surrounding_quotes(arg).trim().to_string();
|
||||
if !normalized.is_empty() {
|
||||
use crate::command_utils::parse_execution_mode_token;
|
||||
if let Some((approval, sandbox)) = parse_execution_mode_token(&normalized) {
|
||||
widget.update_execution_mode_and_reconfigure(approval, sandbox);
|
||||
} else {
|
||||
widget.add_diff_output(format!(
|
||||
"`/approvals {normalized}` — unrecognized execution mode"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn new(
|
||||
config: Config,
|
||||
initial_prompt: Option<String>,
|
||||
@@ -202,6 +234,9 @@ impl App<'_> {
|
||||
pending_redraw,
|
||||
chat_args,
|
||||
enhanced_keys_supported,
|
||||
fixup_viewport_after_danger: false,
|
||||
pending_show_danger: None,
|
||||
last_bottom_pane_area: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +284,31 @@ impl App<'_> {
|
||||
}
|
||||
AppEvent::Redraw => {
|
||||
std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??;
|
||||
if let Some((approval, sandbox)) = self.pending_show_danger.take() {
|
||||
if let Some(area) = self.last_bottom_pane_area {
|
||||
use crossterm::cursor::MoveTo;
|
||||
use crossterm::terminal::{Clear, ClearType};
|
||||
use crossterm::queue;
|
||||
use std::io::Write;
|
||||
for y in area.y..area.bottom() {
|
||||
let _ = queue!(std::io::stdout(), MoveTo(0, y), Clear(ClearType::CurrentLine));
|
||||
}
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
if let AppState::Chat { widget } = std::mem::replace(
|
||||
&mut self.app_state,
|
||||
AppState::GitWarning { screen: GitWarningScreen::new() },
|
||||
) {
|
||||
let _ = ct_execute!(std::io::stdout(), EnterAlternateScreen);
|
||||
self.app_state = AppState::DangerWarning {
|
||||
screen: DangerWarningScreen::new(),
|
||||
widget,
|
||||
pending_approval: approval,
|
||||
pending_sandbox: sandbox,
|
||||
};
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::KeyEvent(key_event) => {
|
||||
match key_event {
|
||||
@@ -265,6 +325,9 @@ impl App<'_> {
|
||||
AppState::GitWarning { .. } => {
|
||||
// No-op.
|
||||
}
|
||||
AppState::DangerWarning { .. } => {
|
||||
// No-op on Ctrl+C in danger screen
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
@@ -287,6 +350,9 @@ impl App<'_> {
|
||||
AppState::GitWarning { .. } => {
|
||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||
}
|
||||
AppState::DangerWarning { .. } => {
|
||||
// Ignore Ctrl+D while danger screen is visible.
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
@@ -315,8 +381,24 @@ impl App<'_> {
|
||||
}
|
||||
}
|
||||
AppEvent::SelectExecutionMode { approval, sandbox } => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.update_execution_mode_and_reconfigure(approval, sandbox);
|
||||
// Intercept the dangerous preset with a full‑screen warning.
|
||||
if let AppState::Chat { widget: _ } = &self.app_state {
|
||||
if crate::command_utils::ExecutionPreset::from_policies(approval, &sandbox)
|
||||
== Some(crate::command_utils::ExecutionPreset::FullYolo)
|
||||
{
|
||||
// Defer opening the danger screen until after the next redraw so the
|
||||
// selection popup is closed and the normal screen is clean.
|
||||
self.pending_show_danger = Some((approval, sandbox));
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
} else if let AppState::Chat { widget } = std::mem::replace(
|
||||
&mut self.app_state,
|
||||
AppState::GitWarning { screen: GitWarningScreen::new() },
|
||||
) {
|
||||
// Restore chat state and apply immediately for safe presets.
|
||||
let mut w = widget;
|
||||
w.update_execution_mode_and_reconfigure(approval, sandbox);
|
||||
self.app_state = AppState::Chat { widget: w };
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::OpenModelSelector => {
|
||||
@@ -332,10 +414,12 @@ impl App<'_> {
|
||||
AppEvent::CodexOp(op) => match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.submit_op(op),
|
||||
AppState::GitWarning { .. } => {}
|
||||
AppState::DangerWarning { widget, .. } => widget.submit_op(op),
|
||||
},
|
||||
AppEvent::LatestLog(line) => match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.update_latest_log(line),
|
||||
AppState::GitWarning { .. } => {}
|
||||
AppState::DangerWarning { widget, .. } => widget.update_latest_log(line),
|
||||
},
|
||||
AppEvent::DispatchCommand(command) => match command {
|
||||
SlashCommand::New => {
|
||||
@@ -345,15 +429,11 @@ impl App<'_> {
|
||||
None,
|
||||
Vec::new(),
|
||||
self.enhanced_keys_supported,
|
||||
self
|
||||
.chat_args
|
||||
self.chat_args
|
||||
.as_ref()
|
||||
.map(|a| a.cli_flags_used.clone())
|
||||
.unwrap_or_default(),
|
||||
self
|
||||
.chat_args
|
||||
.as_ref()
|
||||
.and_then(|a| a.cli_model.clone()),
|
||||
self.chat_args.as_ref().and_then(|a| a.cli_model.clone()),
|
||||
));
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
@@ -435,45 +515,16 @@ impl App<'_> {
|
||||
}
|
||||
},
|
||||
AppEvent::DispatchCommandWithArgs(command, args) => match command {
|
||||
SlashCommand::Model => {
|
||||
let arg = args.trim();
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
// Normalize commonly quoted inputs like \"o3\" or 'o3' or “o3”.
|
||||
let normalized = strip_surrounding_quotes(arg).trim().to_string();
|
||||
if !normalized.is_empty() {
|
||||
widget.update_model_and_reconfigure(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Approvals => {
|
||||
let arg = args.trim();
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
let normalized = strip_surrounding_quotes(arg).trim().to_string();
|
||||
if !normalized.is_empty() {
|
||||
use crate::bottom_pane::selection_popup::parse_execution_mode_token;
|
||||
if let Some((approval, sandbox)) =
|
||||
parse_execution_mode_token(&normalized)
|
||||
{
|
||||
widget.update_execution_mode_and_reconfigure(approval, sandbox);
|
||||
} else {
|
||||
widget.add_diff_output(format!(
|
||||
"`/approvals {normalized}` — unrecognized execution mode"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Model => self.handle_model_command(&args),
|
||||
SlashCommand::Approvals => self.handle_approvals_command(&args),
|
||||
#[cfg(debug_assertions)]
|
||||
SlashCommand::TestApproval => {
|
||||
// Ignore args; forward to the existing no-args handler
|
||||
self.app_event_tx.send(AppEvent::DispatchCommand(command));
|
||||
}
|
||||
SlashCommand::New
|
||||
| SlashCommand::Quit
|
||||
| SlashCommand::Diff
|
||||
| SlashCommand::Compact => {
|
||||
// For other commands, fall back to existing handling.
|
||||
// We can ignore args for now.
|
||||
self.app_event_tx.send(AppEvent::DispatchCommand(command));
|
||||
}
|
||||
},
|
||||
@@ -496,13 +547,14 @@ impl App<'_> {
|
||||
match &self.app_state {
|
||||
AppState::Chat { widget } => widget.token_usage().clone(),
|
||||
AppState::GitWarning { .. } => codex_core::protocol::TokenUsage::default(),
|
||||
AppState::DangerWarning { widget, .. } => widget.token_usage().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_next_frame(&mut self, terminal: &mut tui::Tui) -> Result<()> {
|
||||
let screen_size = terminal.size()?;
|
||||
let last_known_screen_size = terminal.last_known_screen_size;
|
||||
if screen_size != last_known_screen_size {
|
||||
if screen_size != last_known_screen_size && !self.fixup_viewport_after_danger {
|
||||
let cursor_pos = terminal.get_cursor_position()?;
|
||||
let last_known_cursor_pos = terminal.last_known_cursor_pos;
|
||||
if cursor_pos.y != last_known_cursor_pos.y {
|
||||
@@ -524,8 +576,28 @@ impl App<'_> {
|
||||
let desired_height = match &self.app_state {
|
||||
AppState::Chat { widget } => widget.desired_height(size.width),
|
||||
AppState::GitWarning { .. } => 10,
|
||||
AppState::DangerWarning { .. } => size.height,
|
||||
};
|
||||
|
||||
// If we just left the alternate-screen danger modal, refresh our internal
|
||||
// cursor position and anchor the viewport to the bottom so the input bar
|
||||
// isn't pushed to the top. Additionally, scroll the region above the new
|
||||
// viewport down to eliminate any stale bottom-pane artifacts without
|
||||
// clearing the chat history.
|
||||
if self.fixup_viewport_after_danger {
|
||||
self.fixup_viewport_after_danger = false;
|
||||
let pos = terminal.get_cursor_position()?;
|
||||
terminal.last_known_cursor_pos = pos;
|
||||
let old_area = terminal.viewport_area;
|
||||
let mut new_area = old_area;
|
||||
new_area.height = desired_height.min(size.height);
|
||||
new_area.width = size.width;
|
||||
new_area.y = size.height.saturating_sub(new_area.height);
|
||||
if new_area != old_area {
|
||||
terminal.set_viewport_area(new_area);
|
||||
}
|
||||
}
|
||||
|
||||
let mut area = terminal.viewport_area;
|
||||
area.height = desired_height.min(size.height);
|
||||
area.width = size.width;
|
||||
@@ -549,9 +621,15 @@ impl App<'_> {
|
||||
match &mut self.app_state {
|
||||
AppState::Chat { widget } => {
|
||||
terminal.draw(|frame| frame.render_widget_ref(&**widget, frame.area()))?;
|
||||
self.last_bottom_pane_area = Some(area);
|
||||
}
|
||||
AppState::GitWarning { screen } => {
|
||||
terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
|
||||
self.last_bottom_pane_area = None;
|
||||
}
|
||||
AppState::DangerWarning { screen, .. } => {
|
||||
terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
|
||||
self.last_bottom_pane_area = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -591,6 +669,50 @@ impl App<'_> {
|
||||
// do nothing
|
||||
}
|
||||
},
|
||||
AppState::DangerWarning { screen, .. } => match screen.handle_key_event(key_event) {
|
||||
DangerWarningOutcome::Continue => {
|
||||
let taken = std::mem::replace(
|
||||
&mut self.app_state,
|
||||
AppState::GitWarning {
|
||||
screen: GitWarningScreen::new(),
|
||||
},
|
||||
);
|
||||
let _ = ct_execute!(std::io::stdout(), LeaveAlternateScreen);
|
||||
// After leaving the alternate screen, resync our viewport/cursor
|
||||
// so the chat composer stays anchored at the bottom.
|
||||
self.fixup_viewport_after_danger = true;
|
||||
if let AppState::DangerWarning {
|
||||
mut widget,
|
||||
pending_approval,
|
||||
pending_sandbox,
|
||||
..
|
||||
} = taken
|
||||
{
|
||||
let approval = pending_approval;
|
||||
let sandbox = pending_sandbox;
|
||||
widget.update_execution_mode_and_reconfigure(approval, sandbox);
|
||||
self.app_state = AppState::Chat { widget };
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
}
|
||||
DangerWarningOutcome::Cancel => {
|
||||
let taken = std::mem::replace(
|
||||
&mut self.app_state,
|
||||
AppState::GitWarning {
|
||||
screen: GitWarningScreen::new(),
|
||||
},
|
||||
);
|
||||
let _ = ct_execute!(std::io::stdout(), LeaveAlternateScreen);
|
||||
// After leaving the alternate screen, resync our viewport/cursor
|
||||
// so the chat composer stays anchored at the bottom.
|
||||
self.fixup_viewport_after_danger = true;
|
||||
if let AppState::DangerWarning { widget, .. } = taken {
|
||||
self.app_state = AppState::Chat { widget };
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
}
|
||||
DangerWarningOutcome::None => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +720,7 @@ impl App<'_> {
|
||||
match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.handle_paste(pasted),
|
||||
AppState::GitWarning { .. } => {}
|
||||
AppState::DangerWarning { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,6 +728,7 @@ impl App<'_> {
|
||||
match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.handle_codex_event(event),
|
||||
AppState::GitWarning { .. } => {}
|
||||
AppState::DangerWarning { widget, .. } => widget.handle_codex_event(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,10 @@ pub(crate) enum AppEvent {
|
||||
OpenModelSelector,
|
||||
|
||||
/// User selected an execution mode (approval + sandbox) from the dropdown or via /approvals.
|
||||
SelectExecutionMode { approval: AskForApproval, sandbox: SandboxPolicy },
|
||||
SelectExecutionMode {
|
||||
approval: AskForApproval,
|
||||
sandbox: SandboxPolicy,
|
||||
},
|
||||
|
||||
/// Request the app to open the approval selector (populate options and show popup).
|
||||
OpenApprovalSelector,
|
||||
|
||||
@@ -22,7 +22,7 @@ use super::file_search_popup::FileSearchPopup;
|
||||
use super::selection_popup::SelectionKind;
|
||||
use super::selection_popup::SelectionPopup;
|
||||
use super::selection_popup::SelectionValue;
|
||||
use super::selection_popup::parse_execution_mode_token;
|
||||
use crate::command_utils::parse_execution_mode_token;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
@@ -316,8 +316,7 @@ impl ChatComposer<'_> {
|
||||
) {
|
||||
match &mut self.active_popup {
|
||||
ActivePopup::Selection(popup) if popup.kind() == SelectionKind::Execution => {
|
||||
*popup =
|
||||
SelectionPopup::new_execution_modes(current_approval, current_sandbox);
|
||||
*popup = SelectionPopup::new_execution_modes(current_approval, current_sandbox);
|
||||
}
|
||||
_ => {
|
||||
self.active_popup = ActivePopup::Selection(SelectionPopup::new_execution_modes(
|
||||
@@ -576,13 +575,10 @@ impl ChatComposer<'_> {
|
||||
SelectionKind::Execution
|
||||
if cmd_token == SlashCommand::Approvals.command() =>
|
||||
{
|
||||
if let Some((approval, sandbox)) =
|
||||
parse_execution_mode_token(&args)
|
||||
if let Some((approval, sandbox)) = parse_execution_mode_token(&args)
|
||||
{
|
||||
self.app_event_tx.send(AppEvent::SelectExecutionMode {
|
||||
approval,
|
||||
sandbox,
|
||||
});
|
||||
self.app_event_tx
|
||||
.send(AppEvent::SelectExecutionMode { approval, sandbox });
|
||||
self.textarea.select_all();
|
||||
self.textarea.cut();
|
||||
self.pending_pastes.clear();
|
||||
@@ -1656,6 +1652,54 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: slash command with args is usually handled via the selection popup.
|
||||
|
||||
#[test]
|
||||
fn approvals_selection_full_yolo_emits_select_execution_mode() {
|
||||
use crate::app_event::AppEvent;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(true, sender, false);
|
||||
|
||||
// Open the execution selector popup with a benign current mode.
|
||||
composer.open_execution_selector(
|
||||
AskForApproval::OnFailure,
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
include_default_writable_roots: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Immediately move selection up once to wrap to the last item (Full yolo), then Enter.
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
// Expect a SelectExecutionMode with DangerFullAccess.
|
||||
let mut saw = false;
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::SelectExecutionMode { approval, sandbox }) => {
|
||||
assert_eq!(approval, AskForApproval::Never);
|
||||
assert!(matches!(sandbox, SandboxPolicy::DangerFullAccess));
|
||||
saw = true;
|
||||
break;
|
||||
}
|
||||
Ok(_) => continue,
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
assert!(saw, "expected SelectExecutionMode for Full yolo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_placeholder_deletion() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -168,12 +168,9 @@ mod tests {
|
||||
fn selection_list_query_and_navigation() {
|
||||
// Build a small list with aliases similar to execution-mode popup.
|
||||
let items = vec![
|
||||
SelectionItem::new("a", "Auto".to_string())
|
||||
.with_aliases(vec!["auto".into()]),
|
||||
SelectionItem::new("u", "Untrusted".to_string())
|
||||
.with_aliases(vec!["untrusted".into()]),
|
||||
SelectionItem::new("r", "Read only".to_string())
|
||||
.with_aliases(vec!["read-only".into()]),
|
||||
SelectionItem::new("a", "Auto".to_string()).with_aliases(vec!["auto".into()]),
|
||||
SelectionItem::new("u", "Untrusted".to_string()).with_aliases(vec!["untrusted".into()]),
|
||||
SelectionItem::new("r", "Read only".to_string()).with_aliases(vec!["read-only".into()]),
|
||||
];
|
||||
|
||||
let mut list = SelectionList::new(items);
|
||||
|
||||
@@ -8,14 +8,21 @@ use super::selection_list::SelectionItem;
|
||||
use super::selection_list::SelectionList;
|
||||
use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_popup_common::render_rows;
|
||||
use crate::command_utils::ExecutionPreset;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SelectionKind { Model, Execution }
|
||||
pub(crate) enum SelectionKind {
|
||||
Model,
|
||||
Execution,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum SelectionValue {
|
||||
Model(String),
|
||||
Execution { approval: AskForApproval, sandbox: SandboxPolicy },
|
||||
Execution {
|
||||
approval: AskForApproval,
|
||||
sandbox: SandboxPolicy,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) struct SelectionPopup {
|
||||
@@ -48,43 +55,18 @@ impl SelectionPopup {
|
||||
current_approval: AskForApproval,
|
||||
current_sandbox: &SandboxPolicy,
|
||||
) -> Self {
|
||||
fn display_name(approval: AskForApproval, sandbox: &SandboxPolicy) -> &'static str {
|
||||
match (approval, sandbox) {
|
||||
(AskForApproval::Never, SandboxPolicy::ReadOnly) => "Read only",
|
||||
(AskForApproval::OnFailure, SandboxPolicy::ReadOnly) => "Untrusted",
|
||||
(AskForApproval::OnFailure, SandboxPolicy::WorkspaceWrite { .. }) => "Auto",
|
||||
_ => "Custom",
|
||||
}
|
||||
}
|
||||
fn description_for(approval: AskForApproval, sandbox: &SandboxPolicy) -> &'static str {
|
||||
match (approval, sandbox) {
|
||||
(AskForApproval::Never, SandboxPolicy::ReadOnly) =>
|
||||
"never prompt; read-only filesystem (flags: --ask-for-approval never --sandbox read-only)",
|
||||
(AskForApproval::OnFailure, SandboxPolicy::ReadOnly) =>
|
||||
"ask to retry outside sandbox only on sandbox breach; read-only (flags: --ask-for-approval on-failure --sandbox read-only)",
|
||||
(AskForApproval::OnFailure, SandboxPolicy::WorkspaceWrite { .. }) =>
|
||||
"auto in workspace sandbox; ask to retry outside sandbox on breach (flags: --ask-for-approval on-failure --sandbox workspace-write)",
|
||||
_ => "custom combination",
|
||||
}
|
||||
}
|
||||
|
||||
let presets: Vec<(AskForApproval, SandboxPolicy)> = vec![
|
||||
(AskForApproval::Never, SandboxPolicy::ReadOnly),
|
||||
(AskForApproval::OnFailure, SandboxPolicy::ReadOnly),
|
||||
(
|
||||
AskForApproval::OnFailure,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
include_default_writable_roots: true,
|
||||
},
|
||||
),
|
||||
let presets: Vec<ExecutionPreset> = vec![
|
||||
ExecutionPreset::ReadOnly,
|
||||
ExecutionPreset::Untrusted,
|
||||
ExecutionPreset::Auto,
|
||||
ExecutionPreset::FullYolo,
|
||||
];
|
||||
|
||||
let mut items: Vec<SelectionItem<SelectionValue>> = Vec::new();
|
||||
for (a, s) in presets.into_iter() {
|
||||
let name = display_name(a, &s).to_string();
|
||||
let desc = Some(description_for(a, &s).to_string());
|
||||
for p in presets.into_iter() {
|
||||
let (a, s) = p.to_policies();
|
||||
let name = p.label().to_string();
|
||||
let desc = Some(p.description().to_string());
|
||||
let mut item = SelectionItem::new(
|
||||
SelectionValue::Execution {
|
||||
approval: a,
|
||||
@@ -93,18 +75,15 @@ impl SelectionPopup {
|
||||
name,
|
||||
)
|
||||
.with_description(desc);
|
||||
if a == current_approval
|
||||
&& matches!(
|
||||
(&s, current_sandbox),
|
||||
(SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly)
|
||||
| (SandboxPolicy::WorkspaceWrite { .. }, SandboxPolicy::WorkspaceWrite { .. })
|
||||
)
|
||||
{
|
||||
if ExecutionPreset::from_policies(current_approval, current_sandbox) == Some(p) {
|
||||
item = item.mark_current(true);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
Self { kind: SelectionKind::Execution, list: SelectionList::new(items) }
|
||||
Self {
|
||||
kind: SelectionKind::Execution,
|
||||
list: SelectionList::new(items),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn kind(&self) -> SelectionKind {
|
||||
@@ -143,40 +122,55 @@ impl WidgetRef for &SelectionPopup {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a free-form token to an execution preset (approval+sandbox).
|
||||
pub(crate) fn parse_execution_mode_token(
|
||||
s: &str,
|
||||
) -> Option<(AskForApproval, SandboxPolicy)> {
|
||||
let t = s.trim().to_ascii_lowercase();
|
||||
match t.as_str() {
|
||||
"read-only" => Some((AskForApproval::Never, SandboxPolicy::ReadOnly)),
|
||||
"untrusted" => Some((AskForApproval::OnFailure, SandboxPolicy::ReadOnly)),
|
||||
"auto" => Some((
|
||||
AskForApproval::OnFailure,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
include_default_writable_roots: true,
|
||||
},
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_execution_mode_token as parse;
|
||||
use crate::command_utils::parse_execution_mode_token as parse;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
|
||||
#[test]
|
||||
fn parse_approval_mode_aliases() {
|
||||
// Only accept the three canonical tokens
|
||||
assert!(matches!(parse("auto").unwrap(), (AskForApproval::OnFailure, SandboxPolicy::WorkspaceWrite { .. })));
|
||||
assert_eq!(parse("untrusted"), Some((AskForApproval::OnFailure, SandboxPolicy::ReadOnly)));
|
||||
assert_eq!(parse("read-only"), Some((AskForApproval::Never, SandboxPolicy::ReadOnly)));
|
||||
assert!(matches!(
|
||||
parse("auto").unwrap(),
|
||||
(
|
||||
AskForApproval::OnFailure,
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
)
|
||||
));
|
||||
assert_eq!(
|
||||
parse("untrusted"),
|
||||
Some((AskForApproval::OnFailure, SandboxPolicy::ReadOnly))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("read-only"),
|
||||
Some((AskForApproval::Never, SandboxPolicy::ReadOnly))
|
||||
);
|
||||
// Unknown and case/whitespace handling
|
||||
assert_eq!(parse("unknown"), None);
|
||||
assert_eq!(parse(" AUTO ").is_some(), true);
|
||||
assert!(parse(" AUTO ").is_some());
|
||||
assert_eq!(
|
||||
parse("full-yolo"),
|
||||
Some((AskForApproval::Never, SandboxPolicy::DangerFullAccess))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_selector_includes_full_yolo() {
|
||||
// Set a benign current mode; we only care about rows.
|
||||
let popup = super::SelectionPopup::new_execution_modes(
|
||||
AskForApproval::OnFailure,
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
include_default_writable_roots: true,
|
||||
},
|
||||
);
|
||||
let rows = popup.visible_rows();
|
||||
let labels: Vec<String> = rows.into_iter().map(|r| r.name).collect();
|
||||
assert!(
|
||||
labels.iter().any(|l| l.contains("Full yolo")),
|
||||
"selector should include 'Full yolo'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,12 +617,9 @@ impl ChatWidget<'_> {
|
||||
self.config.sandbox_policy = sandbox.clone();
|
||||
|
||||
if approval_changed || sandbox_changed {
|
||||
let label = match (approval, &sandbox) {
|
||||
(AskForApproval::Never, SandboxPolicy::ReadOnly) => "Read only",
|
||||
(AskForApproval::OnFailure, SandboxPolicy::ReadOnly) => "Untrusted",
|
||||
(AskForApproval::OnFailure, SandboxPolicy::WorkspaceWrite { .. }) => "Auto",
|
||||
_ => "Custom",
|
||||
};
|
||||
let label = crate::command_utils::ExecutionPreset::from_policies(approval, &sandbox)
|
||||
.map(|p| p.label())
|
||||
.unwrap_or("Custom");
|
||||
self.add_to_history(HistoryCell::new_background_event(format!(
|
||||
"Set execution mode to {label}."
|
||||
)));
|
||||
|
||||
202
codex-rs/tui/src/command_utils.rs
Normal file
202
codex-rs/tui/src/command_utils.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
//! Small shared helpers for slash-command argument parsing and execution-mode display.
|
||||
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
|
||||
/// Canonical execution presets that combine approval policy and sandbox policy.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExecutionPreset {
|
||||
/// never prompt; read-only FS
|
||||
ReadOnly,
|
||||
/// ask to retry outside sandbox only on sandbox breach; read-only FS
|
||||
Untrusted,
|
||||
/// auto within workspace sandbox; ask to retry outside on breach
|
||||
Auto,
|
||||
/// DANGEROUS: disables sandbox and approvals entirely.
|
||||
FullYolo,
|
||||
}
|
||||
|
||||
impl ExecutionPreset {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
ExecutionPreset::ReadOnly => "Read only",
|
||||
ExecutionPreset::Untrusted => "Untrusted",
|
||||
ExecutionPreset::Auto => "Auto",
|
||||
ExecutionPreset::FullYolo => "Full yolo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(self) -> &'static str {
|
||||
match self {
|
||||
ExecutionPreset::ReadOnly => {
|
||||
"never prompt; read-only filesystem (flags: --ask-for-approval never --sandbox read-only)"
|
||||
}
|
||||
ExecutionPreset::Untrusted => {
|
||||
"ask to retry outside sandbox only on sandbox breach; read-only (flags: --ask-for-approval on-failure --sandbox read-only)"
|
||||
}
|
||||
ExecutionPreset::Auto => {
|
||||
"auto in workspace sandbox; ask to retry outside sandbox on breach (flags: --ask-for-approval on-failure --sandbox workspace-write)"
|
||||
}
|
||||
ExecutionPreset::FullYolo => {
|
||||
"DANGEROUS: disables sandbox and approvals; the agent can run any commands with full system access (flags: --dangerously-bypass-approvals-and-sandbox)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonical CLI flags string for the preset.
|
||||
pub fn cli_flags(self) -> &'static str {
|
||||
match self {
|
||||
ExecutionPreset::ReadOnly => "--ask-for-approval never --sandbox read-only",
|
||||
ExecutionPreset::Untrusted => "--ask-for-approval on-failure --sandbox read-only",
|
||||
ExecutionPreset::Auto => "--ask-for-approval on-failure --sandbox workspace-write",
|
||||
ExecutionPreset::FullYolo => "--dangerously-bypass-approvals-and-sandbox",
|
||||
}
|
||||
}
|
||||
|
||||
/// Mapping from preset to policies.
|
||||
pub fn to_policies(self) -> (AskForApproval, SandboxPolicy) {
|
||||
match self {
|
||||
ExecutionPreset::ReadOnly => (AskForApproval::Never, SandboxPolicy::ReadOnly),
|
||||
ExecutionPreset::Untrusted => (AskForApproval::OnFailure, SandboxPolicy::ReadOnly),
|
||||
ExecutionPreset::Auto => (
|
||||
AskForApproval::OnFailure,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
include_default_writable_roots: true,
|
||||
},
|
||||
),
|
||||
ExecutionPreset::FullYolo => (AskForApproval::Never, SandboxPolicy::DangerFullAccess),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mapping from policies to a known preset.
|
||||
pub fn from_policies(
|
||||
approval: AskForApproval,
|
||||
sandbox: &SandboxPolicy,
|
||||
) -> Option<ExecutionPreset> {
|
||||
match (approval, sandbox) {
|
||||
(AskForApproval::Never, SandboxPolicy::ReadOnly) => Some(ExecutionPreset::ReadOnly),
|
||||
(AskForApproval::OnFailure, SandboxPolicy::ReadOnly) => {
|
||||
Some(ExecutionPreset::Untrusted)
|
||||
}
|
||||
(AskForApproval::OnFailure, SandboxPolicy::WorkspaceWrite { .. }) => {
|
||||
Some(ExecutionPreset::Auto)
|
||||
}
|
||||
(AskForApproval::Never, SandboxPolicy::DangerFullAccess)
|
||||
| (AskForApproval::OnFailure, SandboxPolicy::DangerFullAccess) => {
|
||||
Some(ExecutionPreset::FullYolo)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse one of the canonical tokens: read-only | untrusted | auto.
|
||||
pub fn parse_token(s: &str) -> Option<ExecutionPreset> {
|
||||
let t = s.trim().to_ascii_lowercase();
|
||||
let t = t.replace(' ', "-");
|
||||
match t.as_str() {
|
||||
"read-only" => Some(ExecutionPreset::ReadOnly),
|
||||
"untrusted" => Some(ExecutionPreset::Untrusted),
|
||||
"auto" => Some(ExecutionPreset::Auto),
|
||||
"full-yolo" => Some(ExecutionPreset::FullYolo),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip a single pair of surrounding quotes from the provided string if present.
|
||||
/// Supports straight and common curly quotes: '…', "…", ‘…’, “…”.
|
||||
pub fn strip_surrounding_quotes(s: &str) -> &str {
|
||||
// Opening/closing pairs (note curly quotes differ on each side)
|
||||
const QUOTE_PAIRS: &[(char, char)] = &[('\"', '\"'), ('\'', '\''), ('“', '”'), ('‘', '’')];
|
||||
|
||||
let t = s.trim();
|
||||
if t.len() < 2 {
|
||||
return t;
|
||||
}
|
||||
|
||||
for &(open, close) in QUOTE_PAIRS {
|
||||
if t.starts_with(open) && t.ends_with(close) {
|
||||
let start = open.len_utf8();
|
||||
let end = t.len() - close.len_utf8();
|
||||
return &t[start..end];
|
||||
}
|
||||
}
|
||||
|
||||
t
|
||||
}
|
||||
|
||||
/// Normalize a free-form token: trim whitespace and remove a single pair of surrounding quotes.
|
||||
pub fn normalize_token(s: &str) -> String {
|
||||
strip_surrounding_quotes(s).trim().to_string()
|
||||
}
|
||||
|
||||
/// Map an (approval, sandbox) pair to a concise preset label used in the UI.
|
||||
pub fn execution_mode_label(approval: AskForApproval, sandbox: &SandboxPolicy) -> &'static str {
|
||||
ExecutionPreset::from_policies(approval, sandbox)
|
||||
.map(|p| p.label())
|
||||
.unwrap_or("Custom")
|
||||
}
|
||||
|
||||
/// Describe the current execution preset including CLI flag equivalents.
|
||||
pub fn execution_mode_description(
|
||||
approval: AskForApproval,
|
||||
sandbox: &SandboxPolicy,
|
||||
) -> &'static str {
|
||||
ExecutionPreset::from_policies(approval, sandbox)
|
||||
.map(|p| p.description())
|
||||
.unwrap_or("custom combination")
|
||||
}
|
||||
|
||||
/// Parse a free-form token to an execution preset (approval+sandbox).
|
||||
pub fn parse_execution_mode_token(s: &str) -> Option<(AskForApproval, SandboxPolicy)> {
|
||||
ExecutionPreset::parse_token(s).map(|p| p.to_policies())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn strip_quotes_variants() {
|
||||
assert_eq!(strip_surrounding_quotes("\"o3\""), "o3");
|
||||
assert_eq!(strip_surrounding_quotes("'o3'"), "o3");
|
||||
assert_eq!(strip_surrounding_quotes("“o3”"), "o3");
|
||||
assert_eq!(strip_surrounding_quotes("‘o3’"), "o3");
|
||||
assert_eq!(strip_surrounding_quotes("o3"), "o3");
|
||||
assert_eq!(strip_surrounding_quotes(" o3 "), "o3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_execution_mode_aliases() {
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
let parse = parse_execution_mode_token;
|
||||
assert!(matches!(
|
||||
parse("auto").unwrap(),
|
||||
(
|
||||
AskForApproval::OnFailure,
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
)
|
||||
));
|
||||
assert_eq!(
|
||||
parse("untrusted"),
|
||||
Some((AskForApproval::OnFailure, SandboxPolicy::ReadOnly))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("read-only"),
|
||||
Some((AskForApproval::Never, SandboxPolicy::ReadOnly))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("full-yolo"),
|
||||
Some((AskForApproval::Never, SandboxPolicy::DangerFullAccess))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("Full Yolo"),
|
||||
Some((AskForApproval::Never, SandboxPolicy::DangerFullAccess))
|
||||
);
|
||||
assert_eq!(parse("unknown"), None);
|
||||
assert!(parse(" AUTO ").is_some());
|
||||
}
|
||||
}
|
||||
147
codex-rs/tui/src/danger_warning_screen.rs
Normal file
147
codex-rs/tui/src/danger_warning_screen.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
//! Full‑screen warning displayed when the user selects the fully‑unsafe
|
||||
//! execution preset (Full yolo). This screen blocks input until the user
|
||||
//! explicitly confirms or cancels the action.
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Alignment;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Direction;
|
||||
use ratatui::layout::Layout;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::BorderType;
|
||||
use ratatui::widgets::Borders;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
const DANGER_TEXT: &str = "You're about to disable both approvals and sandboxing.\n\
|
||||
This gives the agent full, unrestricted access to your system.\n\
|
||||
\n\
|
||||
This can seriously damage your computer. Only proceed if you fully understand the risks.";
|
||||
|
||||
pub(crate) enum DangerWarningOutcome {
|
||||
Continue,
|
||||
Cancel,
|
||||
None,
|
||||
}
|
||||
|
||||
pub(crate) struct DangerWarningScreen;
|
||||
|
||||
impl DangerWarningScreen {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub(crate) fn handle_key_event(&self, key_event: KeyEvent) -> DangerWarningOutcome {
|
||||
match key_event.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => DangerWarningOutcome::Continue,
|
||||
KeyCode::Char('n') | KeyCode::Esc | KeyCode::Char('q') => DangerWarningOutcome::Cancel,
|
||||
_ => DangerWarningOutcome::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for &DangerWarningScreen {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
const MIN_WIDTH: u16 = 45;
|
||||
const MIN_HEIGHT: u16 = 15;
|
||||
if area.width < MIN_WIDTH || area.height < MIN_HEIGHT {
|
||||
let p = Paragraph::new(DANGER_TEXT)
|
||||
.wrap(Wrap { trim: true })
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
p.render(area, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
let popup_width = std::cmp::max(MIN_WIDTH, (area.width as f32 * 0.6) as u16);
|
||||
let popup_height = std::cmp::max(MIN_HEIGHT, (area.height as f32 * 0.3) as u16);
|
||||
let popup_x = area.x + (area.width.saturating_sub(popup_width)) / 2;
|
||||
let popup_y = area.y + (area.height.saturating_sub(popup_height)) / 2;
|
||||
let popup_area = Rect::new(popup_x, popup_y, popup_width, popup_height);
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Plain)
|
||||
.title(Span::styled(
|
||||
"Danger: Full system access",
|
||||
Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
|
||||
));
|
||||
let inner = block.inner(popup_area);
|
||||
block.render(popup_area, buf);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(3), Constraint::Length(3)])
|
||||
.split(inner);
|
||||
|
||||
let text_block = Block::default().borders(Borders::ALL);
|
||||
let text_inner = text_block.inner(chunks[0]);
|
||||
text_block.render(chunks[0], buf);
|
||||
|
||||
let p = Paragraph::new(DANGER_TEXT)
|
||||
.wrap(Wrap { trim: true })
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
p.render(text_inner, buf);
|
||||
|
||||
let action_block = Block::default().borders(Borders::ALL);
|
||||
let action_inner = action_block.inner(chunks[1]);
|
||||
action_block.render(chunks[1], buf);
|
||||
|
||||
let action_text = Paragraph::new("press 'y' to proceed, 'n' to cancel")
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default().add_modifier(Modifier::BOLD));
|
||||
action_text.render(action_inner, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
#[test]
|
||||
fn keys_map_to_expected_outcomes() {
|
||||
let screen = DangerWarningScreen::new();
|
||||
// Continue confirmations
|
||||
assert!(matches!(
|
||||
screen.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)),
|
||||
DangerWarningOutcome::Continue
|
||||
));
|
||||
assert!(matches!(
|
||||
screen.handle_key_event(KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::SHIFT)),
|
||||
DangerWarningOutcome::Continue
|
||||
));
|
||||
|
||||
// Cancellations
|
||||
assert!(matches!(
|
||||
screen.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)),
|
||||
DangerWarningOutcome::Cancel
|
||||
));
|
||||
assert!(matches!(
|
||||
screen.handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)),
|
||||
DangerWarningOutcome::Cancel
|
||||
));
|
||||
assert!(matches!(
|
||||
screen.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
|
||||
DangerWarningOutcome::Cancel
|
||||
));
|
||||
|
||||
// Irrelevant key is ignored
|
||||
assert!(matches!(
|
||||
screen.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)),
|
||||
DangerWarningOutcome::None
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,9 @@ mod bottom_pane;
|
||||
mod chatwidget;
|
||||
mod citation_regex;
|
||||
mod cli;
|
||||
mod command_utils;
|
||||
mod custom_terminal;
|
||||
mod danger_warning_screen;
|
||||
mod exec_command;
|
||||
mod file_search;
|
||||
mod get_git_diff;
|
||||
@@ -229,11 +231,14 @@ fn run_ratatui_app(
|
||||
let mut cli_flags_used: Vec<String> = Vec::new();
|
||||
if let Some(ap) = cli.approval_policy {
|
||||
// kebab-case variants via clap ValueEnum Debug formatting is fine here.
|
||||
cli_flags_used.push(format!("--ask-for-approval {}", match ap {
|
||||
codex_common::ApprovalModeCliArg::Untrusted => "untrusted",
|
||||
codex_common::ApprovalModeCliArg::OnFailure => "on-failure",
|
||||
codex_common::ApprovalModeCliArg::Never => "never",
|
||||
}));
|
||||
cli_flags_used.push(format!(
|
||||
"--ask-for-approval {}",
|
||||
match ap {
|
||||
codex_common::ApprovalModeCliArg::Untrusted => "untrusted",
|
||||
codex_common::ApprovalModeCliArg::OnFailure => "on-failure",
|
||||
codex_common::ApprovalModeCliArg::Never => "never",
|
||||
}
|
||||
));
|
||||
}
|
||||
if let Some(sm) = cli.sandbox_mode {
|
||||
let mode = match sm {
|
||||
@@ -241,7 +246,7 @@ fn run_ratatui_app(
|
||||
codex_common::SandboxModeCliArg::WorkspaceWrite => "workspace-write",
|
||||
codex_common::SandboxModeCliArg::DangerFullAccess => "danger-full-access",
|
||||
};
|
||||
cli_flags_used.push(format!("--sandbox {}", mode));
|
||||
cli_flags_used.push(format!("--sandbox {mode}"));
|
||||
}
|
||||
if cli.full_auto {
|
||||
cli_flags_used.push("--full-auto".to_string());
|
||||
|
||||
Reference in New Issue
Block a user