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