magi-code 0.103.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::{hotkey_line_with_theme, render_scroll_view_persistent_with_theme};
#[cfg(any(test, debug_assertions))]
use crate::tui::PADDED_INLINE_SEPARATOR;
use crate::tui::{
    input,
    state::{HEADER_PANELS, MissionControlState, ModalKind},
};
use ratatui::{
    Frame,
    layout::{Margin, Rect},
    text::{Line, Span},
    widgets::{Block, Clear, Paragraph, Wrap},
};

const CYCLE_TABS_LABEL: &str = "[Shift-Tab] Cycle Tabs";
const PANELS_LABEL: &str = "[Alt+`] Panels";
const CONTROL_SEPARATOR: &str = " • ";
const PIN_LABEL: &str = "[Alt-A] Pin";
/// Menu labels in `HEADER_PANELS` order.
const PANEL_MENU_LABELS: [&str; 4] = [
    "Activities",
    "Session Files",
    "AI Summary",
    "Background Jobs",
];
/// Longest row, `[ ] Background Jobs [Alt+4]`, plus padding and borders.
const PANEL_MENU_WIDTH: u16 = 31;

fn header_controls_width() -> u16 {
    (PANELS_LABEL.len() + CONTROL_SEPARATOR.chars().count() + PIN_LABEL.len()) as u16
}

fn header_controls_area(header: Rect, state: &MissionControlState) -> Option<Rect> {
    let content = super::inset_when_space_allows(header, 1, 0);
    let width = header_controls_width().min(content.width);
    (!state.transcript_only_layout && !content.is_empty())
        .then(|| Rect::new(content.right() - width, content.y, width, 1))
}

fn header_control_area(
    header: Rect,
    state: &MissionControlState,
    offset: u16,
    width: u16,
) -> Option<Rect> {
    let controls = header_controls_area(header, state)?;
    let clipped = header_controls_width().saturating_sub(controls.width);
    let start = offset.saturating_sub(clipped).min(controls.width);
    let end = offset
        .saturating_add(width)
        .saturating_sub(clipped)
        .min(controls.width);
    (end > start).then(|| Rect::new(controls.x + start, controls.y, end - start, 1))
}

pub(crate) fn header_panels_toggle_area(header: Rect, state: &MissionControlState) -> Option<Rect> {
    header_control_area(header, state, 0, PANELS_LABEL.len() as u16)
}

pub(crate) fn header_pin_toggle_area(header: Rect, state: &MissionControlState) -> Option<Rect> {
    let width = PIN_LABEL.len() as u16;
    header_control_area(header, state, header_controls_width() - width, width)
}

/// Card below the header, aligned under the Panels control; `area` is the terminal.
pub(crate) fn header_panel_menu_area(area: Rect, state: &MissionControlState) -> Option<Rect> {
    if !state.header_panel_menu_visible() {
        return None;
    }
    let header = super::layout_for(area, state).header;
    let toggle = header_panels_toggle_area(header, state)?;
    let width = PANEL_MENU_WIDTH.min(area.width);
    let x = toggle.x.min(area.right().saturating_sub(width));
    let card =
        Rect::new(x, header.bottom(), width, HEADER_PANELS.len() as u16 + 2).intersection(area);
    (!card.is_empty()).then_some(card)
}

pub(crate) fn header_panel_menu_row_at(
    area: Rect,
    state: &MissionControlState,
    column: u16,
    row: u16,
) -> Option<ModalKind> {
    let rows = header_panel_menu_area(area, state)?.inner(Margin::new(1, 1));
    rows.contains((column, row).into())
        .then(|| HEADER_PANELS.get(usize::from(row - rows.y)).copied())
        .flatten()
}

pub(super) fn draw_header_panel_menu(
    frame: &mut Frame<'_>,
    area: Rect,
    state: &MissionControlState,
) {
    let Some(card) = header_panel_menu_area(area, state) else {
        return;
    };
    let theme = state.theme;
    let block = super::bordered_block()
        .border_style(theme.border(true))
        .style(theme.app());
    let rows = block.inner(card).inner(Margin::new(1, 0));
    frame.render_widget(Clear, card);
    frame.render_widget(block, card);
    let lines = HEADER_PANELS
        .into_iter()
        .zip(PANEL_MENU_LABELS)
        .enumerate()
        .map(|(index, (modal, label))| {
            let open = state.modals.header_modals.contains(&modal);
            Line::from(vec![
                Span::styled(
                    format!("[{}] {label:<15} ", if open { "•" } else { " " }),
                    if open {
                        theme.accent(false)
                    } else {
                        theme.muted(false)
                    },
                ),
                super::hotkey_span_with_theme(theme, format!("[Alt+{}]", index + 1)),
            ])
        })
        .collect::<Vec<_>>();
    frame.render_widget(Paragraph::new(lines).style(theme.app()), rows);
}

