Skip to main content

mermaid_cli/render/widgets/
status_line.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::Style,
5    text::{Line, Span},
6    widgets::{Paragraph, Widget},
7};
8use std::collections::VecDeque;
9
10use super::GenerationStatus;
11use crate::render::theme::Theme;
12
13/// Props for StatusLineWidget (stateless widget showing generation progress)
14pub struct StatusLineWidget<'a> {
15    pub status: GenerationStatus,
16    pub elapsed_secs: u64,
17    pub tokens_received: usize,
18    /// Whether tokens_received is an estimate (show ~ prefix)
19    pub tokens_estimated: bool,
20    pub theme: &'a Theme,
21    /// Queued messages waiting to be processed
22    pub queued_messages: &'a VecDeque<String>,
23    /// The in-flight tool's label while running tools (e.g. `Bash npm run dev`),
24    /// so the status line names what it's waiting on. `None` for other phases.
25    pub active_tool: Option<String>,
26}
27
28impl<'a> Widget for StatusLineWidget<'a> {
29    fn render(self, area: Rect, buf: &mut Buffer) {
30        // Don't render if area is too small
31        if area.height == 0 || area.width < 10 {
32            return;
33        }
34
35        // Only render if status is not Idle
36        if self.status == GenerationStatus::Idle {
37            return;
38        }
39
40        // While running tools, append the in-flight tool so the user can see
41        // what's executing (a long `npm run dev` etc. no longer looks opaque).
42        let status_text = match (&self.status, &self.active_tool) {
43            (GenerationStatus::RunningTools, Some(tool)) => {
44                format!("{}: {}", self.status.display_text(), tool)
45            },
46            _ => self.status.display_text().to_string(),
47        };
48
49        let info_color = self.theme.colors.info.to_color();
50
51        // Determine arrow direction based on state
52        let (arrow, flow_direction) = match self.status {
53            GenerationStatus::Sending | GenerationStatus::Thinking => ("↑ ", "upstream"),
54            GenerationStatus::Streaming => ("↓ ", "downstream"),
55            GenerationStatus::RunningTools => ("• ", "tools"),
56            GenerationStatus::Compacting => ("• ", "compaction"),
57            GenerationStatus::Cancelling => ("• ", "cleanup"),
58            GenerationStatus::Idle => ("", ""),
59        };
60
61        let spans = vec![
62            // Arrow indicator showing message direction (cyan)
63            Span::styled(arrow, Style::new().fg(info_color)),
64            // Status text with ellipsis (cyan)
65            Span::styled(format!("{}... ", status_text), Style::new().fg(info_color)),
66            // Metadata in parentheses (dimmed)
67            // Show ~ prefix when tokens are estimated (during streaming)
68            Span::styled(
69                format!(
70                    "(esc to interrupt • {}s • {} {}{} tokens)",
71                    self.elapsed_secs,
72                    if flow_direction == "downstream" {
73                        "↓"
74                    } else if flow_direction == "tools" {
75                        "tools"
76                    } else if flow_direction == "compaction" {
77                        "compact"
78                    } else if flow_direction == "cleanup" {
79                        "cleanup"
80                    } else {
81                        "↑"
82                    },
83                    if self.tokens_estimated { "~" } else { "" },
84                    self.tokens_received
85                ),
86                Style::new()
87                    .fg(self.theme.colors.text_secondary.to_color())
88                    .dim(),
89            ),
90        ];
91
92        let mut lines = vec![Line::from(spans)];
93
94        // Show all queued messages below the status line with highlight
95        let max_len = area.width.saturating_sub(4) as usize;
96        for queued in self.queued_messages.iter() {
97            // Truncate long messages to fit in the area
98            let display_msg = if queued.len() > max_len {
99                let end = queued.floor_char_boundary(max_len.saturating_sub(3));
100                format!("> {}...", &queued[..end])
101            } else {
102                format!("> {}", queued)
103            };
104
105            lines.push(Line::from(vec![Span::styled(
106                display_msg,
107                Style::new()
108                    .fg(self.theme.colors.text_primary.to_color())
109                    .bg(ratatui::style::Color::Rgb(60, 60, 80)), // Subtle purple highlight
110            )]));
111        }
112
113        let paragraph = Paragraph::new(lines);
114
115        paragraph.render(area, buf);
116    }
117}