magi-code 0.63.1

Repository-aware CLI coding agent for terminal work
Documentation
pub(in crate::tui) mod activity;
mod header;
mod modals;
mod prompt;
pub(in crate::tui) mod transcript;

pub(crate) use super::layout::{TuiPane, layout_for};
#[cfg(test)]
use super::state::CustomProviderSetupStep;
use super::state::MissionControlState;
use super::theme::MATRIX_GREEN;
#[cfg(test)]
pub(in crate::tui::render) use modals::{PICKER_MODAL, SYSTEM_PROMPT_MODAL, modal_rect};
pub(crate) use modals::{
    auth_picker_list_area, mcp_modal_list_area, model_picker_list_area, models_modal_list_area,
    rewind_modal_text_area, sessions_modal_areas, sessions_modal_list_area,
    sessions_modal_preview_area, skills_modal_list_area, subagents_modal_list_area,
    system_prompt_text_area, tools_modal_list_area, usage_modal_text_area,
};
use ratatui::{
    Frame,
    layout::{Alignment, Margin, Position, Rect, Size},
    style::{Color, Modifier, Style},
    symbols::scrollbar::{Set, VERTICAL},
    text::{Line, Span},
    widgets::{
        Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Widget,
    },
};
use tui_scrollview::ScrollView;

pub(super) fn usize_to_u16_saturating(value: usize) -> u16 {
    value.min(usize::from(u16::MAX)) as u16
}

pub(super) fn scroll_view_content_width(area: Rect) -> u16 {
    area.width.saturating_sub(1)
}

pub(super) fn render_scroll_view_persistent<W: Widget>(
    frame: &mut Frame<'_>,
    area: Rect,
    offset: u16,
    content_height: u16,
    widget: W,
    scroll_state: &std::cell::RefCell<tui_scrollview::ScrollViewState>,
) {
    render_scroll_view_persistent_impl(
        frame,
        area,
        offset,
        content_height,
        None,
        widget,
        scroll_state,
    );
}

#[allow(clippy::too_many_arguments)]
pub(super) fn render_scroll_view_persistent_with_metrics<W: Widget>(
    frame: &mut Frame<'_>,
    area: Rect,
    offset: u16,
    content_height: u16,
    scrollbar_content_length: usize,
    scrollbar_position: usize,
    scrollbar_viewport_length: usize,
    widget: W,
    scroll_state: &std::cell::RefCell<tui_scrollview::ScrollViewState>,
) {
    render_scroll_view_persistent_impl(
        frame,
        area,
        offset,
        content_height,
        Some((
            scrollbar_content_length,
            scrollbar_position,
            scrollbar_viewport_length,
        )),
        widget,
        scroll_state,
    );
}

#[allow(clippy::too_many_arguments)]
fn render_scroll_view_persistent_impl<W: Widget>(
    frame: &mut Frame<'_>,
    area: Rect,
    offset: u16,
    content_height: u16,
    scrollbar_metrics: Option<(usize, usize, usize)>,
    widget: W,
    scroll_state: &std::cell::RefCell<tui_scrollview::ScrollViewState>,
) {
    if area.width == 0 || area.height == 0 {
        return;
    }
    let size = Size::new(
        scroll_view_content_width(area),
        content_height.max(area.height),
    );
    let mut view = ScrollView::new(size)
        .vertical_scrollbar_visibility(tui_scrollview::ScrollbarVisibility::Always)
        .horizontal_scrollbar_visibility(tui_scrollview::ScrollbarVisibility::Never);
    view.render_widget(widget, Rect::new(0, 0, size.width, size.height));
    let mut scroll_state = scroll_state.borrow_mut();
    if scroll_state.offset() == Position::ORIGIN && offset != 0 {
        scroll_state.set_offset(Position::new(0, offset));
    }
    frame.render_stateful_widget(view, area, &mut *scroll_state);
    let mut scrollbar_state =
        if let Some((content_length, position, viewport_length)) = scrollbar_metrics {
            ScrollbarState::new(content_length)
                .viewport_content_length(viewport_length)
                .position(position.min(content_length.saturating_sub(1)))
        } else {
            let overflow = content_height.saturating_sub(area.height);
            ScrollbarState::new(usize::from(overflow.max(1)))
                .position(usize::from(scroll_state.offset().y.min(overflow)))
        };
    let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
        .symbols(Set {
            track: "│",
            thumb: "┃",
            begin: VERTICAL.begin,
            end: VERTICAL.end,
        })
        .thumb_style(Style::new().fg(MATRIX_GREEN.green_bright).bg(Color::Reset))
        .track_style(Style::new().fg(MATRIX_GREEN.green_dim).bg(Color::Reset))
        .begin_style(Style::new().fg(MATRIX_GREEN.green_dim).bg(Color::Reset))
        .end_style(Style::new().fg(MATRIX_GREEN.green_dim).bg(Color::Reset));
    frame.render_stateful_widget(scrollbar, area, &mut scrollbar_state);
}
pub(super) fn render_scroll_view_persistent_state<W: Widget>(
    frame: &mut Frame<'_>,
    area: Rect,
    content_height: u16,
    widget: W,
    scroll_state: &std::cell::RefCell<tui_scrollview::ScrollViewState>,
) {
    render_scroll_view_persistent(frame, area, 0, content_height, widget, scroll_state);
}

