qqq-cli 0.1.0

Local-first task queue for coding agent sessions
use super::{block, field, link, parent, status, status_color, styled};
use serde_json::Value;

fn heading(label: &str, color: bool) -> String {
    styled(&format!("{label}:"), color.then_some("1;36"))
}

fn row(label: &str, value: &str, color: bool) -> String {
    format!(
        "  {}{}  {}",
        styled(label, color.then_some("2")),
        " ".repeat(21_usize.saturating_sub(label.len())),
        styled(value, (color && value == "-").then_some("2"))
    )
}

fn message(value: &Value, color: bool) -> String {
    let body = block(value, "body")
        .lines()
        .map(|line| format!("  {line}"))
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        "  {} · {} · {}\n{body}",
        styled(&format!("#{}", field(value, "id")), color.then_some("1")),
        field(value, "session"),
        styled(&field(value, "created_at"), color.then_some("2"))
    )
}

fn image(value: &Value, color: bool) -> String {
    format!(
        "  {}: {} {}",
        styled(
            &format!("Image #{}", field(value, "id")),
            color.then_some("1")
        ),
        field(value, "name"),
        styled(
            &format!(
                "({}, {} bytes)",
                field(value, "media_type"),
                field(value, "bytes")
            ),
            color.then_some("2")
        )
    )
}

fn event(value: &Value, color: bool) -> String {
    let action = field(value, "action");
    let code = if color {
        match action.as_str() {
            "claim" => Some("36"),
            "release" => Some("33"),
            "complete" => Some("90"),
            "error" => Some("31"),
            _ => None,
        }
    } else {
        None
    };
    format!(
        "  {}  {}  {}",
        styled(&field(value, "created_at"), color.then_some("2")),
        styled(&format!("{action:<8}"), code),
        field(value, "session")
    )
}

fn collection(label: &str, rows: &[Value], color: bool) -> String {
    let title = heading(label, color);
    if rows.is_empty() {
        return format!("{title} {}", styled("None", color.then_some("2")));
    }
    let body = rows
        .iter()
        .map(|value| match label {
            "Messages" => message(value, color),
            "Images" => image(value, color),
            _ => event(value, color),
        })
        .collect::<Vec<_>>()
        .join(if label == "Messages" { "\n\n" } else { "\n" });
    format!(
        "{title} {}\n{body}",
        styled(&format!("({})", rows.len()), color.then_some("2"))
    )
}

pub(super) fn render(value: &Value, color: bool) -> String {
    let task = &value["task"];
    let mut sections = vec![format!(
        "{} · {}",
        styled(&format!("#{}", field(task, "id")), color.then_some("1")),
        styled(status(task), status_color(task, color))
    )];
    if task["description"]
        .as_str()
        .is_some_and(|text| !text.is_empty())
    {
        sections.push(format!(
            "{}\n{}",
            heading("Description", color),
            block(task, "description")
        ));
    }
    sections.push(format!(
        "{}\n{}\n{}\n{}",
        heading("Details", color),
        row("Parent:", &parent(task), color),
        row(
            "Created:",
            &styled(&field(task, "created_at"), color.then_some("2")),
            color
        ),
        row(
            "Updated:",
            &styled(&field(task, "updated_at"), color.then_some("2")),
            color
        )
    ));
    let assignment = [
        ("Harness name:", "harness_name"),
        ("Harness session:", "harness_session"),
        ("Orchestrator name:", "orchestrator_name"),
        ("Orchestrator session:", "orchestrator_session"),
    ]
    .iter()
    .map(|(label, key)| row(label, &field(task, key), color))
    .collect::<Vec<_>>()
    .join("\n");
    sections.push(format!("{}\n{assignment}", heading("Assignment", color)));
    for (key, label) in [
        ("messages", "Messages"),
        ("images", "Images"),
        ("events", "History"),
    ] {
        sections.push(collection(
            label,
            value[key].as_array().expect("detail section is an array"),
            color,
        ));
    }
    let saved_link = link(&value["herdr"], false);
    sections.push(match saved_link.split_once('\n') {
        Some((_, body)) => format!(
            "{}\n{}",
            heading("Herdr", color),
            body.lines()
                .map(|line| format!("  {line}"))
                .collect::<Vec<_>>()
                .join("\n")
        ),
        None => format!(
            "{} {}",
            heading("Herdr", color),
            styled("Not linked", color.then_some("2"))
        ),
    });
    if let Some(export) = value.get("export") {
        sections.push(format!(
            "{} image #{} to {}",
            styled("Exported", color.then_some("32")),
            field(export, "id"),
            field(export, "path")
        ));
    }
    sections.join("\n\n")
}