use super::*;
use super::super::protection::public_decision;
pub(super) fn humanize_protection_metadata(detail: Option<&ToolActivityDetail>) -> Vec<String> {
public_decision(detail, "humanize_protection")
.map(|(_, decision)| vec![format!("Jev: {decision}")])
.unwrap_or_default()
}
pub(super) fn prompt_injection_protection_metadata(
detail: Option<&ToolActivityDetail>,
) -> Option<String> {
let protection = &detail?.metadata["prompt_injection_protection"];
let action = match protection["action"].as_str()? {
"allow" => "allow",
"annotate" => "annotate",
"quarantine" => "quarantine",
"escalate" => "escalate",
_ => return None,
};
let mode = match protection["enforced"].as_bool() {
Some(true) => "enforced",
Some(false) => "observe-only",
None => "enforcement unavailable",
};
Some(format!("Jev: prompt injection: {action} • {mode}"))
}
pub(super) fn bash_jev_metadata(detail: Option<&ToolActivityDetail>) -> Vec<String> {
let mut lines = humanize_protection_metadata(detail);
if let Some((_, decision)) = public_decision(detail, "bash_protection") {
lines.push(format!("Jev: {decision}"));
}
if let Some(status) =
detail.and_then(|detail| detail.metadata["bash_file_tracking"]["status"].as_str())
{
let label = match status {
"completed" => "completed",
"command_limit" => "skipped (command limit)",
"canceled" => "skipped (canceled)",
"busy" => "skipped (worker busy)",
"unavailable" => "skipped (worker unavailable)",
"timed_out" => "timed out",
"error" => "assessment failed",
"redacted" => "skipped (sensitive command)",
"unsupported" => "skipped (unsupported command)",
"no_candidates" => "skipped (no literal paths)",
"command_failed" => "skipped (command did not succeed)",
_ => return lines,
};
lines.push(format!("Jev: file tracking • {label}"));
}
if let Some(activity) =
detail.and_then(|detail| detail.metadata["inferred_file_activity"].as_array())
{
let mut files = std::collections::BTreeMap::<&str, u8>::new();
for item in activity.iter().take(32) {
let Some(path) = item["path"].as_str().filter(|path| !path.is_empty()) else {
continue;
};
let bits = match item["operation"].as_str() {
Some("read") => 1,
Some("write") => 2,
Some("edit") => 4,
Some("read_write") => 3,
_ => continue,
};
*files.entry(path).or_default() |= bits;
}
let count = |bit| files.values().filter(|bits| **bits & bit != 0).count();
lines.push(format!(
"Jev: file tracking • {} files inferred • R {} / W {} / E {}",
files.len(),
count(1),
count(2),
count(4)
));
}
lines
}
pub(super) fn attempted_change_preview(value: Option<&serde_json::Value>) -> Vec<String> {
let Some(text) = value.and_then(serde_json::Value::as_str) else {
return Vec::new();
};
let mut lines = text
.lines()
.take(TOOL_PREVIEW_LINES)
.map(|line| truncate_preview_line(&sanitize_display_text(line), PREVIEW_LINE_MAX_CHARS))
.collect::<Vec<_>>();
if text.lines().count() > TOOL_PREVIEW_LINES {
lines.push(format!(
"… change preview limited to {TOOL_PREVIEW_LINES} lines"
));
}
lines
}
pub(super) fn bounded_preview_lines(text: &str) -> String {
let safe = sanitize_display_text(text);
let mut source = safe.lines();
let mut lines = source
.by_ref()
.take(TOOL_PREVIEW_LINES)
.map(|line| truncate_preview_line(line, PREVIEW_LINE_MAX_CHARS))
.collect::<Vec<_>>();
if source.next().is_some() {
lines.push(format!(
"… response preview limited to {TOOL_PREVIEW_LINES} lines"
));
}
lines.join("\n")
}
pub(in crate::output::tool_card) fn bounded_error_line(text: &str) -> String {
let line = text
.lines()
.find(|line| !line.trim().is_empty())
.unwrap_or("operation failed");
truncate_preview_line(&sanitize_display_text(line), PREVIEW_LINE_MAX_CHARS)
}
pub(super) fn tool_was_canceled(detail: Option<&crate::output::ToolActivityDetail>) -> bool {
detail.is_some_and(|detail| {
detail
.output
.lines()
.map(str::trim)
.any(|line| line == "prompt canceled")
})
}
pub(super) fn plural<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
if count == 1 { singular } else { plural }
}
pub(super) fn split_role_line(
text: &str,
split: usize,
prefix: TextMeaning,
suffix: TextMeaning,
) -> TextLine {
tool_display_line(vec![
TextSpan::new(&text[..split], prefix),
TextSpan::new(&text[split..], suffix),
])
}
pub(super) fn tool_display_line(spans: Vec<TextSpan>) -> TextLine {
use unicode_segmentation::UnicodeSegmentation;
let line = TextLine { spans };
let text = line.plain_text();
let mut boundary = 0;
for span in line.spans.iter().take(line.spans.len().saturating_sub(1)) {
boundary += span.text.len();
if boundary != text.len()
&& !text
.grapheme_indices(true)
.any(|(index, _)| index == boundary)
{
return TextLine::plain(text);
}
}
line
}