rho-coding-agent 1.25.1

A lightweight agent harness inspired by Pi
Documentation
use pretty_assertions::assert_eq;
use ratatui::layout::Rect;

use super::super::{
    PickerAction, PickerBadge, PickerBadgePlacement, PickerBadgeTone, PickerItem, PickerLayout,
    UiPicker,
};
use super::*;

fn sample_picker(detail_a: &str, detail_b: &str) -> UiPicker {
    UiPicker::new(
        "loaded agents",
        "help",
        vec![
            PickerItem {
                section: None,
                label: "explorer".into(),
                detail: Some(detail_a.into()),
                preview: None,
                badge: Some(PickerBadge {
                    text: "internal".into(),
                    tone: PickerBadgeTone::Internal,
                }),
                value: "explorer".into(),
                selection_verb: None,
            },
            PickerItem {
                section: None,
                label: "worker".into(),
                detail: Some(detail_b.into()),
                preview: None,
                badge: None,
                value: "worker".into(),
                selection_verb: None,
            },
        ],
        PickerAction::ViewAgent,
    )
    .with_layout(PickerLayout::Overlay)
    .with_overlay_chrome(OverlayChrome {
        nav_label: " AGENTS".into(),
        detail_label: Some(" DETAILS".into()),
        nav_keys_hint: "↑↓ agents".into(),
    })
}

fn long_detail() -> String {
    (0..40)
        .map(|index| format!("detail line {index:02}"))
        .collect::<Vec<_>>()
        .join("\n")
}

fn nav_and_detail_panes(layout: &OverlayLayout) -> OverlayPanes {
    match layout.panes {
        panes @ OverlayPanes::NavAndDetail { .. } => panes,
        OverlayPanes::NavOnly { .. } => panic!("expected nav+detail panes, got nav-only"),
    }
}

#[test]
fn section_headers_follow_filtered_items_without_becoming_selectable() {
    let mut picker = sample_picker("agent detail", "worker detail");
    picker.items[0].section = Some("INTERNAL".into());
    picker.items[1].section = Some("CUSTOM".into());
    picker.filter = "custom".into();
    picker.select_first_match();

    assert_eq!(picker.matching_indices(), vec![1]);
    assert_eq!(picker.selected_item().unwrap().label, "worker");
    assert_eq!(
        picker.selected_item().unwrap().section.as_deref(),
        Some("CUSTOM")
    );
}

#[test]
fn detail_badge_rows_never_exceed_narrow_overlay_widths() {
    let picker = sample_picker("agent detail", "worker detail")
        .with_badge_placement(PickerBadgePlacement::Detail);
    let mut long_badge = picker;
    long_badge.items[0].badge = Some(PickerBadge {
        text: "healthy-and-also-very-long-status-label".into(),
        tone: PickerBadgeTone::Healthy,
    });

    for width in [8_u16, 12, 18, 24, 36, 48] {
        let frame = render_picker_overlay(&long_badge, Rect::new(0, 0, width, 20));
        for line in &frame.lines {
            let text_width = super::super::display_width(
                &line
                    .spans
                    .iter()
                    .map(|span| span.content.as_ref())
                    .collect::<String>(),
            );
            assert!(
                text_width <= width as usize,
                "width {width}: overflow text_width {text_width}"
            );
        }
    }
}

#[test]
fn tiny_stacked_layout_keeps_viewports_within_the_body() {
    let layout = picker_overlay_layout(Rect::new(0, 0, 20, 1), /*has_details*/ true);
    let OverlayPanes::NavAndDetail {
        orientation,
        detail_viewport_rows,
        nav_viewport_rows,
        ..
    } = nav_and_detail_panes(&layout)
    else {
        unreachable!()
    };

    assert_eq!(orientation, OverlayOrientation::Stacked);
    let separator_rows = usize::from(layout.body_rows > 2);
    assert!(detail_viewport_rows + nav_viewport_rows + separator_rows <= layout.body_rows);
    assert_eq!(nav_viewport_rows, 1);
}

#[test]
fn clamp_detail_scroll_respects_viewport() {
    assert_eq!(clamp_detail_scroll(100, 12, 5), 7);
    assert_eq!(clamp_detail_scroll(0, 3, 5), 0);
    assert_eq!(clamp_detail_scroll(2, 10, 10), 0);
}

#[test]
fn overlay_detail_end_scroll_uses_max_without_sentinel() {
    let area = Rect::new(0, 0, 80, 16);
    let layout = picker_overlay_layout(area, /*has_details*/ true);
    let mut picker = sample_picker(&long_detail(), "other");
    let viewport = layout.detail_viewport().expect("detail viewport");
    picker.scroll_detail_end(viewport);
    let line_count = overlay_detail_lines(picker.selected_detail(), viewport.width).len();
    let expected = line_count.saturating_sub(viewport.rows.max(1));
    assert_eq!(picker.detail_scroll, expected);
}