rho-coding-agent 0.8.0

A lightweight agent harness inspired by Pi
use super::{PickerAction, PickerItem, UiPicker};
use crate::session::SessionSummary;

pub(super) fn session_picker(
    sessions: Vec<SessionSummary>,
    current_session_id: Option<&str>,
) -> UiPicker {
    UiPicker::new(
        "resume session",
        "type regex filter, tab complete, up/down select, enter confirm, esc cancel",
        sessions
            .into_iter()
            .filter(|session| current_session_id != Some(session.id.as_str()))
            .map(session_item)
            .collect(),
        PickerAction::ResumeSession,
    )
}

fn session_item(session: SessionSummary) -> PickerItem {
    let short_id = short_session_id(&session.id);
    let first_user_preview = session.first_user_message.as_deref().map(preview_text);
    let title = session
        .title
        .as_deref()
        .map(preview_text)
        .or_else(|| first_user_preview.clone())
        .unwrap_or_else(|| format!("session {short_id}"));
    let preview = session
        .title
        .as_ref()
        .and(first_user_preview)
        .filter(|preview| preview != &title);
    let last_user = session.last_user_message.as_deref().map(preview_text);
    let detail = match last_user {
        Some(last_user) if last_user != title => {
            format!(
                "updated {} · last: {last_user} · id {short_id}",
                session.updated_at
            )
        }
        Some(_) | None => format!("updated {} · id {short_id}", session.updated_at),
    };
    PickerItem {
        label: title,
        detail: Some(detail),
        preview,
        badge: None,
        value: session.id,
    }
}

fn short_session_id(id: &str) -> String {
    id.chars().take(8).collect()
}

fn preview_text(text: &str) -> String {
    let text = text.replace('\n', " ");
    if text.chars().count() <= 80 {
        return text;
    }
    let mut preview = text.chars().take(79).collect::<String>();
    preview.push('');
    preview
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn picker_uses_first_user_message_when_title_is_missing() {
        let id = "bbbbbbbb-bbbb-4ccc-8ddd-eeeeeeeeeeee".to_string();
        let picker = session_picker(
            vec![SessionSummary {
                id,
                path: PathBuf::from("session.jsonl"),
                cwd: PathBuf::from("/tmp/project"),
                created_at: 10,
                updated_at: 20,
                message_count: 3,
                title: None,
                first_user_message: Some("implement interactive resume".into()),
                last_user_message: Some("latest follow up".into()),
            }],
            None,
        );

        let item = picker.selected_item().unwrap();
        assert_eq!(item.label, "implement interactive resume");
        assert!(!item.label.contains("3 msgs"));
        assert!(item.detail.as_ref().unwrap().contains("latest follow up"));
    }

    #[test]
    fn picker_excludes_current_session() {
        let id = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee".to_string();
        let picker = session_picker(
            vec![SessionSummary {
                id: id.clone(),
                path: PathBuf::from("session.jsonl"),
                cwd: PathBuf::from("/tmp/project"),
                created_at: 10,
                updated_at: 20,
                message_count: 3,
                title: Some("planned feature work".into()),
                first_user_message: Some("hello from the previous session".into()),
                last_user_message: Some("hello from the previous session".into()),
            }],
            Some(&id),
        );

        assert!(picker.items.is_empty());
    }

    #[test]
    fn picker_shows_title_and_first_message_preview() {
        let id = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee".to_string();
        let picker = session_picker(
            vec![SessionSummary {
                id: id.clone(),
                path: PathBuf::from("session.jsonl"),
                cwd: PathBuf::from("/tmp/project"),
                created_at: 10,
                updated_at: 20,
                message_count: 3,
                title: Some("planned feature work".into()),
                first_user_message: Some("hello from the previous session".into()),
                last_user_message: Some("hello from the previous session".into()),
            }],
            None,
        );

        let item = picker.selected_item().unwrap();
        assert_eq!(item.value, id);
        assert_eq!(item.label, "planned feature work");
        assert_eq!(
            item.preview.as_deref(),
            Some("hello from the previous session")
        );
        assert!(item.detail.as_ref().unwrap().contains("id aaaaaaaa"));
    }
}