- Introduce `RoleLabelDisplay` enum (inline, above, none) and integrate it into UI rendering and message formatting. - Replace `show_role_labels` boolean with `role_label_mode` across config, formatter, session, and TUI components. - Add `syntax_highlighting` boolean to UI settings with default `false` and support in message rendering. - Update configuration schema version to 1.3.0 and provide deserialization handling for legacy boolean values. - Extend theme definitions with code block styling fields (background, border, text, keyword, string, comment) and default values in `Theme`. - Adjust related modules (`formatting.rs`, `ui.rs`, `session.rs`, `chat_app.rs`) to use the new settings and theme fields.
113 lines
3.5 KiB
Rust
113 lines
3.5 KiB
Rust
use crate::types::Message;
|
|
use crate::ui::RoleLabelDisplay;
|
|
|
|
/// Formats messages for display across different clients.
|
|
#[derive(Debug, Clone)]
|
|
pub struct MessageFormatter {
|
|
wrap_width: usize,
|
|
role_label_mode: RoleLabelDisplay,
|
|
preserve_empty_lines: bool,
|
|
}
|
|
|
|
impl MessageFormatter {
|
|
/// Create a new formatter
|
|
pub fn new(wrap_width: usize, role_label_mode: RoleLabelDisplay) -> Self {
|
|
Self {
|
|
wrap_width: wrap_width.max(20),
|
|
role_label_mode,
|
|
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);
|
|
}
|
|
|
|
/// The configured role label layout preference.
|
|
pub fn role_label_mode(&self) -> RoleLabelDisplay {
|
|
self.role_label_mode
|
|
}
|
|
|
|
/// Whether any role label should be shown alongside messages.
|
|
pub fn show_role_labels(&self) -> bool {
|
|
!matches!(self.role_label_mode, RoleLabelDisplay::None)
|
|
}
|
|
|
|
/// Update the role label layout preference.
|
|
pub fn set_role_label_mode(&mut self, mode: RoleLabelDisplay) {
|
|
self.role_label_mode = mode;
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|