use std::path::PathBuf;
use std::process::{ExitStatus, Output};
use anyhow::{Result, anyhow};
use super::{muse, opencode, said};
use crate::interrupt;
mod stream;
pub(super) use stream::Stream;
pub struct Interpretation {
cli: &'static str,
decoder: Box<dyn Decoder>,
retained: Retained,
}
pub struct Completion {
pub report: Option<Report>,
pub outcome: Result<Ended>,
}
pub struct Report {
pub warnings: Vec<String>,
pub summary: Option<String>,
}
#[derive(Debug)]
pub struct Ended {
pub session_id: Option<String>,
pub killed: Vec<String>,
pub final_message: Option<String>,
}
impl Ended {
pub fn killed_work(&self) -> Vec<&str> {
self.killed.iter().map(String::as_str).collect()
}
}
pub(super) enum TurnOutcome {
NotFailed,
Failed,
}
impl TurnOutcome {
pub(super) fn from_failed(failed: bool) -> Self {
if failed {
Self::Failed
} else {
Self::NotFailed
}
}
}
pub(super) struct Facts {
pub report: Report,
pub ended: Ended,
pub outcome: TurnOutcome,
pub diagnostic: Option<String>,
}
pub(super) trait Decoder: Send {
fn condense(&mut self, raw: &str) -> Vec<String>;
fn complete(self: Box<Self>) -> Facts;
}
pub(super) enum Retained {
None,
Muse(Option<PathBuf>),
OpenCode(PathBuf),
}
impl Retained {
fn reconcile(self, facts: &mut Facts) -> Result<()> {
match self {
Self::None => {}
Self::Muse(root) => {
if let Some(log) = root
.as_deref()
.zip(facts.ended.session_id.as_deref())
.and_then(|(root, id)| muse::log::read(root, id))
{
facts.report.summary = log.summary();
if log.message.is_some() {
facts.ended.final_message = log.message;
}
}
}
Self::OpenCode(worktree) => {
if let Some(id) = facts.ended.session_id.as_deref()
&& let Some(export) = opencode::export::read(&worktree, id)?
{
facts.ended.final_message = export.message;
facts.report.summary = export.summary;
if export.failure.is_some() {
facts.outcome = TurnOutcome::Failed;
facts.diagnostic = facts.diagnostic.take().or(export.failure);
}
}
}
}
Ok(())
}
}
enum Failure {
Execution(anyhow::Error),
Rejected {
status: ExitStatus,
diagnostic: Option<String>,
},
}
impl Failure {
fn session_error(self, cli: &str) -> anyhow::Error {
match self {
Self::Execution(error) => error,
Self::Rejected { status, diagnostic } => {
let ended = if status.success() {
"'s turn failed".to_string()
} else {
format!(
" exited {}",
status
.code()
.map_or("by signal".to_string(), |code| code.to_string())
)
};
match diagnostic {
Some(error) => anyhow!("{cli}{ended}: {error}"),
None => anyhow!("{cli}{ended}"),
}
}
}
}
}
impl Interpretation {
pub(super) fn new(cli: &'static str, decoder: Box<dyn Decoder>, retained: Retained) -> Self {
Self {
cli,
decoder,
retained,
}
}
pub fn condense(&mut self, raw: &str) -> Vec<String> {
self.decoder.condense(raw)
}
pub fn finish(self, execution: Result<ExitStatus>) -> Completion {
let cli = self.cli;
let (report, outcome) = self.complete(execution);
Completion {
report,
outcome: outcome.map_err(|failure| failure.session_error(cli)),
}
}
pub(super) fn check_output(mut self, output: &Output, refusal: &str) -> Result<()> {
for line in String::from_utf8_lossy(&output.stdout).lines() {
self.condense(line);
}
let (_, outcome) = self.complete(Ok(output.status));
outcome.map(drop).map_err(|failure| match failure {
Failure::Execution(error) => error,
Failure::Rejected { diagnostic, .. } => {
anyhow!("{refusal}: {}", diagnostic.unwrap_or_else(|| said(output)))
}
})
}
fn complete(self, execution: Result<ExitStatus>) -> (Option<Report>, Result<Ended, Failure>) {
if let Err(error) = interrupt::check() {
return (None, Err(Failure::Execution(error)));
}
let mut facts = self.decoder.complete();
let recovered = self.retained.reconcile(&mut facts);
if let Err(error) = interrupt::check() {
return (None, Err(Failure::Execution(error)));
}
let outcome = execution
.and_then(|status| recovered.map(|()| status))
.map_err(Failure::Execution)
.and_then(|status| {
if !status.success() || matches!(facts.outcome, TurnOutcome::Failed) {
Err(Failure::Rejected {
status,
diagnostic: facts.diagnostic,
})
} else {
Ok(facts.ended)
}
});
(Some(facts.report), outcome)
}
}