use crate::app::App;
use crate::preview::markdown::{parse_markdown, PreviewBlock};
use crate::review::{ReviewSection, ReviewSource, SectionState};
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Wrap};
use ratatui::Frame;
pub fn render_review_with_diff(app: &App, frame: &mut Frame, area: Rect) {
let mut lines: Vec<Line> = Vec::new();
render_banner(&app.review_source, &mut lines, &app.theme);
if let Some(hash) = app.active_review_group {
if let Some(review) = app.review_cache.get(&hash) {
let label = find_group_label(app, hash);
let completed = review
.sections
.values()
.filter(|s| s.is_complete())
.count();
lines.push(Line::from(vec![
Span::styled(
format!(" Review: \"{}\"", label),
Style::default()
.fg(app.theme.help_section_fg)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" [{}/4]", completed),
Style::default().fg(app.theme.help_dismiss_fg),
),
]));
lines.push(Line::raw(""));
for section in ReviewSection::all() {
if let Some(state) = review.sections.get(§ion) {
render_section(section, state, &mut lines, &app.theme, area.width);
}
}
}
}
let hr = "─".repeat(area.width as usize);
lines.push(Line::raw(""));
lines.push(Line::from(Span::styled(
hr,
Style::default().fg(app.theme.help_dismiss_fg),
)));
lines.push(Line::raw(""));
let diff_lines = build_diff_lines(app);
lines.extend(diff_lines);
let scroll = app.review_scroll as u16;
let paragraph = Paragraph::new(lines)
.scroll((scroll, 0))
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_banner(source: &ReviewSource, lines: &mut Vec<Line>, theme: &crate::theme::Theme) {
match source {
ReviewSource::Skill { name, path } => {
lines.push(Line::from(vec![
Span::styled(
" Reviewed with: ",
Style::default().fg(theme.help_dismiss_fg),
),
Span::styled(
format!("\"{}\"", name),
Style::default()
.fg(theme.help_section_fg)
.add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(Span::styled(
format!(" {}", path.display()),
Style::default().fg(theme.help_dismiss_fg),
)));
}
ReviewSource::BuiltIn => {
lines.push(Line::from(Span::styled(
" Reviewed with: built-in generic reviewer",
Style::default().fg(theme.help_dismiss_fg),
)));
lines.push(Line::from(Span::styled(
" Tip: Create a review SKILL in .claude/skills/ or ~/.claude/skills/",
Style::default().fg(theme.help_dismiss_fg),
)));
}
}
lines.push(Line::raw(""));
}
fn render_section(
section: ReviewSection,
state: &SectionState,
lines: &mut Vec<Line>,
theme: &crate::theme::Theme,
width: u16,
) {
match state {
SectionState::Loading => {
lines.push(Line::from(Span::styled(
format!(" {} Loading {}...", "⠋", section.label()),
Style::default().fg(Color::Yellow),
)));
lines.push(Line::raw(""));
}
SectionState::Ready(content) => {
lines.push(Line::from(Span::styled(
format!(" {} ", section.label()),
Style::default()
.fg(theme.help_section_fg)
.add_modifier(Modifier::BOLD),
)));
let blocks = parse_markdown(content, width.saturating_sub(2), theme);
for block in blocks {
match block {
PreviewBlock::Text(md_lines) => {
for line in md_lines {
let mut spans = vec![Span::raw(" ")];
spans.extend(line.spans);
lines.push(Line::from(spans));
}
}
PreviewBlock::Mermaid(mermaid_block) => {
lines.push(Line::from(Span::styled(
" ```mermaid".to_string(),
Style::default().fg(theme.help_dismiss_fg),
)));
for src_line in mermaid_block.source.lines() {
lines.push(Line::from(Span::styled(
format!(" {src_line}"),
Style::default().fg(theme.md_code_block_fg),
)));
}
lines.push(Line::from(Span::styled(
" ```".to_string(),
Style::default().fg(theme.help_dismiss_fg),
)));
}
}
}
lines.push(Line::raw(""));
}
SectionState::Error(msg) => {
lines.push(Line::from(Span::styled(
format!(" [{} failed: {}]", section.label(), msg),
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::DIM),
)));
lines.push(Line::raw(""));
}
SectionState::Skipped => {
}
}
}
fn find_group_label(app: &App, hash: u64) -> String {
if let Some(groups) = &app.semantic_groups {
for group in groups {
if crate::review::group_content_hash(group) == hash {
return group.label.clone();
}
}
}
"Unknown".to_string()
}
fn build_diff_lines(app: &App) -> Vec<Line<'static>> {
let items = app.visible_items();
let mut lines = Vec::new();
for (idx, item) in items.iter().enumerate() {
let is_selected = idx == app.ui_state.selected_index;
lines.push(crate::ui::diff_view::render_item(app, item, is_selected));
}
lines
}