use std::path::PathBuf; use codex_core::config::set_project_trusted; use codex_core::git_info::resolve_root_git_project_for_trust; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; use crate::key_hint; use crate::onboarding::onboarding_screen::KeyboardHandler; use crate::onboarding::onboarding_screen::StepStateProvider; use crate::render::Insets; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; use crate::render::renderable::RenderableExt as _; use crate::selection_list::selection_option_row; use super::onboarding_screen::StepState; pub(crate) struct TrustDirectoryWidget { pub codex_home: PathBuf, pub cwd: PathBuf, pub is_git_repo: bool, pub selection: Option, pub highlighted: TrustDirectorySelection, pub error: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TrustDirectorySelection { Trust, DontTrust, } impl WidgetRef for &TrustDirectoryWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let mut column = ColumnRenderable::new(); column.push(Line::from(vec![ "> ".into(), "You are running Codex in ".bold(), self.cwd.to_string_lossy().to_string().into(), ])); column.push(""); let guidance = if self.is_git_repo { "Since this folder is version controlled, you may wish to allow Codex to work in this folder without asking for approval." } else { "Since this folder is not version controlled, we recommend requiring approval of all edits and commands." }; column.push( Paragraph::new(guidance.to_string()) .wrap(Wrap { trim: true }) .inset(Insets::tlbr(0, 2, 0, 0)), ); column.push(""); let mut options: Vec<(&str, TrustDirectorySelection)> = Vec::new(); if self.is_git_repo { options.push(( "Yes, allow Codex to work in this folder without asking for approval", TrustDirectorySelection::Trust, )); options.push(( "No, ask me to approve edits and commands", TrustDirectorySelection::DontTrust, )); } else { options.push(( "Allow Codex to work in this folder without asking for approval", TrustDirectorySelection::Trust, )); options.push(( "Require approval of edits and commands", TrustDirectorySelection::DontTrust, )); } for (idx, (text, selection)) in options.iter().enumerate() { column.push(selection_option_row( idx, text.to_string(), self.highlighted == *selection, )); } column.push(""); if let Some(error) = &self.error { column.push( Paragraph::new(error.to_string()) .red() .wrap(Wrap { trim: true }) .inset(Insets::tlbr(0, 2, 0, 0)), ); column.push(""); } column.push( Line::from(vec![ "Press ".dim(), key_hint::plain(KeyCode::Enter).into(), " to continue".dim(), ]) .inset(Insets::tlbr(0, 2, 0, 0)), ); column.render(area, buf); } } impl KeyboardHandler for TrustDirectoryWidget { fn handle_key_event(&mut self, key_event: KeyEvent) { if key_event.kind == KeyEventKind::Release { return; } match key_event.code { KeyCode::Up | KeyCode::Char('k') => { self.highlighted = TrustDirectorySelection::Trust; } KeyCode::Down | KeyCode::Char('j') => { self.highlighted = TrustDirectorySelection::DontTrust; } KeyCode::Char('1') | KeyCode::Char('y') => self.handle_trust(), KeyCode::Char('2') | KeyCode::Char('n') => self.handle_dont_trust(), KeyCode::Enter => match self.highlighted { TrustDirectorySelection::Trust => self.handle_trust(), TrustDirectorySelection::DontTrust => self.handle_dont_trust(), }, _ => {} } } } impl StepStateProvider for TrustDirectoryWidget { fn get_step_state(&self) -> StepState { match self.selection { Some(_) => StepState::Complete, None => StepState::InProgress, } } } impl TrustDirectoryWidget { fn handle_trust(&mut self) { let target = resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone()); if let Err(e) = set_project_trusted(&self.codex_home, &target) { tracing::error!("Failed to set project trusted: {e:?}"); self.error = Some(format!("Failed to set trust for {}: {e}", target.display())); } self.selection = Some(TrustDirectorySelection::Trust); } fn handle_dont_trust(&mut self) { self.highlighted = TrustDirectorySelection::DontTrust; self.selection = Some(TrustDirectorySelection::DontTrust); } } #[cfg(test)] mod tests { use crate::test_backend::VT100Backend; use super::*; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; use pretty_assertions::assert_eq; use ratatui::Terminal; use std::path::PathBuf; #[test] fn release_event_does_not_change_selection() { let mut widget = TrustDirectoryWidget { codex_home: PathBuf::from("."), cwd: PathBuf::from("."), is_git_repo: false, selection: None, highlighted: TrustDirectorySelection::DontTrust, error: None, }; let release = KeyEvent { kind: KeyEventKind::Release, ..KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE) }; widget.handle_key_event(release); assert_eq!(widget.selection, None); let press = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE); widget.handle_key_event(press); assert_eq!(widget.selection, Some(TrustDirectorySelection::DontTrust)); } #[test] fn renders_snapshot_for_git_repo() { let widget = TrustDirectoryWidget { codex_home: PathBuf::from("."), cwd: PathBuf::from("/workspace/project"), is_git_repo: true, selection: None, highlighted: TrustDirectorySelection::Trust, error: None, }; let mut terminal = Terminal::new(VT100Backend::new(70, 14)).expect("terminal"); terminal .draw(|f| (&widget).render_ref(f.area(), f.buffer_mut())) .expect("draw"); insta::assert_snapshot!(terminal.backend()); } }