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        format!(
45            "{}{}{}",
46            "┌─ Plan Created ".bright_green(),
47            "─".repeat(inner_width.saturating_sub(15)).dimmed(),
48            "┐".dimmed()
49        )
50    );
51
52    // Plan path
53    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    // Task count
63    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    // Bottom border
73    println!(
74        "{}",
75        format!(
76            "{}{}{}",
77            "└".dimmed(),
78            "─".repeat(box_width - 2).dimmed(),
79            "┘".dimmed()
80        )
81    );
82    println!();
83}
84
85/// Show the post-plan action menu
86///
87/// Displays after a plan is created, offering execution options:
88/// 1. Execute and auto-accept - runs all tasks without confirmation prompts
89/// 2. Execute and review - requires confirmation for each file write
90/// 3. Change something - lets user provide feedback to modify the plan
91pub 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                // User wants to change the plan
121                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    // Note: Interactive tests require manual testing
146    // These are placeholder tests for non-interactive functionality
147
148    #[test]
149    fn test_plan_action_result_variants() {
150        // Ensure all variants are constructible
151        let _ = PlanActionResult::ExecuteAutoAccept;
152        let _ = PlanActionResult::ExecuteWithReview;
153        let _ = PlanActionResult::ChangePlan("test".to_string());
154        let _ = PlanActionResult::Cancel;
155    }
156}