rho-coding-agent 1.16.0

A lightweight agent harness inspired by Pi
Documentation
use super::*;
use crate::tui::Entry;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::tui) enum TrailingBlank {
    Include,
    Omit,
}

impl TrailingBlank {
    fn is_included(self) -> bool {
        match self {
            Self::Include => true,
            Self::Omit => false,
        }
    }
}

pub(in crate::tui) fn entry_lines(
    entry: &Entry,
    width: usize,
    max_tool_output_lines: usize,
) -> Vec<Line<'static>> {
    render_entry(entry, width, max_tool_output_lines).lines
}

pub(in crate::tui) fn render_entry(
    entry: &Entry,
    width: usize,
    max_tool_output_lines: usize,
) -> RenderedEntry {
    render_entry_with_options(entry, width, max_tool_output_lines, TrailingBlank::Include)
}

/// Render an entry, optionally omitting the trailing separator blank.
///
/// Open stream tails omit the trailing blank so a live continuation can abut the
/// committed content without a mid-message gap.
pub(in crate::tui) fn render_entry_with_options(
    entry: &Entry,
    width: usize,
    max_tool_output_lines: usize,
    trailing_blank: TrailingBlank,
) -> RenderedEntry {
    let inner_width = padded_inner_width(width);
    let (mut lines, code_blocks, image_sources, image_rows) = match entry {
        Entry::Assistant(text) => {
            let rendered = render_assistant_content(text, width);
            (
                rendered.lines,
                rendered.code_blocks,
                rendered.image_sources,
                rendered.image_rows,
            )
        }
        Entry::Reasoning(reasoning) => {
            let mut lines = Vec::new();
            let mut code_blocks = Vec::new();
            let mut image_sources = Vec::new();
            let mut image_rows = Vec::new();
            if !reasoning.text.is_empty() {
                let rendered = render_reasoning_content(&reasoning.text, width);
                lines = rendered.lines;
                code_blocks = rendered.code_blocks;
                image_sources = rendered.image_sources;
                image_rows = rendered.image_rows;
            }
            if let Some(elapsed) = reasoning.thought_for {
                push_wrapped_text(
                    &mut lines,
                    &crate::tui::reasoning_phase::thought_summary(elapsed),
                    inner_width,
                    Theme::dim().add_modifier(Modifier::DIM),
                    LineFill::Natural,
                );
            }
            (lines, code_blocks, image_sources, image_rows)
        }
        _ => {
            let mut lines = Vec::new();
            render_non_assistant_entry(&mut lines, entry, inner_width, max_tool_output_lines);
            (lines, Vec::new(), Vec::new(), Vec::new())
        }
    };

    let image_placement = reserve_entry_image_rows(&mut lines, entry, width);
    let block_style = lines
        .first()
        .and_then(|line| line.spans.first())
        .map(|span| span.style)
        .unwrap_or_default();
    let mut padded =
        Vec::with_capacity(lines.len() + 1 + usize::from(trailing_blank.is_included()));
    padded.push(styled_blank_line(width, block_style));
    padded.extend(lines.into_iter().map(pad_line));
    if trailing_blank.is_included() {
        padded.push(styled_blank_line(width, block_style));
    }
    RenderedEntry {
        lines: padded,
        code_blocks,
        image_placement,
        image_sources,
        image_rows,
    }
}

pub(in crate::tui) fn apply_markdown_images(
    rendered: &mut RenderedEntry,
    images: &[(usize, FeedImage)],
    width: usize,
) {
    if images.is_empty() {
        return;
    }

    for block in &mut rendered.code_blocks {
        let original_top_line = block.top_line;
        let preceding_image_rows = images
            .iter()
            .filter_map(|(source_index, image)| {
                rendered
                    .image_rows
                    .get(*source_index)
                    .filter(|&&row| row < original_top_line)
                    .map(|_| image.height_for_width(width).saturating_sub(1))
            })
            .sum::<usize>();
        block.top_line = block.top_line.saturating_add(preceding_image_rows);
    }

    let padded_image_rows = rendered
        .image_rows
        .iter()
        .map(|row| row.saturating_add(1))
        .collect::<Vec<_>>();
    if let Some(placements) =
        reserve_markdown_image_rows(&mut rendered.lines, &padded_image_rows, images, width)
    {
        rendered.image_placement = Some(placements);
    }
}

#[cfg(test)]
pub(in crate::tui) fn render_entry_with_images(
    entry: &Entry,
    width: usize,
    max_tool_output_lines: usize,
    markdown_images: Option<&[(usize, FeedImage)]>,
) -> RenderedEntry {
    let mut rendered = render_entry(entry, width, max_tool_output_lines);
    if let Some(images) = markdown_images {
        apply_markdown_images(&mut rendered, images, width);
    }
    rendered
}