aico/ui/
live_display.rs

1use crate::models::DisplayItem;
2use crate::ui::markdown_streamer::MarkdownStreamer;
3use std::io::Write;
4
5pub struct LiveDisplay {
6    engine: MarkdownStreamer,
7    last_rendered_tail: String,
8    has_started_content: bool,
9    last_status_len: usize,
10    width: u16,
11}
12
13impl LiveDisplay {
14    pub fn new(width: u16) -> Self {
15        let mut engine = MarkdownStreamer::new();
16        engine.set_width(width as usize);
17        engine.set_margin(2);
18        Self {
19            engine,
20            last_rendered_tail: String::new(),
21            has_started_content: false,
22            last_status_len: 0,
23            width,
24        }
25    }
26
27    pub fn update_status(&mut self, text: &str) {
28        if self.has_started_content {
29            return;
30        }
31        let width = self.width as usize;
32        let limit = width.saturating_sub(10);
33
34        let truncated_owned: String;
35        let truncated = if text.chars().count() > limit {
36            truncated_owned = text.chars().take(limit).collect();
37            &truncated_owned
38        } else {
39            text
40        };
41
42        let mut stdout = std::io::stdout();
43        // Clear previous status line: CR, then spaces, then CR
44        let _ = write!(stdout, "\r{}\r", " ".repeat(self.last_status_len));
45
46        let status = format!("\x1b[2m{}...\x1b[0m", truncated);
47        let _ = write!(stdout, "{}", status);
48        let _ = stdout.flush();
49        self.last_status_len = truncated.chars().count() + 3;
50    }
51
52    pub fn render(&mut self, items: &[DisplayItem]) {
53        if items.is_empty() {
54            return;
55        }
56
57        if !self.has_started_content {
58            self.has_started_content = true;
59            let mut stdout = std::io::stdout();
60            // Final clear of reasoning status before starting markdown
61            let _ = write!(stdout, "\r{}\r", " ".repeat(self.last_status_len));
62            let _ = stdout.flush();
63        }
64
65        let mut stdout = std::io::stdout();
66
67        for item in items {
68            match item {
69                DisplayItem::Markdown(m) => {
70                    // Overlap check: if this new item starts with what we rendered last time
71                    // (which was the unstable tail), render only the new part.
72                    let to_print = if m.starts_with(&self.last_rendered_tail)
73                        && !self.last_rendered_tail.is_empty()
74                    {
75                        &m[self.last_rendered_tail.len()..]
76                    } else {
77                        m.as_str()
78                    };
79
80                    if !to_print.is_empty() {
81                        let _ = self.engine.print_chunk(&mut stdout, to_print);
82                    }
83                    self.last_rendered_tail = m.clone();
84                }
85                DisplayItem::Diff(d) => {
86                    let _ = self.engine.print_chunk(&mut stdout, "\n~~~~~diff\n");
87                    let _ = self.engine.print_chunk(&mut stdout, d);
88                    let _ = self.engine.print_chunk(&mut stdout, "\n~~~~~\n");
89                    // A diff block breaks the text overlap chain
90                    self.last_rendered_tail.clear();
91                }
92            }
93        }
94        let _ = stdout.flush();
95    }
96
97    pub fn finish(&mut self, items: &[DisplayItem]) {
98        // Just delegate to render to flush out the final state of text.
99        // We pass the full confirmation list, render() logic handles deduplication.
100        self.render(items);
101
102        let mut stdout = std::io::stdout();
103        let _ = self.engine.flush(&mut stdout);
104        let _ = stdout.flush();
105    }
106}