aether-wisp 0.1.7

A terminal UI for AI coding agents via the Agent Client Protocol (ACP)
Documentation
use tui::{FitOptions, Frame, Line, Style, Theme, ViewContext};

pub struct ThoughtMessage<'a> {
    pub text: &'a str,
}

impl ThoughtMessage<'_> {
    fn format_line(text: &str, theme: &Theme) -> Line {
        Line::with_style(text, Style::fg(theme.muted()).italic())
    }

    fn format_lines(text: &str, theme: &Theme) -> Vec<Line> {
        text.lines().map(|line| Self::format_line(line, theme)).collect()
    }
}

impl ThoughtMessage<'_> {
    pub fn render(&self, context: &ViewContext) -> Frame {
        if self.text.is_empty() {
            return Frame::empty();
        }

        let lines = Self::format_lines(self.text, &context.theme);
        Frame::new(lines).fit(context.size.width, FitOptions::wrap())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn renders_italic_muted_thought_line() {
        let component = ThoughtMessage { text: "check plan" };
        let context = ViewContext::new((80, 24));
        let frame = component.render(&context);
        let lines = frame.lines();
        assert_eq!(lines.len(), 1);
        assert_eq!(lines[0].plain_text(), "check plan");
        let style = lines[0].spans()[0].style();
        assert_eq!(style.fg, Some(context.theme.muted()));
        assert!(style.italic);
    }

    #[test]
    fn renders_all_lines_as_italic_muted() {
        let component = ThoughtMessage { text: "line one\nline two" };
        let context = ViewContext::new((80, 24));
        let frame = component.render(&context);
        let lines = frame.lines();
        assert_eq!(lines.len(), 2);
        assert_eq!(lines[0].plain_text(), "line one");
        assert_eq!(lines[1].plain_text(), "line two");
        for line in lines {
            let style = line.spans()[0].style();
            assert_eq!(style.fg, Some(context.theme.muted()));
            assert!(style.italic);
        }
    }

    #[test]
    fn wrapped_continuation_rows_remain_italic_muted() {
        let component = ThoughtMessage { text: "abcdefghijklmnopqrstuvwxyz" };
        let context = ViewContext::new((80, 24));
        let frame = component.render(&context);
        let wrapped = frame.lines()[0].soft_wrap(12);
        assert!(wrapped.len() > 1);

        for row in wrapped.iter().skip(1) {
            assert!(!row.spans().is_empty());
            assert!(
                row.spans()
                    .iter()
                    .all(|span| { span.style().fg == Some(context.theme.muted()) && span.style().italic })
            );
        }
    }
}