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        // While tools run, advertise Ctrl+B (send a running command to the
62        // background). Harmless no-op for non-detachable tools.
63        let bg_hint = if self.status == GenerationStatus::RunningTools {
64            " • ctrl+b to background"
65        } else {
66            ""
67        };
68
69        let spans = vec![
70            // Arrow indicator showing message direction (cyan)
71            Span::styled(arrow, Style::new().fg(info_color)),
72            // Status text with ellipsis (cyan)
73            Span::styled(format!("{}... ", status_text), Style::new().fg(info_color)),
74            // Metadata in parentheses (dimmed)
75            // Show ~ prefix when tokens are estimated (during streaming)
76            Span::styled(
77                format!(
78                    "(esc to interrupt{bg_hint} • {}s • {} {}{} tokens)",
79                    self.elapsed_secs,
80                    if flow_direction == "downstream" {
81                        "↓"
82                    } else if flow_direction == "tools" {
83                        "tools"
84                    } else if flow_direction == "compaction" {
85                        "compact"
86                    } else if flow_direction == "cleanup" {
87                        "cleanup"
88                    } else {
89                        "↑"
90                    },
91                    if self.tokens_estimated { "~" } else { "" },
92                    self.tokens_received
93                ),
94                Style::new()
95                    .fg(self.theme.colors.text_secondary.to_color())
96                    .dim(),
97            ),
98        ];
99
100        let mut lines = vec![Line::from(spans)];
101
102        // Show all queued messages below the status line with highlight
103        let max_len = area.width.saturating_sub(4) as usize;
104        for queued in self.queued_messages.iter() {
105            // Truncate long messages to fit in the area
106            let display_msg = if queued.len() > max_len {
107                let end = queued.floor_char_boundary(max_len.saturating_sub(3));
108                format!("> {}...", &queued[..end])
109            } else {
110                format!("> {}", queued)
111            };
112
113            lines.push(Line::from(vec![Span::styled(
114                display_msg,
115                Style::new()
116                    .fg(self.theme.colors.text_primary.to_color())
117                    .bg(ratatui::style::Color::Rgb(60, 60, 80)), // Subtle purple highlight
118            )]));
119        }
120
121        let paragraph = Paragraph::new(lines);
122
123        paragraph.render(area, buf);
124    }
125}