feat(ui): add glass modals and theme preview
AC:\n- Theme, help, command, and model modals share the glass chrome.\n- Theme selector shows a live preview for the highlighted palette.\n- Updated docs and screenshots explain the refreshed cockpit.\n\nTests:\n- cargo test -p owlen-tui
This commit is contained in:
@@ -23,7 +23,7 @@ This project is currently in **alpha** and under active development. Core featur
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
The OWLEN interface features a clean, multi-panel layout with vim-inspired navigation. See more screenshots in the [`images/`](images/) directory.
|
The refreshed chrome introduces a cockpit-style header with live gradient gauges for context and cloud usage, plus glassy panels that keep vim-inspired navigation easy to follow. See more screenshots in the [`images/`](images/) directory.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ The OWLEN interface features a clean, multi-panel layout with vim-inspired navig
|
|||||||
- **Advanced Text Editing**: Multi-line input, history, and clipboard support.
|
- **Advanced Text Editing**: Multi-line input, history, and clipboard support.
|
||||||
- **Session Management**: Save, load, and manage conversations.
|
- **Session Management**: Save, load, and manage conversations.
|
||||||
- **Code Side Panel**: Switch to code mode (`:mode code`) and open files inline with `:open <path>` for LLM-assisted coding.
|
- **Code Side Panel**: Switch to code mode (`:mode code`) and open files inline with `:open <path>` for LLM-assisted coding.
|
||||||
|
- **Cockpit Header**: Gradient context and cloud usage bars with live quota bands and provider fallbacks.
|
||||||
- **Theming System**: 10 built-in themes and support for custom themes.
|
- **Theming System**: 10 built-in themes and support for custom themes.
|
||||||
- **Modular Architecture**: Extensible provider system orchestrated by the new `ProviderManager`, ready for additional MCP-backed providers.
|
- **Modular Architecture**: Extensible provider system orchestrated by the new `ProviderManager`, ready for additional MCP-backed providers.
|
||||||
- **Dual-Source Model Picker**: Merge local and cloud catalogues with real-time availability badges powered by the background health worker.
|
- **Dual-Source Model Picker**: Merge local and cloud catalogues with real-time availability badges powered by the background health worker.
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ pub struct ContextUsage {
|
|||||||
pub(crate) struct LayoutSnapshot {
|
pub(crate) struct LayoutSnapshot {
|
||||||
pub(crate) frame: Rect,
|
pub(crate) frame: Rect,
|
||||||
pub(crate) content: Rect,
|
pub(crate) content: Rect,
|
||||||
|
pub(crate) header_panel: Option<Rect>,
|
||||||
pub(crate) file_panel: Option<Rect>,
|
pub(crate) file_panel: Option<Rect>,
|
||||||
pub(crate) chat_panel: Option<Rect>,
|
pub(crate) chat_panel: Option<Rect>,
|
||||||
pub(crate) thinking_panel: Option<Rect>,
|
pub(crate) thinking_panel: Option<Rect>,
|
||||||
@@ -131,6 +132,7 @@ impl LayoutSnapshot {
|
|||||||
Self {
|
Self {
|
||||||
frame,
|
frame,
|
||||||
content,
|
content,
|
||||||
|
header_panel: None,
|
||||||
file_panel: None,
|
file_panel: None,
|
||||||
chat_panel: None,
|
chat_panel: None,
|
||||||
thinking_panel: None,
|
thinking_panel: None,
|
||||||
@@ -155,6 +157,11 @@ impl LayoutSnapshot {
|
|||||||
return Some(UiRegion::ModelInfo);
|
return Some(UiRegion::ModelInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(rect) = self.header_panel {
|
||||||
|
if Self::contains(rect, column, row) {
|
||||||
|
return Some(UiRegion::Header);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(rect) = self.code_panel {
|
if let Some(rect) = self.code_panel {
|
||||||
if Self::contains(rect, column, row) {
|
if Self::contains(rect, column, row) {
|
||||||
return Some(UiRegion::Code);
|
return Some(UiRegion::Code);
|
||||||
@@ -213,6 +220,7 @@ impl Default for LayoutSnapshot {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum UiRegion {
|
enum UiRegion {
|
||||||
|
Header,
|
||||||
Frame,
|
Frame,
|
||||||
Content,
|
Content,
|
||||||
FileTree,
|
FileTree,
|
||||||
@@ -7992,7 +8000,8 @@ impl ChatApp {
|
|||||||
| UiRegion::Status
|
| UiRegion::Status
|
||||||
| UiRegion::Chat
|
| UiRegion::Chat
|
||||||
| UiRegion::Content
|
| UiRegion::Content
|
||||||
| UiRegion::Frame => {
|
| UiRegion::Frame
|
||||||
|
| UiRegion::Header => {
|
||||||
if self.focus_panel(FocusedPanel::Chat) {
|
if self.focus_panel(FocusedPanel::Chat) {
|
||||||
self.auto_scroll
|
self.auto_scroll
|
||||||
.on_user_scroll(amount, self.viewport_height);
|
.on_user_scroll(amount, self.viewport_height);
|
||||||
@@ -8036,7 +8045,8 @@ impl ChatApp {
|
|||||||
| UiRegion::Status
|
| UiRegion::Status
|
||||||
| UiRegion::Chat
|
| UiRegion::Chat
|
||||||
| UiRegion::Content
|
| UiRegion::Content
|
||||||
| UiRegion::Frame => {
|
| UiRegion::Frame
|
||||||
|
| UiRegion::Header => {
|
||||||
self.focus_panel(FocusedPanel::Chat);
|
self.focus_panel(FocusedPanel::Chat);
|
||||||
self.set_input_mode(InputMode::Normal);
|
self.set_input_mode(InputMode::Normal);
|
||||||
}
|
}
|
||||||
|
|||||||
144
crates/owlen-tui/src/glass.rs
Normal file
144
crates/owlen-tui/src/glass.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
use owlen_core::theme::Theme;
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct GlassPalette {
|
||||||
|
pub active: Color,
|
||||||
|
pub inactive: Color,
|
||||||
|
pub highlight: Color,
|
||||||
|
pub track: Color,
|
||||||
|
pub label: Color,
|
||||||
|
pub shadow: Color,
|
||||||
|
pub context_stops: [Color; 3],
|
||||||
|
pub usage_stops: [Color; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlassPalette {
|
||||||
|
pub fn for_theme(theme: &Theme) -> Self {
|
||||||
|
let luminance = color_luminance(theme.background);
|
||||||
|
if luminance < 0.5 {
|
||||||
|
Self {
|
||||||
|
active: Color::Rgb(26, 28, 40),
|
||||||
|
inactive: Color::Rgb(18, 20, 30),
|
||||||
|
highlight: Color::Rgb(32, 35, 48),
|
||||||
|
track: Color::Rgb(35, 38, 50),
|
||||||
|
label: Color::Rgb(241, 245, 249),
|
||||||
|
shadow: Color::Rgb(8, 9, 16),
|
||||||
|
context_stops: [
|
||||||
|
Color::Rgb(56, 189, 248),
|
||||||
|
Color::Rgb(250, 204, 21),
|
||||||
|
Color::Rgb(248, 113, 113),
|
||||||
|
],
|
||||||
|
usage_stops: [
|
||||||
|
Color::Rgb(34, 211, 238),
|
||||||
|
Color::Rgb(250, 204, 21),
|
||||||
|
Color::Rgb(248, 113, 113),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
active: Color::Rgb(242, 247, 255),
|
||||||
|
inactive: Color::Rgb(229, 235, 250),
|
||||||
|
highlight: Color::Rgb(224, 230, 248),
|
||||||
|
track: Color::Rgb(203, 210, 230),
|
||||||
|
label: Color::Rgb(31, 41, 55),
|
||||||
|
shadow: Color::Rgb(200, 205, 220),
|
||||||
|
context_stops: [
|
||||||
|
Color::Rgb(59, 130, 246),
|
||||||
|
Color::Rgb(234, 179, 8),
|
||||||
|
Color::Rgb(239, 68, 68),
|
||||||
|
],
|
||||||
|
usage_stops: [
|
||||||
|
Color::Rgb(20, 184, 166),
|
||||||
|
Color::Rgb(245, 158, 11),
|
||||||
|
Color::Rgb(239, 68, 68),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gradient_color(stops: &[Color; 3], t: f64) -> Color {
|
||||||
|
let clamped = t.clamp(0.0, 1.0);
|
||||||
|
let segments = stops.len() - 1;
|
||||||
|
let scaled = clamped * segments as f64;
|
||||||
|
let index = scaled.floor() as usize;
|
||||||
|
let frac = scaled - index as f64;
|
||||||
|
let start = stops[index.min(stops.len() - 1)];
|
||||||
|
let end = stops[(index + 1).min(stops.len() - 1)];
|
||||||
|
let (sr, sg, sb) = color_to_rgb(start);
|
||||||
|
let (er, eg, eb) = color_to_rgb(end);
|
||||||
|
let mix = |a: u8, b: u8| -> u8 { (a as f64 + (b as f64 - a as f64) * frac).round() as u8 };
|
||||||
|
Color::Rgb(mix(sr, er), mix(sg, eg), mix(sb, eb))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_luminance(color: Color) -> f64 {
|
||||||
|
let (r, g, b) = color_to_rgb(color);
|
||||||
|
let r = r as f64 / 255.0;
|
||||||
|
let g = g as f64 / 255.0;
|
||||||
|
let b = b as f64 / 255.0;
|
||||||
|
0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_to_rgb(color: Color) -> (u8, u8, u8) {
|
||||||
|
match color {
|
||||||
|
Color::Reset => (0, 0, 0),
|
||||||
|
Color::Black => (0, 0, 0),
|
||||||
|
Color::Red => (205, 49, 49),
|
||||||
|
Color::Green => (49, 205, 49),
|
||||||
|
Color::Yellow => (205, 198, 49),
|
||||||
|
Color::Blue => (49, 49, 205),
|
||||||
|
Color::Magenta => (205, 49, 205),
|
||||||
|
Color::Cyan => (49, 205, 205),
|
||||||
|
Color::Gray => (170, 170, 170),
|
||||||
|
Color::DarkGray => (100, 100, 100),
|
||||||
|
Color::LightRed => (255, 128, 128),
|
||||||
|
Color::LightGreen => (144, 238, 144),
|
||||||
|
Color::LightYellow => (255, 255, 170),
|
||||||
|
Color::LightBlue => (173, 216, 230),
|
||||||
|
Color::LightMagenta => (255, 182, 255),
|
||||||
|
Color::LightCyan => (175, 238, 238),
|
||||||
|
Color::White => (255, 255, 255),
|
||||||
|
Color::Rgb(r, g, b) => (r, g, b),
|
||||||
|
Color::Indexed(idx) => indexed_to_rgb(idx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indexed_to_rgb(idx: u8) -> (u8, u8, u8) {
|
||||||
|
match idx {
|
||||||
|
0 => (0, 0, 0),
|
||||||
|
1 => (128, 0, 0),
|
||||||
|
2 => (0, 128, 0),
|
||||||
|
3 => (128, 128, 0),
|
||||||
|
4 => (0, 0, 128),
|
||||||
|
5 => (128, 0, 128),
|
||||||
|
6 => (0, 128, 128),
|
||||||
|
7 => (192, 192, 192),
|
||||||
|
8 => (128, 128, 128),
|
||||||
|
9 => (255, 0, 0),
|
||||||
|
10 => (0, 255, 0),
|
||||||
|
11 => (255, 255, 0),
|
||||||
|
12 => (92, 92, 255),
|
||||||
|
13 => (255, 0, 255),
|
||||||
|
14 => (0, 255, 255),
|
||||||
|
15 => (255, 255, 255),
|
||||||
|
16..=231 => {
|
||||||
|
let idx = idx - 16;
|
||||||
|
let r = idx / 36;
|
||||||
|
let g = (idx % 36) / 6;
|
||||||
|
let b = idx % 6;
|
||||||
|
let convert = |component: u8| {
|
||||||
|
if component == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
component.saturating_mul(40).saturating_add(55)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(convert(r), convert(g), convert(b))
|
||||||
|
}
|
||||||
|
232..=255 => {
|
||||||
|
let shade = 8 + (idx - 232) * 10;
|
||||||
|
(shade, shade, shade)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ pub mod code_app;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub(crate) mod glass;
|
||||||
pub mod highlight;
|
pub mod highlight;
|
||||||
pub mod model_info_panel;
|
pub mod model_info_panel;
|
||||||
pub mod slash;
|
pub mod slash;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ use ratatui::{
|
|||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
|
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, block::Padding},
|
||||||
};
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
@@ -16,6 +16,7 @@ use crate::chat_app::{
|
|||||||
ChatApp, HighlightMask, ModelAvailabilityState, ModelScope, ModelSearchInfo,
|
ChatApp, HighlightMask, ModelAvailabilityState, ModelScope, ModelSearchInfo,
|
||||||
ModelSelectorItemKind,
|
ModelSelectorItemKind,
|
||||||
};
|
};
|
||||||
|
use crate::glass::GlassPalette;
|
||||||
|
|
||||||
/// Filtering modes for the model picker popup.
|
/// Filtering modes for the model picker popup.
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
@@ -29,6 +30,7 @@ pub enum FilterMode {
|
|||||||
|
|
||||||
pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||||
let theme = app.theme();
|
let theme = app.theme();
|
||||||
|
let palette = GlassPalette::for_theme(theme);
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
if area.width == 0 || area.height == 0 {
|
if area.width == 0 || area.height == 0 {
|
||||||
return;
|
return;
|
||||||
@@ -62,17 +64,33 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let popup_area = Rect::new(x, y, width, height);
|
let popup_area = Rect::new(x, y, width, height);
|
||||||
|
if popup_area.width > 2 && popup_area.height > 2 {
|
||||||
|
let shadow_area = Rect::new(
|
||||||
|
popup_area.x.saturating_add(1),
|
||||||
|
popup_area.y.saturating_add(1),
|
||||||
|
popup_area.width.saturating_sub(1),
|
||||||
|
popup_area.height.saturating_sub(1),
|
||||||
|
);
|
||||||
|
if shadow_area.width > 0 && shadow_area.height > 0 {
|
||||||
|
frame.render_widget(
|
||||||
|
Block::default().style(Style::default().bg(palette.shadow)),
|
||||||
|
shadow_area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
frame.render_widget(Clear, popup_area);
|
frame.render_widget(Clear, popup_area);
|
||||||
|
|
||||||
let mut title_spans = vec![
|
let mut title_spans = vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
" Model Selector ",
|
" Model Selector ",
|
||||||
Style::default().fg(theme.info).add_modifier(Modifier::BOLD),
|
Style::default()
|
||||||
|
.fg(palette.label)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("· Provider: {}", app.selected_provider),
|
format!("· Provider: {}", app.selected_provider),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(theme.placeholder)
|
.fg(palette.label)
|
||||||
.add_modifier(Modifier::DIM),
|
.add_modifier(Modifier::DIM),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -83,9 +101,10 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(Line::from(title_spans))
|
.title(Line::from(title_spans))
|
||||||
.borders(Borders::ALL)
|
.title_style(Style::default().fg(palette.label))
|
||||||
.border_style(Style::default().fg(theme.info))
|
.borders(Borders::NONE)
|
||||||
.style(Style::default().bg(theme.background).fg(theme.text));
|
.padding(Padding::new(2, 2, 1, 1))
|
||||||
|
.style(Style::default().bg(palette.active).fg(palette.label));
|
||||||
|
|
||||||
let inner = block.inner(popup_area);
|
let inner = block.inner(popup_area);
|
||||||
frame.render_widget(block, popup_area);
|
frame.render_widget(block, popup_area);
|
||||||
@@ -104,10 +123,10 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
|
|
||||||
let matches = app.visible_model_count();
|
let matches = app.visible_model_count();
|
||||||
let search_prefix = Style::default()
|
let search_prefix = Style::default()
|
||||||
.fg(theme.placeholder)
|
.fg(palette.label)
|
||||||
.add_modifier(Modifier::DIM);
|
.add_modifier(Modifier::DIM);
|
||||||
let bracket_style = Style::default()
|
let bracket_style = Style::default()
|
||||||
.fg(theme.placeholder)
|
.fg(palette.label)
|
||||||
.add_modifier(Modifier::DIM);
|
.add_modifier(Modifier::DIM);
|
||||||
let caret_style = if search_active {
|
let caret_style = if search_active {
|
||||||
Style::default()
|
Style::default()
|
||||||
@@ -115,7 +134,7 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(theme.placeholder)
|
.fg(palette.label)
|
||||||
.add_modifier(Modifier::DIM)
|
.add_modifier(Modifier::DIM)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,8 +154,8 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
search_spans.push(Span::styled(
|
search_spans.push(Span::styled(
|
||||||
"Type to search…",
|
"Type to search…",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(theme.placeholder)
|
.fg(palette.label)
|
||||||
.add_modifier(Modifier::DIM),
|
.add_modifier(Modifier::DIM | Modifier::ITALIC),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,35 +172,37 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
suffix_label,
|
suffix_label,
|
||||||
if matches == 1 { "" } else { "s" }
|
if matches == 1 { "" } else { "s" }
|
||||||
),
|
),
|
||||||
Style::default().fg(theme.placeholder),
|
Style::default()
|
||||||
|
.fg(palette.label)
|
||||||
|
.add_modifier(Modifier::DIM),
|
||||||
));
|
));
|
||||||
|
|
||||||
let search_line = Line::from(search_spans);
|
let search_line = Line::from(search_spans);
|
||||||
|
|
||||||
let instruction_line = if search_active {
|
let instruction_line = if search_active {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("Backspace", Style::default().fg(theme.placeholder)),
|
Span::styled("Backspace", Style::default().fg(palette.label)),
|
||||||
Span::raw(": delete "),
|
Span::raw(": delete "),
|
||||||
Span::styled("Ctrl+U", Style::default().fg(theme.placeholder)),
|
Span::styled("Ctrl+U", Style::default().fg(palette.label)),
|
||||||
Span::raw(": clear "),
|
Span::raw(": clear "),
|
||||||
Span::styled("Enter", Style::default().fg(theme.placeholder)),
|
Span::styled("Enter", Style::default().fg(palette.label)),
|
||||||
Span::raw(": select "),
|
Span::raw(": select "),
|
||||||
Span::styled("Esc", Style::default().fg(theme.placeholder)),
|
Span::styled("Esc", Style::default().fg(palette.label)),
|
||||||
Span::raw(": close"),
|
Span::raw(": close"),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("Enter", Style::default().fg(theme.placeholder)),
|
Span::styled("Enter", Style::default().fg(palette.label)),
|
||||||
Span::raw(": select "),
|
Span::raw(": select "),
|
||||||
Span::styled("Space", Style::default().fg(theme.placeholder)),
|
Span::styled("Space", Style::default().fg(palette.label)),
|
||||||
Span::raw(": toggle provider "),
|
Span::raw(": toggle provider "),
|
||||||
Span::styled("Esc", Style::default().fg(theme.placeholder)),
|
Span::styled("Esc", Style::default().fg(palette.label)),
|
||||||
Span::raw(": close"),
|
Span::raw(": close"),
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
|
||||||
let search_paragraph = Paragraph::new(vec![search_line, instruction_line])
|
let search_paragraph = Paragraph::new(vec![search_line, instruction_line])
|
||||||
.style(Style::default().bg(theme.background).fg(theme.text));
|
.style(Style::default().bg(palette.highlight).fg(palette.label));
|
||||||
frame.render_widget(search_paragraph, layout[0]);
|
frame.render_widget(search_paragraph, layout[0]);
|
||||||
|
|
||||||
let highlight_style = Style::default()
|
let highlight_style = Style::default()
|
||||||
@@ -236,7 +257,7 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let line = clip_line_to_width(Line::from(spans), max_line_width);
|
let line = clip_line_to_width(Line::from(spans), max_line_width);
|
||||||
items.push(ListItem::new(vec![line]).style(Style::default().bg(theme.background)));
|
items.push(ListItem::new(vec![line]).style(Style::default().bg(palette.active)));
|
||||||
}
|
}
|
||||||
ModelSelectorItemKind::Scope { label, status, .. } => {
|
ModelSelectorItemKind::Scope { label, status, .. } => {
|
||||||
let (style, icon) = scope_status_style(*status, theme);
|
let (style, icon) = scope_status_style(*status, theme);
|
||||||
@@ -248,7 +269,7 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
]),
|
]),
|
||||||
max_line_width,
|
max_line_width,
|
||||||
);
|
);
|
||||||
items.push(ListItem::new(vec![line]).style(Style::default().bg(theme.background)));
|
items.push(ListItem::new(vec![line]).style(Style::default().bg(palette.active)));
|
||||||
}
|
}
|
||||||
ModelSelectorItemKind::Model { model_index, .. } => {
|
ModelSelectorItemKind::Model { model_index, .. } => {
|
||||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||||
@@ -286,7 +307,7 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
max_line_width,
|
max_line_width,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
items.push(ListItem::new(lines).style(Style::default().bg(theme.background)));
|
items.push(ListItem::new(lines).style(Style::default().bg(palette.active)));
|
||||||
}
|
}
|
||||||
ModelSelectorItemKind::Empty {
|
ModelSelectorItemKind::Empty {
|
||||||
message, status, ..
|
message, status, ..
|
||||||
@@ -299,7 +320,7 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
let mut spans = vec![Span::styled(icon, style), Span::raw(" ")];
|
let mut spans = vec![Span::styled(icon, style), Span::raw(" ")];
|
||||||
spans.push(Span::styled(format!(" {}", msg), style));
|
spans.push(Span::styled(format!(" {}", msg), style));
|
||||||
let line = clip_line_to_width(Line::from(spans), max_line_width);
|
let line = clip_line_to_width(Line::from(spans), max_line_width);
|
||||||
items.push(ListItem::new(vec![line]).style(Style::default().bg(theme.background)));
|
items.push(ListItem::new(vec![line]).style(Style::default().bg(palette.active)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +332,8 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
.fg(theme.selection_fg)
|
.fg(theme.selection_fg)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
)
|
)
|
||||||
.highlight_symbol(" ");
|
.highlight_symbol(" ")
|
||||||
|
.style(Style::default().bg(palette.active).fg(palette.label));
|
||||||
|
|
||||||
let mut state = ListState::default();
|
let mut state = ListState::default();
|
||||||
state.select(app.selected_model_item());
|
state.select(app.selected_model_item());
|
||||||
@@ -325,10 +347,10 @@ pub fn render_model_picker(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
|
|
||||||
let footer = Paragraph::new(Line::from(Span::styled(
|
let footer = Paragraph::new(Line::from(Span::styled(
|
||||||
footer_text,
|
footer_text,
|
||||||
Style::default().fg(theme.placeholder),
|
Style::default().fg(palette.label),
|
||||||
)))
|
)))
|
||||||
.alignment(ratatui::layout::Alignment::Center)
|
.alignment(ratatui::layout::Alignment::Center)
|
||||||
.style(Style::default().bg(theme.background).fg(theme.placeholder));
|
.style(Style::default().bg(palette.highlight).fg(palette.label));
|
||||||
frame.render_widget(footer, layout[2]);
|
frame.render_widget(footer, layout[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,5 +121,6 @@ If you are experiencing performance issues, you can try the following:
|
|||||||
|
|
||||||
- **Reduce context size:** A smaller context size will result in faster responses from the LLM.
|
- **Reduce context size:** A smaller context size will result in faster responses from the LLM.
|
||||||
- **Use a less resource-intensive model:** Some models are faster but less capable than others.
|
- **Use a less resource-intensive model:** Some models are faster but less capable than others.
|
||||||
|
- **Watch the header gauges:** The cockpit header now shows live context usage and cloud quota bands—if either bar turns amber or red, trim the prompt or switch providers before retrying.
|
||||||
|
|
||||||
If you are still having trouble, please [open an issue](https://github.com/Owlibou/owlen/issues) on our GitHub repository.
|
If you are still having trouble, please [open an issue](https://github.com/Owlibou/owlen/issues) on our GitHub repository.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 64 KiB |
@@ -1,30 +1,30 @@
|
|||||||
name = "default_dark"
|
name = "default_dark"
|
||||||
text = "white"
|
text = "#e2e8f0"
|
||||||
background = "black"
|
background = "#020617"
|
||||||
focused_panel_border = "lightmagenta"
|
focused_panel_border = "#7dd3fc"
|
||||||
unfocused_panel_border = "#5f1487"
|
unfocused_panel_border = "#1e293b"
|
||||||
user_message_role = "lightblue"
|
user_message_role = "#38bdf8"
|
||||||
assistant_message_role = "yellow"
|
assistant_message_role = "#fbbf24"
|
||||||
tool_output = "gray"
|
tool_output = "#94a3b8"
|
||||||
thinking_panel_title = "lightmagenta"
|
thinking_panel_title = "#a855f7"
|
||||||
command_bar_background = "black"
|
command_bar_background = "#0f172a"
|
||||||
status_background = "black"
|
status_background = "#111827"
|
||||||
mode_normal = "lightblue"
|
mode_normal = "#38bdf8"
|
||||||
mode_editing = "lightgreen"
|
mode_editing = "#34d399"
|
||||||
mode_model_selection = "lightyellow"
|
mode_model_selection = "#fbbf24"
|
||||||
mode_provider_selection = "lightcyan"
|
mode_provider_selection = "#22d3ee"
|
||||||
mode_help = "lightmagenta"
|
mode_help = "#a855f7"
|
||||||
mode_visual = "magenta"
|
mode_visual = "#f472b6"
|
||||||
mode_command = "yellow"
|
mode_command = "#facc15"
|
||||||
selection_bg = "lightblue"
|
selection_bg = "#1d4ed8"
|
||||||
selection_fg = "black"
|
selection_fg = "#f8fafc"
|
||||||
cursor = "magenta"
|
cursor = "#f472b6"
|
||||||
code_block_background = "#191919"
|
code_block_background = "#111827"
|
||||||
code_block_border = "lightmagenta"
|
code_block_border = "#2563eb"
|
||||||
code_block_text = "white"
|
code_block_text = "#e2e8f0"
|
||||||
code_block_keyword = "yellow"
|
code_block_keyword = "#fbbf24"
|
||||||
code_block_string = "lightgreen"
|
code_block_string = "#34d399"
|
||||||
code_block_comment = "gray"
|
code_block_comment = "#64748b"
|
||||||
placeholder = "darkgray"
|
placeholder = "#64748b"
|
||||||
error = "red"
|
error = "#f87171"
|
||||||
info = "lightgreen"
|
info = "#38bdf8"
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
name = "default_light"
|
name = "default_light"
|
||||||
text = "black"
|
text = "#0f172a"
|
||||||
background = "white"
|
background = "#f8fafc"
|
||||||
focused_panel_border = "#4a90e2"
|
focused_panel_border = "#2563eb"
|
||||||
unfocused_panel_border = "#dddddd"
|
unfocused_panel_border = "#c7d2fe"
|
||||||
user_message_role = "#0055a4"
|
user_message_role = "#2563eb"
|
||||||
assistant_message_role = "#8e44ad"
|
assistant_message_role = "#9333ea"
|
||||||
tool_output = "gray"
|
tool_output = "#64748b"
|
||||||
thinking_panel_title = "#8e44ad"
|
thinking_panel_title = "#7c3aed"
|
||||||
command_bar_background = "white"
|
command_bar_background = "#e2e8f0"
|
||||||
status_background = "white"
|
status_background = "#e0e7ff"
|
||||||
mode_normal = "#0055a4"
|
mode_normal = "#2563eb"
|
||||||
mode_editing = "#2e8b57"
|
mode_editing = "#0ea5e9"
|
||||||
mode_model_selection = "#b58900"
|
mode_model_selection = "#facc15"
|
||||||
mode_provider_selection = "#008b8b"
|
mode_provider_selection = "#0ea5e9"
|
||||||
mode_help = "#8e44ad"
|
mode_help = "#7c3aed"
|
||||||
mode_visual = "#8e44ad"
|
mode_visual = "#7c3aed"
|
||||||
mode_command = "#b58900"
|
mode_command = "#f97316"
|
||||||
selection_bg = "#a4c8f0"
|
selection_bg = "#bfdbfe"
|
||||||
selection_fg = "black"
|
selection_fg = "#0f172a"
|
||||||
cursor = "#d95f02"
|
cursor = "#f97316"
|
||||||
code_block_background = "#f5f5f5"
|
code_block_background = "#e2e8f0"
|
||||||
code_block_border = "#009688"
|
code_block_border = "#2563eb"
|
||||||
code_block_text = "black"
|
code_block_text = "#0f172a"
|
||||||
code_block_keyword = "#b58900"
|
code_block_keyword = "#b45309"
|
||||||
code_block_string = "#388e3c"
|
code_block_string = "#15803d"
|
||||||
code_block_comment = "#90a4ae"
|
code_block_comment = "#94a3b8"
|
||||||
placeholder = "gray"
|
placeholder = "#64748b"
|
||||||
error = "#c0392b"
|
error = "#dc2626"
|
||||||
info = "green"
|
info = "#2563eb"
|
||||||
|
|||||||
Reference in New Issue
Block a user