use std::path::PathBuf;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::KeyBindingListExt;use crate::onboarding::keys;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 cwd: PathBuf, pub trust_target: PathBuf, pub show_windows_create_sandbox_hint: bool, pub should_quit: bool, pub selection: Option<TrustDirectorySelection>, pub highlighted: TrustDirectorySelection, pub error: Option<String>,}#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum TrustDirectorySelection { Trust, Quit,}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 in ".bold(), self.cwd.to_string_lossy().to_string().into(), ])); column.push(""); if self.cwd != self.trust_target { #[allow(clippy::disallowed_methods)] let git_root_warning = Paragraph::new(format!( "Note: You’re in a subdirectory of a Git project. Trusting will apply to the repository root: {}", self.trust_target.display() )) .yellow(); column.push( git_root_warning .wrap(Wrap { trim: true }) .inset(Insets::tlbr( /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, )), ); column.push(""); } column.push( Paragraph::new( "Do you trust the contents of this directory? Working with untrusted \ contents comes with higher risk of prompt injection. Trusting the \ directory allows project-local config, hooks, and exec policies to load." .to_string(), ) .wrap(Wrap { trim: true }) .inset(Insets::tlbr( /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, )), ); column.push(""); let options: Vec<(&str, TrustDirectorySelection)> = vec![ ("Yes, continue", TrustDirectorySelection::Trust), ("No, quit", TrustDirectorySelection::Quit), ]; 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( /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, )), ); column.push(""); } column.push( Line::from(vec![ "Press ".dim(), keys::CONFIRM[0].into(), if self.show_windows_create_sandbox_hint { " to continue and create a sandbox...".dim() } else { " to continue".dim() }, ]) .inset(Insets::tlbr( /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 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; } if keys::MOVE_UP.is_pressed(key_event) { self.highlighted = TrustDirectorySelection::Trust; } else if keys::MOVE_DOWN.is_pressed(key_event) { self.highlighted = TrustDirectorySelection::Quit; } else if keys::SELECT_FIRST.is_pressed(key_event) { self.handle_trust(); } else if keys::SELECT_SECOND.is_pressed(key_event) || keys::QUIT.is_pressed(key_event) || keys::CANCEL.is_pressed(key_event) { self.handle_quit(); } else if keys::CONFIRM.is_pressed(key_event) { match self.highlighted { TrustDirectorySelection::Trust => self.handle_trust(), TrustDirectorySelection::Quit => self.handle_quit(), } } }}impl StepStateProvider for TrustDirectoryWidget { fn get_step_state(&self) -> StepState { if self.selection.is_some() || self.should_quit { StepState::Complete } else { StepState::InProgress } }}impl TrustDirectoryWidget { fn handle_trust(&mut self) { self.highlighted = TrustDirectorySelection::Trust; self.error = None; self.selection = Some(TrustDirectorySelection::Trust); } fn handle_quit(&mut self) { self.highlighted = TrustDirectorySelection::Quit; self.should_quit = true; } pub fn should_quit(&self) -> bool { self.should_quit }}#[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; fn widget(error: Option<String>) -> TrustDirectoryWidget { TrustDirectoryWidget { cwd: PathBuf::from("/workspace/project"), trust_target: PathBuf::from("/workspace/project"), show_windows_create_sandbox_hint: false, should_quit: false, selection: None, highlighted: TrustDirectorySelection::Trust, error, } } #[test] fn release_event_does_not_change_selection() { let mut widget = TrustDirectoryWidget { cwd: PathBuf::from("."), trust_target: PathBuf::from("."), show_windows_create_sandbox_hint: false, should_quit: false, selection: None, highlighted: TrustDirectorySelection::Quit, 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!(widget.should_quit); } #[test] fn renders_snapshot_for_git_repo() { let widget = widget(/*error*/ None); let mut terminal = Terminal::new(VT100Backend::new(/*width*/ 70, /*height*/ 14)).expect("terminal"); terminal .draw(|f| (&widget).render_ref(f.area(), f.buffer_mut())) .expect("draw"); insta::assert_snapshot!(terminal.backend()); } #[test] fn renders_snapshot_for_trust_error() { let widget = widget(Some( "Failed to set trust for /workspace/project: config/batchWrite failed in TUI: Invalid configuration: features.fast_mode=true is not supported; allowed set [fast_mode=false]" .to_string(), )); let mut terminal = Terminal::new(VT100Backend::new(/*width*/ 70, /*height*/ 18)).expect("terminal"); terminal .draw(|f| (&widget).render_ref(f.area(), f.buffer_mut())) .expect("draw"); insta::assert_snapshot!(terminal.backend()); }}