rho-coding-agent 1.7.0

A lightweight agent harness inspired by Pi
Documentation
use std::time::{Duration, Instant};

use ratatui::text::{Line, Span};

use super::{render::display_width, theme::Theme};

const WORKING_LABEL: &str = "⠋ working";

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ActivityStatus {
    Working,
    Subagents(usize),
    WorkingWithSubagents(usize),
}

pub(super) fn activity_width(available: usize, status: ActivityStatus) -> usize {
    activity_labels(status)
        .into_iter()
        .find(|label| display_width(label) <= available)
        .map_or_else(
            || available.min(display_width("")),
            |label| display_width(&label),
        )
}

fn activity_labels(status: ActivityStatus) -> Vec<String> {
    let subagent_count = match status {
        ActivityStatus::Working => 0,
        ActivityStatus::Subagents(count) | ActivityStatus::WorkingWithSubagents(count) => count,
    };
    let agents = if subagent_count == 1 {
        "1 agent".into()
    } else {
        format!("{subagent_count} agents")
    };
    match status {
        ActivityStatus::Working => vec![WORKING_LABEL.into(), "".into()],
        ActivityStatus::WorkingWithSubagents(_) => vec![
            format!("{WORKING_LABEL}  ·  {agents}"),
            format!("⠋ working · {subagent_count}"),
            format!("{subagent_count}"),
            "".into(),
        ],
        ActivityStatus::Subagents(_) => vec![
            format!("{agents} working"),
            format!("{subagent_count} agents"),
            format!("{subagent_count}"),
            "".into(),
        ],
    }
}

#[derive(Clone, Debug, Default)]
pub(super) struct LoadingSpinner {
    started_at: Option<Instant>,
}

impl LoadingSpinner {
    const FRAMES: [&'static str; 10] = ["", "", "", "", "", "", "", "", "", ""];
    pub(super) const FRAME_INTERVAL: Duration = Duration::from_millis(80);

    pub(super) fn start(&mut self) {
        self.started_at = Some(Instant::now());
    }

    pub(super) fn start_if_needed(&mut self) {
        if self.started_at.is_none() {
            self.start();
        }
    }

    pub(super) fn stop(&mut self) {
        self.started_at = None;
    }

    fn frame_at(&self, now: Instant) -> &'static str {
        let Some(started_at) = self.started_at else {
            return Self::FRAMES[0];
        };
        let interval_ms = Self::FRAME_INTERVAL.as_millis().max(1);
        let frame = now
            .saturating_duration_since(started_at)
            .as_millis()
            .checked_div(interval_ms)
            .unwrap_or(0) as usize;
        Self::FRAMES[frame % Self::FRAMES.len()]
    }

    pub(super) fn line(
        &self,
        now: Instant,
        available: usize,
        status: ActivityStatus,
    ) -> Line<'static> {
        let label = activity_labels(status)
            .into_iter()
            .find(|label| display_width(label) <= available)
            .unwrap_or_else(|| "".chars().take(available).collect());
        let Some(rest) = label.strip_prefix('') else {
            return Line::default();
        };
        Line::from(vec![
            Span::styled(self.frame_at(now), Theme::accent()),
            Span::styled(rest.to_string(), Theme::dim()),
        ])
    }
}

pub(super) fn jump_to_bottom_text(width: usize, binding: &str, alongside_activity: bool) -> String {
    let full = format!("↓ jump to bottom  {binding}");
    let compact = format!("↓ bottom {binding}");
    let shortcut = format!("{binding}");
    let activity_width = usize::from(alongside_activity) * (display_width(WORKING_LABEL) + 1);
    let available = width.saturating_sub(activity_width);

    if display_width(&full) <= available {
        full
    } else if display_width(&compact) <= available {
        compact
    } else if display_width(&shortcut) <= width {
        shortcut
    } else {
        super::render::truncate_one_line(&shortcut, width)
    }
}

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