wisp/components/
plan_view.rs1use agent_client_protocol::schema::{PlanEntry, PlanEntryStatus};
2
3use tui::{FitOptions, Frame, Line, Style, ViewContext};
4
5const CHECKBOX_EMPTY: &str = "\u{2610}"; const CHECKBOX_FILLED: &str = "\u{2611}"; const SQUARE_FILLED: &str = "\u{25A0}"; pub struct PlanView<'a> {
18 pub entries: &'a [PlanEntry],
19}
20
21impl PlanView<'_> {
22 pub fn render(&self, context: &ViewContext) -> Frame {
23 if self.entries.is_empty() {
24 return Frame::empty();
25 }
26
27 let mut lines = Vec::with_capacity(self.entries.len() + 2);
28 lines.push(Line::default());
29
30 let mut header = Line::default();
31 header.push_styled("Plan".to_string(), context.theme.muted());
32 lines.push(header);
33
34 for entry in self.entries {
35 let mut line = Line::default();
36 match entry.status {
37 PlanEntryStatus::Completed => {
38 line.push_styled(format!(" {CHECKBOX_FILLED} "), context.theme.muted());
39 let completed_style = Style::fg(context.theme.muted()).strikethrough();
40 line.push_with_style(entry.content.clone(), completed_style);
41 }
42 PlanEntryStatus::InProgress => {
43 line.push_styled(format!(" {SQUARE_FILLED} "), context.theme.info());
44 line.push_text(entry.content.clone());
45 }
46 _ => {
47 line.push_styled(format!(" {CHECKBOX_EMPTY} "), context.theme.muted());
48 line.push_styled(entry.content.clone(), context.theme.muted());
49 }
50 }
51 lines.push(line);
52 }
53
54 Frame::new(lines).fit(context.size.width, FitOptions::wrap())
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use agent_client_protocol::schema::{PlanEntry, PlanEntryPriority, PlanEntryStatus};
62
63 fn ctx() -> ViewContext {
64 ViewContext::new((80, 24))
65 }
66
67 fn entry(content: &str, status: PlanEntryStatus) -> PlanEntry {
68 PlanEntry::new(content.to_string(), PlanEntryPriority::Medium, status)
69 }
70
71 #[test]
72 fn empty_entries_render_nothing() {
73 let view = PlanView { entries: &[] };
74 assert!(view.render(&ctx()).lines().is_empty());
75 }
76
77 #[test]
78 fn renders_header_plus_entries() {
79 let entries = vec![
80 entry("Research", PlanEntryStatus::Completed),
81 entry("Implement", PlanEntryStatus::InProgress),
82 entry("Test", PlanEntryStatus::Pending),
83 ];
84 let view = PlanView { entries: &entries };
85 let frame = view.render(&ctx());
86 let lines = frame.lines();
87 assert_eq!(lines.len(), 5);
88 assert_eq!(lines[0].plain_text(), "");
89 assert_eq!(lines[1].plain_text(), "Plan");
90 }
91
92 #[test]
93 fn completed_entry_has_filled_checkbox() {
94 let entries = vec![entry("Done task", PlanEntryStatus::Completed)];
95 let view = PlanView { entries: &entries };
96 let frame = view.render(&ctx());
97 let text = frame.lines()[2].plain_text();
98 assert!(text.contains(CHECKBOX_FILLED));
99 assert!(text.contains("Done task"));
100 }
101
102 #[test]
103 fn completed_entry_has_strikethrough() {
104 let entries = vec![entry("Done task", PlanEntryStatus::Completed)];
105 let view = PlanView { entries: &entries };
106 let frame = view.render(&ctx());
107 let spans = frame.lines()[2].spans();
108 let text_span = &spans[1];
109 assert!(text_span.style().strikethrough);
110 }
111
112 #[test]
113 fn in_progress_entry_has_filled_square() {
114 let entries = vec![entry("Working", PlanEntryStatus::InProgress)];
115 let view = PlanView { entries: &entries };
116 let frame = view.render(&ctx());
117 let text = frame.lines()[2].plain_text();
118 assert!(text.contains(SQUARE_FILLED));
119 assert!(text.contains("Working"));
120 }
121
122 #[test]
123 fn in_progress_marker_uses_info_theme_color() {
124 let context = ctx();
125 let entries = vec![entry("Working", PlanEntryStatus::InProgress)];
126 let view = PlanView { entries: &entries };
127 let frame = view.render(&context);
128 let spans = frame.lines()[2].spans();
129 assert_eq!(spans[0].style().fg, Some(context.theme.info()));
130 }
131
132 #[test]
133 fn pending_entry_has_empty_checkbox() {
134 let entries = vec![entry("Todo", PlanEntryStatus::Pending)];
135 let view = PlanView { entries: &entries };
136 let frame = view.render(&ctx());
137 let text = frame.lines()[2].plain_text();
138 assert!(text.contains(CHECKBOX_EMPTY));
139 assert!(text.contains("Todo"));
140 }
141}