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 "┌─ Plan Created ".bright_green(),
45 "─".repeat(inner_width.saturating_sub(15)).dimmed(),
46 "┐".dimmed()
47 );
48
49 let path_display = format!(" {}", plan_path);
51 println!(
52 "{}{}{}{}",
53 "│".dimmed(),
54 path_display.cyan(),
55 " ".repeat(inner_width.saturating_sub(path_display.len())),
56 "│".dimmed()
57 );
58
59 let tasks_display = format!(" {} tasks ready to execute", task_count);
61 println!(
62 "{}{}{}{}",
63 "│".dimmed(),
64 tasks_display.white(),
65 " ".repeat(inner_width.saturating_sub(tasks_display.len())),
66 "│".dimmed()
67 );
68
69 println!(
71 "{}{}{}",
72 "└".dimmed(),
73 "─".repeat(box_width - 2).dimmed(),
74 "┘".dimmed()
75 );
76 println!();
77}
78
79pub fn show_plan_action_menu(plan_path: &str, task_count: usize) -> PlanActionResult {
86 display_plan_box(plan_path, task_count);
87
88 let options = vec![
89 "Execute and auto-accept changes".to_string(),
90 "Execute and review each change".to_string(),
91 "Change something in the plan".to_string(),
92 ];
93
94 println!("{}", "What would you like to do?".white());
95
96 let selection = Select::new("", options.clone())
97 .with_render_config(get_plan_menu_render_config())
98 .with_page_size(3)
99 .with_help_message("↑↓ to move, Enter to select, Esc to cancel")
100 .prompt();
101
102 match selection {
103 Ok(answer) => {
104 if answer == options[0] {
105 println!("{}", "→ Will execute plan with auto-accept".green());
106 PlanActionResult::ExecuteAutoAccept
107 } else if answer == options[1] {
108 println!(
109 "{}",
110 "→ Will execute plan with review for each change".yellow()
111 );
112 PlanActionResult::ExecuteWithReview
113 } else {
114 println!();
116 match Text::new("What should be changed in the plan?")
117 .with_help_message("Press Enter to submit, Esc to cancel")
118 .prompt()
119 {
120 Ok(feedback) if !feedback.trim().is_empty() => {
121 PlanActionResult::ChangePlan(feedback)
122 }
123 _ => PlanActionResult::Cancel,
124 }
125 }
126 }
127 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
128 println!("{}", "Plan execution cancelled.".dimmed());
129 PlanActionResult::Cancel
130 }
131 Err(_) => PlanActionResult::Cancel,
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
143 fn test_plan_action_result_variants() {
144 let _ = PlanActionResult::ExecuteAutoAccept;
146 let _ = PlanActionResult::ExecuteWithReview;
147 let _ = PlanActionResult::ChangePlan("test".to_string());
148 let _ = PlanActionResult::Cancel;
149 }
150}