magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::{
    PICKER_MODAL, draw_modal_panel_with_hotkeys, picker_list_area, picker_modal_list_area,
};
use crate::tui::{
    render::{render_prepared_scroll_view_with_theme, scroll_view_content_width},
    state::MissionControlState,
};
use ratatui::{
    Frame,
    layout::{Rect, Size},
    text::Line,
    widgets::{Block, Borders, Paragraph},
};
use tui_scrollview::ScrollView;

pub(crate) fn skills_modal_list_area(area: Rect) -> Option<Rect> {
    picker_modal_list_area(area, "Skills", 1)
}

pub(crate) fn skill_row_at(
    area: Rect,
    state: &MissionControlState,
    column: u16,
    row: u16,
) -> Option<(String, bool)> {
    let list_area = skills_modal_list_area(area)?;
    if !list_area.contains((column, row).into()) {
        return None;
    }
    let modal = state.modals.skills_modal.as_ref()?;
    let document_row = usize::from(row - list_area.y)
        .saturating_add(state.scroll_offset(&state.scroll_views.skills_modal));
    let mut card_y = 0_usize;
    for rows in modal
        .rows
        .chunk_by(|left, right| left.source == right.source)
    {
        let first_skill_row = card_y.saturating_add(1);
        if document_row >= first_skill_row
            && document_row < first_skill_row.saturating_add(rows.len())
        {
            let skill = &rows[document_row - first_skill_row];
            return Some((skill.name.clone(), !skill.enabled));
        }
        card_y = card_y.saturating_add(rows.len()).saturating_add(3);
    }
    None
}

pub(in crate::tui::render) fn draw_skills_modal(
    frame: &mut Frame<'_>,
    area: Rect,
    state: &MissionControlState,
) {
    let Some(modal) = &state.modals.skills_modal else {
        return;
    };
    let theme = state.theme;
    let Some(content_area) = draw_modal_panel_with_hotkeys(
        frame,
        area,
        "Skills",
        PICKER_MODAL,
        theme,
        "[Tab] scope • [↑/↓] move • [Enter] toggle",
        Some("[Esc] close"),
    ) else {
        return;
    };
    if content_area.height == 0 {
        return;
    }
    frame.render_widget(
        Paragraph::new(format!("Scope: {}", modal.scope_label)).style(theme.pane()),
        Rect::new(content_area.x, content_area.y, content_area.width, 1),
    );
    let Some(list_area) = picker_list_area(content_area, 1) else {
        return;
    };
    if modal.rows.is_empty() {
        frame.render_widget(
            Paragraph::new("No skills discovered").style(theme.pane()),
            list_area,
        );
        return;
    }

    let content_width = scroll_view_content_width(list_area);
    if content_width == 0 {
        return;
    }
    let groups = || {
        modal
            .rows
            .chunk_by(|left, right| left.source == right.source)
    };
    let content_height = groups()
        .map(|rows| rows.len().saturating_add(3))
        .sum::<usize>()
        .saturating_sub(1)
        .max(usize::from(list_area.height));
    let mut view = ScrollView::new(Size::new(
        content_width,
        u16::try_from(content_height).unwrap_or(u16::MAX),
    ));
    let mut y = 0_u16;
    let mut row_index = 0_usize;
    for rows in groups() {
        let card_height = u16::try_from(rows.len().saturating_add(2)).unwrap_or(u16::MAX);
        let card_area = Rect::new(0, y, content_width, card_height);
        view.render_widget(
            Block::default()
                .title(Line::from(rows[0].source.as_str()).style(theme.accent(true)))
                .borders(Borders::ALL)
                .border_style(theme.border(false))
                .style(theme.app()),
            card_area,
        );
        for row in rows {
            let selected = row_index == modal.selected;
            let text = format!("{} {}", if row.enabled { "[x]" } else { "[ ]" }, row.name);
            view.render_widget(
                Paragraph::new(text).style(if selected {
                    theme.selection()
                } else {
                    theme.pane()
                }),
                Rect::new(2, y.saturating_add(1), content_width.saturating_sub(4), 1),
            );
            y = y.saturating_add(1);
            row_index = row_index.saturating_add(1);
        }
        y = y.saturating_add(3);
    }
    let mut scroll_state = *state.scroll_views.skills_modal.borrow();
    render_prepared_scroll_view_with_theme(frame, list_area, view, &mut scroll_state, None, theme);
}