rho-coding-agent 0.8.0

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

#[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,
}

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 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 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 split_at = scan_start + prefix.byte_index;
        if split_at == 0 {
            return None;
        }
        Some(self.take_pending_prefix(split_at, skip_leading_newline, prefix.ends_with_wrap))
    }

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

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