rho-coding-agent 0.16.1

A lightweight agent harness inspired by Pi
use super::{
    markdown::{markdown_preview_width, markdown_stream_prefix},
    render::{complete_visual_prefix, display_width},
};

#[derive(Debug, Default)]
pub(super) struct AppendOnlyStream {
    pending: String,
    emitted_text: String,
    leading_blank_emitted: bool,
    previous_emission_ended_at_wrap: bool,
}

#[derive(Debug, PartialEq, Eq)]
pub(super) struct StreamFragment {
    text: String,
    include_leading_blank: bool,
    skip_leading_newline: bool,
}

#[derive(Debug, PartialEq, Eq)]
pub(super) struct StreamPreview {
    text: String,
    include_leading_blank: bool,
    skip_leading_newline: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct RenderableSplit {
    byte_index: usize,
    skip_leading_newline: bool,
    ends_with_wrap: bool,
}

impl AppendOnlyStream {
    pub(super) fn reset(&mut self) {
        self.pending.clear();
        self.emitted_text.clear();
        self.leading_blank_emitted = false;
        self.previous_emission_ended_at_wrap = false;
    }

    pub(super) fn push_delta(&mut self, delta: &str) {
        self.pending.push_str(delta);
    }

    pub(super) fn pending_text(&self) -> &str {
        &self.pending
    }

    pub(super) fn drain_renderable(&mut self, inner_width: usize) -> Option<StreamFragment> {
        self.drain_renderable_with_prefix(inner_width, |_pending, byte_index| byte_index)
    }

    pub(super) fn drain_renderable_markdown(
        &mut self,
        inner_width: usize,
        in_code_block: bool,
    ) -> Option<StreamFragment> {
        let split = self.markdown_renderable_split(inner_width, in_code_block, false)?;
        Some(self.take_pending_prefix(
            split.byte_index,
            split.skip_leading_newline,
            split.ends_with_wrap,
        ))
    }

    pub(super) fn drain_preview(&mut self) -> Option<StreamPreview> {
        self.preview_with_width(display_width)
    }

    pub(super) fn drain_preview_markdown(
        &self,
        inner_width: usize,
        in_code_block: bool,
    ) -> Option<StreamPreview> {
        let split = self.markdown_renderable_split(inner_width, in_code_block, true)?;
        Some(self.pending_preview(split.byte_index, split.skip_leading_newline))
    }

    pub(super) fn finish(&mut self) -> Option<StreamFragment> {
        if self.pending.is_empty() {
            return None;
        }
        let skip_leading_newline = self.should_skip_leading_newline();
        Some(self.take_pending_prefix(self.pending.len(), skip_leading_newline, false))
    }

    pub(super) fn emitted_text(&self) -> &str {
        &self.emitted_text
    }

    pub(super) fn is_empty(&self) -> bool {
        self.pending.is_empty() && self.emitted_text.is_empty()
    }

    fn should_skip_leading_newline(&self) -> bool {
        self.previous_emission_ended_at_wrap && self.pending.starts_with('\n')
    }

    fn markdown_renderable_split(
        &self,
        inner_width: usize,
        in_code_block: bool,
        allow_partial_line: bool,
    ) -> Option<RenderableSplit> {
        let skip_leading_newline = self.should_skip_leading_newline();
        let scan_start = usize::from(skip_leading_newline);
        let pending = &self.pending[scan_start..];
        let prefix = markdown_stream_prefix(pending, inner_width, in_code_block);
        let mut renderable_byte_index = prefix.byte_index;
        let mut ends_with_wrap = prefix.ends_with_wrap;
        if allow_partial_line && renderable_byte_index == 0 {
            if let Some(byte_index) = self.preview_byte_index(pending, |text| {
                markdown_preview_width(text, inner_width, in_code_block)
            }) {
                renderable_byte_index = byte_index;
                ends_with_wrap = false;
            }
        }
        let split_at = scan_start + renderable_byte_index;
        if split_at == 0 {
            return None;
        }
        Some(RenderableSplit {
            byte_index: split_at,
            skip_leading_newline,
            ends_with_wrap,
        })
    }

    fn preview_with_width(&self, rendered_width: impl Fn(&str) -> usize) -> Option<StreamPreview> {
        let skip_leading_newline = self.should_skip_leading_newline();
        let scan_start = usize::from(skip_leading_newline);
        let pending = &self.pending[scan_start..];
        let split_at = scan_start + self.preview_byte_index(pending, rendered_width)?;
        Some(self.pending_preview(split_at, skip_leading_newline))
    }

    fn preview_byte_index(
        &self,
        pending: &str,
        rendered_width: impl Fn(&str) -> usize,
    ) -> Option<usize> {
        if pending.is_empty() || pending.starts_with('\n') {
            return None;
        }
        let line_end = pending.find('\n').unwrap_or(pending.len());
        let current_line = &pending[..line_end];
        let width = rendered_width(current_line);
        (width > 0).then_some(line_end)
    }

    fn drain_renderable_with_prefix(
        &mut self,
        inner_width: usize,
        renderable_prefix: impl FnOnce(&str, usize) -> usize,
    ) -> Option<StreamFragment> {
        let skip_leading_newline = self.should_skip_leading_newline();
        let scan_start = usize::from(skip_leading_newline);
        let pending = &self.pending[scan_start..];
        let prefix = complete_visual_prefix(pending, inner_width);
        let renderable_byte_index = renderable_prefix(pending, prefix.byte_index);
        let split_at = scan_start + renderable_byte_index;
        if split_at == 0 {
            return None;
        }
        Some(self.take_pending_prefix(
            split_at,
            skip_leading_newline,
            prefix.ends_with_wrap && renderable_byte_index == prefix.byte_index,
        ))
    }

    fn pending_preview(&self, byte_index: usize, skip_leading_newline: bool) -> StreamPreview {
        StreamPreview {
            text: self.pending[..byte_index].to_string(),
            include_leading_blank: !self.leading_blank_emitted,
            skip_leading_newline,
        }
    }

    fn take_pending_prefix(
        &mut self,
        byte_index: usize,
        skip_leading_newline: bool,
        ends_with_wrap: bool,
    ) -> StreamFragment {
        let text: String = self.pending.drain(..byte_index).collect();
        let include_leading_blank = !self.leading_blank_emitted;
        self.leading_blank_emitted = true;
        self.previous_emission_ended_at_wrap = ends_with_wrap;
        self.emitted_text.push_str(&text);
        StreamFragment {
            text,
            include_leading_blank,
            skip_leading_newline,
        }
    }
}

impl StreamFragment {
    pub(super) fn render_text(&self) -> &str {
        if self.skip_leading_newline {
            &self.text['\n'.len_utf8()..]
        } else {
            &self.text
        }
    }

    pub(super) fn into_text(self) -> String {
        self.text
    }

    pub(super) fn include_leading_blank(&self) -> bool {
        self.include_leading_blank
    }
}

impl StreamPreview {
    pub(super) fn render_text(&self) -> &str {
        if self.skip_leading_newline {
            &self.text['\n'.len_utf8()..]
        } else {
            &self.text
        }
    }

    pub(super) fn include_leading_blank(&self) -> bool {
        self.include_leading_blank
    }
}

#[cfg(test)]
#[path = "stream_tests.rs"]
mod tests;