From 083b621b7d5460aeb6bc72ea33e73696138083fa Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sun, 12 Oct 2025 15:16:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(tui):=20replace=20hard=E2=80=91coded=20col?= =?UTF-8?q?ors=20with=20Theme=20values=20and=20propagate=20Theme=20through?= =?UTF-8?q?=20UI=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- crates/owlen-core/src/theme.rs | 264 +++++++++++++++++++++++ crates/owlen-tui/src/chat_app.rs | 9 +- crates/owlen-tui/src/model_info_panel.rs | 4 +- crates/owlen-tui/src/ui.rs | 105 +++++---- 4 files changed, 337 insertions(+), 45 deletions(-) diff --git a/crates/owlen-core/src/theme.rs b/crates/owlen-core/src/theme.rs index d8d9b90..a5baa00 100644 --- a/crates/owlen-core/src/theme.rs +++ b/crates/owlen-core/src/theme.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; +pub type ThemePalette = Theme; + /// A complete theme definition for OWLEN TUI #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Theme { @@ -128,6 +130,84 @@ pub struct Theme { #[serde(deserialize_with = "deserialize_color")] #[serde(serialize_with = "serialize_color")] pub info: Color, + + /// Agent action coloring (ReAct THOUGHT) + #[serde(default = "Theme::default_agent_thought")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_thought: Color, + + /// Agent action coloring (ReAct ACTION) + #[serde(default = "Theme::default_agent_action")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_action: Color, + + /// Agent action coloring (ReAct ACTION_INPUT) + #[serde(default = "Theme::default_agent_action_input")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_action_input: Color, + + /// Agent action coloring (ReAct OBSERVATION) + #[serde(default = "Theme::default_agent_observation")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_observation: Color, + + /// Agent action coloring (ReAct FINAL_ANSWER) + #[serde(default = "Theme::default_agent_final_answer")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_final_answer: Color, + + /// Status badge foreground when agent is running + #[serde(default = "Theme::default_agent_badge_running_fg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_badge_running_fg: Color, + + /// Status badge background when agent is running + #[serde(default = "Theme::default_agent_badge_running_bg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_badge_running_bg: Color, + + /// Status badge foreground when agent mode is idle + #[serde(default = "Theme::default_agent_badge_idle_fg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_badge_idle_fg: Color, + + /// Status badge background when agent mode is idle + #[serde(default = "Theme::default_agent_badge_idle_bg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub agent_badge_idle_bg: Color, + + /// Operating mode badge foreground (Chat) + #[serde(default = "Theme::default_operating_chat_fg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub operating_chat_fg: Color, + + /// Operating mode badge background (Chat) + #[serde(default = "Theme::default_operating_chat_bg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub operating_chat_bg: Color, + + /// Operating mode badge foreground (Code) + #[serde(default = "Theme::default_operating_code_fg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub operating_code_fg: Color, + + /// Operating mode badge background (Code) + #[serde(default = "Theme::default_operating_code_bg")] + #[serde(deserialize_with = "deserialize_color")] + #[serde(serialize_with = "serialize_color")] + pub operating_code_bg: Color, } impl Default for Theme { @@ -136,6 +216,60 @@ impl Default for Theme { } } +impl Theme { + const fn default_agent_thought() -> Color { + Color::LightBlue + } + + const fn default_agent_action() -> Color { + Color::Yellow + } + + const fn default_agent_action_input() -> Color { + Color::LightCyan + } + + const fn default_agent_observation() -> Color { + Color::LightGreen + } + + const fn default_agent_final_answer() -> Color { + Color::Magenta + } + + const fn default_agent_badge_running_fg() -> Color { + Color::Black + } + + const fn default_agent_badge_running_bg() -> Color { + Color::Yellow + } + + const fn default_agent_badge_idle_fg() -> Color { + Color::Black + } + + const fn default_agent_badge_idle_bg() -> Color { + Color::Cyan + } + + const fn default_operating_chat_fg() -> Color { + Color::Black + } + + const fn default_operating_chat_bg() -> Color { + Color::Blue + } + + const fn default_operating_code_fg() -> Color { + Color::Black + } + + const fn default_operating_code_bg() -> Color { + Color::Magenta + } +} + /// Get the default themes directory path pub fn default_themes_dir() -> PathBuf { let config_dir = PathBuf::from(shellexpand::tilde(crate::config::DEFAULT_CONFIG_PATH).as_ref()) @@ -294,6 +428,19 @@ fn default_dark() -> Theme { placeholder: Color::DarkGray, error: Color::Red, info: Color::LightGreen, + agent_thought: Color::LightBlue, + agent_action: Color::Yellow, + agent_action_input: Color::LightCyan, + agent_observation: Color::LightGreen, + agent_final_answer: Color::Magenta, + agent_badge_running_fg: Color::Black, + agent_badge_running_bg: Color::Yellow, + agent_badge_idle_fg: Color::Black, + agent_badge_idle_bg: Color::Cyan, + operating_chat_fg: Color::Black, + operating_chat_bg: Color::Blue, + operating_code_fg: Color::Black, + operating_code_bg: Color::Magenta, } } @@ -324,6 +471,19 @@ fn default_light() -> Theme { placeholder: Color::Gray, error: Color::Rgb(192, 57, 43), info: Color::Green, + agent_thought: Color::Rgb(0, 85, 164), + agent_action: Color::Rgb(181, 137, 0), + agent_action_input: Color::Rgb(0, 139, 139), + agent_observation: Color::Rgb(46, 139, 87), + agent_final_answer: Color::Rgb(142, 68, 173), + agent_badge_running_fg: Color::White, + agent_badge_running_bg: Color::Rgb(241, 196, 15), + agent_badge_idle_fg: Color::White, + agent_badge_idle_bg: Color::Rgb(0, 150, 136), + operating_chat_fg: Color::White, + operating_chat_bg: Color::Rgb(0, 85, 164), + operating_code_fg: Color::White, + operating_code_bg: Color::Rgb(142, 68, 173), } } @@ -354,6 +514,19 @@ fn gruvbox() -> Theme { placeholder: Color::Rgb(102, 92, 84), error: Color::Rgb(251, 73, 52), // #fb4934 info: Color::Rgb(184, 187, 38), + agent_thought: Color::Rgb(131, 165, 152), + agent_action: Color::Rgb(250, 189, 47), + agent_action_input: Color::Rgb(142, 192, 124), + agent_observation: Color::Rgb(184, 187, 38), + agent_final_answer: Color::Rgb(211, 134, 155), + agent_badge_running_fg: Color::Rgb(40, 40, 40), + agent_badge_running_bg: Color::Rgb(250, 189, 47), + agent_badge_idle_fg: Color::Rgb(40, 40, 40), + agent_badge_idle_bg: Color::Rgb(131, 165, 152), + operating_chat_fg: Color::Rgb(40, 40, 40), + operating_chat_bg: Color::Rgb(131, 165, 152), + operating_code_fg: Color::Rgb(40, 40, 40), + operating_code_bg: Color::Rgb(211, 134, 155), } } @@ -384,6 +557,19 @@ fn dracula() -> Theme { placeholder: Color::Rgb(98, 114, 164), error: Color::Rgb(255, 85, 85), // #ff5555 info: Color::Rgb(80, 250, 123), + agent_thought: Color::Rgb(139, 233, 253), + agent_action: Color::Rgb(241, 250, 140), + agent_action_input: Color::Rgb(189, 147, 249), + agent_observation: Color::Rgb(80, 250, 123), + agent_final_answer: Color::Rgb(255, 121, 198), + agent_badge_running_fg: Color::Rgb(40, 42, 54), + agent_badge_running_bg: Color::Rgb(241, 250, 140), + agent_badge_idle_fg: Color::Rgb(40, 42, 54), + agent_badge_idle_bg: Color::Rgb(139, 233, 253), + operating_chat_fg: Color::Rgb(40, 42, 54), + operating_chat_bg: Color::Rgb(139, 233, 253), + operating_code_fg: Color::Rgb(40, 42, 54), + operating_code_bg: Color::Rgb(189, 147, 249), } } @@ -414,6 +600,19 @@ fn solarized() -> Theme { placeholder: Color::Rgb(88, 110, 117), error: Color::Rgb(220, 50, 47), // #dc322f (red) info: Color::Rgb(133, 153, 0), + agent_thought: Color::Rgb(42, 161, 152), + agent_action: Color::Rgb(181, 137, 0), + agent_action_input: Color::Rgb(38, 139, 210), + agent_observation: Color::Rgb(133, 153, 0), + agent_final_answer: Color::Rgb(108, 113, 196), + agent_badge_running_fg: Color::Rgb(0, 43, 54), + agent_badge_running_bg: Color::Rgb(181, 137, 0), + agent_badge_idle_fg: Color::Rgb(0, 43, 54), + agent_badge_idle_bg: Color::Rgb(42, 161, 152), + operating_chat_fg: Color::Rgb(0, 43, 54), + operating_chat_bg: Color::Rgb(42, 161, 152), + operating_code_fg: Color::Rgb(0, 43, 54), + operating_code_bg: Color::Rgb(108, 113, 196), } } @@ -444,6 +643,19 @@ fn midnight_ocean() -> Theme { placeholder: Color::Rgb(110, 118, 129), error: Color::Rgb(248, 81, 73), info: Color::Rgb(158, 206, 106), + agent_thought: Color::Rgb(121, 192, 255), + agent_action: Color::Rgb(255, 212, 59), + agent_action_input: Color::Rgb(137, 221, 255), + agent_observation: Color::Rgb(158, 206, 106), + agent_final_answer: Color::Rgb(246, 140, 245), + agent_badge_running_fg: Color::Rgb(13, 17, 23), + agent_badge_running_bg: Color::Rgb(255, 212, 59), + agent_badge_idle_fg: Color::Rgb(13, 17, 23), + agent_badge_idle_bg: Color::Rgb(137, 221, 255), + operating_chat_fg: Color::Rgb(13, 17, 23), + operating_chat_bg: Color::Rgb(121, 192, 255), + operating_code_fg: Color::Rgb(13, 17, 23), + operating_code_bg: Color::Rgb(246, 140, 245), } } @@ -474,6 +686,19 @@ fn rose_pine() -> Theme { placeholder: Color::Rgb(110, 106, 134), error: Color::Rgb(235, 111, 146), info: Color::Rgb(156, 207, 216), + agent_thought: Color::Rgb(156, 207, 216), + agent_action: Color::Rgb(246, 193, 119), + agent_action_input: Color::Rgb(196, 167, 231), + agent_observation: Color::Rgb(235, 188, 186), + agent_final_answer: Color::Rgb(235, 111, 146), + agent_badge_running_fg: Color::Rgb(25, 23, 36), + agent_badge_running_bg: Color::Rgb(246, 193, 119), + agent_badge_idle_fg: Color::Rgb(25, 23, 36), + agent_badge_idle_bg: Color::Rgb(156, 207, 216), + operating_chat_fg: Color::Rgb(25, 23, 36), + operating_chat_bg: Color::Rgb(156, 207, 216), + operating_code_fg: Color::Rgb(25, 23, 36), + operating_code_bg: Color::Rgb(196, 167, 231), } } @@ -504,6 +729,19 @@ fn monokai() -> Theme { placeholder: Color::Rgb(117, 113, 94), error: Color::Rgb(249, 38, 114), info: Color::Rgb(166, 226, 46), + agent_thought: Color::Rgb(102, 217, 239), + agent_action: Color::Rgb(230, 219, 116), + agent_action_input: Color::Rgb(174, 129, 255), + agent_observation: Color::Rgb(166, 226, 46), + agent_final_answer: Color::Rgb(249, 38, 114), + agent_badge_running_fg: Color::Rgb(39, 40, 34), + agent_badge_running_bg: Color::Rgb(230, 219, 116), + agent_badge_idle_fg: Color::Rgb(39, 40, 34), + agent_badge_idle_bg: Color::Rgb(102, 217, 239), + operating_chat_fg: Color::Rgb(39, 40, 34), + operating_chat_bg: Color::Rgb(102, 217, 239), + operating_code_fg: Color::Rgb(39, 40, 34), + operating_code_bg: Color::Rgb(174, 129, 255), } } @@ -534,6 +772,19 @@ fn material_dark() -> Theme { placeholder: Color::Rgb(84, 110, 122), error: Color::Rgb(240, 113, 120), info: Color::Rgb(195, 232, 141), + agent_thought: Color::Rgb(128, 203, 196), + agent_action: Color::Rgb(255, 203, 107), + agent_action_input: Color::Rgb(199, 146, 234), + agent_observation: Color::Rgb(195, 232, 141), + agent_final_answer: Color::Rgb(240, 113, 120), + agent_badge_running_fg: Color::Rgb(38, 50, 56), + agent_badge_running_bg: Color::Rgb(255, 203, 107), + agent_badge_idle_fg: Color::Rgb(38, 50, 56), + agent_badge_idle_bg: Color::Rgb(128, 203, 196), + operating_chat_fg: Color::Rgb(38, 50, 56), + operating_chat_bg: Color::Rgb(130, 170, 255), + operating_code_fg: Color::Rgb(38, 50, 56), + operating_code_bg: Color::Rgb(199, 146, 234), } } @@ -564,6 +815,19 @@ fn material_light() -> Theme { placeholder: Color::Rgb(144, 164, 174), error: Color::Rgb(211, 47, 47), info: Color::Rgb(56, 142, 60), + agent_thought: Color::Rgb(68, 138, 255), + agent_action: Color::Rgb(245, 124, 0), + agent_action_input: Color::Rgb(124, 77, 255), + agent_observation: Color::Rgb(56, 142, 60), + agent_final_answer: Color::Rgb(211, 47, 47), + agent_badge_running_fg: Color::White, + agent_badge_running_bg: Color::Rgb(245, 124, 0), + agent_badge_idle_fg: Color::White, + agent_badge_idle_bg: Color::Rgb(0, 150, 136), + operating_chat_fg: Color::White, + operating_chat_bg: Color::Rgb(68, 138, 255), + operating_code_fg: Color::White, + operating_code_bg: Color::Rgb(124, 77, 255), } } diff --git a/crates/owlen-tui/src/chat_app.rs b/crates/owlen-tui/src/chat_app.rs index 66aa44f..2d07660 100644 --- a/crates/owlen-tui/src/chat_app.rs +++ b/crates/owlen-tui/src/chat_app.rs @@ -9,7 +9,7 @@ use owlen_core::{ types::{ChatParameters, ChatResponse, Conversation, ModelInfo, Role}, ui::{AppState, AutoScroll, FocusedPanel, InputMode}, }; -use ratatui::style::{Color, Modifier, Style}; +use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use textwrap::wrap; use tokio::{sync::mpsc, task::JoinHandle}; @@ -1372,9 +1372,10 @@ impl ChatApp { // Sync buffer to textarea before entering visual mode self.sync_buffer_to_textarea(); // Set a visible selection style - self.textarea.set_selection_style( - Style::default().bg(Color::LightBlue).fg(Color::Black), - ); + let selection_style = Style::default() + .bg(self.theme.selection_bg) + .fg(self.theme.selection_fg); + self.textarea.set_selection_style(selection_style); // Start visual selection at current cursor position self.textarea.start_selection(); self.visual_start = Some(self.textarea.cursor()); diff --git a/crates/owlen-tui/src/model_info_panel.rs b/crates/owlen-tui/src/model_info_panel.rs index 07bce55..7cf0835 100644 --- a/crates/owlen-tui/src/model_info_panel.rs +++ b/crates/owlen-tui/src/model_info_panel.rs @@ -3,7 +3,7 @@ use owlen_core::theme::Theme; use ratatui::{ Frame, layout::Rect, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, widgets::{Block, Borders, Paragraph, Wrap}, }; @@ -57,7 +57,7 @@ impl ModelInfoPanel { .block(block) .style( Style::default() - .fg(Color::DarkGray) + .fg(theme.placeholder) .add_modifier(Modifier::ITALIC), ) .wrap(Wrap { trim: true }); diff --git a/crates/owlen-tui/src/ui.rs b/crates/owlen-tui/src/ui.rs index 4515f4f..bd487fa 100644 --- a/crates/owlen-tui/src/ui.rs +++ b/crates/owlen-tui/src/ui.rs @@ -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)