use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use crate::tools::todo::TodoStatus;
use crate::tui::app::App;
use crate::tui::ui::{command_menu, input, modal, spinner, status, transcript};
fn todo_panel_height(app: &App) -> u16 {
if app.current_todos.is_empty() {
0
} else {
(app.current_todos.len().min(6) as u16) + 2
}
}
pub fn render(frame: &mut Frame, app: &App) {
let input_total = input::total_height(app);
let todo_height = todo_panel_height(app);
let plan_banner_height: u16 = if app.plan_awaiting_approval || app.plan_mode_request_pending {
1
} else {
0
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(3), Constraint::Length(todo_height), Constraint::Length(1), Constraint::Length(plan_banner_height), Constraint::Length(input_total), ])
.split(frame.area());
let inflight = &app.blocks[app.last_printed_idx..];
let mut lines = transcript::render_blocks(inflight, &app.usage, app.theme);
if app.turn.running {
let elapsed = app
.turn
.started_at
.map(|t| t.elapsed().as_secs_f64())
.unwrap_or(0.0);
lines.push(Line::raw(""));
lines.push(Line::from(vec![Span::styled(
spinner::format_line(app.spinner_frame, app.turn.spinner_verb, elapsed),
Style::default().fg(Color::Yellow),
)]));
}
let messages_area = chunks[0];
let todo_area = chunks[1];
let inner_width = messages_area.width;
let visible_rows = messages_area.height;
let total_rows: u16 = if inner_width == 0 {
lines.len() as u16
} else {
let w = inner_width as usize;
let sum: usize = lines
.iter()
.map(|l| {
let lw = l.width();
if lw == 0 {
1
} else {
lw.div_ceil(w)
}
})
.sum();
sum.try_into().unwrap_or(u16::MAX)
};
let max_scroll = total_rows.saturating_sub(visible_rows);
let effective_scroll = max_scroll.saturating_sub(app.scroll_offset.min(max_scroll));
let messages_widget = Paragraph::new(lines)
.wrap(Wrap { trim: false })
.scroll((effective_scroll, 0));
frame.render_widget(messages_widget, messages_area);
if !app.current_todos.is_empty() {
render_todo_panel(frame, todo_area, app);
}
status::render(frame, chunks[2], app);
if app.plan_awaiting_approval {
render_plan_approval_banner(frame, chunks[3], app);
} else if app.plan_mode_request_pending {
render_plan_mode_request_banner(frame, chunks[3]);
}
input::render(frame, chunks[4], app);
command_menu::render(frame, chunks[4], app);
command_menu::render_atfile(frame, chunks[4], app);
command_menu::render_history_search(frame, chunks[4], app);
command_menu::render_permission_modal(frame, app);
if !app.modals.is_empty() {
modal::render(frame, app);
}
}
fn render_todo_panel(frame: &mut Frame, area: Rect, app: &App) {
let completed = app
.current_todos
.iter()
.filter(|t| t.status == TodoStatus::Completed)
.count();
let total = app.current_todos.len();
let title = format!(" Tasks ({completed}/{total} done) ");
let items: Vec<Line> = app
.current_todos
.iter()
.take(6)
.map(|item| {
let (icon, style) = match item.status {
TodoStatus::Completed => ("✓", Style::default().fg(Color::Green)),
TodoStatus::InProgress => ("◉", Style::default().fg(Color::Yellow)),
TodoStatus::Pending => ("○", Style::default().fg(Color::DarkGray)),
TodoStatus::Cancelled => ("✗", Style::default().fg(Color::DarkGray)),
};
let label = item
.active_form
.as_deref()
.filter(|_| item.status == TodoStatus::InProgress)
.unwrap_or(&item.content);
Line::from(vec![
Span::styled(format!(" {icon} "), style),
Span::styled(label.to_string(), style),
])
})
.collect();
let widget = Paragraph::new(items)
.block(Block::default().borders(Borders::ALL).title(title))
.wrap(Wrap { trim: true });
frame.render_widget(widget, area);
}
fn render_plan_approval_banner(frame: &mut Frame, area: Rect, _app: &App) {
use ratatui::style::Modifier;
let line = Line::from(vec![
Span::styled(
" ⚡ Plan awaiting approval — ",
Style::default()
.fg(Color::Black)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"[y/Enter]",
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" Approve ",
Style::default().fg(Color::Black).bg(Color::Yellow),
),
Span::styled(
"[n/Esc]",
Style::default()
.fg(Color::White)
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" Reject ",
Style::default().fg(Color::Black).bg(Color::Yellow),
),
Span::styled(
"[e]",
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" Edit ",
Style::default().fg(Color::Black).bg(Color::Yellow),
),
]);
let widget = Paragraph::new(line);
frame.render_widget(widget, area);
}
fn render_plan_mode_request_banner(frame: &mut Frame, area: Rect) {
use ratatui::style::Modifier;
let line = Line::from(vec![
Span::styled(
" ⓘ Plan mode request — ",
Style::default()
.fg(Color::Black)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"[y/Enter]",
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" Allow ",
Style::default().fg(Color::Black).bg(Color::Blue),
),
Span::styled(
"[n/Esc]",
Style::default()
.fg(Color::White)
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" Skip — execute directly ",
Style::default().fg(Color::White).bg(Color::Blue),
),
]);
let widget = Paragraph::new(line)
.style(Style::default().bg(Color::Blue))
.wrap(Wrap { trim: true });
frame.render_widget(widget, area);
}