travelagent 1.11.1

Agent-first TUI code review tool
use ratatui::{
    Frame,
    layout::Rect,
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::Paragraph,
};

use crate::app::App;
use crate::ui::{markdown, styles};

pub fn render(frame: &mut Frame, app: &mut App, area: Rect) {
    let body_width = (area.width as usize).saturating_sub(2).max(1);
    let markdown_enabled = app.markdown_rendering_enabled;
    // Precompute styles so the remaining &app.theme borrow drops before
    // we mutably borrow the remote state below.
    let dim_style = styles::dim_style(&app.theme);
    let panel_style = styles::panel_style(&app.theme);

    let lines = {
        let theme = &app.theme;
        if let Some(meta) = app.remote().and_then(|r| r.pr_metadata.as_ref()) {
            let state_display = meta.state.display();
            let draft_marker = if meta.is_draft { " (draft)" } else { "" };

            let mut text = vec![
                Line::from(Span::styled(
                    format!("  {} [{}{}]", meta.title, state_display, draft_marker),
                    Style::default().add_modifier(Modifier::BOLD),
                )),
                Line::from(format!(
                    "  {} -> {} -> {}",
                    meta.author, meta.head_branch, meta.base_branch
                )),
                Line::from(format!(
                    "  Created: {}",
                    meta.created_at.format("%Y-%m-%d %H:%M")
                )),
            ];

            if let Some(ref mergeable) = meta.mergeable {
                text.push(Line::from(format!("  Mergeable: {mergeable:?}")));
            }

            text.push(Line::from(""));
            text.push(Line::from(Span::styled(
                "  Description",
                Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
            )));
            text.push(Line::from(""));

            if meta.body.is_empty() {
                text.push(Line::from(Span::styled("  (no description)", dim_style)));
            } else if markdown_enabled {
                for mut md_line in markdown::render_markdown(&meta.body, theme, body_width) {
                    md_line.spans.insert(0, Span::raw("  "));
                    text.push(md_line);
                }
            } else {
                for line in meta.body.lines() {
                    text.push(Line::from(format!("  {line}")));
                }
            }

            text
        } else {
            vec![Line::from(Span::styled(
                "  No metadata available",
                dim_style,
            ))]
        }
    };

    let total_lines = lines.len();
    let viewport_height = area.height as usize;

    // Bound scroll offset, then read it for slicing below.
    let max_scroll = total_lines.saturating_sub(viewport_height);
    let scroll = if let Some(r) = app.remote_mut() {
        if r.description_scroll > max_scroll {
            r.description_scroll = max_scroll;
        }
        r.description_scroll
    } else {
        0
    };

    let visible: Vec<Line> = lines
        .into_iter()
        .skip(scroll)
        .take(viewport_height)
        .collect();

    let paragraph = Paragraph::new(visible).style(panel_style);
    frame.render_widget(paragraph, area);
}