use super::*;
pub(super) fn bash_card(input: &ToolCardInput<'_>) -> CardDraft {
let detail = input.detail;
let mut status = input_status(input);
if status == CardStatus::Failed && tool_was_canceled(detail) {
status = CardStatus::Canceled;
}
let timed_out = detail
.and_then(|detail| detail.metadata.get("timed_out"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let exit_code = detail
.and_then(|detail| detail.metadata.get("exit_code"))
.and_then(serde_json::Value::as_i64);
let command = input
.bash_command
.map(sanitize_display_text)
.unwrap_or_default();
let mut title = bash_title(status, timed_out, exit_code);
if detail.is_some_and(|detail| {
detail
.params
.get("background")
.and_then(serde_json::Value::as_bool)
== Some(true)
|| detail
.metadata
.get("job_id")
.is_some_and(serde_json::Value::is_string)
}) {
title.push_str(" • background");
}
let intent = detail
.and_then(|detail| detail.params.get(crate::tool_contract::arg_key::INTENT))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.map(sanitized_single_line)
.filter(|intent| !intent.is_empty());
let mut body_rows = intent.iter().cloned().collect::<Vec<_>>();
if !command.is_empty() {
body_rows.push(command);
}
append_job_status(&mut body_rows, detail);
let mut display_lines = Vec::new();
let notices = super::super::bash_notices::bash_execution_notices(detail);
body_rows.extend(notices.iter().map(|notice| {
format!(
"{} • {}",
notice.label,
notice.value.as_deref().unwrap_or("not reported")
)
}));
let status_row = match status {
CardStatus::Canceled => Some("canceled"),
CardStatus::Failed if notices.iter().all(|notice| notice.value.is_none()) => {
Some("command failed")
}
_ => None,
};
if let Some(status_row) = status_row {
display_lines.push(TextLine::plain(status_row));
body_rows.push(status_row.to_owned());
}
CardDraft {
role: ToolCardRole::Bash,
status,
title,
metadata: bash_jev_metadata(detail),
body: body_rows.join("\n"),
body_lines: display_lines,
activity_rows: Vec::new(),
}
}
fn bash_title(status: CardStatus, timed_out: bool, exit_code: Option<i64>) -> String {
if timed_out {
return "Bash • timed out".to_string();
}
if matches!(status, CardStatus::Success | CardStatus::Failed)
&& let Some(exit_code) = exit_code
{
format!("Bash • exit {exit_code}")
} else if status == CardStatus::Canceled {
"Bash • canceled".to_string()
} else {
"Bash".to_string()
}
}
fn append_job_status(rows: &mut Vec<String>, detail: Option<&ToolActivityDetail>) {
let Some(detail) = detail else {
return;
};
for key in [
"job_id",
"status",
"exit_code",
"stdout_bytes_dropped",
"stderr_bytes_dropped",
"cleanup_warning",
] {
if let Some(value) = detail.metadata.get(key).filter(|value| !value.is_null()) {
rows.push(sanitized_single_line(&format!("{key}: {value}")));
}
}
}
pub(super) fn job_card(input: &ToolCardInput<'_>) -> CardDraft {
let mut rows = Vec::new();
if let Some(action) = input
.detail
.and_then(|detail| detail.params.get("action"))
.and_then(serde_json::Value::as_str)
{
rows.push(sanitized_single_line(action));
}
append_job_status(&mut rows, input.detail);
let mut card = basic_card(ToolCardRole::Bash, "Bash job", &rows.join("\n"));
card.status = input_status(input);
card
}