zeph-tui 0.11.6

Ratatui-based TUI dashboard with real-time metrics for Zeph
Documentation
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, Paragraph};

use crate::metrics::MetricsSnapshot;
use crate::theme::Theme;

pub fn render(metrics: &MetricsSnapshot, frame: &mut Frame, area: Rect) {
    let theme = Theme::default();

    let has_mcp = !metrics.active_mcp_tools.is_empty() || metrics.mcp_tool_count > 0;
    let chunks = if has_mcp {
        Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area)
    } else {
        Layout::vertical([Constraint::Percentage(100), Constraint::Min(0)]).split(area)
    };

    let skill_lines: Vec<Line<'_>> = metrics
        .active_skills
        .iter()
        .map(|s| Line::from(format!("  - {s}")))
        .collect();
    let skills = Paragraph::new(skill_lines).block(
        Block::default()
            .borders(Borders::ALL)
            .border_style(theme.panel_border)
            .title(format!(
                " Skills ({}/{}) ",
                metrics.active_skills.len(),
                metrics.total_skills
            )),
    );
    frame.render_widget(skills, chunks[0]);

    if has_mcp {
        let mcp_lines: Vec<Line<'_>> = metrics
            .active_mcp_tools
            .iter()
            .map(|t| Line::from(format!("  - {t}")))
            .collect();
        let mcp = Paragraph::new(mcp_lines).block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(theme.panel_border)
                .title(format!(
                    " MCP Tools ({}/{}) ",
                    metrics.active_mcp_tools.len(),
                    metrics.mcp_tool_count
                )),
        );
        frame.render_widget(mcp, chunks[1]);
    }
}

#[cfg(test)]
mod tests {
    use insta::assert_snapshot;

    use crate::metrics::MetricsSnapshot;
    use crate::test_utils::render_to_string;

    #[test]
    fn skills_with_data() {
        let mut metrics = MetricsSnapshot::default();
        metrics.active_skills = vec!["web-search".into(), "code-gen".into()];
        metrics.total_skills = 5;

        let output = render_to_string(30, 10, |frame, area| {
            super::render(&metrics, frame, area);
        });
        assert_snapshot!(output);
    }
}