Skip to main content

rab/agent/ui/components/
message_components.rs

1use crate::agent::ui::components::AssistantMessageComponent;
2use crate::agent::ui::components::UserMessageComponent;
3use crate::agent::ui::components::bash_execution::{BashExecution, BashStatus};
4use crate::agent::ui::components::info_message::InfoMessageComponent;
5use crate::agent::ui::messages::DisplayMsg;
6use crate::agent::ui::theme::{RabTheme, current_theme};
7use crate::tui::Component;
8use crate::tui::components::Spacer;
9use crate::tui::components::Text;
10use crate::tui::components::r#box::TuiBox;
11
12/// Convert a single DisplayMsg to a Box<dyn Component> for use in chat_container.
13/// This is used for initial session message loading.
14/// New messages during the session should be added as Components directly in handle_agent_event.
15pub fn display_msg_to_component(msg: &DisplayMsg) -> Option<std::boxed::Box<dyn Component>> {
16    // Clone the theme so we can use it in closures without borrow issues
17    let theme: RabTheme = current_theme().clone();
18    match msg {
19        DisplayMsg::User(text) => Some(std::boxed::Box::new(UserMessageComponent::new(
20            text.clone(),
21        ))),
22        DisplayMsg::AssistantText(text) => {
23            if text.is_empty() {
24                return None;
25            }
26            Some(std::boxed::Box::new(AssistantMessageComponent::new(
27                text.clone(),
28            )))
29        }
30        DisplayMsg::Thinking { text, level: _ } => {
31            let styled = theme.fg("thinkingText", &theme.italic(text));
32            Some(std::boxed::Box::new(Text::new(styled, 0, 0, None)))
33        }
34        DisplayMsg::ToolCall { name: _, args: _ } => {
35            // ToolCall uses ToolExecutionComponent - skip for initial load
36            None
37        }
38        DisplayMsg::ToolResult {
39            content,
40            compact: _,
41            is_error,
42        } => {
43            let color = if *is_error { "error" } else { "toolOutput" };
44            let styled = theme.fg(color, content);
45            let bg_key = if *is_error {
46                "toolErrorBg"
47            } else {
48                "toolSuccessBg"
49            };
50            let bg_ansi = theme.bg_ansi(bg_key).to_string();
51            let mut msg_box = TuiBox::new(
52                1,
53                1,
54                Some(std::boxed::Box::new(move |s: &str| -> String {
55                    format!("{}{}\x1b[49m", bg_ansi, s)
56                })),
57            );
58            msg_box.add_child(std::boxed::Box::new(Text::new(styled, 0, 0, None)));
59            Some(std::boxed::Box::new(msg_box))
60        }
61        DisplayMsg::BashCommand {
62            command,
63            output_lines,
64            status,
65            expanded: _,
66        } => {
67            let mut bash = BashExecution::new(command.clone());
68            for line in output_lines {
69                bash.append_output(line.clone());
70            }
71            match status {
72                BashStatus::Running => {}
73                BashStatus::Complete { exit_code } => {
74                    bash.set_complete(*exit_code);
75                }
76                BashStatus::Cancelled => {
77                    bash.set_cancelled();
78                }
79                BashStatus::Error(msg) => {
80                    bash.set_error(msg.clone());
81                }
82            }
83            Some(std::boxed::Box::new(bash))
84        }
85        DisplayMsg::Info(text) => Some(std::boxed::Box::new(InfoMessageComponent::new(text))),
86        DisplayMsg::Separator => Some(std::boxed::Box::new(Spacer::new(1))),
87    }
88}