magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::tui::{
    layout::TuiPane, selection::ActiveSelection, state::MissionControlState,
    theme::MissionControlTheme,
};
use ratatui::text::{Line, Span};
use unicode_segmentation::UnicodeSegmentation;

pub(in crate::tui) fn visible_transcript_lines(
    state: &MissionControlState,
    width: u16,
    visible_rows: u16,
) -> crate::tui::transcript_cards::VisibleTranscriptLines {
    let active_selection = state.selection.as_ref().filter(|selection| {
        selection.pane == TuiPane::Transcript && selection.byte_range().is_some()
    });

    let visible = crate::tui::transcript_cards::visible_anchored_scrollback_visual_lines(
        state,
        width,
        visible_rows,
    );
    let mut line_start_row = -(visible.scroll_offset as isize);
    let mut image_rows = Vec::new();
    let mut projected_row = 0usize;
    let images_enabled = state.images.borrow().enabled();
    let lines = visible
        .lines
        .into_iter()
        .map(|visual_line| {
            if let Some(image_row) = &visual_line.image_row {
                image_rows.push((projected_row, image_row.clone()));
            }
            if images_enabled {
                projected_row +=
                    crate::tui::transcript_cards::visual_line_rows(&visual_line, width);
            }
            let mut selected = if let Some(selection) = active_selection {
                let rows = crate::tui::transcript_cards::visual_line_rows(&visual_line, width);
                let line = visual_line_with_active_selection(
                    &visual_line,
                    selection,
                    line_start_row,
                    rows,
                    width,
                    state.theme,
                );
                line_start_row = line_start_row.saturating_add(rows as isize);
                line
            } else {
                visual_line.line
            };
            decorate_status_indicators(state, &visual_line.status_indicators, &mut selected);
            selected
        })
        .collect();

    crate::tui::transcript_cards::VisibleTranscriptLines {
        image_rows,
        lines,
        scroll_offset: visible.scroll_offset,
        scrollbar: visible.scrollbar,
    }
}

fn visual_line_with_active_selection(
    visual_line: &crate::tui::transcript_cards::TranscriptVisualLine,
    selection: &ActiveSelection,
    line_start_row: isize,
    line_rows: usize,
    width: u16,
    theme: MissionControlTheme,
) -> Line<'static> {
    if !visual_line.copyable || line_rows == 0 {
        return visual_line.line.clone();
    }

    let (selection_start, selection_end) = if selection.anchor.byte <= selection.current.byte {
        (selection.anchor, selection.current)
    } else {
        (selection.current, selection.anchor)
    };
    let line_end_row = line_start_row.saturating_add(line_rows as isize);
    if selection_end.row < line_start_row.max(0) as usize
        || selection_start.row as isize >= line_end_row
    {
        return visual_line.line.clone();
    }

    let first_row = (selection_start.row as isize).max(line_start_row);
    let last_row = (selection_end.row as isize).min(line_end_row - 1);
    let mut selected_copy_start = usize::MAX;
    let mut selected_copy_end = 0usize;
    let column_offset = usize::from(visual_line.visual_copy_column_offset);
    let copy_text_len = visual_line.copy_text.len();
    for row in first_row..=last_row {
        let row_usize = row as usize;
        let start_col = if row_usize == selection_start.row {
            selection_start.col
        } else {
            0
        };
        let end_col = if row_usize == selection_end.row {
            selection_end.col
        } else {
            usize::from(width.max(1))
        };
        if start_col >= end_col {
            continue;
        }
        let row_in_line = (row - line_start_row) as usize;
        let Some(start) = copy_byte_for_visual_cell(
            &visual_line.copy_text,
            &visual_line.visual_copy_byte_boundaries,
            column_offset,
            copy_text_len,
            row_in_line,
            start_col,
            width,
        ) else {
            continue;
        };
        let Some(end) = copy_byte_for_visual_cell(
            &visual_line.copy_text,
            &visual_line.visual_copy_byte_boundaries,
            column_offset,
            copy_text_len,
            row_in_line,
            end_col,
            width,
        ) else {
            continue;
        };

        if start < end {
            selected_copy_start = selected_copy_start.min(start);
            selected_copy_end = selected_copy_end.max(end);
        }
    }

    if selected_copy_start >= selected_copy_end {
        visual_line.line.clone()
    } else {
        visual_line_with_selection(visual_line, selected_copy_start, selected_copy_end, theme)
    }
}

fn copy_byte_for_visual_cell(
    copy_text: &str,
    copy_byte_boundaries: &[usize],
    column_offset: usize,
    copy_text_len: usize,
    row: usize,
    column: usize,
    width: u16,
) -> Option<usize> {
    if row == 0 && !copy_byte_boundaries.is_empty() {
        let relative_column = column.saturating_sub(column_offset);
        return Some(
            copy_byte_boundaries
                .get(relative_column)
                .copied()
                .or_else(|| copy_byte_boundaries.last().copied())?
                .min(copy_text_len),
        );
    }
    let position = crate::tui::transcript::text_position_for_visual_cell_with_leading_columns(
        copy_text,
        column_offset,
        row,
        column,
        width.max(1),
    )?;
    Some(position.byte.min(copy_text_len))
}

