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!("{}", "→ Will execute plan with review for each change".yellow());
115                PlanActionResult::ExecuteWithReview
116            } else {
117                // User wants to change the plan
118                println!();
119                match Text::new("What should be changed in the plan?")
120                    .with_help_message("Press Enter to submit, Esc to cancel")
121                    .prompt()
122                {
123                    Ok(feedback) if !feedback.trim().is_empty() => {
124                        PlanActionResult::ChangePlan(feedback)
125                    }
126                    _ => PlanActionResult::Cancel,
127                }
128            }
129        }
130        Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
131            println!("{}", "Plan execution cancelled.".dimmed());
132            PlanActionResult::Cancel
133        }
134        Err(_) => PlanActionResult::Cancel,
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    // Note: Interactive tests require manual testing
143    // These are placeholder tests for non-interactive functionality
144
145    #[test]
146    fn test_plan_action_result_variants() {
147        // Ensure all variants are constructible
148        let _ = PlanActionResult::ExecuteAutoAccept;
149        let _ = PlanActionResult::ExecuteWithReview;
150        let _ = PlanActionResult::ChangePlan("test".to_string());
151        let _ = PlanActionResult::Cancel;
152    }
153}