mermaid_cli/render/widgets/
plan_config.rs1use ratatui::{
5 buffer::Buffer,
6 layout::Rect,
7 style::Style,
8 text::{Line, Span},
9 widgets::{Block, Borders, Paragraph, Widget},
10};
11
12use crate::render::theme::Theme;
13use mermaid_domain::PlanConfig;
14
15pub const PLAN_CONFIG_ROWS: usize = 9;
19
20pub const PLAN_CONFIG_HEIGHT: u16 = PLAN_CONFIG_ROWS as u16 + 3;
22
23#[must_use]
26pub fn plan_config_rows(plan: &PlanConfig, session_model: &str) -> Vec<(String, String)> {
27 let perms = &plan.permissions;
28 vec![
29 (
30 "permissions".to_string(),
31 perms.preset_name().unwrap_or("custom").to_string(),
32 ),
33 (
34 " builds/tests".to_string(),
35 perms.builds.as_str().to_string(),
36 ),
37 (" web".to_string(), perms.web.as_str().to_string()),
38 (
39 " memory writes".to_string(),
40 perms.memory.as_str().to_string(),
41 ),
42 (" task tools".to_string(), perms.tasks.as_str().to_string()),
43 (
44 "plan model".to_string(),
45 plan.model.clone().unwrap_or_else(|| {
46 format!("unset (plans with the session model, now {session_model})")
47 }),
48 ),
49 (
50 "plan reasoning".to_string(),
51 plan.reasoning
52 .map(|r| r.as_str().to_string())
53 .unwrap_or_else(|| "unset".to_string()),
54 ),
55 (
56 "auto-approve plans".to_string(),
57 if plan.auto_approve { "on" } else { "off" }.to_string(),
58 ),
59 (
60 "after approval".to_string(),
61 match plan.post_approve {
62 None => "ask each time".to_string(),
63 Some(mermaid_domain::PlanPostApprove::Start) => "always start".to_string(),
64 Some(mermaid_domain::PlanPostApprove::Wait) => "always wait".to_string(),
65 },
66 ),
67 ]
68}
69
70pub struct PlanConfigWidget<'a> {
71 pub theme: &'a Theme,
72 pub plan: &'a PlanConfig,
73 pub session_model: &'a str,
74 pub cursor: usize,
75}
76
77impl<'a> Widget for PlanConfigWidget<'a> {
78 fn render(self, area: Rect, buf: &mut Buffer) {
79 let rows = plan_config_rows(self.plan, self.session_model);
80 let selected_style = Style::new().fg(self.theme.colors.info.to_color()).bold();
81 let label_style = Style::new().fg(self.theme.colors.text_primary.to_color());
82 let value_style = Style::new().fg(self.theme.colors.text_secondary.to_color());
83 let hint_style = Style::new().fg(self.theme.colors.text_disabled.to_color());
84
85 let mut lines: Vec<Line> = rows
86 .iter()
87 .enumerate()
88 .map(|(i, (label, value))| {
89 let marker = if i == self.cursor { "> " } else { " " };
90 let ls = if i == self.cursor {
91 selected_style
92 } else {
93 label_style
94 };
95 Line::from(vec![
96 Span::styled(format!("{marker}{label:<18}"), ls),
97 Span::styled(value.clone(), value_style),
98 ])
99 })
100 .collect();
101 lines.push(Line::from(Span::styled(
102 "Enter/Left/Right change - Up/Down navigate - Esc close",
103 hint_style,
104 )));
105
106 Paragraph::new(lines)
107 .block(
108 Block::default()
109 .borders(Borders::ALL)
110 .title(" Plan mode settings "),
111 )
112 .render(area, buf);
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn row_count_matches_the_reducer_contract() {
122 let rows = plan_config_rows(&PlanConfig::default(), "ollama/test");
125 assert_eq!(rows.len(), PLAN_CONFIG_ROWS);
126 assert_eq!(rows.len(), 9);
127 assert_eq!(rows[0].1, "default");
128 assert_eq!(rows[4].1, "deny");
129 assert!(rows[5].1.starts_with("unset"));
130 }
131}