1use crate::app::App;
2use ratatui::{
3 layout::{Alignment, Constraint, Direction, Layout},
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Borders, Gauge, Paragraph},
7 Frame,
8};
9
10#[allow(clippy::too_many_lines)]
11pub fn render(frame: &mut Frame, app: &App) {
12 let area = frame.area();
13 let theme = &app.tui_theme;
14
15 let chunks_outer = Layout::default()
17 .direction(Direction::Vertical)
18 .constraints([
19 Constraint::Min(0), Constraint::Length(1), ])
22 .split(area);
23
24 let dashboard_area = chunks_outer[0];
25 let footer_area = chunks_outer[1];
26
27 let block = Block::default()
28 .title("Agent Dashboard")
29 .borders(Borders::ALL)
30 .border_style(Style::default().fg(theme.border))
31 .title_style(
32 Style::default()
33 .fg(theme.primary)
34 .add_modifier(Modifier::BOLD),
35 );
36
37 let inner = block.inner(dashboard_area);
38 frame.render_widget(block, dashboard_area);
39
40 let (total, passing, in_progress, _pending) = if let Some(ref stats) = app.feature_stats {
42 (stats.total, stats.passing, stats.in_progress, stats.failing)
43 } else {
44 let total = app.features.len();
45 let passing = app.features.iter().filter(|f| f.passes).count();
46 let in_progress = app.features.iter().filter(|f| f.in_progress).count();
47 let pending = app
48 .features
49 .iter()
50 .filter(|f| !f.passes && !f.in_progress)
51 .count();
52 (total, passing, in_progress, pending)
53 };
54
55 #[allow(clippy::cast_precision_loss)]
56 let progress_ratio = if total > 0 {
57 passing as f64 / total as f64
58 } else {
59 0.0
60 };
61
62 let chunks = Layout::default()
64 .direction(Direction::Vertical)
65 .margin(2)
66 .constraints([
67 Constraint::Length(3), Constraint::Length(2), Constraint::Length(3), Constraint::Length(2), Constraint::Min(5), ])
73 .split(inner);
74
75 let gauge_label = format!("{}/{} ({:.0}%)", passing, total, progress_ratio * 100.0);
77 let gauge = Gauge::default()
78 .block(Block::default())
79 .gauge_style(Style::default().fg(theme.done))
80 .label(gauge_label)
81 .ratio(progress_ratio);
82 frame.render_widget(gauge, chunks[0]);
83
84 let agent_info = Paragraph::new(vec![Line::from(vec![
86 Span::styled(
87 format!("Coding Agents: {in_progress}/5 "),
88 Style::default().fg(theme.in_progress),
89 ),
90 Span::styled(
91 "Testing Agents: 1/5".to_string(),
92 Style::default().fg(theme.secondary),
93 ),
94 ])])
95 .alignment(Alignment::Left);
96 frame.render_widget(agent_info, chunks[2]);
97
98 let activity = Paragraph::new(vec![
100 Line::from(Span::styled(
101 "Recent Activity:",
102 Style::default()
103 .fg(theme.foreground)
104 .add_modifier(Modifier::BOLD),
105 )),
106 Line::from(""),
107 Line::from(vec![
108 Span::styled(" • ", Style::default().fg(theme.done)),
109 Span::styled(
110 format!(
111 "{} features completed",
112 app.features.iter().filter(|f| f.passes).count()
113 ),
114 Style::default().fg(theme.foreground),
115 ),
116 ]),
117 Line::from(vec![
118 Span::styled(" • ", Style::default().fg(theme.in_progress)),
119 Span::styled(
120 format!(
121 "{} features in progress",
122 app.features.iter().filter(|f| f.in_progress).count()
123 ),
124 Style::default().fg(theme.foreground),
125 ),
126 ]),
127 Line::from(vec![
128 Span::styled(" • ", Style::default().fg(theme.pending)),
129 Span::styled(
130 format!(
131 "{} features pending",
132 app.features
133 .iter()
134 .filter(|f| !f.passes && !f.in_progress)
135 .count()
136 ),
137 Style::default().fg(theme.foreground),
138 ),
139 ]),
140 ])
141 .alignment(Alignment::Left);
142 frame.render_widget(activity, chunks[4]);
143
144 let footer_text = if let Some(status) = app.get_status_message() {
146 status.to_string()
147 } else {
148 " 1-4:views r:refresh t:theme ?:help q:quit".to_string()
149 };
150
151 let footer = Paragraph::new(footer_text).style(Style::default().fg(theme.muted));
152 frame.render_widget(footer, footer_area);
153}