steer_tui/tui/widgets/chat_widgets/
command_response.rs

1use crate::tui::theme::{Component, Theme};
2use crate::tui::widgets::chat_list_state::ViewMode;
3use crate::tui::widgets::chat_widgets::chat_widget::{ChatRenderable, HeightCache};
4use ratatui::text::{Line, Span};
5
6/// Widget for command responses (both app commands and tui commands)
7pub struct CommandResponseWidget {
8    command: String,
9    response: String,
10    cache: HeightCache,
11    rendered_lines: Option<Vec<Line<'static>>>,
12}
13
14impl CommandResponseWidget {
15    pub fn new(command: String, response: String) -> Self {
16        Self {
17            command,
18            response,
19            cache: HeightCache::new(),
20            rendered_lines: None,
21        }
22    }
23}
24
25impl ChatRenderable for CommandResponseWidget {
26    fn lines(&mut self, width: u16, _mode: ViewMode, theme: &Theme) -> &[Line<'static>] {
27        if self.rendered_lines.is_none() || self.cache.last_width != width {
28            let mut lines = vec![];
29            let wrap_width = width.saturating_sub(2) as usize;
30
31            // Split response into lines
32            let response_lines: Vec<&str> = self.response.lines().collect();
33
34            if response_lines.is_empty()
35                || (response_lines.len() == 1 && response_lines[0].len() <= 50)
36            {
37                // Single short line - render inline
38                let spans = vec![
39                    Span::styled(self.command.clone(), theme.style(Component::CommandPrompt)),
40                    Span::raw(": "),
41                    Span::styled(self.response.clone(), theme.style(Component::CommandText)),
42                ];
43                lines.push(Line::from(spans));
44            } else {
45                // Multi-line or long response
46                lines.push(Line::from(vec![
47                    Span::styled(self.command.clone(), theme.style(Component::CommandPrompt)),
48                    Span::raw(":"),
49                ]));
50
51                // Add response lines with proper wrapping
52                for line in response_lines {
53                    let wrapped = textwrap::wrap(line, wrap_width);
54                    if wrapped.is_empty() {
55                        lines.push(Line::from(""));
56                    } else {
57                        for wrapped_line in wrapped {
58                            lines.push(Line::from(Span::styled(
59                                wrapped_line.to_string(),
60                                theme.style(Component::CommandText),
61                            )));
62                        }
63                    }
64                }
65            }
66
67            self.rendered_lines = Some(lines);
68        }
69
70        self.rendered_lines.as_ref().unwrap()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_command_response_widget_inline() {
80        let theme = Theme::default();
81        let mut widget = CommandResponseWidget::new("/help".to_string(), "Shows help".to_string());
82
83        let height = widget.lines(80, ViewMode::Compact, &theme).len();
84        assert_eq!(height, 1); // Short response should be inline
85    }
86
87    #[test]
88    fn test_command_response_widget_multiline() {
89        let theme = Theme::default();
90        let mut widget =
91            CommandResponseWidget::new("/help".to_string(), "Line 1\nLine 2\nLine 3".to_string());
92
93        let height = widget.lines(80, ViewMode::Compact, &theme).len();
94        assert_eq!(height, 4); // Command line + 3 response lines
95    }
96}