Skip to main content

rab/agent/ui/components/
user_message.rs

1use std::cell::RefCell;
2
3use crate::agent::ui::theme::current_theme;
4use crate::tui::Component;
5use crate::tui::components::r#box::TuiBox;
6use crate::tui::components::markdown::{DefaultTextStyle, Markdown, MarkdownOptions};
7
8const OSC133_ZONE_START: &str = "\x1b]133;A\x07";
9const OSC133_ZONE_END: &str = "\x1b]133;B\x07";
10const OSC133_ZONE_FINAL: &str = "\x1b]133;C\x07";
11
12/// User message component — matches pi's UserMessageComponent.
13/// Renders text in a Box with `userMessageBg` background, `userMessageText` color.
14pub struct UserMessageComponent {
15    box_component: TuiBox,
16    cached_lines: RefCell<Option<Vec<String>>>,
17    cached_width: RefCell<usize>,
18}
19
20impl UserMessageComponent {
21    pub fn new(text: impl Into<String>) -> Self {
22        let text = text.into();
23        let theme = current_theme();
24        let bg_ansi = theme.bg_ansi("userMessageBg").to_string();
25        drop(theme);
26
27        let mut msg_box = TuiBox::new(
28            1,
29            1,
30            Some(std::boxed::Box::new(move |s: &str| -> String {
31                format!("{}{}\x1b[49m", bg_ansi, s)
32            })),
33        );
34
35        // Build the markdown renderer with userMessageText color
36        let md_theme = crate::agent::ui::theme::get_markdown_theme();
37        let default_style = DefaultTextStyle {
38            color: Some(std::sync::Arc::new(|s: &str| -> String {
39                let t = current_theme();
40                t.fg("userMessageText", s)
41            })),
42            bg_color: None,
43            bold: false,
44            italic: false,
45            strikethrough: false,
46            underline: false,
47        };
48        let md = Markdown::new(
49            text.clone(),
50            0,
51            0,
52            md_theme,
53            Some(default_style),
54            Some(MarkdownOptions {
55                preserve_ordered_list_markers: true,
56            }),
57        );
58        msg_box.add_child(std::boxed::Box::new(md));
59
60        Self {
61            box_component: msg_box,
62            cached_lines: RefCell::new(None),
63            cached_width: RefCell::new(0),
64        }
65    }
66}
67
68impl Component for UserMessageComponent {
69    fn set_expanded(&mut self, _expanded: bool) {
70        // User messages are always fully visible
71    }
72
73    fn render(&self, width: usize) -> Vec<String> {
74        if *self.cached_width.borrow() == width
75            && let Some(ref lines) = *self.cached_lines.borrow()
76        {
77            return lines.clone();
78        }
79
80        let mut lines = self.box_component.render(width);
81        if !lines.is_empty() {
82            lines[0] = format!("{}{}", OSC133_ZONE_START, &lines[0]);
83            if let Some(last) = lines.last_mut() {
84                last.push_str(OSC133_ZONE_END);
85                last.push_str(OSC133_ZONE_FINAL);
86            }
87        }
88
89        // Cache
90        let result = lines.clone();
91        *self.cached_lines.borrow_mut() = Some(lines);
92        *self.cached_width.borrow_mut() = width;
93        result
94    }
95
96    fn invalidate(&mut self) {
97        *self.cached_lines.borrow_mut() = None;
98        self.box_component.invalidate();
99    }
100}