Files
owlen/crates/owlen-core/src/formatting.rs
vikingowl d002d35bde feat(theme): add tool_output color to themes
- Added a `tool_output` color to the `Theme` struct.
- Updated all built-in themes to include the new color.
- Modified the TUI to use the `tool_output` color for rendering tool output.
2025-10-06 22:18:17 +02:00

102 lines
3.2 KiB
Rust

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<String> {
message
.content
.trim()
.lines()
.map(|s| s.to_string())
.collect()
}
/// Extract thinking content from <think> 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<String>) {
let mut result = String::new();
let mut thinking = String::new();
let mut current_pos = 0;
while let Some(start_pos) = content[current_pos..].find("<think>") {
let abs_start = current_pos + start_pos;
// Add content before <think> tag to result
result.push_str(&content[current_pos..abs_start]);
// Find closing tag
if let Some(end_pos) = content[abs_start..].find("</think>") {
let abs_end = abs_start + end_pos;
let think_content = &content[abs_start + 7..abs_end]; // 7 = len("<think>")
if !thinking.is_empty() {
thinking.push_str("\n\n");
}
thinking.push_str(think_content.trim());
current_pos = abs_end + 8; // 8 = len("</think>")
} else {
// Unclosed tag - this is streaming content
// Extract everything after <think> as thinking content
let think_content = &content[abs_start + 7..]; // 7 = len("<think>")
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)
};
// If the result is empty but we have thinking content, show a placeholder
if result.trim().is_empty() && thinking_result.is_some() {
result.push_str("[Thinking...]");
}
(result, thinking_result)
}
}