ai-agent 0.88.0

Idiomatic agent sdk inspired by the claude code source leak
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextHighlight {
    pub start: usize,
    pub end: usize,
    pub color: Option<String>,
    #[serde(default)]
    pub dim_color: Option<bool>,
    #[serde(default)]
    pub inverse: Option<bool>,
    #[serde(default)]
    pub shimmer_color: Option<String>,
    pub priority: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextSegment {
    pub text: String,
    pub start: usize,
    #[serde(default)]
    pub highlight: Option<TextHighlight>,
}

pub fn segment_text_by_highlights(text: &str, highlights: Vec<TextHighlight>) -> Vec<TextSegment> {
    if highlights.is_empty() {
        return vec![TextSegment {
            text: text.to_string(),
            start: 0,
            highlight: None,
        }];
    }

    let mut sorted = highlights;
    sorted.sort_by(|a, b| {
        if a.start != b.start {
            a.start.cmp(&b.start)
        } else {
            b.priority.cmp(&a.priority)
        }
    });

    let mut resolved: Vec<TextHighlight> = Vec::new();
    let mut used_ranges: Vec<(usize, usize)> = Vec::new();

    for highlight in sorted {
        if highlight.start == highlight.end {
            continue;
        }

        let overlaps = used_ranges.iter().any(|(s, e)| {
            (highlight.start >= *s && highlight.start < *e)
                || (highlight.end > *s && highlight.end <= *e)
                || (highlight.start <= *s && highlight.end >= *e)
        });

        if !overlaps {
            resolved.push(highlight);
            used_ranges.push((highlight.start, highlight.end));
        }
    }

    segment_by_resolved(text, &resolved)
}

fn segment_by_resolved(text: &str, highlights: &[TextHighlight]) -> Vec<TextSegment> {
    let mut segments = Vec::new();
    let mut pos = 0;
    let text_chars: Vec<char> = text.chars().collect();

    for highlight in highlights {
        if highlight.start > pos {
            let text_part: String = text_chars[pos..highlight.start.min(text_chars.len())]
                .iter()
                .collect();
            segments.push(TextSegment {
                text: text_part,
                start: pos,
                highlight: None,
            });
        }

        let end = highlight.end.min(text_chars.len());
        if highlight.start < end {
            let highlighted: String = text_chars[highlight.start..end].iter().collect();
            segments.push(TextSegment {
                text: highlighted,
                start: highlight.start,
                highlight: Some(highlight.clone()),
            });
        }

        pos = highlight.end;
    }

    if pos < text_chars.len() {
        let remaining: String = text_chars[pos..].iter().collect();
        segments.push(TextSegment {
            text: remaining,
            start: pos,
            highlight: None,
        });
    }

    segments
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_no_highlights() {
        let result = segment_text_by_highlights("hello", vec![]);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].text, "hello");
    }

    #[test]
    fn test_with_highlight() {
        let highlights = vec![TextHighlight {
            start: 0,
            end: 3,
            color: Some("red".to_string()),
            priority: 1,
            ..Default::default()
        }];
        let result = segment_text_by_highlights("hello", highlights);
        assert_eq!(result.len(), 2);
    }
}