Enhance TUI and core functionality: add header rendering, improve message formatting, and refine provider/model handling logic. Update dependencies.

This commit is contained in:
2025-09-27 07:06:57 +02:00
parent 9df489158c
commit 306104c5b4
5 changed files with 196 additions and 130 deletions

View File

@@ -15,7 +15,7 @@ impl MessageFormatter {
Self {
wrap_width: wrap_width.max(20),
show_role_labels,
preserve_empty_lines: true,
preserve_empty_lines: false,
}
}
@@ -25,36 +25,51 @@ impl MessageFormatter {
self
}
/// Render a message to a list of visual lines ready for display
pub fn format_message(&self, message: &Message) -> Vec<String> {
let mut lines = Vec::new();
/// Update the wrap width
pub fn set_wrap_width(&mut self, width: usize) {
self.wrap_width = width.max(20);
}
pub fn format_message(&self, message: &Message) -> Vec<String> {
// 1) Normalize line breaks to '\n' (handles CR, NEL, LS, PS)
let normalized: String = message
.content
.chars()
.map(|ch| match ch {
'\r' | '\u{0085}' | '\u{2028}' | '\u{2029}' => '\n',
_ => ch,
})
.collect();
// 2) Collapse: remove whitespace-only lines; keep exactly one '\n' between content lines
let mut content = normalized
.split('\n')
.map(|l| l.trim_end()) // trim trailing spaces per line
.filter(|l| !l.trim().is_empty()) // drop blank/whitespace-only lines
.collect::<Vec<_>>()
.join("\n")
.trim() // trim leading/trailing whitespace
.to_string();
let mut content = message.content.trim_end().to_string();
if content.is_empty() && self.preserve_empty_lines {
content.push(' ');
}
// 3) Wrap
let options = Options::new(self.wrap_width)
.break_words(true)
.word_separator(textwrap::WordSeparator::UnicodeBreakProperties);
let wrapped = wrap(&content, &options);
// 4) Post: rtrim each visual line; drop any whitespace-only lines
let mut lines: Vec<String> = wrap(&content, &options)
.into_iter()
.map(|s| s.trim_end().to_string())
.filter(|s| !s.trim().is_empty())
.collect();
if self.show_role_labels {
let label = format!("{}:", message.role.to_string().to_uppercase());
if let Some(first) = wrapped.first() {
lines.push(format!("{label} {first}"));
for line in wrapped.iter().skip(1) {
lines.push(format!("{:width$} {line}", "", width = label.len()));
}
} else {
lines.push(label);
}
} else {
for line in wrapped {
lines.push(line.into_owned());
}
}
// 5) Belt & suspenders: remove leading/trailing blanks if any survived
while lines.first().map_or(false, |s| s.trim().is_empty()) { lines.remove(0); }
while lines.last().map_or(false, |s| s.trim().is_empty()) { lines.pop(); }
lines
}

View File

@@ -87,6 +87,11 @@ impl SessionController {
&self.formatter
}
/// Update the wrap width of the message formatter
pub fn set_formatter_wrap_width(&mut self, width: usize) {
self.formatter.set_wrap_width(width);
}
/// Access configuration
pub fn config(&self) -> &Config {
&self.config