magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::super::render_scrollbar_with_theme;
use crate::tui::modal_container::{draw_modal_container, modal_area, modal_tab};
use crate::tui::{state::MissionControlState, theme::MissionControlTheme};
use ratatui::{
    Frame,
    layout::{Alignment, Margin, Rect},
    widgets::{Block, Clear, Paragraph, Wrap},
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SubagentViewerModalAreas {
    pub(crate) tabs: Rect,
    pub(crate) timeline: Rect,
    pub(crate) prompt: Rect,
}

pub(crate) fn subagent_viewer_modal_areas(area: Rect) -> Option<SubagentViewerModalAreas> {
    let text_area = modal_area(area, 60, 60).inner(Margin::new(1, 1));
    if text_area.height == 0 || text_area.width == 0 {
        return None;
    }
    // Hide tabs before sacrificing the last live-output row.
    let tabs = Rect::new(
        text_area.x,
        text_area.y,
        text_area.width,
        u16::from(text_area.height > 1),
    );
    let timeline_y = tabs.bottom();
    let remaining_height = text_area.bottom().saturating_sub(timeline_y);
    // Keep one live-output row on short terminals; reserve one typing row and borders.
    let prompt_height = remaining_height.saturating_sub(1).min(3);
    let prompt_height = if prompt_height >= 3 { prompt_height } else { 0 };
    let timeline = Rect::new(
        text_area.x,
        timeline_y,
        text_area.width,
        remaining_height.saturating_sub(prompt_height),
    );
    let prompt = Rect::new(
        text_area.x,
        timeline.bottom(),
        text_area.width,
        prompt_height,
    );
    (timeline.height > 0).then_some(SubagentViewerModalAreas {
        tabs,
        timeline,
        prompt,
    })
}

pub(crate) fn subagent_viewer_timeline_area(area: Rect) -> Option<Rect> {
    subagent_viewer_modal_areas(area).map(|areas| areas.timeline)
}

pub(crate) fn subagent_viewer_prompt_area(area: Rect) -> Option<Rect> {
    subagent_viewer_modal_areas(area)
        .map(|areas| areas.prompt)
        .filter(|area| area.height >= 3 && area.width > 2)
}

pub(crate) fn subagent_viewer_tab_at(
    area: Rect,
    state: &MissionControlState,
    column: u16,
    row: u16,
) -> Option<usize> {
    let areas = subagent_viewer_modal_areas(area)?;
    if !contains(areas.tabs, column, row) {
        return None;
    }
    let tabs = state.subagent_viewer_tabs();
    let selected = selected_tab_index(state, &tabs);
    visible_tab_areas(areas.tabs, tabs.len(), selected)
        .into_iter()
        .find(|tab| contains(tab.area, column, row))
        .map(|tab| tab.actual_index)
}

pub(in crate::tui::render) fn draw_subagent_viewer_modal(
    frame: &mut Frame<'_>,
    area: Rect,
    state: &MissionControlState,
) {
    if !state.subagent_viewer_visible() {
        return;
    }
    let theme = state.theme;
    draw_modal_container(
        frame,
        modal_area(area, 60, 60),
        "Subagent Viewer",
        "[Tab]/[Shift-Tab] switch · [PgUp]/[PgDn] scroll",
        theme,
        Some("[Esc] close"),
    );
    let Some(areas) = subagent_viewer_modal_areas(area) else {
        return;
    };
    draw_timeline(frame, areas.timeline, state, theme);
    draw_tabs(frame, areas.tabs, state, theme);
    // Keep the main prompt's editor styling as a distinct input surface.
    if areas.prompt.height >= 3 && areas.prompt.width > 2 {
        super::super::prompt::draw_subagent_prompt(frame, areas.prompt, state);
        if state.subagent_prompt_editable()
            && let Some(autocomplete) = state
                .selected_subagent_prompt()
                .and_then(|draft| draft.autocomplete.as_ref())
        {
            let completion_bounds = Rect::new(
                areas.timeline.x,
                areas.timeline.y,
                areas.timeline.width,
                areas.timeline.height + areas.prompt.height,
            );
            super::super::prompt::draw_prompt_autocomplete(
                frame,
                completion_bounds,
                areas.prompt,
                autocomplete,
                theme,
            );
        }
    }
}

fn draw_tabs(
    frame: &mut Frame<'_>,
    area: Rect,
    state: &MissionControlState,
    theme: MissionControlTheme,
) {
    let tabs = state.subagent_viewer_tabs();
    if tabs.is_empty() {
        frame.render_widget(
            Paragraph::new("No sibling subagents available")
                .style(theme.muted(false).bg(theme.surface_background())),
            area,
        );
        return;
    }
    let selected = selected_tab_index(state, &tabs);
    for visible_tab in visible_tab_areas(area, tabs.len(), selected) {
        let Some(task_id) = tabs.get(visible_tab.actual_index) else {
            continue;
        };
        if visible_tab.area.width == 0 {
            continue;
        }
        let label_width = usize::from(visible_tab.area.width.saturating_sub(2)).max(1);
        let label = state.subagent_viewer_tab_label(task_id, label_width);
        frame.render_widget(
            Paragraph::new(modal_tab(
                &label,
                visible_tab.actual_index == selected,
                theme,
            ))
            .alignment(Alignment::Center),
            visible_tab.area,
        );
    }
}

fn draw_timeline(
    frame: &mut Frame<'_>,
    area: Rect,
    state: &MissionControlState,
    theme: MissionControlTheme,
) {
    if area.width == 0 || area.height == 0 {
        return;
    }
    let Some(task_id) = state.subagent_viewer_selected_task_id() else {
        frame.render_widget(
            Paragraph::new("No live activity available")
                .style(theme.pane())
                .wrap(Wrap { trim: false }),
            area,
        );
        return;
    };
    let content_width = super::super::scroll_view_content_width(area).max(1);
    let offset = state.subagent_viewer_scroll_offset(area.height, content_width);
    let timeline = state.cached_subagent_viewer_timeline(&task_id, content_width);
    let content = Rect {
        width: content_width.min(area.width.saturating_sub(1)),
        ..area
    };
    let (visible_lines, skipped_rows) = timeline.visible_line_range(offset, area.height);
    let lines = timeline.lines[visible_lines]
        .iter()
        .map(|line| line.line.clone())
        .collect::<Vec<_>>();
    frame.render_widget(
        Paragraph::new(lines)
            .style(theme.pane())
            .wrap(Wrap { trim: false })
            .scroll((skipped_rows, 0)),
        content,
    );
    let gutter = Rect::new(
        area.x + content.width,
        area.y,
        area.width - content.width,
        area.height,
    );
    frame.render_widget(Clear, gutter);
    frame.render_widget(Block::default().style(theme.pane()), gutter);
    render_scrollbar_with_theme(
        frame,
        area,
        timeline.rows.saturating_sub(usize::from(area.height)) + 1,
        offset,
        usize::from(area.height),
        theme,
    );
}

const MIN_TAB_WIDTH: u16 = 12;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct VisibleTabArea {
    actual_index: usize,
    area: Rect,
}

fn selected_tab_index(state: &MissionControlState, tabs: &[crate::output::ActivityId]) -> usize {
    state
        .subagent_viewer_selected_task_id()
        .and_then(|selected| tabs.iter().position(|task_id| task_id == &selected))
        .unwrap_or(0)
}

fn visible_tab_areas(area: Rect, count: usize, selected: usize) -> Vec<VisibleTabArea> {
    if count == 0 || area.width == 0 {
        return Vec::new();
    }
    let visible_count = count.min((usize::from(area.width) / usize::from(MIN_TAB_WIDTH)).max(1));
    let selected = selected.min(count - 1);
    let start = selected
        .saturating_sub(visible_count / 2)
        .min(count - visible_count);
    let width = usize::from(area.width);
    let base = width / visible_count;
    let remainder = width % visible_count;
    let mut x = area.x;
    (start..start + visible_count)
        .enumerate()
        .map(|(visible_index, actual_index)| {
            let tab_width = base + usize::from(visible_index < remainder);
            let tab = Rect::new(x, area.y, tab_width as u16, area.height);
            x = x.saturating_add(tab.width);
            VisibleTabArea {
                actual_index,
                area: tab,
            }
        })
        .collect()
}

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)
}