argonauts-app 0.1.0

Argonauts Code's app controller and run host: the conversation, routing, choosing Captain or crew, and the run host each run has.
//! Status, diff, and raw views built from a run's files alone, for when no engine is running:
//! a waiting host answers with these, and so does a client that finds no run host.

use std::path::{Path, PathBuf};

use argonauts_core::api::{ApiError, DiffScope, DiffView, RawView, StatusSummary};
use argonauts_core::events::session::SessionEvent;
use argonauts_core::events::task::TaskEvent;
use argonauts_core::events::{RunEvent, RunEventKind};
use argonauts_core::journal;
use argonauts_core::layout::{run_files, session_dir, session_files};
use argonauts_core::names::SessionKey;
use argonauts_core::{DiffStat, RunId, TaskId};
use argonauts_crew::status::{self, Viewer};

/// How much of a session's raw output a view holds, as `RawView` says.
const RAW_LINES: usize = 5_000;

pub(crate) fn status(run_dir: &Path, run: &RunId) -> Result<StatusSummary, ApiError> {
    let events = events_of(run_dir)?;
    if events.is_empty() {
        return Ok(StatusSummary {
            run: Some(run.clone()),
            ..StatusSummary::default()
        });
    }
    Ok(status::summary(&events, &Viewer::User))
}

/// The task's last diff. `shown` writes a path for an error message.
pub(crate) fn diff(
    run_dir: &Path,
    task: &TaskId,
    scope: DiffScope,
    shown: impl Fn(&Path) -> String,
) -> Result<DiffView, ApiError> {
    if scope == DiffScope::LastTurn {
        return Err(ApiError::NotNow(
            "the last turn's diff needs the run's run host; /resume it first".to_owned(),
        ));
    }
    let (stat, path) = last_task_diff(&events_of(run_dir)?, task)
        .ok_or_else(|| ApiError::NotNow(format!("{task} has no diff yet")))?;
    let path = run_dir.join(path);
    let text = std::fs::read_to_string(&path)
        .map_err(|err| ApiError::RunHost(format!("reading {}: {err}", shown(&path))))?;
    Ok(DiffView {
        task: task.clone(),
        stat,
        text,
        path,
    })
}

pub(crate) fn raw(run_dir: &Path, run: &RunId, session: &SessionKey) -> Result<RawView, ApiError> {
    let dir = session_dir(run_dir, session);
    if !dir.is_dir() {
        return Err(ApiError::Refused(format!(
            "run {run} has no session {session}"
        )));
    }
    let path = dir.join(session_files::RAW_STDOUT);
    let text = match std::fs::read_to_string(&path) {
        Ok(text) => text,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
        Err(err) => return Err(unreadable(err)),
    };
    let all: Vec<&str> = text.lines().collect();
    let keep = all.len().saturating_sub(RAW_LINES);
    let cli_transcript = events_of(run_dir)?
        .iter()
        .rev()
        .filter(|event| event.session.as_ref() == Some(session))
        .find_map(|event| match &event.kind {
            RunEventKind::Session(SessionEvent::CliSession { transcript, .. }) => {
                transcript.clone()
            }
            _ => None,
        });
    Ok(RawView {
        session: session.clone(),
        lines: all[keep..].iter().map(|line| (*line).to_owned()).collect(),
        truncated: keep > 0,
        path,
        cli_transcript,
    })
}

fn events_of(run_dir: &Path) -> Result<Vec<RunEvent>, ApiError> {
    let path = run_dir.join(run_files::EVENTS);
    if !path.exists() {
        return Ok(Vec::new());
    }
    journal::read_all(&path)
        .map(|back| back.events)
        .map_err(unreadable)
}

fn last_task_diff(events: &[RunEvent], task: &TaskId) -> Option<(DiffStat, PathBuf)> {
    events
        .iter()
        .rev()
        .filter(|event| event.task_id.as_ref() == Some(task))
        .find_map(|event| match &event.kind {
            RunEventKind::Task(TaskEvent::Diff { stat, path }) => Some((*stat, path.clone())),
            _ => None,
        })
}

fn unreadable(err: impl std::fmt::Display) -> ApiError {
    ApiError::RunHost(format!("{err:#}"))
}