pub(crate) fn draw(frame: &mut Frame<'_>, state: &MissionControlState) {
    let layout = layout_for(frame.area(), state);
    frame.render_widget(Block::default().style(MATRIX_GREEN.app()), frame.area());
    transcript::draw_transcript(frame, layout.transcript, state);
    activity::draw_tree(frame, layout.activity_tree, state);
    activity::draw_detail(frame, layout.detail, state);

    prompt::draw_prompt(frame, layout.prompt, state);
    prompt::draw_autocomplete(frame, frame.area(), layout.prompt, state);
    header::draw_status(frame, layout.status, state);

    if let Some(help) = layout.help {
        header::draw_help(frame, help, state);
    }
    modals::draw_system_prompt_modal(frame, frame.area(), state);
    modals::draw_rewind_modal(frame, frame.area(), state);
    modals::draw_sessions_modal(frame, frame.area(), state);
    modals::draw_skills_modal(frame, frame.area(), state);
    modals::draw_mcp_modal(frame, frame.area(), state);
    modals::draw_tools_modal(frame, frame.area(), state);
    modals::draw_subagents_modal(frame, frame.area(), state);
    modals::draw_models_modal(frame, frame.area(), state);
    modals::draw_usage_modal(frame, frame.area(), state);
    modals::draw_model_picker(frame, frame.area(), state);
    modals::draw_login_picker(frame, frame.area(), state);
    modals::draw_logout_confirmation(frame, frame.area(), state);
    modals::draw_custom_provider_setup(frame, frame.area(), state);
    modals::draw_active_login(frame, frame.area(), state);
    modals::draw_custom_provider_replacement_confirmation(frame, frame.area(), state);
    draw_toast(frame, frame.area(), state);
}

fn draw_toast(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
    let Some(toast) = &state.toast else {
        return;
    };
    if area.width < 8 || area.height < 3 {
        return;
    }
    let width = (toast.message.chars().count() as u16)
        .saturating_add(4)
        .min(area.width);
    let height = 3.min(area.height);
    let x = area.x + area.width.saturating_sub(width) / 2;
    let y = area.y + area.height.saturating_sub(height + 1);
    let toast_area = Rect::new(x, y, width, height);
    let style = match toast.severity {
        super::toast::ToastSeverity::Success => MATRIX_GREEN.selection(),
        super::toast::ToastSeverity::Error => MATRIX_GREEN.transcript_error_body(),
    };
    frame.render_widget(Clear, toast_area);
    let block = bordered_block().border_style(style).style(style);
    let inner = block.inner(toast_area);
    frame.render_widget(block, toast_area);
    frame.render_widget(
        Paragraph::new(toast.message.clone())
            .style(style)
            .alignment(Alignment::Center),
        inner,
    );
}

pub(super) fn hotkey_style() -> Style {
    Style::new()
        .fg(MATRIX_GREEN.green_dim)
        .bg(MATRIX_GREEN.background)
        .remove_modifier(Modifier::BOLD)
}

pub(super) fn hotkey_span(text: impl Into<String>) -> Span<'static> {
    Span::styled(text.into(), hotkey_style())
}

