magi-code 0.96.1

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::MissionControlState};
use ratatui::{
    Frame,
    layout::Rect,
    text::{Line, Span},
    widgets::{Block, Paragraph, Wrap},
};

const CYCLE_TABS_LABEL: &str = "[Shift-Tab] Cycle Tabs";
const PANEL_SHORTCUTS: &str = "[Alt+1-3] ";
const PANEL_TOGGLE_LABELS: [&str; 3] = ["Activities", "Files", "Summary"];
const PIN_LABEL: &str = "[Alt-A] Pin";

fn header_panels_width() -> u16 {
    (PANEL_SHORTCUTS.len()
        + PANEL_TOGGLE_LABELS
            .iter()
            .map(|label| label.len() + 4)
            .sum::<usize>()
        + 4
        + 3
        + PIN_LABEL.len()) as u16
}

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

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

pub(crate) fn header_toggle_areas(header: Rect, state: &MissionControlState) -> [Option<Rect>; 3] {
    let mut offset = PANEL_SHORTCUTS.len() as u16;
    PANEL_TOGGLE_LABELS.map(|label| {
        let width = label.len() as u16 + 4;
        let area = header_panel_control_area(header, state, offset, width);
        offset += width + 2;
        area
    })
}

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

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_panels_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 pin_style = if state.modals.header_modals_pinned {
            theme.accent(false)
        } else {
            theme.muted(false)
        };
        let mut spans = vec![Span::styled(
            PANEL_SHORTCUTS,
            super::hotkey_style_with_theme(theme),
        )];
        for (index, (label, modal)) in PANEL_TOGGLE_LABELS
            .into_iter()
            .zip([
                super::super::state::ModalKind::ActivityTree,
                super::super::state::ModalKind::SessionFiles,
                super::super::state::ModalKind::Summary,
            ])
            .enumerate()
        {
            if index > 0 {
                spans.push(Span::styled(", ", theme.muted(false)));
            }
            let open = state.modals.header_modals.contains(&modal);
            spans.push(Span::styled(
                format!("[{}] {label}", if open { "•" } else { " " }),
                if open {
                    theme.accent(false)
                } else {
                    theme.muted(false)
                },
            ));
        }
        spans.push(Span::styled(" • ", theme.muted(false)));
        spans.extend(hotkey_line_with_theme(theme, PIN_LABEL, pin_style).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,
    );
}