rho-coding-agent 0.28.0

A lightweight agent harness inspired by Pi
Documentation
use std::{fs, path::Path, process::Command};

use crate::{
    credentials::{provider_has_credentials, provider_has_env_override, CredentialStore},
    model::catalog,
    provider::{self, ProviderModelSource},
};

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

pub(super) struct DoctorContext<'a> {
    pub(super) provider: &'a str,
    pub(super) model: &'a str,
    pub(super) auth: &'a str,
    pub(super) available_auths: &'a [String],
    pub(super) credential_store: &'a dyn CredentialStore,
    pub(super) config_path: &'a Path,
    pub(super) session_root: &'a Path,
    pub(super) herdr_enabled: bool,
    pub(super) herdr_socket_reachable: Option<bool>,
}

pub(super) fn picker(context: DoctorContext<'_>) -> UiPicker {
    let mut items = Vec::new();
    for descriptor in provider::providers() {
        let (healthy, status, detail) = match provider_has_credentials(
            context.credential_store,
            descriptor.name,
        ) {
            Ok(true) if provider_has_env_override(descriptor.name) => (
                true,
                "authenticated",
                "Credentials are provided by an environment variable.",
            ),
            Ok(true) => (
                true,
                "authenticated",
                "Credentials are available in the OS credential store.",
            ),
            Ok(false) => (
                false,
                "missing",
                "No credentials found. Run /login to authenticate this provider.",
            ),
            Err(_) => (
                false,
                "error",
                "The OS credential store could not be read. No secret values were inspected or displayed.",
            ),
        };
        items.push(item(
            format!("{} authentication", descriptor.display_name),
            status,
            healthy,
            detail.into(),
        ));
    }

    let model_available = catalog::resolve_model_selection_for_auths(
        &format!("{}/{}", context.provider, context.model),
        context.provider,
        context.auth,
        context.available_auths,
    )
    .is_ok();
    items.push(item(
        "Selected model",
        if model_available {
            "available"
        } else {
            "unavailable"
        },
        model_available,
        format!(
            "{}/{} using {} authentication",
            context.provider, context.model, context.auth
        ),
    ));

    let config_writable = probe_writable(context.config_path, PathKind::File);
    items.push(item(
        "Configuration",
        writable_status(config_writable),
        config_writable,
        context.config_path.display().to_string(),
    ));
    let sessions_writable = probe_writable(context.session_root, PathKind::Directory);
    items.push(item(
        "Sessions",
        writable_status(sessions_writable),
        sessions_writable,
        context.session_root.display().to_string(),
    ));

    for descriptor in provider::providers() {
        if descriptor.model_source == ProviderModelSource::CachedProviderModels {
            let count =
                crate::model::provider_models::cached_provider_models(descriptor.name).len();
            items.push(item(
                format!("{} model cache", descriptor.display_name),
                if count > 0 { "populated" } else { "empty" },
                count > 0,
                format!("{count} cached model{}", if count == 1 { "" } else { "s" }),
            ));
        }
    }

    let helpers = clipboard_helpers();
    items.push(item(
        "Clipboard image helper",
        if helpers.is_empty() {
            "not found"
        } else {
            "available"
        },
        !helpers.is_empty(),
        if helpers.is_empty() {
            "Install a supported platform clipboard helper to paste images.".into()
        } else {
            format!("Detected: {}", helpers.join(", "))
        },
    ));
    let rtk = crate::tools::rtk::is_available();
    items.push(item(
        "rtk",
        if rtk { "available" } else { "unavailable" },
        rtk,
        "Optional shell-command rewriting helper.".into(),
    ));
    let (herdr_healthy, herdr_status, herdr_detail) =
        match (context.herdr_enabled, context.herdr_socket_reachable) {
            (false, _) => (true, "not configured", "Rho is not running inside Herdr."),
            (true, Some(true)) => (
                true,
                "connected",
                "The configured Herdr socket accepted a connection.",
            ),
            (true, Some(false)) => (
                false,
                "unreachable",
                "Herdr environment variables are set, but the socket did not accept a connection.",
            ),
            (true, None) => (
                false,
                "unavailable",
                "Herdr is configured, but socket reachability could not be determined.",
            ),
        };
    items.push(item(
        "Herdr",
        herdr_status,
        herdr_healthy,
        herdr_detail.into(),
    ));

    UiPicker::new(
        "Doctor diagnostics",
        "up/down inspect, type to filter, enter or esc close",
        items,
        PickerAction::Doctor,
    )
}

fn item(
    label: impl Into<String>,
    status: impl Into<String>,
    healthy: bool,
    detail: String,
) -> PickerItem {
    let label = label.into();
    PickerItem {
        value: label.clone(),
        label,
        detail: Some(detail),
        preview: None,
        badge: Some(PickerBadge {
            text: status.into(),
            tone: if healthy {
                PickerBadgeTone::Healthy
            } else {
                PickerBadgeTone::Warning
            },
        }),
    }
}

fn writable_status(writable: bool) -> &'static str {
    if writable {
        "writable"
    } else {
        "not writable"
    }
}

#[derive(Clone, Copy)]
enum PathKind {
    File,
    Directory,
}

fn probe_writable(path: &Path, kind: PathKind) -> bool {
    if path.exists() {
        return match kind {
            PathKind::File if path.is_file() => {
                fs::OpenOptions::new().write(true).open(path).is_ok()
            }
            PathKind::Directory if path.is_dir() => probe_directory(path),
            PathKind::File | PathKind::Directory => false,
        };
    }
    let directory = match kind {
        PathKind::File => path.parent().unwrap_or(path),
        PathKind::Directory => path,
    };
    if fs::create_dir_all(directory).is_err() {
        return false;
    }
    probe_directory(directory)
}

fn probe_directory(directory: &Path) -> bool {
    let probe = directory.join(format!(".rho-doctor-{}", uuid::Uuid::new_v4()));
    let result = fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&probe)
        .is_ok();
    let _ = fs::remove_file(probe);
    result
}

fn clipboard_helpers() -> Vec<&'static str> {
    let candidates: &[&str] = if cfg!(target_os = "linux") {
        &["wl-paste", "xclip"]
    } else if cfg!(target_os = "macos") {
        &["pngpaste"]
    } else if cfg!(target_os = "windows") {
        &["powershell"]
    } else {
        &[]
    };
    candidates
        .iter()
        .copied()
        .filter(|command| command_available(command))
        .collect()
}

fn command_available(command: &str) -> bool {
    Command::new(command).arg("--help").output().is_ok()
}

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

    #[test]
    fn directory_probe_rejects_regular_file() {
        let directory = tempfile::tempdir().unwrap();
        let path = directory.path().join("sessions");
        fs::write(&path, "not a directory").unwrap();

        assert!(!probe_writable(&path, PathKind::Directory));
    }
}