use super::{
spawn,
stream::{TerminalClassification, TerminalResult},
};
pub(crate) enum TerminalOutcome {
Success(TerminalResult),
Failure {
terminal: Option<TerminalResult>,
detail: String,
prefer_detail: bool,
},
}
pub(crate) fn assess_terminal(
pending: Option<TerminalResult>,
exit_status: std::process::ExitStatus,
stderr: &str,
) -> TerminalOutcome {
if !exit_status.success() {
let detail = non_zero_exit_detail(pending.as_ref(), exit_status, stderr);
return TerminalOutcome::Failure {
terminal: pending,
detail,
prefer_detail: true,
};
}
match pending {
Some(
terminal @ TerminalResult {
classification: TerminalClassification::Success { .. },
..
},
) => TerminalOutcome::Success(terminal),
Some(
terminal @ TerminalResult {
classification:
TerminalClassification::Failure { .. } | TerminalClassification::Invalid { .. },
..
},
) => {
let detail = terminal
.error
.clone()
.or_else(|| terminal.result_text.clone())
.unwrap_or_else(|| "claude code: terminal result was not success".into());
TerminalOutcome::Failure {
terminal: Some(terminal),
detail,
prefer_detail: false,
}
}
None => TerminalOutcome::Failure {
terminal: None,
detail: "claude code: stream ended without a terminal result message".into(),
prefer_detail: true,
},
}
}
fn non_zero_exit_detail(
pending: Option<&TerminalResult>,
exit_status: std::process::ExitStatus,
stderr: &str,
) -> String {
if spawn::looks_like_max_turns_unsupported(stderr) {
return "claude code: this claude binary rejected --max-turns; upgrade Claude Code or remove the turn cap".into();
}
let process_detail = if stderr.is_empty() {
format!("claude code: process exited with {exit_status}")
} else {
format!("claude code: process exited with {exit_status}: {stderr}")
};
let Some(stream_error) = stream_failure_text(pending) else {
return process_detail;
};
if stderr.is_empty() {
stream_error
} else {
format!("{process_detail}\n{stream_error}")
}
}
fn stream_failure_text(pending: Option<&TerminalResult>) -> Option<String> {
let terminal = pending?;
match &terminal.classification {
TerminalClassification::Success { .. } => None,
TerminalClassification::Failure { .. } | TerminalClassification::Invalid { .. } => terminal
.error
.as_deref()
.map(str::trim)
.filter(|text| !text.is_empty())
.map(str::to_owned)
.or_else(|| {
terminal
.result_text
.as_deref()
.map(str::trim)
.filter(|text| !text.is_empty())
.map(str::to_owned)
}),
}
}
#[cfg(test)]
#[path = "terminal_tests.rs"]
mod tests;