use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
use super::{App, Mode};
use super::markdown;
pub fn draw(f: &mut Frame, app: &mut App) {
let input_height = if app.permission_details.is_some() {
let detail_lines = app.permission_details.as_ref().map(|d| d.len()).unwrap_or(0);
(detail_lines as u16 + 4).min(f.area().height / 2) } else {
3
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(1), Constraint::Length(input_height), Constraint::Length(1), ])
.split(f.area());
let header = Paragraph::new(Line::from(vec![
Span::styled(" claux ", Style::default().fg(app.theme.assistant_bold).add_modifier(Modifier::BOLD)),
Span::styled(
format!("v{}", env!("CARGO_PKG_VERSION")),
Style::default().fg(app.theme.dim),
),
]));
f.render_widget(header, chunks[0]);
let msg_area = chunks[1];
let _msg_width = msg_area.width.saturating_sub(2) as usize;
let mut lines: Vec<Line> = Vec::new();
for msg in &app.messages {
if !lines.is_empty() {
lines.push(Line::from(""));
}
match msg.role.as_str() {
"user" => {
let bubble_lines: Vec<Line> = msg.content.lines().map(|line| {
Line::from(Span::styled(
format!(" {line}"),
Style::default()
.fg(app.theme.user_message_fg)
.bg(app.theme.user_message_bg),
))
}).collect();
lines.push(Line::from(vec![
Span::styled("● ", Style::default().fg(app.theme.user)),
Span::styled("You", Style::default().fg(app.theme.user).add_modifier(Modifier::BOLD)),
]));
for line in bubble_lines {
lines.push(line);
}
}
"assistant" => {
lines.push(Line::from(vec![
Span::styled("● ", Style::default().fg(app.theme.assistant)),
Span::styled(format!("{} ", app.model), Style::default().fg(app.theme.assistant_bold).add_modifier(Modifier::BOLD)),
]));
let rendered = markdown::render(&msg.content, Style::default().fg(app.theme.fg));
for line in rendered {
let mut indented = vec![Span::raw(" ")];
indented.extend(line.spans);
lines.push(Line::from(indented));
}
}
"system" => {
lines.push(Line::from(Span::styled("● ", Style::default().fg(app.theme.warning))));
for line in msg.content.lines() {
lines.push(Line::from(Span::styled(
format!(" {line}"),
Style::default().fg(app.theme.warning),
)));
}
}
"error" => {
lines.push(Line::from(Span::styled("● ", Style::default().fg(app.theme.error))));
for line in msg.content.lines() {
lines.push(Line::from(Span::styled(
format!(" {line}"),
Style::default().fg(app.theme.error),
)));
}
}
_ => {
lines.push(Line::from(Span::styled("● ", Style::default().fg(app.theme.dim))));
for line in msg.content.lines() {
lines.push(Line::from(Span::styled(
format!(" {line}"),
Style::default().fg(app.theme.fg),
)));
}
}
}
}
if !app.stream_buffer.is_empty() {
if !lines.is_empty() {
lines.push(Line::from(""));
}
lines.push(Line::from(vec![
Span::styled("● ", Style::default().fg(app.theme.success)),
Span::styled(format!("{} ", app.model), Style::default().fg(app.theme.success).add_modifier(Modifier::BOLD)),
]));
let rendered = markdown::render(&app.stream_buffer, Style::default().fg(app.theme.success));
for line in rendered {
let mut indented = vec![Span::raw(" ")];
indented.extend(line.spans);
lines.push(Line::from(indented));
}
lines.push(Line::from(Span::styled(" ▊", Style::default().fg(app.theme.success))));
}
app.total_lines = lines.len() as u16;
let visible_height = msg_area.height.saturating_sub(2);
let max_scroll = app.total_lines.saturating_sub(visible_height);
if !app.manual_scroll {
app.scroll = 0; }
let scroll_offset = if app.manual_scroll {
max_scroll.saturating_sub(app.scroll.min(max_scroll))
} else {
max_scroll
};
let messages_widget = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::LEFT | Borders::RIGHT)
.border_style(Style::default().fg(app.theme.dim)),
)
.scroll((scroll_offset, 0));
f.render_widget(messages_widget, msg_area);
let input_style = match app.mode {
Mode::Input => Style::default().fg(app.theme.fg),
Mode::Permission => Style::default().fg(app.theme.warning),
Mode::Streaming => Style::default().fg(app.theme.dim),
};
if let (Some(ref prompt), Some(ref details)) = (&app.permission_prompt, &app.permission_details) {
let mut perm_lines: Vec<Line> = Vec::new();
perm_lines.push(Line::from(vec![
Span::styled("⚡ ", Style::default().fg(app.theme.warning)),
Span::styled(prompt.as_str(), Style::default().fg(app.theme.warning).add_modifier(Modifier::BOLD)),
]));
perm_lines.push(Line::from(""));
for detail in details {
let style = if detail.starts_with(" +") {
Style::default().fg(app.theme.success)
} else if detail.starts_with(" -") {
Style::default().fg(app.theme.error)
} else if detail.ends_with(':') {
Style::default().fg(app.theme.fg).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(app.theme.dim)
};
perm_lines.push(Line::from(Span::styled(detail.clone(), style)));
}
perm_lines.push(Line::from(""));
let help_text = if prompt.starts_with("bash:") {
vec![
Span::styled(" (y)es ", Style::default().fg(app.theme.success)),
Span::styled("(n)o ", Style::default().fg(app.theme.error)),
Span::styled("(a)lways this cmd ", Style::default().fg(app.theme.warning)),
Span::styled("(A)lways all bash", Style::default().fg(app.theme.info)),
]
} else {
vec![
Span::styled(" (y)es ", Style::default().fg(app.theme.success)),
Span::styled("(n)o ", Style::default().fg(app.theme.error)),
Span::styled("(a)lways allow", Style::default().fg(app.theme.warning)),
]
};
perm_lines.push(Line::from(help_text));
let perm_widget = Paragraph::new(perm_lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(app.theme.warning))
.title(" Permission Required "),
);
f.render_widget(perm_widget, chunks[2]);
} else {
let input_text = if app.mode == Mode::Streaming {
"...".to_string()
} else {
app.input.clone()
};
let input_widget = Paragraph::new(input_text)
.style(input_style)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(if app.mode == Mode::Input {
app.theme.user
} else {
app.theme.dim
}))
.title(" > "),
);
f.render_widget(input_widget, chunks[2]);
}
if app.mode == Mode::Input {
f.set_cursor_position((
chunks[2].x + app.cursor as u16 + 1,
chunks[2].y + 1,
));
}
let thinking_indicator = if app.thinking {
let spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let idx = (chrono::Local::now().timestamp_millis() / 100) as usize % spinner.len();
format!(" {} ", spinner[idx])
} else {
" ".to_string()
};
let status = Paragraph::new(Line::from(vec![
Span::styled(thinking_indicator, Style::default().fg(app.theme.success)),
Span::styled(format!(" {} ", app.status), Style::default().fg(app.theme.dim)),
]));
f.render_widget(status, chunks[3]);
}