use crate::types::Message; /// Formats messages for display across different clients. #[derive(Debug, Clone)] pub struct MessageFormatter { wrap_width: usize, show_role_labels: bool, preserve_empty_lines: bool, } impl MessageFormatter { /// Create a new formatter pub fn new(wrap_width: usize, show_role_labels: bool) -> Self { Self { wrap_width: wrap_width.max(20), show_role_labels, preserve_empty_lines: false, } } /// Override whether empty lines should be preserved pub fn with_preserve_empty(mut self, preserve: bool) -> Self { self.preserve_empty_lines = preserve; self } /// Update the wrap width pub fn set_wrap_width(&mut self, width: usize) { self.wrap_width = width.max(20); } /// Whether role labels should be shown alongside messages pub fn show_role_labels(&self) -> bool { self.show_role_labels } pub fn format_message(&self, message: &Message) -> Vec { message .content .trim() .lines() .map(|s| s.to_string()) .collect() } /// Extract thinking content from tags, returning (content_without_think, thinking_content) /// This handles both complete and incomplete (streaming) think tags. pub fn extract_thinking(&self, content: &str) -> (String, Option) { let mut result = String::new(); let mut thinking = String::new(); let mut current_pos = 0; while let Some(start_pos) = content[current_pos..].find("") { let abs_start = current_pos + start_pos; // Add content before tag to result result.push_str(&content[current_pos..abs_start]); // Find closing tag if let Some(end_pos) = content[abs_start..].find("") { let abs_end = abs_start + end_pos; let think_content = &content[abs_start + 7..abs_end]; // 7 = len("") if !thinking.is_empty() { thinking.push_str("\n\n"); } thinking.push_str(think_content.trim()); current_pos = abs_end + 8; // 8 = len("") } else { // Unclosed tag - this is streaming content // Extract everything after as thinking content let think_content = &content[abs_start + 7..]; // 7 = len("") if !thinking.is_empty() { thinking.push_str("\n\n"); } thinking.push_str(think_content); current_pos = content.len(); break; } } // Add remaining content result.push_str(&content[current_pos..]); let thinking_result = if thinking.is_empty() { None } else { Some(thinking) }; (result, thinking_result) } }