magi-code 0.80.2

Repository-aware CLI coding agent for terminal work
Documentation
use super::DiffPage;
use crate::{
    diff_review::{ReviewComment, ReviewLineKind, ReviewSide},
    output::sanitize_display_text,
    rendering::{DisplayLine, DisplayRole},
    tui::{state::MissionControlState, theme::MissionControlTheme},
};
use ratatui::{
    Frame,
    layout::{Constraint, Layout, Rect},
    style::Modifier,
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
};

pub(super) fn columns(area: Rect) -> [Rect; 3] {
    Layout::horizontal([
        Constraint::Percentage(20),
        Constraint::Percentage(40),
        Constraint::Percentage(40),
    ])
    .areas(area)
}

pub(crate) fn draw(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
    let theme = state.theme;
    let page = &state.diff;
    let columns = columns(area);
    for (index, column) in columns.iter().enumerate() {
        let title = if index == 0 {
            page.snapshot.as_ref().map_or_else(
                || "Files".to_owned(),
                |s| {
                    format!(
                        "Files · {}",
                        sanitize_display_text(&s.root.display().to_string())
                    )
                },
            )
        } else {
            ["Files", "Original", "Diff (old → new)"][index].to_owned()
        };
        let border_style = theme.border(!state.is_prompt_focused() && page.focus == index + 1);
        frame.render_widget(
            Block::default()
                .borders(Borders::ALL)
                .title(title)
                .style(theme.pane())
                .border_style(border_style),
            *column,
        );
    }
    let Some(snapshot) = &page.snapshot else {
        frame.render_widget(
            Paragraph::new(sanitize_display_text(
                page.error.as_deref().unwrap_or("Loading changes…"),
            ))
            .style(theme.pane()),
            inner(columns[1]),
        );
        return;
    };
    let entries = page.tree_entries();
    let visible = inner(columns[0]).height as usize;
    let offsets = page.viewport_offsets(visible);
    let lines: Vec<_> = entries
        .iter()
        .enumerate()
        .skip(offsets[0])
        .take(visible)
        .map(|(index, entry)| {
            let style = if index == page.tree_row {
                theme.right_column_rail_active_tab()
            } else {
                theme.pane()
            };
            let mut line = Line::styled(entry.label.as_str(), style);
            if let Some(comment) = entry.comment.and_then(|index| snapshot.comments.get(index)) {
                let marker = comment_marker(comment.stale, comment.resolved);
                if let Some(start) = entry.label.find(marker) {
                    line = Line::from(vec![
                        Span::styled(&entry.label[..start], style),
                        Span::styled(marker, comment_style(theme, comment)),
                        Span::styled(&entry.label[start + marker.len()..], style),
                    ]);
                }
            }
            line
        })
        .collect();
    frame.render_widget(Paragraph::new(lines), content_area(columns[0]));
    for (index, column) in columns.iter().enumerate() {
        crate::tui::render::render_scrollbar_with_theme(
            frame,
            inner(*column),
            page.row_count(index + 1).saturating_sub(visible) + 1,
            offsets[index],
            visible,
            theme,
        );
    }
    if let Some(file) = snapshot.files.get(page.selected) {
        let display = &snapshot.files_display[page.selected];
        let visible = inner(columns[1]).height as usize;
        let original_digits = display.original.len().max(1).ilog10() as usize + 1;
        let current_digits = display.current.len().max(1).ilog10() as usize + 1;
        let original: Vec<_> = display
            .original
            .iter()
            .enumerate()
            .skip(offsets[1])
            .take(visible)
            .map(|(index, text)| {
                let marker = line_marker(page, theme, ReviewSide::Original, index + 1, 2, index);
                source_line(
                    theme,
                    Some(text),
                    (marker, format!("{:>original_digits$} ", index + 1)),
                    display.removed[index].then_some(DisplayRole::DiffRemoved),
                    page.focus == 2 && index == page.original_row,
                )
            })
            .collect();
        frame.render_widget(Paragraph::new(original), content_area(columns[1]));
        let visible = inner(columns[2]).height as usize;
        let changed: Vec<_> = file
            .rows
            .iter()
            .enumerate()
            .skip(offsets[2])
            .take(visible)
            .map(|(index, row)| {
                let (prefix, role) = match row.kind {
                    ReviewLineKind::Added => ("+", Some(DisplayRole::DiffInserted)),
                    ReviewLineKind::Removed => ("-", Some(DisplayRole::DiffRemoved)),
                    ReviewLineKind::Context => (" ", None),
                };
                let (side, number) = row
                    .new_line
                    .map_or((ReviewSide::Original, row.old_line.unwrap_or(0)), |line| {
                        (ReviewSide::Changed, line)
                    });
                let marker = line_marker(page, theme, side, number, 3, index);
                source_line(
                    theme,
                    number.checked_sub(1).and_then(|index| {
                        if side == ReviewSide::Original {
                            display.original.get(index)
                        } else {
                            display.current.get(index)
                        }
                    }),
                    (
                        marker,
                        format!(
                            "{:>original_digits$} {:>current_digits$}{prefix}",
                            row.old_line
                                .map_or_else(String::new, |line| line.to_string()),
                            row.new_line
                                .map_or_else(String::new, |line| line.to_string()),
                        ),
                    ),
                    role,
                    page.focus == 3 && index == page.diff_row,
                )
            })
            .collect();
        frame.render_widget(Paragraph::new(changed), content_area(columns[2]));
        if let Some(notice) = &file.notice {
            footer(frame, columns[2], notice, theme);
        }
    } else {
        frame.render_widget(
            Paragraph::new("No working-tree changes").style(theme.pane()),
            inner(columns[1]),
        );
    }
    if let Some(error) = &page.error {
        footer(frame, area, error, theme);
    }
}

