magi-code 0.63.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::state::{MissionControlState, TuiLayoutMode};
use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TuiPane {
    Transcript,
    ActivityTree,
    Detail,
    Prompt,
    Status,
    Help,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TuiLayout {
    pub(super) transcript: Rect,
    pub(super) activity_tree: Rect,
    pub(super) detail: Rect,
    pub(super) prompt: Rect,
    pub(super) status: Rect,
    pub(super) help: Option<Rect>,
}

impl TuiLayout {
    pub(crate) fn pane_at(&self, column: u16, row: u16) -> Option<TuiPane> {
        if self.help.is_some_and(|area| contains(area, column, row)) {
            return Some(TuiPane::Help);
        }
        [
            (TuiPane::Transcript, self.transcript),
            (TuiPane::ActivityTree, self.activity_tree),
            (TuiPane::Detail, self.detail),
            (TuiPane::Prompt, self.prompt),
            (TuiPane::Status, self.status),
        ]
        .into_iter()
        .find_map(|(pane, area)| contains(area, column, row).then_some(pane))
    }

    pub(crate) fn pane_area(&self, pane: TuiPane) -> Option<Rect> {
        match pane {
            TuiPane::Transcript => Some(self.transcript),
            TuiPane::ActivityTree => Some(self.activity_tree),
            TuiPane::Detail => Some(self.detail),
            TuiPane::Prompt => Some(self.prompt),
            TuiPane::Status => Some(self.status),
            TuiPane::Help => self.help,
        }
    }
}

pub(crate) fn layout_for(area: Rect, state: &MissionControlState) -> TuiLayout {
    let prompt_wrap_width = area.width.saturating_sub(5).max(1);
    let prompt_content_rows = state.prompt_visible_rows(prompt_wrap_width);
    let prompt_height = prompt_content_rows.saturating_add(2);
    let root = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(8),
            Constraint::Length(prompt_height),
            Constraint::Length(1),
        ])
        .split(area);
    let body_constraints = match state.layout_mode {
        TuiLayoutMode::TranscriptFocused => {
            [Constraint::Percentage(60), Constraint::Percentage(40)]
        }
        TuiLayoutMode::ActivityFocused => [Constraint::Percentage(40), Constraint::Percentage(60)],
    };
    let body = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(body_constraints)
        .split(root[0]);
    let activity = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Percentage(45), Constraint::Percentage(55)])
        .split(body[1]);
    TuiLayout {
        transcript: body[0],
        activity_tree: activity[0],
        detail: activity[1],
        prompt: root[1],
        status: root[2],
        help: state.show_help.then_some(centered_rect(70, 60, area)),
    }
}

fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
    let [vertical] = Layout::vertical([Constraint::Percentage(percent_y)])
        .flex(Flex::Center)
        .areas(area);
    let [centered] = Layout::horizontal([Constraint::Percentage(percent_x)])
        .flex(Flex::Center)
        .areas(vertical);
    centered
}

fn contains(area: Rect, column: u16, row: u16) -> bool {
    column >= area.x
        && column < area.x.saturating_add(area.width)
        && row >= area.y
        && row < area.y.saturating_add(area.height)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn centered_rect_centers_overlay_within_area() {
        let area = Rect::new(0, 0, 100, 30);
        let centered = centered_rect(70, 60, area);

        assert_eq!(centered.width, 70);
        assert_eq!(centered.height, 18);

        let left_margin = centered.x - area.x;
        let right_margin = area.width - left_margin - centered.width;
        let top_margin = centered.y - area.y;
        let bottom_margin = area.height - top_margin - centered.height;

        assert!(left_margin.abs_diff(right_margin) <= 1);
        assert!(top_margin.abs_diff(bottom_margin) <= 1);
    }

    #[test]
    fn layout_body_starts_at_top_without_header_row() {
        let state = MissionControlState::default();
        let layout = layout_for(Rect::new(0, 0, 100, 30), &state);

        assert_eq!(layout.transcript.y, 0);
        assert_eq!(layout.activity_tree.y, 0);
        assert_eq!(layout.pane_at(0, 0), Some(TuiPane::Transcript));
    }

    #[test]
    fn selected_activity_detail_gets_taller_activity_pane() {
        let state = MissionControlState::default();
        let layout = layout_for(Rect::new(0, 0, 100, 30), &state);

        assert!(layout.detail.height > layout.activity_tree.height);
        assert_eq!(
            layout
                .activity_tree
                .y
                .saturating_add(layout.activity_tree.height),
            layout.detail.y
        );
    }
}