magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::super::theme::MissionControlTheme;
use super::{
    bordered_block, overlay_block_with_theme, overlay_text_area, render_overlay_fill_with_theme,
};
use ratatui::{
    Frame,
    layout::{Rect, Size},
    widgets::{Clear, List, ListItem, ListState},
};
use std::cell::RefCell;
use tui_scrollview::ScrollView;

use super::super::state::{MissionControlState, ModalKind};

pub(super) fn draw_top_modal(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
    match state.top_modal() {
        Some(ModalKind::SubagentViewer) => draw_subagent_viewer_modal(frame, area, state),
        Some(ModalKind::SessionPicker) => draw_sessions_modal(frame, area, state),
        Some(ModalKind::Skills) => draw_skills_modal(frame, area, state),
        Some(ModalKind::Mcp) => draw_mcp_modal(frame, area, state),
        Some(ModalKind::Tools) => draw_tools_modal(frame, area, state),
        Some(ModalKind::Subagents) => draw_subagents_modal(frame, area, state),
        Some(ModalKind::Models) => draw_models_modal(frame, area, state),
        Some(ModalKind::Usage) => draw_usage_modal(frame, area, state),
        Some(ModalKind::SystemPrompt) => draw_system_prompt_modal(frame, area, state),
        Some(ModalKind::Rewind) => draw_rewind_modal(frame, area, state),
        Some(ModalKind::ConnectProvider) => draw_connect_provider(frame, area, state),
        Some(ModalKind::LogoutConfirmation) => draw_logout_confirmation(frame, area, state),
        Some(ModalKind::LogoutPicker) => draw_logout_picker(frame, area, state),
        Some(ModalKind::ModelPicker) => draw_model_picker(frame, area, state),
        Some(ModalKind::ThemePicker) => draw_theme_picker(frame, area, state),
        None => {}
    }
}

pub(super) mod auth;
pub(super) mod mcp;
pub(super) mod model;
pub(super) mod models;
pub(super) mod rewind;
pub(super) mod sessions;
pub(super) mod skills;
pub(super) mod subagent_viewer;
pub(super) mod subagents;
pub(super) mod system_prompt;
pub(super) mod theme;
pub(super) mod tools;
pub(super) mod usage;

pub(crate) use auth::{
    connect_provider_choice_list_area, connect_provider_custom_field_visible,
    connect_provider_modal_visible, connect_provider_oauth_controls_visible,
    connect_provider_oauth_fallback_visible, logout_confirmation_action_row_area,
    logout_picker_list_area,
};
pub(super) use auth::{draw_connect_provider, draw_logout_confirmation, draw_logout_picker};
pub(super) use mcp::draw_mcp_modal;
pub(crate) use mcp::mcp_modal_list_area;
pub(super) use model::draw_model_picker;
pub(crate) use model::model_picker_list_area;
pub(super) use models::draw_models_modal;
pub(crate) use models::models_modal_list_area;
pub(super) use rewind::draw_rewind_modal;
pub(crate) use rewind::rewind_modal_text_area;
pub(super) use sessions::draw_sessions_modal;
pub(crate) use sessions::{
    sessions_modal_areas, sessions_modal_list_area, sessions_modal_preview_area,
};
pub(super) use skills::draw_skills_modal;
pub(crate) use skills::skills_modal_list_area;
pub(super) use subagent_viewer::draw_subagent_viewer_modal;
pub(crate) use subagent_viewer::{subagent_viewer_tab_at, subagent_viewer_timeline_area};
pub(super) use subagents::draw_subagents_modal;
pub(crate) use subagents::subagents_modal_list_area;
pub(super) use system_prompt::draw_system_prompt_modal;
pub(crate) use system_prompt::system_prompt_text_area;
pub(super) use theme::draw_theme_picker;
pub(crate) use theme::{theme_picker_list_area, theme_picker_query_visible};
pub(super) use tools::draw_tools_modal;
pub(crate) use tools::tools_modal_list_area;
pub(super) use usage::draw_usage_modal;
pub(crate) use usage::usage_modal_text_area;

#[derive(Debug, Clone, Copy)]
pub(in crate::tui::render) struct ModalSpec {
    min_width: u16,
    min_height: u16,
    width_ratio: (u16, u16),
    height: ModalHeight,
}

#[derive(Debug, Clone, Copy)]
pub(in crate::tui::render) enum ModalHeight {
    Ratio(u16, u16),
    Fixed(u16),
}

