feat(ui): add configurable role label display and syntax highlighting support

- 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.
This commit is contained in:
2025-10-12 16:44:53 +02:00
parent ae9c3af096
commit 55e6b0583d
22 changed files with 1484 additions and 140 deletions

View File

@@ -12,7 +12,7 @@ use crate::chat_app::{ChatApp, HELP_TAB_COUNT, MessageRenderContext, ModelSelect
use owlen_core::model::DetailedModelInfo;
use owlen_core::theme::Theme;
use owlen_core::types::{ModelInfo, Role};
use owlen_core::ui::{FocusedPanel, InputMode};
use owlen_core::ui::{FocusedPanel, InputMode, RoleLabelDisplay};
const PRIVACY_TAB_INDEX: usize = HELP_TAB_COUNT - 1;
@@ -687,7 +687,7 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
// Build the lines for messages using cached rendering
let mut lines: Vec<Line<'static>> = Vec::new();
let show_role_labels = formatter.show_role_labels();
let role_label_mode = formatter.role_label_mode();
for message_index in 0..total_messages {
let is_streaming = {
let conversation = app.conversation();
@@ -701,12 +701,13 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
message_index,
MessageRenderContext::new(
&mut formatter,
show_role_labels,
role_label_mode,
content_width as usize,
message_index + 1 == total_messages,
is_streaming,
app.get_loading_indicator(),
&theme,
app.should_highlight_code(),
),
);
lines.extend(message_lines);
@@ -729,20 +730,53 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
};
if app.get_loading_indicator() != "" && last_message_is_user {
let loading_spans = vec![
Span::raw("🤖 "),
Span::styled(
"Assistant:",
Style::default()
.fg(theme.assistant_message_role)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" {}", app.get_loading_indicator()),
Style::default().fg(theme.info),
),
];
lines.push(Line::from(loading_spans));
match role_label_mode {
RoleLabelDisplay::Inline => {
let (emoji, title) = crate::chat_app::role_label_parts(&Role::Assistant);
let inline_label = format!("{emoji} {title}:");
let label_width = UnicodeWidthStr::width(inline_label.as_str());
let max_label_width = crate::chat_app::max_inline_label_width();
let padding = max_label_width.saturating_sub(label_width);
let mut loading_spans = vec![
Span::raw(format!("{emoji} ")),
Span::styled(
format!("{title}:"),
Style::default()
.fg(theme.assistant_message_role)
.add_modifier(Modifier::BOLD),
),
];
if padding > 0 {
loading_spans.push(Span::raw(" ".repeat(padding)));
}
loading_spans.push(Span::raw(" "));
loading_spans.push(Span::styled(
app.get_loading_indicator().to_string(),
Style::default().fg(theme.info),
));
lines.push(Line::from(loading_spans));
}
_ => {
let loading_spans = vec![
Span::raw("🤖 "),
Span::styled(
"Assistant:",
Style::default()
.fg(theme.assistant_message_role)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" {}", app.get_loading_indicator()),
Style::default().fg(theme.info),
),
];
lines.push(Line::from(loading_spans));
}
}
}
if lines.is_empty() {