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