fn visual_line_with_selection(
    visual_line: &crate::tui::transcript_cards::TranscriptVisualLine,
    copy_start: usize,
    copy_end: usize,
    theme: MissionControlTheme,
) -> Line<'static> {
    let rendered_text = crate::tui::transcript_cards::rendered_line_text(&visual_line.line);
    let copy_start = copy_start.min(visual_line.copy_text.len());
    let copy_end = copy_end.min(visual_line.copy_text.len());
    let rendered_start_column = visual_column_for_copy_byte(visual_line, copy_start, false);
    let rendered_end_column = visual_column_for_copy_byte(visual_line, copy_end, true);
    let rendered_start = byte_index_for_display_column(&rendered_text, rendered_start_column);
    let rendered_end = byte_index_for_display_column(&rendered_text, rendered_end_column);
    line_with_rendered_selection(&visual_line.line, rendered_start, rendered_end, theme)
}

fn display_width_for_prefix(text: &str, byte_end: usize) -> usize {
    let safe_end = grapheme_boundary_at_or_before(text, byte_end.min(text.len()));
    unicode_width::UnicodeWidthStr::width(&text[..safe_end])
}

fn visual_column_for_copy_byte(
    visual_line: &crate::tui::transcript_cards::TranscriptVisualLine,
    byte: usize,
    end: bool,
) -> usize {
    let column_offset = usize::from(visual_line.visual_copy_column_offset);
    let byte = byte.min(visual_line.copy_text.len());
    if !visual_line.visual_copy_byte_boundaries.is_empty() {
        let relative_column = if end {
            visual_line
                .visual_copy_byte_boundaries
                .iter()
                .rposition(|boundary| *boundary <= byte)
                .unwrap_or_default()
        } else {
            visual_line
                .visual_copy_byte_boundaries
                .iter()
                .position(|boundary| *boundary >= byte)
                .unwrap_or_else(|| visual_line.visual_copy_byte_boundaries.len() - 1)
        };
        return column_offset.saturating_add(relative_column);
    }
    column_offset.saturating_add(display_width_for_prefix(&visual_line.copy_text, byte))
}

fn line_with_rendered_selection(
    line: &Line<'static>,
    selection_start: usize,
    selection_end: usize,
    theme: MissionControlTheme,
) -> Line<'static> {
    if selection_start >= selection_end {
        return line.clone();
    }

    let mut spans = Vec::new();
    let mut span_start = 0usize;
    for span in &line.spans {
        let text = span.content.as_ref();
        let span_end = span_start.saturating_add(text.len());
        if selection_end <= span_start || selection_start >= span_end {
            spans.push(span.clone());
            span_start = span_end;
            continue;
        }
        let start = grapheme_boundary_at_or_before(
            text,
            selection_start.saturating_sub(span_start).min(text.len()),
        );
        let end = grapheme_boundary_at_or_before(
            text,
            selection_end.saturating_sub(span_start).min(text.len()),
        );
        if start > 0 {
            spans.push(Span::styled(text[..start].to_string(), span.style));
        }
        if start < end {
            spans.push(Span::styled(
                text[start..end].to_string(),
                theme.selection(),
            ));
        }
        if end < text.len() {
            spans.push(Span::styled(text[end..].to_string(), span.style));
        }
        span_start = span_end;
    }
    Line::from(spans)
}

fn byte_index_for_display_column(text: &str, column: usize) -> usize {
    if column == 0 {
        return 0;
    }
    let mut width = 0usize;
    for (offset, grapheme) in text.grapheme_indices(true) {
        if grapheme.chars().any(char::is_control) {
            continue;
        }
        let next_width = width.saturating_add(unicode_width::UnicodeWidthStr::width(grapheme));
        if next_width > column {
            return offset;
        }
        width = next_width;
        if width == column {
            return offset + grapheme.len();
        }
    }
    text.len()
}

fn grapheme_boundary_at_or_before(text: &str, byte_index: usize) -> usize {
    let byte_index = byte_index.min(text.len());
    if byte_index == text.len() {
        return text.len();
    }
    text.grapheme_indices(true)
        .map(|(index, _)| index)
        .take_while(|index| *index <= byte_index)
        .last()
        .unwrap_or(0)
}

/// Decorate only the visible projection; cached text and copy coordinates stay static.
pub(crate) fn decorate_status_indicators(
    state: &MissionControlState,
    indicators: &[(u16, Option<crate::output::ActivityId>)],
    line: &mut Line<'static>,
) {
    if indicators.is_empty() || state.reduced_motion || state.top_modal().is_some() {
        return;
    }
    let mut column = 0usize;
    let mut spans = Vec::new();
    for span in &line.spans {
        let mut content = String::new();
        for grapheme in span.content.graphemes(true) {
            let indicator = indicators
                .iter()
                .find(|(target, _)| usize::from(*target) == column);
            let highlight = grapheme == "✓"
                && indicator
                    .and_then(|(_, id)| id.as_ref())
                    .is_some_and(|id| state.card_completion_highlight_visible(id));
            if highlight {
                if !content.is_empty() {
                    spans.push(Span::styled(std::mem::take(&mut content), span.style));
                }
                spans.push(Span::styled(
                    grapheme.to_string(),
                    span.style.add_modifier(
                        ratatui::style::Modifier::BOLD | ratatui::style::Modifier::UNDERLINED,
                    ),
                ));
            } else if grapheme == "●"
                && indicator
                    .and_then(|(_, id)| id.as_ref())
                    .is_some_and(|id| state.card_spinner_visible(id))
            {
                content.push_str(state.activity_spinner());
            } else {
                content.push_str(grapheme);
            }
            column += unicode_width::UnicodeWidthStr::width(grapheme);
        }
        if !content.is_empty() {
            spans.push(Span::styled(content, span.style));
        }
    }
    line.spans = spans;
}