claux 20260404.0.0

Terminal AI coding assistant with tool execution
use ratatui::{
    layout::{Constraint, Direction, Layout},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
    Frame,
};

use super::{App, Mode};
use super::markdown;

const FG: Color = Color::Rgb(213, 196, 161); // gruvbox fg2
const BG_DARK: Color = Color::Rgb(40, 40, 40); // gruvbox bg
const BLUE: Color = Color::Rgb(131, 165, 152); // gruvbox blue
const GREEN: Color = Color::Rgb(184, 187, 38); // gruvbox green
const YELLOW: Color = Color::Rgb(250, 189, 47); // gruvbox yellow
const RED: Color = Color::Rgb(251, 73, 52); // gruvbox red
const PURPLE: Color = Color::Rgb(211, 134, 155); // gruvbox purple
const GRAY: Color = Color::Rgb(146, 131, 116); // gruvbox gray

pub fn draw(f: &mut Frame, app: &mut App) {
    // Expand input area when showing permission details
    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) // +4 for borders, summary, controls
    } else {
        3
    };

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),          // Header
            Constraint::Min(1),            // Messages
            Constraint::Length(input_height), // Input (expands for permissions)
            Constraint::Length(1),          // Status bar
        ])
        .split(f.area());

    // Header
    let header = Paragraph::new(Line::from(vec![
        Span::styled(" claux ", Style::default().fg(PURPLE).add_modifier(Modifier::BOLD)),
        Span::styled(
            format!("v{}", env!("CARGO_PKG_VERSION")),
            Style::default().fg(GRAY),
        ),
    ]));
    f.render_widget(header, chunks[0]);

    // Messages area
    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 {
        // Add a blank line before each message
        if !lines.is_empty() {
            lines.push(Line::from(""));
        }

        match msg.role.as_str() {
            "user" => {
                lines.push(Line::from(vec![
                    Span::styled("", Style::default().fg(BLUE)),
                    Span::styled("You", Style::default().fg(BLUE).add_modifier(Modifier::BOLD)),
                ]));
                for line in msg.content.lines() {
                    lines.push(Line::from(Span::styled(
                        format!("  {}", line),
                        Style::default().fg(BLUE),
                    )));
                }
            }
            "assistant" => {
                lines.push(Line::from(Span::styled("", Style::default().fg(PURPLE))));
                let rendered = markdown::render(&msg.content, Style::default().fg(FG));
                // Indent assistant content to align with the dot
                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(YELLOW))));
                for line in msg.content.lines() {
                    lines.push(Line::from(Span::styled(
                        format!("  {}", line),
                        Style::default().fg(YELLOW),
                    )));
                }
            }
            "error" => {
                lines.push(Line::from(Span::styled("", Style::default().fg(RED))));
                for line in msg.content.lines() {
                    lines.push(Line::from(Span::styled(
                        format!("  {}", line),
                        Style::default().fg(RED),
                    )));
                }
            }
            _ => {
                lines.push(Line::from(Span::styled("", Style::default().fg(GRAY))));
                for line in msg.content.lines() {
                    lines.push(Line::from(Span::styled(
                        format!("  {}", line),
                        Style::default().fg(FG),
                    )));
                }
            }
        }
    }

    // Streaming buffer (assistant response in progress)
    if !app.stream_buffer.is_empty() {
        if !lines.is_empty() {
            lines.push(Line::from(""));
        }
        lines.push(Line::from(Span::styled("", Style::default().fg(GREEN))));
        let rendered = markdown::render(&app.stream_buffer, Style::default().fg(GREEN));
        for line in rendered {
            let mut indented = vec![Span::raw("  ")];
            indented.extend(line.spans);
            lines.push(Line::from(indented));
        }
        // Cursor indicator
        lines.push(Line::from(Span::styled("", Style::default().fg(GREEN))));
    }

    app.total_lines = lines.len() as u16;

    // Auto-scroll to bottom unless user scrolled up
    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; // 0 means bottom
    }

    // Calculate actual scroll offset (we scroll from bottom)
    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(GRAY)),
        )
        .scroll((scroll_offset, 0));
    f.render_widget(messages_widget, msg_area);

    // Input area
    let input_style = match app.mode {
        Mode::Input => Style::default().fg(FG),
        Mode::Permission => Style::default().fg(YELLOW),
        Mode::Streaming => Style::default().fg(GRAY),
    };

    if let (Some(ref prompt), Some(ref details)) = (&app.permission_prompt, &app.permission_details) {
        // Expanded permission panel
        let mut perm_lines: Vec<Line> = Vec::new();
        perm_lines.push(Line::from(vec![
            Span::styled("", Style::default().fg(YELLOW)),
            Span::styled(prompt.as_str(), Style::default().fg(YELLOW).add_modifier(Modifier::BOLD)),
        ]));
        perm_lines.push(Line::from(""));
        for detail in details {
            let style = if detail.starts_with("  +") {
                Style::default().fg(GREEN)
            } else if detail.starts_with("  -") {
                Style::default().fg(RED)
            } else if detail.ends_with(':') {
                Style::default().fg(FG).add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(GRAY)
            };
            perm_lines.push(Line::from(Span::styled(detail.clone(), style)));
        }
        perm_lines.push(Line::from(""));
        perm_lines.push(Line::from(vec![
            Span::styled("  (y)es  ", Style::default().fg(GREEN)),
            Span::styled("(n)o  ", Style::default().fg(RED)),
            Span::styled("(a)lways allow", Style::default().fg(YELLOW)),
        ]));

        let perm_widget = Paragraph::new(perm_lines)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_style(Style::default().fg(YELLOW))
                    .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 {
                        BLUE
                    } else {
                        GRAY
                    }))
                    .title(" > "),
            );
        f.render_widget(input_widget, chunks[2]);
    }

    // Set cursor position in input mode
    if app.mode == Mode::Input {
        f.set_cursor_position((
            chunks[2].x + app.cursor as u16 + 1,
            chunks[2].y + 1,
        ));
    }

    // Status bar
    let status = Paragraph::new(Line::from(vec![
        Span::styled(format!(" {} ", app.status), Style::default().fg(GRAY)),
    ]));
    f.render_widget(status, chunks[3]);
}