rho-coding-agent 1.48.0

A lightweight agent harness inspired by Pi
use std::collections::VecDeque;

use {
    crate::app::interactive_presenter::InteractiveToolPresenter,
    rho_providers::model::{image_summary, ContentBlock, ImageContent, Message, ToolCall},
    rho_tools::tool_card::{ToolCard, ToolFact, ToolFamily, ToolHeader, ToolStatus},
};

use super::{feed_image::FeedImage, ChatMedia, Entry, ToolEntry};

pub(super) fn text_blocks(blocks: &[ContentBlock]) -> String {
    blocks
        .iter()
        .filter_map(|block| match block {
            ContentBlock::Text(text) => Some(text.as_str()),
            ContentBlock::Image(_) | ContentBlock::ToolCall(_) => None,
        })
        .collect::<Vec<_>>()
        .join("\n")
}

pub(super) fn render_message_blocks(blocks: &[ContentBlock]) -> String {
    blocks
        .iter()
        .filter_map(|block| match block {
            ContentBlock::Text(text) => Some(text.clone()),
            ContentBlock::Image(image) => Some(format!("[image: {}]", image_summary(image))),
            ContentBlock::ToolCall(_) => None,
        })
        .collect::<Vec<_>>()
        .join("\n")
}

pub(super) fn generated_image_entry(
    preview: Result<Option<FeedImage>, String>,
    source: &ImageContent,
) -> Entry {
    let mut card = ToolCard::new(
        ToolStatus::Ok,
        ToolFamily::Default,
        ToolHeader::call("image_generation", None),
    );
    let image = apply_generated_image_preview(&mut card, preview, source);
    card.push_fact(ToolFact::Meta {
        text: "finished".into(),
    });
    Entry::Tool(ToolEntry::new(card, false, image, None))
}

fn apply_generated_image_preview(
    card: &mut ToolCard,
    preview: Result<Option<FeedImage>, String>,
    source: &ImageContent,
) -> Option<FeedImage> {
    match preview {
        Ok(None) => {
            card.push_fact(ToolFact::Text {
                text: image_summary(source),
            });
            None
        }
        Ok(image) => image,
        Err(error) => {
            card.push_fact(ToolFact::Error {
                text: format!("image preview unavailable: {error}"),
            });
            None
        }
    }
}

pub(super) fn render_user_entry(prompt: &str, media: &[ChatMedia]) -> String {
    let mut parts = Vec::new();
    if !prompt.is_empty() {
        parts.push(prompt.to_string());
    }
    parts.extend(
        media
            .iter()
            .enumerate()
            .map(|(index, media)| media.composer_label(index + 1)),
    );
    parts.join("\n")
}

pub(super) fn transcript_entries_from_messages(
    messages: &[Message],
    cwd: &std::path::Path,
) -> Vec<Entry> {
    let presenter = InteractiveToolPresenter::new(cwd.to_path_buf());
    let mut entries = Vec::new();
    let mut pending_tools = VecDeque::new();
    for message in messages {
        match message {
            Message::System(_) => {}
            Message::User(blocks) => {
                let text = render_message_blocks(blocks);
                if !text.is_empty() {
                    entries.push(Entry::User(text));
                }
            }
            Message::Assistant(blocks) => {
                let text = text_blocks(blocks);
                if !text.is_empty() {
                    entries.push(Entry::Assistant(text.into()));
                }
                push_generated_image_entries(&mut entries, blocks);
                pending_tools.extend(blocks.iter().filter_map(|block| match block {
                    ContentBlock::ToolCall(call) => Some(call.clone()),
                    ContentBlock::Text(_) | ContentBlock::Image(_) => None,
                }));
            }
            Message::EnrichedAssistant(message) => {
                let blocks = &message.content;
                let text = text_blocks(blocks);
                if !text.is_empty() {
                    entries.push(Entry::Assistant(text.into()));
                }
                push_generated_image_entries(&mut entries, blocks);
                pending_tools.extend(blocks.iter().filter_map(|block| match block {
                    ContentBlock::ToolCall(call) => Some(call.clone()),
                    ContentBlock::Text(_) | ContentBlock::Image(_) => None,
                }));
            }
            Message::AbortedAssistant(message) => {
                let text = text_blocks(&message.content);
                if !text.is_empty() {
                    entries.push(Entry::Assistant(text.into()));
                }
                push_generated_image_entries(&mut entries, &message.content);
                if let Some(tool_call) = message.tool_calls.last() {
                    let presented =
                        presenter.interrupted(tool_call.name.as_deref(), &tool_call.arguments);
                    entries.push(Entry::Tool(ToolEntry::new(
                        presented.card,
                        false,
                        None,
                        None,
                    )));
                }
                entries.push(Entry::Notice("model interrupted".into()));
            }
            Message::ToolResult(result) => {
                let call = pending_tools.pop_front().unwrap_or_else(|| ToolCall {
                    id: result.id.clone(),
                    name: "tool".into(),
                    arguments: serde_json::Value::Object(Default::default()),
                });
                let presented = presenter.historical(&call, result.ok, &result.content);
                entries.push(Entry::Tool(ToolEntry::new(
                    presented.card,
                    false,
                    None,
                    None,
                )));
            }
        }
    }
    entries
}

fn push_generated_image_entries(entries: &mut Vec<Entry>, blocks: &[ContentBlock]) {
    for block in blocks {
        if let ContentBlock::Image(image) = block {
            entries.push(generated_image_entry(Ok(None), image));
        }
    }
}

impl super::App {
    pub(super) fn transcript_entries(&self, messages: &[Message]) -> Vec<Entry> {
        transcript_entries_from_messages(messages, &self.info.runtime.cwd)
    }

    pub(super) fn set_history_entries(&mut self, entries: Vec<Entry>) {
        self.history.set_entries(entries);
        self.history.images_mut().clear();
    }
}