feat(tui): replace hard‑coded colors with Theme values and propagate Theme through UI rendering
- Introduce `Theme` import and pass a cloned `theme` instance to UI helpers (e.g., `render_editable_textarea`). - Remove direct `Color` usage; UI now derives colors from `Theme` fields for placeholders, selections, ReAct components (thought, action, input, observation, final answer), status badges, operating mode badges, and model info panel. - Extend `Theme` with new color fields for agent ReAct stages, badge foreground/background, and operating mode colors. - Update rendering logic to apply these theme colors throughout the TUI (input panel, help text, status lines, model selection UI, etc.). - Adjust imports to drop unused `Color` references.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
|
||||
use serde_json;
|
||||
@@ -11,6 +11,7 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::chat_app::{ChatApp, HELP_TAB_COUNT, MessageRenderContext, ModelSelectorItemKind};
|
||||
use owlen_core::model::DetailedModelInfo;
|
||||
use owlen_core::theme::Theme;
|
||||
use owlen_core::types::{ModelInfo, Role};
|
||||
use owlen_core::ui::{FocusedPanel, InputMode};
|
||||
|
||||
@@ -160,6 +161,7 @@ fn render_editable_textarea(
|
||||
area: Rect,
|
||||
textarea: &mut TextArea<'static>,
|
||||
mut wrap_lines: bool,
|
||||
theme: &Theme,
|
||||
) {
|
||||
let block = textarea.block().cloned();
|
||||
let inner = block.as_ref().map(|b| b.inner(area)).unwrap_or(area);
|
||||
@@ -183,7 +185,7 @@ fn render_editable_textarea(
|
||||
|
||||
if is_empty {
|
||||
if !placeholder_text.is_empty() {
|
||||
let style = placeholder_style.unwrap_or_else(|| Style::default().fg(Color::DarkGray));
|
||||
let style = placeholder_style.unwrap_or_else(|| Style::default().fg(theme.placeholder));
|
||||
render_lines.push(Line::from(vec![Span::styled(placeholder_text, style)]));
|
||||
} else {
|
||||
render_lines.push(Line::default());
|
||||
@@ -954,7 +956,7 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
|
||||
// Detect ReAct components and apply color coding
|
||||
if line_trimmed.starts_with("THOUGHT:") {
|
||||
// Blue for THOUGHT
|
||||
let thought_color = theme.agent_thought;
|
||||
let thought_content = line_trimmed.strip_prefix("THOUGHT:").unwrap_or("").trim();
|
||||
let wrapped = wrap(thought_content, content_width as usize);
|
||||
|
||||
@@ -964,10 +966,10 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
Span::styled(
|
||||
"THOUGHT: ",
|
||||
Style::default()
|
||||
.fg(Color::Blue)
|
||||
.fg(thought_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(first.to_string(), Style::default().fg(Color::Blue)),
|
||||
Span::styled(first.to_string(), Style::default().fg(thought_color)),
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -975,28 +977,28 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
for chunk in wrapped.iter().skip(1) {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {}", chunk),
|
||||
Style::default().fg(Color::Blue),
|
||||
Style::default().fg(thought_color),
|
||||
)));
|
||||
}
|
||||
} else if line_trimmed.starts_with("ACTION:") {
|
||||
// Yellow for ACTION
|
||||
let action_color = theme.agent_action;
|
||||
let action_content = line_trimmed.strip_prefix("ACTION:").unwrap_or("").trim();
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
"ACTION: ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.fg(action_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
action_content,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.fg(action_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
} else if line_trimmed.starts_with("ACTION_INPUT:") {
|
||||
// Cyan for ACTION_INPUT
|
||||
let input_color = theme.agent_action_input;
|
||||
let input_content = line_trimmed
|
||||
.strip_prefix("ACTION_INPUT:")
|
||||
.unwrap_or("")
|
||||
@@ -1008,21 +1010,21 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
Span::styled(
|
||||
"ACTION_INPUT: ",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.fg(input_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(first.to_string(), Style::default().fg(Color::Cyan)),
|
||||
Span::styled(first.to_string(), Style::default().fg(input_color)),
|
||||
]));
|
||||
}
|
||||
|
||||
for chunk in wrapped.iter().skip(1) {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {}", chunk),
|
||||
Style::default().fg(Color::Cyan),
|
||||
Style::default().fg(input_color),
|
||||
)));
|
||||
}
|
||||
} else if line_trimmed.starts_with("OBSERVATION:") {
|
||||
// Green for OBSERVATION
|
||||
let observation_color = theme.agent_observation;
|
||||
let obs_content = line_trimmed
|
||||
.strip_prefix("OBSERVATION:")
|
||||
.unwrap_or("")
|
||||
@@ -1034,21 +1036,21 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
Span::styled(
|
||||
"OBSERVATION: ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.fg(observation_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(first.to_string(), Style::default().fg(Color::Green)),
|
||||
Span::styled(first.to_string(), Style::default().fg(observation_color)),
|
||||
]));
|
||||
}
|
||||
|
||||
for chunk in wrapped.iter().skip(1) {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {}", chunk),
|
||||
Style::default().fg(Color::Green),
|
||||
Style::default().fg(observation_color),
|
||||
)));
|
||||
}
|
||||
} else if line_trimmed.starts_with("FINAL_ANSWER:") {
|
||||
// Magenta for FINAL_ANSWER
|
||||
let answer_color = theme.agent_final_answer;
|
||||
let answer_content = line_trimmed
|
||||
.strip_prefix("FINAL_ANSWER:")
|
||||
.unwrap_or("")
|
||||
@@ -1060,13 +1062,13 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
Span::styled(
|
||||
"FINAL_ANSWER: ",
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.fg(answer_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
first.to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.fg(answer_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
@@ -1075,7 +1077,7 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
for chunk in wrapped.iter().skip(1) {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {}", chunk),
|
||||
Style::default().fg(Color::Magenta),
|
||||
Style::default().fg(answer_color),
|
||||
)));
|
||||
}
|
||||
} else if !line_trimmed.is_empty() {
|
||||
@@ -1123,7 +1125,7 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
}
|
||||
|
||||
fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
let theme = app.theme();
|
||||
let theme = app.theme().clone();
|
||||
let title = match app.mode() {
|
||||
InputMode::Editing => " Input (Enter=send · Ctrl+J=newline · Esc=exit input mode) ",
|
||||
InputMode::Visual => " Visual Mode (y=yank · d=cut · Esc=cancel) ",
|
||||
@@ -1154,13 +1156,13 @@ fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||
let textarea = app.textarea_mut();
|
||||
textarea.set_block(input_block.clone());
|
||||
textarea.set_hard_tab_indent(false);
|
||||
render_editable_textarea(frame, area, textarea, true);
|
||||
render_editable_textarea(frame, area, textarea, true, &theme);
|
||||
} else if matches!(app.mode(), InputMode::Visual) {
|
||||
// In visual mode, render textarea in read-only mode with selection
|
||||
let textarea = app.textarea_mut();
|
||||
textarea.set_block(input_block.clone());
|
||||
textarea.set_hard_tab_indent(false);
|
||||
render_editable_textarea(frame, area, textarea, true);
|
||||
render_editable_textarea(frame, area, textarea, true, &theme);
|
||||
} else if matches!(app.mode(), InputMode::Command) {
|
||||
// In command mode, show the command buffer with : prefix
|
||||
let command_text = format!(":{}", app.command_buffer());
|
||||
@@ -1291,31 +1293,35 @@ fn render_status(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
||||
spans.push(Span::styled(
|
||||
" 🤖 AGENT RUNNING ",
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Yellow)
|
||||
.fg(theme.agent_badge_running_fg)
|
||||
.bg(theme.agent_badge_running_bg)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else if app.is_agent_mode() {
|
||||
spans.push(Span::styled(
|
||||
" 🤖 AGENT MODE ",
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Cyan)
|
||||
.fg(theme.agent_badge_idle_fg)
|
||||
.bg(theme.agent_badge_idle_bg)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
}
|
||||
|
||||
// Add operating mode indicator
|
||||
let operating_mode = app.get_mode();
|
||||
let (op_mode_text, op_mode_color) = match operating_mode {
|
||||
owlen_core::mode::Mode::Chat => (" 💬 CHAT", Color::Blue),
|
||||
owlen_core::mode::Mode::Code => (" 💻 CODE", Color::Magenta),
|
||||
let (op_mode_text, op_mode_fg, op_mode_bg) = match operating_mode {
|
||||
owlen_core::mode::Mode::Chat => {
|
||||
(" 💬 CHAT", theme.operating_chat_fg, theme.operating_chat_bg)
|
||||
}
|
||||
owlen_core::mode::Mode::Code => {
|
||||
(" 💻 CODE", theme.operating_code_fg, theme.operating_code_bg)
|
||||
}
|
||||
};
|
||||
spans.push(Span::styled(
|
||||
op_mode_text,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(op_mode_color)
|
||||
.fg(op_mode_fg)
|
||||
.bg(op_mode_bg)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
|
||||
@@ -1707,7 +1713,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
Span::styled(
|
||||
"[1] ",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.fg(theme.mode_provider_selection)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Allow once "),
|
||||
@@ -1720,7 +1726,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
Span::styled(
|
||||
"[2] ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.fg(theme.mode_editing)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Allow session "),
|
||||
@@ -1733,7 +1739,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
Span::styled(
|
||||
"[3] ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.fg(theme.mode_model_selection)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Allow always "),
|
||||
@@ -1745,7 +1751,9 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
"[4] ",
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(theme.error)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Deny "),
|
||||
Span::styled(
|
||||
@@ -1758,7 +1766,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
Span::styled(
|
||||
"[Esc] ",
|
||||
Style::default()
|
||||
.fg(Color::DarkGray)
|
||||
.fg(theme.placeholder)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Cancel"),
|
||||
@@ -1950,7 +1958,7 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
}
|
||||
}
|
||||
|
||||
let help_text = match tab_index {
|
||||
let mut help_text = match tab_index {
|
||||
0 => vec![
|
||||
// Navigation
|
||||
Line::from(""),
|
||||
@@ -2219,6 +2227,25 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
help_text.insert(
|
||||
0,
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
"Current Theme: ",
|
||||
Style::default()
|
||||
.fg(theme.placeholder)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
),
|
||||
Span::styled(
|
||||
theme.name.clone(),
|
||||
Style::default()
|
||||
.fg(theme.mode_model_selection)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]),
|
||||
);
|
||||
help_text.insert(1, Line::from(""));
|
||||
|
||||
// Create layout for tabs and content
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
|
||||
Reference in New Issue
Block a user