pub(super) fn draw_header(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
    let theme = state.theme;
    frame.render_widget(Block::default().style(theme.pane()), area);
    let content = super::inset_when_space_allows(area, 1, 0);
    let title_style = theme.title(state.is_transcript_focused());
    let panels = header_controls_area(area, state);
    for (row, title) in [
        Line::styled(format!("Session: {} ", state.footer_session), title_style),
        Line::styled(branch_text(state), theme.muted(false)),
    ]
    .into_iter()
    .enumerate()
    .take(usize::from(content.height))
    {
        let y = content.y.saturating_add(row as u16);
        let controls_left = if row == 0 {
            panels.map(|panels| panels.x)
        } else if !state.transcript_only_layout {
            Some(
                content
                    .right()
                    .saturating_sub(CYCLE_TABS_LABEL.len() as u16)
                    .max(content.x),
            )
        } else {
            None
        };
        let title_right =
            controls_left.map_or(content.right(), |x| x.saturating_sub(1).max(content.x));
        frame.render_widget(
            Paragraph::new(title).style(theme.pane()),
            Rect::new(content.x, y, title_right.saturating_sub(content.x), 1),
        );
    }
    if let Some(panels) = panels {
        let control_style = |active: bool| {
            if active {
                theme.accent(false)
            } else {
                theme.muted(false)
            }
        };
        let mut spans = hotkey_line_with_theme(
            theme,
            PANELS_LABEL,
            control_style(state.header_panel_menu_visible()),
        )
        .spans;
        spans.push(Span::styled(CONTROL_SEPARATOR, theme.muted(false)));
        spans.extend(
            hotkey_line_with_theme(
                theme,
                PIN_LABEL,
                control_style(state.modals.header_modals_pinned),
            )
            .spans,
        );
        frame.render_widget(
            Line::from(spans).right_aligned().style(theme.pane()),
            panels,
        );
    }
    if !state.transcript_only_layout && content.height > 1 {
        let width = (CYCLE_TABS_LABEL.len() as u16).min(content.width);
        frame.render_widget(
            hotkey_line_with_theme(theme, CYCLE_TABS_LABEL, title_style)
                .right_aligned()
                .style(theme.pane()),
            Rect::new(content.right() - width, content.y + 1, width, 1),
        );
    }
    if content.height > 2 {
        frame.render_widget(
            Paragraph::new(Line::styled(
                "─".repeat(usize::from(content.width)),
                ratatui::style::Style::new().fg(theme.border_alternate()),
            )),
            Rect::new(content.x, content.y.saturating_add(2), content.width, 1),
        );
    }
}

pub(super) fn branch_text(state: &MissionControlState) -> String {
    state
        .footer_git_branch
        .as_deref()
        .filter(|branch| !branch.is_empty())
        .map(|branch| format!("Branch: {branch}"))
        .unwrap_or_default()
}

pub(super) fn version_title(state: &MissionControlState) -> Line<'static> {
    let accent = state.theme.accent(false);
    let mut spans = Vec::new();
    #[cfg(any(test, debug_assertions))]
    if let Some(perf) = state.perf.footer_text() {
        spans.push(Span::styled(
            format!("{perf}{PADDED_INLINE_SEPARATOR}"),
            accent,
        ));
    }
    if state.available_update_version.is_some() {
        spans.push(Span::styled(
            "(/update ready) ",
            accent.fg(state.theme.text_command()),
        ));
    }
    spans.push(Span::styled(
        format!("MAGI-CODE v{}", env!("CARGO_PKG_VERSION")),
        accent,
    ));
    Line::from(spans)
}

pub(super) fn draw_help(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
    let theme = state.theme;
    let inner = crate::tui::modal_container::draw_modal_container(
        frame,
        area,
        "Help",
        "[PgUp]/[PgDn] scroll",
        theme,
        Some("[Esc] close"),
    );
    let text_area = super::overlay_text_area(inner);
    let help_text = input::help_text();
    let wrap_width = super::scroll_view_content_width(text_area).max(1);
    let scroll = state.help_render_offset(text_area.height, wrap_width, help_text);
    render_scroll_view_persistent_with_theme(
        frame,
        text_area,
        super::usize_to_u16_saturating(scroll),
        super::usize_to_u16_saturating(crate::tui::transcript::ratatui_wrapped_visual_rows(
            help_text, wrap_width,
        )),
        Paragraph::new(help_text)
            .style(theme.app())
            .wrap(Wrap { trim: false }),
        &state.scroll_views.help,
        theme,
    );
}