ricecoder_execution/
approval_ui.rs

1//! Approval UI components for execution plans
2//!
3//! Provides UI components for displaying approval requests and handling
4//! user decisions (approve/reject). Designed to integrate with the TUI.
5
6use crate::approval::ApprovalSummary;
7use crate::models::RiskLevel;
8
9/// Approval UI state
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ApprovalUIState {
12    /// Waiting for user input
13    Waiting,
14    /// User approved
15    Approved,
16    /// User rejected
17    Rejected,
18}
19
20/// Approval UI component for displaying and handling approval requests
21#[derive(Debug, Clone)]
22pub struct ApprovalUI {
23    /// Request ID
24    pub request_id: String,
25    /// Plan summary
26    pub summary: ApprovalSummary,
27    /// Current UI state
28    pub state: ApprovalUIState,
29    /// User comments (if any)
30    pub comments: Option<String>,
31}
32
33impl ApprovalUI {
34    /// Create a new approval UI component
35    pub fn new(request_id: String, summary: ApprovalSummary) -> Self {
36        ApprovalUI {
37            request_id,
38            summary,
39            state: ApprovalUIState::Waiting,
40            comments: None,
41        }
42    }
43
44    /// Mark as approved
45    pub fn approve(&mut self, comments: Option<String>) {
46        self.state = ApprovalUIState::Approved;
47        self.comments = comments;
48    }
49
50    /// Mark as rejected
51    pub fn reject(&mut self, comments: Option<String>) {
52        self.state = ApprovalUIState::Rejected;
53        self.comments = comments;
54    }
55
56    /// Get the risk level color for TUI rendering
57    ///
58    /// Returns a color code suitable for terminal rendering.
59    pub fn risk_color(&self) -> &'static str {
60        match self.summary.risk_level {
61            RiskLevel::Low => "green",
62            RiskLevel::Medium => "yellow",
63            RiskLevel::High => "red",
64            RiskLevel::Critical => "bright-red",
65        }
66    }
67
68    /// Get the risk level emoji
69    pub fn risk_emoji(&self) -> &'static str {
70        match self.summary.risk_level {
71            RiskLevel::Low => "✓",
72            RiskLevel::Medium => "⚠",
73            RiskLevel::High => "⚠⚠",
74            RiskLevel::Critical => "🚨",
75        }
76    }
77
78    /// Format the approval UI for display
79    ///
80    /// Returns a formatted string suitable for terminal display.
81    pub fn format_display(&self) -> String {
82        let status = match self.state {
83            ApprovalUIState::Waiting => "⏳ Waiting for approval",
84            ApprovalUIState::Approved => "✅ Approved",
85            ApprovalUIState::Rejected => "❌ Rejected",
86        };
87
88        let mut display = format!(
89            "{}\n\n{} Risk Level: {:?}\n\nPlan: {}\nSteps: {}\nRisk Score: {:.2}\nEstimated Duration: {}s\n\nRisk Factors:\n{}",
90            status,
91            self.risk_emoji(),
92            self.summary.risk_level,
93            self.summary.plan_name,
94            self.summary.step_count,
95            self.summary.risk_score,
96            self.summary.estimated_duration_secs,
97            self.summary.risk_factors
98        );
99
100        if let Some(comments) = &self.comments {
101            display.push_str(&format!("\n\nComments: {}", comments));
102        }
103
104        display
105    }
106
107    /// Get the approval prompt text
108    pub fn get_prompt(&self) -> String {
109        match self.summary.risk_level {
110            RiskLevel::Critical => {
111                "⚠️  CRITICAL RISK - This plan has critical risk factors. Review carefully before approving.\n\nApprove? (y/n): ".to_string()
112            }
113            RiskLevel::High => {
114                "⚠️  HIGH RISK - This plan has high risk factors. Review before approving.\n\nApprove? (y/n): ".to_string()
115            }
116            RiskLevel::Medium => {
117                "Plan requires approval.\n\nApprove? (y/n): ".to_string()
118            }
119            RiskLevel::Low => {
120                "Plan is ready for execution.\n\nApprove? (y/n): ".to_string()
121            }
122        }
123    }
124
125    /// Get the approval instructions
126    pub fn get_instructions(&self) -> String {
127        format!(
128            "Plan: {}\nSteps: {}\nRisk Level: {:?}\n\nPress 'y' to approve, 'n' to reject, or 'q' to cancel",
129            self.summary.plan_name,
130            self.summary.step_count,
131            self.summary.risk_level
132        )
133    }
134}
135
136/// Approval UI builder for constructing approval UI components
137pub struct ApprovalUIBuilder {
138    request_id: Option<String>,
139    summary: Option<ApprovalSummary>,
140}
141
142impl ApprovalUIBuilder {
143    /// Create a new approval UI builder
144    pub fn new() -> Self {
145        ApprovalUIBuilder {
146            request_id: None,
147            summary: None,
148        }
149    }
150
151    /// Set the request ID
152    pub fn request_id(mut self, request_id: String) -> Self {
153        self.request_id = Some(request_id);
154        self
155    }
156
157    /// Set the approval summary
158    pub fn summary(mut self, summary: ApprovalSummary) -> Self {
159        self.summary = Some(summary);
160        self
161    }
162
163    /// Build the approval UI component
164    pub fn build(self) -> Result<ApprovalUI, String> {
165        let request_id = self
166            .request_id
167            .ok_or_else(|| "request_id is required".to_string())?;
168        let summary = self
169            .summary
170            .ok_or_else(|| "summary is required".to_string())?;
171
172        Ok(ApprovalUI::new(request_id, summary))
173    }
174}
175
176impl Default for ApprovalUIBuilder {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn create_test_summary() -> ApprovalSummary {
187        ApprovalSummary {
188            plan_id: "plan1".to_string(),
189            plan_name: "Test Plan".to_string(),
190            step_count: 3,
191            risk_level: RiskLevel::High,
192            risk_score: 1.8,
193            risk_factors: "- file_count: 1 file modified".to_string(),
194            estimated_duration_secs: 60,
195            requires_approval: true,
196        }
197    }
198
199    #[test]
200    fn test_create_approval_ui() {
201        let summary = create_test_summary();
202        let ui = ApprovalUI::new("req1".to_string(), summary);
203
204        assert_eq!(ui.request_id, "req1");
205        assert_eq!(ui.state, ApprovalUIState::Waiting);
206        assert_eq!(ui.summary.plan_name, "Test Plan");
207    }
208
209    #[test]
210    fn test_approve() {
211        let summary = create_test_summary();
212        let mut ui = ApprovalUI::new("req1".to_string(), summary);
213
214        ui.approve(Some("Looks good".to_string()));
215
216        assert_eq!(ui.state, ApprovalUIState::Approved);
217        assert_eq!(ui.comments, Some("Looks good".to_string()));
218    }
219
220    #[test]
221    fn test_reject() {
222        let summary = create_test_summary();
223        let mut ui = ApprovalUI::new("req1".to_string(), summary);
224
225        ui.reject(Some("Needs changes".to_string()));
226
227        assert_eq!(ui.state, ApprovalUIState::Rejected);
228        assert_eq!(ui.comments, Some("Needs changes".to_string()));
229    }
230
231    #[test]
232    fn test_risk_color() {
233        let mut summary = create_test_summary();
234        summary.risk_level = RiskLevel::Low;
235        let ui = ApprovalUI::new("req1".to_string(), summary);
236        assert_eq!(ui.risk_color(), "green");
237
238        let mut summary = create_test_summary();
239        summary.risk_level = RiskLevel::Medium;
240        let ui = ApprovalUI::new("req1".to_string(), summary);
241        assert_eq!(ui.risk_color(), "yellow");
242
243        let mut summary = create_test_summary();
244        summary.risk_level = RiskLevel::High;
245        let ui = ApprovalUI::new("req1".to_string(), summary);
246        assert_eq!(ui.risk_color(), "red");
247
248        let mut summary = create_test_summary();
249        summary.risk_level = RiskLevel::Critical;
250        let ui = ApprovalUI::new("req1".to_string(), summary);
251        assert_eq!(ui.risk_color(), "bright-red");
252    }
253
254    #[test]
255    fn test_risk_emoji() {
256        let mut summary = create_test_summary();
257        summary.risk_level = RiskLevel::Low;
258        let ui = ApprovalUI::new("req1".to_string(), summary);
259        assert_eq!(ui.risk_emoji(), "✓");
260
261        let mut summary = create_test_summary();
262        summary.risk_level = RiskLevel::Medium;
263        let ui = ApprovalUI::new("req1".to_string(), summary);
264        assert_eq!(ui.risk_emoji(), "⚠");
265
266        let mut summary = create_test_summary();
267        summary.risk_level = RiskLevel::High;
268        let ui = ApprovalUI::new("req1".to_string(), summary);
269        assert_eq!(ui.risk_emoji(), "⚠⚠");
270
271        let mut summary = create_test_summary();
272        summary.risk_level = RiskLevel::Critical;
273        let ui = ApprovalUI::new("req1".to_string(), summary);
274        assert_eq!(ui.risk_emoji(), "🚨");
275    }
276
277    #[test]
278    fn test_format_display() {
279        let summary = create_test_summary();
280        let ui = ApprovalUI::new("req1".to_string(), summary);
281
282        let display = ui.format_display();
283        assert!(display.contains("Waiting for approval"));
284        assert!(display.contains("Test Plan"));
285        assert!(display.contains("Steps: 3"));
286    }
287
288    #[test]
289    fn test_get_prompt_critical() {
290        let mut summary = create_test_summary();
291        summary.risk_level = RiskLevel::Critical;
292        let ui = ApprovalUI::new("req1".to_string(), summary);
293
294        let prompt = ui.get_prompt();
295        assert!(prompt.contains("CRITICAL RISK"));
296    }
297
298    #[test]
299    fn test_get_prompt_high() {
300        let mut summary = create_test_summary();
301        summary.risk_level = RiskLevel::High;
302        let ui = ApprovalUI::new("req1".to_string(), summary);
303
304        let prompt = ui.get_prompt();
305        assert!(prompt.contains("HIGH RISK"));
306    }
307
308    #[test]
309    fn test_get_instructions() {
310        let summary = create_test_summary();
311        let ui = ApprovalUI::new("req1".to_string(), summary);
312
313        let instructions = ui.get_instructions();
314        assert!(instructions.contains("Test Plan"));
315        assert!(instructions.contains("Steps: 3"));
316    }
317
318    #[test]
319    fn test_approval_ui_builder() {
320        let summary = create_test_summary();
321        let ui = ApprovalUIBuilder::new()
322            .request_id("req1".to_string())
323            .summary(summary)
324            .build()
325            .unwrap();
326
327        assert_eq!(ui.request_id, "req1");
328        assert_eq!(ui.state, ApprovalUIState::Waiting);
329    }
330
331    #[test]
332    fn test_approval_ui_builder_missing_request_id() {
333        let summary = create_test_summary();
334        let result = ApprovalUIBuilder::new().summary(summary).build();
335
336        assert!(result.is_err());
337    }
338
339    #[test]
340    fn test_approval_ui_builder_missing_summary() {
341        let result = ApprovalUIBuilder::new()
342            .request_id("req1".to_string())
343            .build();
344
345        assert!(result.is_err());
346    }
347}