pub(super) fn hotkey_line(text: impl Into<String>, base_style: Style) -> Line<'static> {
    let text = text.into();
    let mut spans = Vec::new();
    let mut rest = text.as_str();
    while let Some(start) = rest.find('[') {
        let (before, after_start) = rest.split_at(start);
        if !before.is_empty() {
            spans.push(Span::styled(before.to_string(), base_style));
        }
        if let Some(end) = after_start.find(']') {
            let (hotkey, after_end) = after_start.split_at(end + 1);
            spans.push(hotkey_span(hotkey.to_string()));
            rest = after_end;
        } else {
            spans.push(Span::styled(after_start.to_string(), base_style));
            rest = "";
        }
    }
    if !rest.is_empty() {
        spans.push(Span::styled(rest.to_string(), base_style));
    }
    Line::from(spans)
}

const PANE_TEXT_HORIZONTAL_PADDING: u16 = 1;

pub(super) fn bordered_block() -> Block<'static> {
    Block::default().borders(Borders::ALL)
}

pub(super) fn padded_title_text(text: impl Into<String>, style: Style) -> Line<'static> {
    let text = text.into();
    let padded = if text.is_empty() {
        text
    } else {
        format!(" {text} ")
    };
    Line::styled(padded, style)
}
pub(super) fn themed_block(title: impl Into<String>, active: bool) -> Block<'static> {
    bordered_block()
        .border_style(MATRIX_GREEN.border(active))
        .title(padded_title_text(title, MATRIX_GREEN.title(active)))
        .style(MATRIX_GREEN.app())
}

pub(super) fn overlay_block(title: &'static str) -> Block<'static> {
    bordered_block()
        .border_style(MATRIX_GREEN.border_alt().fg(MATRIX_GREEN.green))
        .title(padded_title_text(title, MATRIX_GREEN.title_alt()))
        .style(MATRIX_GREEN.overlay_panel())
}

pub(super) fn pane_text_area(block_inner: Rect) -> Rect {
    inset_when_space_allows(block_inner, PANE_TEXT_HORIZONTAL_PADDING, 0)
}

pub(super) fn overlay_text_area(background: Rect) -> Rect {
    inset_when_space_allows(
        background,
        PANE_TEXT_HORIZONTAL_PADDING,
        PANE_TEXT_HORIZONTAL_PADDING,
    )
}

fn inset_when_space_allows(area: Rect, horizontal: u16, vertical: u16) -> Rect {
    let horizontal = if area.width > horizontal.saturating_mul(2) {
        horizontal
    } else {
        0
    };
    let vertical = if area.height > vertical.saturating_mul(2) {
        vertical
    } else {
        0
    };
    area.inner(Margin {
        horizontal,
        vertical,
    })
}

pub(super) fn render_overlay_fill(frame: &mut Frame<'_>, block_inner: Rect) -> Rect {
    frame.render_widget(
        Block::default().style(MATRIX_GREEN.panel_alt()),
        block_inner,
    );
    overlay_text_area(block_inner)
}

pub(super) fn render_overlay_fill_horizontal(frame: &mut Frame<'_>, block_inner: Rect) -> Rect {
    frame.render_widget(
        Block::default().style(MATRIX_GREEN.panel_alt()),
        block_inner,
    );
    pane_text_area(block_inner)
}

pub(crate) fn transcript_text_area_for_pane(area: Rect) -> Rect {
    let y = area.y.saturating_add(u16::from(area.height > 0));
    let height = area.height.saturating_sub(u16::from(area.height > 0));
    pane_text_area(Rect::new(area.x, y, area.width, height))
}

pub(crate) fn content_text_area_for_pane(pane: TuiPane, area: Rect) -> Rect {
    if matches!(pane, TuiPane::Status) {
        return area;
    }
    if matches!(pane, TuiPane::Transcript) {
        return transcript_text_area_for_pane(area);
    }
    let block_inner = inset_when_space_allows(area, 1, 1);
    match pane {
        TuiPane::ActivityTree | TuiPane::Detail | TuiPane::Prompt => pane_text_area(block_inner),
        TuiPane::Help => overlay_text_area(block_inner),
        TuiPane::Transcript | TuiPane::Status => area,
    }
}

#[cfg(test)]
mod test_support;

#[cfg(test)]
mod tests;

#[cfg(test)]
mod primary_agent_render_tests;