syncable_cli/agent/ui/
plan_menu.rs

1//! Interactive menu for post-plan actions
2//!
3//! Displays after a plan is created with options:
4//! 1. Execute and auto-accept changes
5//! 2. Execute and review each change
6//! 3. Change something - provide feedback
7
8use colored::Colorize;
9use inquire::ui::{Color, IndexPrefix, RenderConfig, StyleSheet, Styled};
10use inquire::{InquireError, Select, Text};
11
12/// Result of the plan action menu
13#[derive(Debug, Clone)]
14pub enum PlanActionResult {
15    /// Execute plan, auto-accept all file writes
16    ExecuteAutoAccept,
17    /// Execute plan, require confirmation for each file write
18    ExecuteWithReview,
19    /// User wants to change the plan, includes feedback
20    ChangePlan(String),
21    /// User cancelled (Esc or Ctrl+C)
22    Cancel,
23}
24
25/// Get custom render config for plan menu
26fn 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
35/// Display plan summary box
36fn 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    // Top border with title
42    println!(
43        "{}{}{}",
44        "┌─ Plan Created ".bright_green(),
45        "─".repeat(inner_width.saturating_sub(15)).dimmed(),
46        "┐".dimmed()
47    );
48
49    // Plan path
50    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    // Task count
60    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    // Bottom border
70    println!(
71        "{}{}{}",
72        "└".dimmed(),
73        "─".repeat(box_width - 2).dimmed(),
74        "┘".dimmed()
75    );
76    println!();
77}
78
79/// Show the post-plan action menu
80///
81/// Displays after a plan is created, offering execution options:
82/// 1. Execute and auto-accept - runs all tasks without confirmation prompts
83/// 2. Execute and review - requires confirmation for each file write
84/// 3. Change something - lets user provide feedback to modify the plan
85pub 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                // User wants to change the plan
115                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    // Note: Interactive tests require manual testing
140    // These are placeholder tests for non-interactive functionality
141
142    #[test]
143    fn test_plan_action_result_variants() {
144        // Ensure all variants are constructible
145        let _ = PlanActionResult::ExecuteAutoAccept;
146        let _ = PlanActionResult::ExecuteWithReview;
147        let _ = PlanActionResult::ChangePlan("test".to_string());
148        let _ = PlanActionResult::Cancel;
149    }
150}