- Removed the Escalate variant from the Verdict enum and associated parsing logic. - Cleaned up the EditorState struct by removing unused fields and methods. - Simplified MCP connection functions by removing unnecessary disconnect and handle dismiss functions. - Added tick count to MiscState for managing UI updates during processing. - Updated chat and status views to display a spinner during AI processing using the tick count. - Created a README.md file to document the project, its features, architecture, usage, key bindings, agent modes, security measures, installation instructions, and configuration details.
124 lines
4.6 KiB
Rust
124 lines
4.6 KiB
Rust
use ratatui::layout::Rect;
|
|
use ratatui::style::{Style, Modifier};
|
|
use ratatui::text::{Line, Span};
|
|
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
|
use ratatui::Frame;
|
|
use super::theme::Theme;
|
|
|
|
fn _role_name(role: &crate::dto::chat::message::Role) -> &'static str {
|
|
match role {
|
|
crate::dto::chat::message::Role::User => "User",
|
|
crate::dto::chat::message::Role::Assistant => "Assistant",
|
|
crate::dto::chat::message::Role::System => "System",
|
|
crate::dto::chat::message::Role::Tool => "Tool",
|
|
}
|
|
}
|
|
|
|
fn role_badge(role: &crate::dto::chat::message::Role) -> &'static str {
|
|
match role {
|
|
crate::dto::chat::message::Role::User => "YOU",
|
|
crate::dto::chat::message::Role::Assistant => "AI ",
|
|
crate::dto::chat::message::Role::System => "SYS",
|
|
crate::dto::chat::message::Role::Tool => "TOOL",
|
|
}
|
|
}
|
|
|
|
fn format_timestamp(ts: i64) -> String {
|
|
if ts <= 0 { return "".to_string(); }
|
|
let secs = ts / 1000;
|
|
let mins = (secs / 60) % 60;
|
|
let hrs = (secs / 3600) % 24;
|
|
format!("{:02}:{:02}", hrs, mins)
|
|
}
|
|
|
|
pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
|
|
let messages = &state.transcript_cache.messages;
|
|
let scroll_offset = state.scroll.offset;
|
|
let max_visible = (area.height as usize).saturating_sub(3);
|
|
|
|
let mut display_lines: Vec<Line> = Vec::new();
|
|
|
|
let _total_msgs = messages.len();
|
|
|
|
for msg in messages.iter() {
|
|
let role_color = match msg.role {
|
|
crate::dto::chat::message::Role::User => Theme::ROLE_USER,
|
|
crate::dto::chat::message::Role::Assistant => Theme::ROLE_ASSISTANT,
|
|
crate::dto::chat::message::Role::System => Theme::ROLE_SYSTEM,
|
|
crate::dto::chat::message::Role::Tool => Theme::ROLE_TOOL,
|
|
};
|
|
let prefix = match msg.role {
|
|
crate::dto::chat::message::Role::User => ">",
|
|
crate::dto::chat::message::Role::Assistant => "•",
|
|
crate::dto::chat::message::Role::System => "#",
|
|
crate::dto::chat::message::Role::Tool => "→",
|
|
};
|
|
|
|
let ts_str = format_timestamp(msg.timestamp);
|
|
let badge = role_badge(&msg.role);
|
|
let time_display = if ts_str.is_empty() { String::new() } else { format!(" [{}]", ts_str) };
|
|
|
|
let header = Line::from(vec![
|
|
Span::styled(
|
|
format!(" {} ", badge),
|
|
Style::default().fg(Theme::BG).bg(role_color).add_modifier(Modifier::BOLD),
|
|
),
|
|
Span::styled(
|
|
time_display,
|
|
Style::default().fg(Theme::DIM),
|
|
),
|
|
]);
|
|
|
|
let content_str = if msg.content.is_empty() {
|
|
"(streaming...)".to_string()
|
|
} else {
|
|
msg.content.clone()
|
|
};
|
|
let mut content_spans = vec![Span::styled(format!("{} ", prefix), Style::default().fg(role_color))];
|
|
content_spans.extend(super::markdown::render_markdown(&content_str, area.width));
|
|
let content_line = Line::from(content_spans);
|
|
|
|
display_lines.push(header);
|
|
display_lines.push(content_line);
|
|
display_lines.push(Line::from(Span::raw("")));
|
|
}
|
|
|
|
if state.turn_in_flight() {
|
|
let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
let frame = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()];
|
|
display_lines.push(Line::from(vec![
|
|
Span::styled(" AI ", Style::default().fg(Theme::BG).bg(Theme::ROLE_ASSISTANT).add_modifier(Modifier::BOLD)),
|
|
Span::styled(format!(" {} Generating...", frame), Style::default().fg(Theme::DIM)),
|
|
]));
|
|
display_lines.push(Line::from(Span::raw("")));
|
|
}
|
|
|
|
let mut title = String::from(" Chat ");
|
|
if !messages.is_empty() {
|
|
title.push_str(&format!("[{} msgs]", messages.len()));
|
|
}
|
|
|
|
let block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_style(Style::default().fg(Theme::BORDER))
|
|
.title(title);
|
|
|
|
let total = display_lines.len();
|
|
let max_offset = total.saturating_sub(max_visible);
|
|
let offset = scroll_offset.min(max_offset);
|
|
|
|
let end_idx = total.saturating_sub(offset);
|
|
let start_idx = end_idx.saturating_sub(max_visible);
|
|
let visible: Vec<Line> = if start_idx < end_idx && start_idx < total {
|
|
display_lines[start_idx..end_idx].to_vec()
|
|
} else {
|
|
display_lines[total.saturating_sub(max_visible)..total].to_vec()
|
|
};
|
|
|
|
let paragraph = Paragraph::new(visible)
|
|
.block(block)
|
|
.wrap(Wrap { trim: false });
|
|
|
|
frame.render_widget(paragraph, area);
|
|
}
|