Skip to main content

rab/agent/ui/components/
user_message.rs

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