use std::cell::RefCell;
use std::path::{Path, PathBuf};
use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use crate::localization::Locale;
use crate::sandbox::SandboxPolicy;
use crate::tui::views::{ModalKind, ModalView, ViewAction, ViewEvent};
use crate::tui::widgets::{ElevationWidget, Renderable};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ElevationOption {
WithNetwork,
WithWriteAccess(Vec<PathBuf>),
FullAccess,
Abort,
}
impl ElevationOption {
#[cfg(test)]
pub fn label(&self) -> &'static str {
match self {
ElevationOption::WithNetwork => "Allow outbound network",
ElevationOption::WithWriteAccess(_) => "Allow extra write access",
ElevationOption::FullAccess => "Full access (filesystem + network)",
ElevationOption::Abort => "Abort",
}
}
#[cfg(test)]
pub fn description(&self) -> &'static str {
match self {
ElevationOption::WithNetwork => {
"Retry this tool call with outbound network access for downloads and HTTP requests"
}
ElevationOption::WithWriteAccess(_) => {
"Retry this tool call with additional writable filesystem scope"
}
ElevationOption::FullAccess => {
"Retry without sandbox limits; grants unrestricted filesystem and network access"
}
ElevationOption::Abort => "Cancel this tool execution",
}
}
pub fn to_policy(&self, base_cwd: &Path) -> SandboxPolicy {
match self {
ElevationOption::WithNetwork => SandboxPolicy::workspace_with_network(),
ElevationOption::WithWriteAccess(paths) => {
let mut roots = paths.clone();
roots.push(base_cwd.to_path_buf());
SandboxPolicy::workspace_with_roots(roots, false)
}
ElevationOption::FullAccess => SandboxPolicy::DangerFullAccess,
ElevationOption::Abort => SandboxPolicy::default(), }
}
}
#[derive(Debug, Clone)]
pub struct ElevationRequest {
pub tool_id: String,
pub tool_name: String,
pub command: Option<String>,
pub denial_reason: String,
pub options: Vec<ElevationOption>,
}
impl ElevationRequest {
pub fn for_shell(
tool_id: &str,
command: &str,
denial_reason: &str,
blocked_network: bool,
blocked_write: bool,
) -> Self {
let mut options = Vec::new();
if blocked_network {
options.push(ElevationOption::WithNetwork);
}
if blocked_write {
options.push(ElevationOption::WithWriteAccess(vec![]));
}
options.push(ElevationOption::FullAccess);
options.push(ElevationOption::Abort);
Self {
tool_id: tool_id.to_string(),
tool_name: "exec_shell".to_string(),
command: Some(command.to_string()),
denial_reason: denial_reason.to_string(),
options,
}
}
#[allow(dead_code)]
pub fn generic(tool_id: &str, tool_name: &str, denial_reason: &str) -> Self {
Self {
tool_id: tool_id.to_string(),
tool_name: tool_name.to_string(),
command: None,
denial_reason: denial_reason.to_string(),
options: vec![
ElevationOption::WithNetwork,
ElevationOption::FullAccess,
ElevationOption::Abort,
],
}
}
}
#[derive(Debug, Clone)]
pub struct ElevationView {
request: ElevationRequest,
pub(super) selected: usize,
locale: Locale,
row_hitboxes: RefCell<Vec<Rect>>,
}
impl ElevationView {
pub fn new(request: ElevationRequest, locale: Locale) -> Self {
Self {
request,
selected: 0,
locale,
row_hitboxes: RefCell::new(Vec::new()),
}
}
fn select_prev(&mut self) {
self.selected =
crate::tui::list_nav::wrap_index(self.selected, self.request.options.len(), -1);
}
fn select_next(&mut self) {
self.selected =
crate::tui::list_nav::wrap_index(self.selected, self.request.options.len(), 1);
}
fn current_option(&self) -> &ElevationOption {
&self.request.options[self.selected]
}
fn emit_decision(&self, option: ElevationOption) -> ViewAction {
ViewAction::EmitAndClose(ViewEvent::ElevationDecision {
tool_id: self.request.tool_id.clone(),
tool_name: self.request.tool_name.clone(),
option,
})
}
#[allow(dead_code)]
pub fn request(&self) -> &ElevationRequest {
&self.request
}
#[allow(dead_code)]
pub fn selected(&self) -> usize {
self.selected
}
}
impl ModalView for ElevationView {
fn kind(&self) -> ModalKind {
ModalKind::Elevation
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn handle_key(&mut self, key: KeyEvent) -> ViewAction {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
self.select_prev();
ViewAction::None
}
KeyCode::Down | KeyCode::Char('j') => {
self.select_next();
ViewAction::None
}
KeyCode::Enter => self.emit_decision(self.current_option().clone()),
KeyCode::Char('n') => self.emit_decision(ElevationOption::WithNetwork),
KeyCode::Char('w') => {
for opt in &self.request.options {
if matches!(opt, ElevationOption::WithWriteAccess(_)) {
return self.emit_decision(opt.clone());
}
}
ViewAction::None
}
KeyCode::Char('f') => self.emit_decision(ElevationOption::FullAccess),
KeyCode::Esc | KeyCode::Char('a') => self.emit_decision(ElevationOption::Abort),
_ => ViewAction::None,
}
}
fn handle_mouse(&mut self, mouse: MouseEvent) -> ViewAction {
match mouse.kind {
MouseEventKind::ScrollUp => {
self.select_prev();
ViewAction::None
}
MouseEventKind::ScrollDown => {
self.select_next();
ViewAction::None
}
MouseEventKind::Down(MouseButton::Left) => {
let clicked = self.row_hitboxes.borrow().iter().position(|rect| {
rect.contains(ratatui::layout::Position::new(mouse.column, mouse.row))
});
if let Some(index) = clicked {
return self.emit_decision(self.request.options[index].clone());
}
ViewAction::None
}
_ => ViewAction::None,
}
}
fn render(&self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
let elevation_widget = ElevationWidget::new_with_hitboxes(
&self.request,
self.selected,
self.locale,
&self.row_hitboxes,
);
elevation_widget.render(area, buf);
}
}