pub(in crate::tui::render) const SYSTEM_PROMPT_MODAL: ModalSpec = ModalSpec {
    min_width: 12,
    min_height: 5,
    width_ratio: (9, 10),
    height: ModalHeight::Ratio(4, 5),
};
pub(super) const SESSIONS_MODAL: ModalSpec = ModalSpec {
    min_width: 12,
    min_height: 5,
    width_ratio: (9, 10),
    height: ModalHeight::Ratio(4, 5),
};
pub(in crate::tui::render) const PICKER_MODAL: ModalSpec = ModalSpec {
    min_width: 12,
    min_height: 5,
    width_ratio: (4, 5),
    height: ModalHeight::Ratio(3, 5),
};
pub(super) const CONFIRMATION_MODAL: ModalSpec = ModalSpec {
    min_width: 12,
    min_height: 5,
    width_ratio: (3, 5),
    height: ModalHeight::Fixed(7),
};
pub(super) const CONNECT_PROVIDER_MODAL: ModalSpec = ModalSpec {
    min_width: 60,
    min_height: 18,
    width_ratio: (9, 10),
    height: ModalHeight::Ratio(4, 5),
};

pub(in crate::tui::render) fn modal_rect(area: Rect, spec: ModalSpec) -> Option<Rect> {
    if area.width < spec.min_width || area.height < spec.min_height {
        return None;
    }
    let width = area
        .width
        .saturating_mul(spec.width_ratio.0)
        .saturating_div(spec.width_ratio.1)
        .clamp(spec.min_width, area.width);
    let height = match spec.height {
        ModalHeight::Ratio(numerator, denominator) => area
            .height
            .saturating_mul(numerator)
            .saturating_div(denominator)
            .clamp(spec.min_height, area.height),
        ModalHeight::Fixed(height) => height.min(area.height).max(spec.min_height),
    };
    Some(Rect::new(
        area.x + area.width.saturating_sub(width) / 2,
        area.y + area.height.saturating_sub(height) / 2,
        width,
        height,
    ))
}

pub(super) fn modal_text_area(area: Rect, _title: &'static str, spec: ModalSpec) -> Option<Rect> {
    let modal = modal_rect(area, spec)?;
    Some(overlay_text_area(bordered_block().inner(modal)))
}

pub(super) fn draw_modal_panel(
    frame: &mut Frame<'_>,
    area: Rect,
    title: &'static str,
    spec: ModalSpec,
    theme: MissionControlTheme,
) -> Option<Rect> {
    let modal = modal_rect(area, spec)?;
    frame.render_widget(Clear, modal);
    let block = overlay_block_with_theme(theme, title);
    let inner = block.inner(modal);
    frame.render_widget(block, modal);
    Some(render_overlay_fill_with_theme(frame, inner, theme))
}

pub(super) fn block_inner(area: Rect) -> Rect {
    Rect::new(
        area.x.saturating_add(1),
        area.y.saturating_add(1),
        area.width.saturating_sub(2),
        area.height.saturating_sub(2),
    )
}

pub(super) fn picker_list_area(text_area: Rect, header_rows: u16) -> Option<Rect> {
    if text_area.width == 0 || text_area.height == 0 {
        return None;
    }
    let list_y = text_area.y.saturating_add(header_rows);
    let list_height = text_area
        .y
        .saturating_add(text_area.height)
        .saturating_sub(list_y);
    (list_height > 0).then_some(Rect::new(text_area.x, list_y, text_area.width, list_height))
}

pub(super) fn picker_modal_list_area(
    area: Rect,
    title: &'static str,
    header_rows: u16,
) -> Option<Rect> {
    let text_area = modal_text_area(area, title, PICKER_MODAL)?;
    picker_list_area(text_area, header_rows)
}

pub(crate) fn settings_scope_visible(area: ratatui::layout::Rect) -> bool {
    modal_text_area(area, "Settings", PICKER_MODAL)
        .is_some_and(|text_area| text_area.width > 0 && text_area.height >= 2)
}

pub(super) fn render_picker_list(
    frame: &mut Frame<'_>,
    area: Rect,
    items: Vec<ListItem<'static>>,
    selected: usize,
    scroll_state: &RefCell<tui_scrollview::ScrollViewState>,
    theme: MissionControlTheme,
) {
    if area.width == 0 || area.height == 0 {
        return;
    }
    let content_height = super::usize_to_u16_saturating(items.len().max(usize::from(area.height)));
    let content_width = super::scroll_view_content_width(area);
    if content_width == 0 {
        return;
    }
    let mut view = ScrollView::new(Size::new(content_width, content_height));
    let mut list_state = ListState::default().with_selected(Some(selected));
    view.render_stateful_widget(
        List::new(items)
            .style(theme.pane())
            .highlight_style(theme.selection())
            .highlight_symbol("> "),
        Rect::new(0, 0, content_width, content_height),
        &mut list_state,
    );
    let mut local_scroll_state = *scroll_state.borrow();
    super::render_prepared_scroll_view_with_theme(
        frame,
        area,
        view,
        &mut local_scroll_state,
        None,
        theme,
    );
}