pub(super) fn content_area(area: Rect) -> Rect {
    let mut area = inner(area);
    area.width = area.width.saturating_sub(1);
    area
}

fn inner(area: Rect) -> Rect {
    area.inner(ratatui::layout::Margin::new(1, 1))
}
fn footer(frame: &mut Frame<'_>, area: Rect, text: &str, theme: MissionControlTheme) {
    if area.height > 0 {
        frame.render_widget(
            Paragraph::new(sanitize_display_text(text)).style(theme.pane()),
            Rect::new(area.x, area.bottom() - 1, area.width, 1),
        );
    }
}
pub(super) fn comment_marker(_stale: bool, resolved: bool) -> &'static str {
    if resolved { "[✓]" } else { "[•]" }
}

fn comment_style(theme: MissionControlTheme, comment: &ReviewComment) -> ratatui::style::Style {
    theme.display_role(if comment.stale {
        DisplayRole::DiffChanged
    } else if comment.resolved {
        DisplayRole::DiffInserted
    } else {
        DisplayRole::Link
    })
}

fn line_marker(
    page: &DiffPage,
    theme: MissionControlTheme,
    side: ReviewSide,
    line: usize,
    pane: usize,
    row: usize,
) -> Span<'static> {
    let comments = page.snapshot.as_ref().and_then(|snapshot| {
        snapshot
            .files
            .get(page.selected)
            .map(|file| (snapshot, file))
    });
    if let Some((snapshot, file)) = comments
        && let Some(comment) = snapshot.comments.iter().find(|comment| {
            comment.path == file.path && comment.side == side && comment.line == line
        })
    {
        return Span::styled(
            comment_marker(comment.stale, comment.resolved),
            comment_style(theme, comment),
        );
    }
    let selected_row = if pane == 2 {
        page.original_row
    } else {
        page.diff_row
    };
    let reserve_marker = page.focus == pane
        || comments.is_some_and(|(snapshot, file)| {
            snapshot.comments.iter().any(|comment| {
                comment.path == file.path && (pane == 3 || comment.side == ReviewSide::Original)
            })
        });
    Span::styled(
        if page.focus == pane && row == selected_row {
            "[+]"
        } else if reserve_marker {
            "   "
        } else {
            ""
        },
        theme.display_role(DisplayRole::CodeBlockGutter),
    )
}
fn source_line<'a>(
    theme: MissionControlTheme,
    line: Option<&'a DisplayLine>,
    gutter: (Span<'static>, String),
    role: Option<DisplayRole>,
    selected: bool,
) -> Line<'a> {
    let mut style = theme.display_role(role.unwrap_or(DisplayRole::CodeBlockGutter));
    if selected {
        style = style.add_modifier(Modifier::UNDERLINED);
    }
    let mut marker = gutter.0;
    if selected {
        marker.style = marker.style.add_modifier(Modifier::UNDERLINED);
    }
    let mut spans = vec![marker, Span::styled(gutter.1, style)];
    if let Some(line) = line {
        spans.extend(line.spans.iter().map(|span| {
            let token_role = if matches!(span.role, DisplayRole::FallbackCode | DisplayRole::Plain)
            {
                role.unwrap_or(span.role)
            } else {
                span.role
            };
            let mut style = theme.display_role(token_role);
            if let Some(role) = role {
                style = style.bg(theme.display_role(role).bg.unwrap_or(theme.surface_code()));
            }
            if selected {
                style = style.add_modifier(Modifier::UNDERLINED);
            }
            Span::styled(span.text.as_str(), style)
        }));
    }
    let line = Line::from(spans);
    if let Some(role) = role {
        line.style(theme.display_role(role))
    } else {
        line
    }
}

fn dialog_area(area: Rect) -> Rect {
    crate::tui::modal_container::compact_modal_area(area, 40, 20)
}

pub(super) fn dialog_text_area(area: Rect) -> Rect {
    inner(dialog_area(area))
}
pub(crate) fn draw_dialog(frame: &mut Frame<'_>, state: &MissionControlState) {
    let Some(dialog) = &state.diff.dialog else {
        return;
    };
    let area = dialog_area(frame.area());
    let theme = state.theme;
    let busy = state.diff.saving;
    let title = format!(
        "Comment {}:{} {}{}",
        sanitize_display_text(&dialog.path),
        dialog.line,
        comment_marker(dialog.stale, dialog.resolved),
        if dialog.stale {
            " · stale"
        } else if dialog.resolved {
            " · resolved"
        } else {
            ""
        }
    );
    let mut hints = "[Alt-S] Save [Alt-D] Discard".to_owned();
    if dialog.id.is_some() {
        hints.push_str(if dialog.resolved {
            " [Alt-R] Reopen"
        } else {
            " [Alt-R] Resolve"
        });
    }
    let hotkeys = if busy {
        "Saving…".to_owned()
    } else if let Some(error) = &state.diff.error {
        sanitize_display_text(error)
    } else {
        hints
    };
    crate::tui::modal_container::draw_modal_container(
        frame,
        area,
        title,
        hotkeys,
        theme,
        (!busy).then_some("[Esc] close"),
    );
    dialog
        .editor
        .render_with_focus(frame, dialog_text_area(frame.area()), true, theme);
}