syncable_cli/agent/ui/
plan_menu.rs1use colored::Colorize;
9use inquire::ui::{Color, IndexPrefix, RenderConfig, StyleSheet, Styled};
10use inquire::{InquireError, Select, Text};
11
12#[derive(Debug, Clone)]
14pub enum PlanActionResult {
15 ExecuteAutoAccept,
17 ExecuteWithReview,
19 ChangePlan(String),
21 Cancel,
23}
24
25fn get_plan_menu_render_config() -> RenderConfig<'static> {
27 RenderConfig::default()
28 .with_highlighted_option_prefix(Styled::new("▸ ").with_fg(Color::LightCyan))
29 .with_option_index_prefix(IndexPrefix::Simple)
30 .with_selected_option(Some(StyleSheet::new().with_fg(Color::LightCyan)))
31 .with_scroll_up_prefix(Styled::new("▲ "))
32 .with_scroll_down_prefix(Styled::new("▼ "))
33}
34
35fn display_plan_box(plan_path: &str, task_count: usize) {
37 let term_width = term_size::dimensions().map(|(w, _)| w).unwrap_or(80);
38 let box_width = term_width.min(70);
39 let inner_width = box_width - 4;
40
41 println!(
43 "{}",
44 format!(
45 "{}{}{}",
46 "┌─ Plan Created ".bright_green(),
47 "─".repeat(inner_width.saturating_sub(15)).dimmed(),
48 "┐".dimmed()
49 )
50 );
51
52 let path_display = format!(" {}", plan_path);
54 println!(
55 "{}{}{}{}",
56 "│".dimmed(),
57 path_display.cyan(),
58 " ".repeat(inner_width.saturating_sub(path_display.len())),
59 "│".dimmed()
60 );
61
62 let tasks_display = format!(" {} tasks ready to execute", task_count);
64 println!(
65 "{}{}{}{}",
66 "│".dimmed(),
67 tasks_display.white(),
68 " ".repeat(inner_width.saturating_sub(tasks_display.len())),
69 "│".dimmed()
70 );
71
72 println!(
74 "{}",
75 format!(
76 "{}{}{}",
77 "└".dimmed(),
78 "─".repeat(box_width - 2).dimmed(),
79 "┘".dimmed()
80 )
81 );
82 println!();
83}
84
85pub fn show_plan_action_menu(plan_path: &str, task_count: usize) -> PlanActionResult {
92 display_plan_box(plan_path, task_count);
93
94 let options = vec![
95 "Execute and auto-accept changes".to_string(),
96 "Execute and review each change".to_string(),
97 "Change something in the plan".to_string(),
98 ];
99
100 println!("{}", "What would you like to do?".white());
101
102 let selection = Select::new("", options.clone())
103 .with_render_config(get_plan_menu_render_config())
104 .with_page_size(3)
105 .with_help_message("↑↓ to move, Enter to select, Esc to cancel")
106 .prompt();
107
108 match selection {
109 Ok(answer) => {
110 if answer == options[0] {
111 println!("{}", "→ Will execute plan with auto-accept".green());
112 PlanActionResult::ExecuteAutoAccept
113 } else if answer == options[1] {
114 println!(
115 "{}",
116 "→ Will execute plan with review for each change".yellow()
117 );
118 PlanActionResult::ExecuteWithReview
119 } else {
120 println!();
122 match Text::new("What should be changed in the plan?")
123 .with_help_message("Press Enter to submit, Esc to cancel")
124 .prompt()
125 {
126 Ok(feedback) if !feedback.trim().is_empty() => {
127 PlanActionResult::ChangePlan(feedback)
128 }
129 _ => PlanActionResult::Cancel,
130 }
131 }
132 }
133 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
134 println!("{}", "Plan execution cancelled.".dimmed());
135 PlanActionResult::Cancel
136 }
137 Err(_) => PlanActionResult::Cancel,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
149 fn test_plan_action_result_variants() {
150 let _ = PlanActionResult::ExecuteAutoAccept;
152 let _ = PlanActionResult::ExecuteWithReview;
153 let _ = PlanActionResult::ChangePlan("test".to_string());
154 let _ = PlanActionResult::Cancel;
155 }
156}