Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 deletions crates/agentic-tui/src/ui/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use super::{
chat::render_chat, footer::render_footer, header::render_header,
model_selection_modal::{render_model_selection_modal, ModelSelectionParams}, settings_modal::render_settings_modal,
chat::render_chat,
footer::render_footer,
header::render_header,
model_selection_modal::{render_model_selection_modal, ModelSelectionParams},
settings_modal::render_settings_modal,
};
use agentic_core::{
models::{ModelValidator, OllamaModel, OpenRouterModel},
Expand Down Expand Up @@ -169,7 +172,13 @@ impl App {
self.agent_status,
&self.settings,
);
render_footer(frame, app_chunks[2], &self.theme, self.mode, &self.edit_buffer);
render_footer(
frame,
app_chunks[2],
&self.theme,
self.mode,
&self.edit_buffer,
);

if matches!(
self.mode,
Expand Down Expand Up @@ -200,7 +209,11 @@ impl App {
frame.render_widget(Clear, modal_area); // clears the background

if self.mode == AppMode::SelectingLocalModel {
let local_models = self.available_local_models.iter().map(|m| (m.name.clone(), m.size.to_string())).collect::<Vec<_>>();
let local_models = self
.available_local_models
.iter()
.map(|m| (m.name.clone(), m.size.to_string()))
.collect::<Vec<_>>();
render_model_selection_modal(
frame,
modal_area,
Expand Down Expand Up @@ -239,7 +252,14 @@ impl App {
);
}
} else {
render_chat(frame, app_chunks[1], &self.theme, self.mode, &self.edit_buffer, self.agent_status);
render_chat(
frame,
app_chunks[1],
&self.theme,
self.mode,
&self.edit_buffer,
self.agent_status,
);
}
})?;
Ok(())
Expand Down Expand Up @@ -443,7 +463,8 @@ impl App {
}
}
KeyCode::Down => {
if self.selected_model_index + 1 < self.available_local_models.len() {
if self.selected_model_index + 1 < self.available_local_models.len()
{
self.selected_model_index += 1;
self.adjust_page_for_selection();
}
Expand Down Expand Up @@ -476,7 +497,8 @@ impl App {
}
}
KeyCode::Down => {
if self.selected_model_index + 1 < self.available_cloud_models.len() {
if self.selected_model_index + 1 < self.available_cloud_models.len()
{
self.selected_model_index += 1;
self.adjust_page_for_selection();
}
Expand Down Expand Up @@ -574,7 +596,7 @@ impl App {

fn handle_chat_message(&mut self) {
let message = self.edit_buffer.trim().to_string();

if message.starts_with('/') {
// Handle slash commands
self.handle_slash_command(&message);
Expand All @@ -584,7 +606,7 @@ impl App {
// For now, just clear the input
println!("Chat message: {}", message);
}

// Clear input after processing
self.edit_buffer.clear();
}
Expand Down Expand Up @@ -688,7 +710,7 @@ impl App {
// Always create a new channel and spawn the task
let (tx, rx) = mpsc::unbounded_channel();
self.validation_rx = Some(rx);

// Spawn async task to fetch cloud models
let api_key = self.settings.api_key.clone();
tokio::spawn(async move {
Expand All @@ -705,12 +727,15 @@ impl App {
}

fn format_cloud_models_with_emojis(&self) -> Vec<(String, String)> {
self.available_cloud_models.iter().map(|m| {
let is_free = m.pricing.prompt == "0" && m.pricing.completion == "0";
let emoji = if is_free { "🆓" } else { "💰" };
let name = format!("{} {}", emoji, m.name);
(name, String::new()) // No secondary info column
}).collect()
self.available_cloud_models
.iter()
.map(|m| {
let is_free = m.pricing.prompt == "0" && m.pricing.completion == "0";
let emoji = if is_free { "🆓" } else { "💰" };
let name = format!("{} {}", emoji, m.name);
(name, String::new()) // No secondary info column
})
.collect()
}

fn previous_page(&mut self) {
Expand Down
54 changes: 44 additions & 10 deletions crates/agentic-tui/src/ui/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ const GAP_HEIGHT: u16 = 1;
const MAIN_TOTAL_HEIGHT: u16 = MAIN_LOGO_HEIGHT + GAP_HEIGHT + TEXT_HEIGHT;
const SPIRAL_TOTAL_HEIGHT: u16 = SPIRAL_GALAXY_HEIGHT;

pub fn render_chat(frame: &mut Frame, area: Rect, theme: &Theme, mode: AppMode, chat_input: &str, agent_status: AgentStatus) {
pub fn render_chat(
frame: &mut Frame,
area: Rect,
theme: &Theme,
mode: AppMode,
chat_input: &str,
agent_status: AgentStatus,
) {
let chat_block = Block::new()
.borders(Borders::ALL)
.title(" 🤨 🔍 💡 ")
Expand Down Expand Up @@ -112,15 +119,42 @@ pub fn render_chat(frame: &mut Frame, area: Rect, theme: &Theme, mode: AppMode,

// Status-based message
let (status_text, status_style) = match agent_status {
AgentStatus::Ready => ("Press [ENTER] to Start Ruixen", theme.ratatui_style(Element::Accent)),
AgentStatus::LocalEndpointError => ("⚠️ Local endpoint error - Check settings [S]", theme.ratatui_style(Element::Warning)),
AgentStatus::CloudEndpointError => ("⚠️ Cloud endpoint error - Check settings [S]", theme.ratatui_style(Element::Warning)),
AgentStatus::CheckLocalModel => ("⚠️ Local model not configured - Check settings [S]", theme.ratatui_style(Element::Warning)),
AgentStatus::CheckCloudModel => ("⚠️ Cloud model not configured - Check settings [S]", theme.ratatui_style(Element::Warning)),
AgentStatus::CheckApiKey => ("⚠️ API key not configured - Check settings [S]", theme.ratatui_style(Element::Warning)),
AgentStatus::ValidatingLocal => ("🔄 Validating local endpoint...", theme.ratatui_style(Element::Info)),
AgentStatus::ValidatingCloud => ("🔄 Validating cloud endpoint...", theme.ratatui_style(Element::Info)),
_ => ("Press [ENTER] when local and cloud models are ready", theme.ratatui_style(Element::Inactive)),
AgentStatus::Ready => (
"Press [ENTER] to Start Ruixen",
theme.ratatui_style(Element::Accent),
),
AgentStatus::LocalEndpointError => (
"⚠️ Local endpoint error - Check settings [S]",
theme.ratatui_style(Element::Warning),
),
AgentStatus::CloudEndpointError => (
"⚠️ Cloud endpoint error - Check settings [S]",
theme.ratatui_style(Element::Warning),
),
AgentStatus::CheckLocalModel => (
"⚠️ Local model not configured - Check settings [S]",
theme.ratatui_style(Element::Warning),
),
AgentStatus::CheckCloudModel => (
"⚠️ Cloud model not configured - Check settings [S]",
theme.ratatui_style(Element::Warning),
),
AgentStatus::CheckApiKey => (
"⚠️ API key not configured - Check settings [S]",
theme.ratatui_style(Element::Warning),
),
AgentStatus::ValidatingLocal => (
"🔄 Validating local endpoint...",
theme.ratatui_style(Element::Info),
),
AgentStatus::ValidatingCloud => (
"🔄 Validating cloud endpoint...",
theme.ratatui_style(Element::Info),
),
_ => (
"Press [ENTER] when local and cloud models are ready",
theme.ratatui_style(Element::Inactive),
),
};

let status_paragraph = Paragraph::new(status_text)
Expand Down
16 changes: 11 additions & 5 deletions crates/agentic-tui/src/ui/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ use ratatui::{
widgets::{Block, Borders, Paragraph},
};

pub fn render_footer(frame: &mut Frame, area: Rect, theme: &Theme, mode: AppMode, chat_input: &str) {
pub fn render_footer(
frame: &mut Frame,
area: Rect,
theme: &Theme,
mode: AppMode,
chat_input: &str,
) {
let footer_block = Block::default()
.borders(Borders::ALL)
.style(theme.ratatui_style(Element::Active));

let inner_area = footer_block.inner(area);

let content = match mode {
AppMode::Chat => {
// Chat input field with cursor
Expand All @@ -21,17 +27,17 @@ pub fn render_footer(frame: &mut Frame, area: Rect, theme: &Theme, mode: AppMode
} else {
chat_input
};

let mut spans = vec![
Span::styled("💬 ", theme.ratatui_style(Element::Accent)),
Span::styled(display_text, theme.text_style()),
];

// Add cursor when in chat mode
if !chat_input.is_empty() || area.width > 50 {
spans.push(Span::styled("_", theme.highlight_style()));
}

Line::from(spans)
}
_ => {
Expand Down
22 changes: 10 additions & 12 deletions crates/agentic-tui/src/ui/model_selection_modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ pub struct ModelSelectionParams<'a> {
pub models_per_page: usize,
}

pub fn render_model_selection_modal(
frame: &mut Frame,
area: Rect,
params: ModelSelectionParams,
) {
pub fn render_model_selection_modal(frame: &mut Frame, area: Rect, params: ModelSelectionParams) {
let block = Block::new()
.title(params.title)
.borders(Borders::ALL)
Expand Down Expand Up @@ -52,10 +48,15 @@ pub fn render_model_selection_modal(
let total_pages = params.models.len().div_ceil(params.models_per_page);
let start_index = params.current_page * params.models_per_page;
let end_index = std::cmp::min(start_index + params.models_per_page, params.models.len());

// Page indicator
let page_info = if total_pages > 1 {
format!("Page {} of {} ({} models)", params.current_page + 1, total_pages, params.models.len())
format!(
"Page {} of {} ({} models)",
params.current_page + 1,
total_pages,
params.models.len()
)
} else {
format!("{} models", params.models.len())
};
Expand All @@ -66,7 +67,7 @@ pub fn render_model_selection_modal(

// Get models for current page
let page_models = &params.models[start_index..end_index];

// Create list items for current page
let items: Vec<ListItem> = page_models
.iter()
Expand All @@ -88,10 +89,7 @@ pub fn render_model_selection_modal(
} else {
// Show name and info in columns when both are present
Line::from(vec![
Span::styled(
format!("{:<40}", name),
style.add_modifier(Modifier::BOLD),
),
Span::styled(format!("{:<40}", name), style.add_modifier(Modifier::BOLD)),
Span::styled(format!("{:>15}", info), style),
])
};
Expand Down
16 changes: 5 additions & 11 deletions crates/agentic-tui/src/ui/settings_modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,9 @@ pub fn render_settings_modal(

// Action Text
let action_text = match mode {
AppMode::EditingApiKey => {
"[ENTER] Save | [CTRL+V] Paste | [ESC] Cancel"
},
AppMode::EditingEndpoint => {
"[ENTER] Save | [ESC] Cancel"
},
_ => {
"[ENTER] Edit | [↑↓] Navigate | [S]ave | [R]eturn"
}
AppMode::EditingApiKey => "[ENTER] Save | [CTRL+V] Paste | [ESC] Cancel",
AppMode::EditingEndpoint => "[ENTER] Save | [ESC] Cancel",
_ => "[ENTER] Edit | [↑↓] Navigate | [S]ave | [R]eturn",
};
let action_style = if selection == SettingsSelection::Save {
theme.highlight_style()
Expand All @@ -164,13 +158,13 @@ fn format_api_key_display(api_key: &str) -> String {
if api_key.is_empty() {
return String::new();
}

// For most API keys, show first 15 characters + "..." + last 3 characters
// This gives us the pattern: "sk-or-v1-7d9200...3ac" (21 chars total)
if api_key.len() <= 21 {
// If it's already short enough, just return it
api_key.to_string()
} else {
format!("{}...{}", &api_key[..15], &api_key[api_key.len()-3..])
format!("{}...{}", &api_key[..15], &api_key[api_key.len() - 3..])
}
}