use super::*;
pub(super) fn humanize_protection_metadata(
detail: Option<&crate::output::ToolActivityDetail>,
) -> Vec<String> {
detail
.and_then(|detail| {
crate::output::tool_display::humanize_protection_summary(&detail.metadata)
})
.map(|summary| vec![format!("Jev: {summary}")])
.unwrap_or_default()
}
pub(super) fn prompt_injection_protection_metadata(
detail: Option<&crate::output::ToolActivityDetail>,
) -> Option<String> {
detail
.and_then(|detail| {
crate::output::tool_display::prompt_injection_protection_summary(&detail.metadata)
})
.map(|summary| format!("Jev: {summary}"))
}
pub(super) fn bash_jev_metadata(detail: Option<&crate::output::ToolActivityDetail>) -> Vec<String> {
let Some(detail) = detail else {
return Vec::new();
};
[
crate::output::tool_display::humanize_protection_summary(&detail.metadata),
crate::output::tool_display::bash_protection_summary(&detail.metadata),
crate::output::tool_display::bash_file_tracking_summary(&detail.metadata),
]
.into_iter()
.flatten()
.map(|summary| format!("Jev: {summary}"))
.collect()
}
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(
&transcript::sanitize_preview(line),
PREVIEW_LINE_MAX_DISPLAY_WIDTH,
)
})
.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 lines = text
.lines()
.map(|line| {
truncate_preview_line(
&transcript::sanitize_preview(line),
PREVIEW_LINE_MAX_DISPLAY_WIDTH,
)
})
.collect::<Vec<_>>();
if lines.len() <= TOOL_PREVIEW_LINES {
return lines.join("\n");
}
format!(
"{}\n… response preview limited to {TOOL_PREVIEW_LINES} lines",
lines[..TOOL_PREVIEW_LINES].join("\n")
)
}
pub(in crate::tui::transcript_cards::projection) fn bounded_error_line(text: &str) -> String {
let line = text
.lines()
.find(|line| !line.trim().is_empty())
.unwrap_or("operation failed");
truncate_preview_line(
&transcript::sanitize_preview(line),
PREVIEW_LINE_MAX_DISPLAY_WIDTH,
)
}
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 generic_tool_card(
state: &MissionControlState,
id: TranscriptCardId,
link: Option<TranscriptActivityLink>,
body: &str,
) -> TranscriptCard {
let activity = link
.as_ref()
.and_then(|link| state.nodes.get(&link.activity_id).map(|node| (link, node)));
let tool_capability = link
.as_ref()
.and_then(|link| ToolCapability::from_dispatch_name(&link.tool_name));
let role = if tool_capability == Some(ToolCapability::Subagents)
|| (link.is_none()
&& transcript_tool_name(state, id.entry_index)
== Some(ToolCapability::Subagents.canonical_name()))
{
TranscriptCardRole::SubagentBatch
} else {
TranscriptCardRole::Tool
};
let bash_mode_response = is_bash_mode_response_card(state, id.entry_index, tool_capability);
let title = if bash_mode_response {
BASH_MODE_RESPONSE_TITLE.to_string()
} else {
tool_title(
link.as_ref().map(|link| link.tool_name.as_str()),
transcript_tool_name(state, id.entry_index),
role,
)
};
let mut status = transcript_tool_status(state, id.entry_index);
let metadata = Vec::new();
let mut preview = String::new();
let children = if role == TranscriptCardRole::SubagentBatch {
subagent_aggregation::subagent_children(
state,
link.as_ref().map(|link| &link.activity_id),
id.entry_index,
)
} else {
Vec::new()
};
if let Some((_, node)) = activity {
status = transcript_status_from_activity(node.status);
preview = if bash_mode_response {
full_activity_preview(&node.preview)
} else if role != TranscriptCardRole::SubagentBatch {
bounded_lines(&node.preview, TOOL_PREVIEW_LINES)
} else {
String::new()
};
}
let body =
normalize_inline_separators(&transcript::sanitize_preview(body.trim_end_matches('\n')));
if preview.is_empty() && role != TranscriptCardRole::SubagentBatch {
preview = body.clone();
}
let subagent_card = if role == TranscriptCardRole::SubagentBatch {
Some(subagent_aggregation::subagent_card_summary_from_children(
state,
link.as_ref().map(|link| &link.activity_id),
&children,
activity.map(|(_, node)| node),
))
} else {
None
};
TranscriptCard {
id,
role,
status,
title: transcript::sanitize_preview(&title),
metadata,
body: preview,
body_lines: Vec::new(),
children,
activity_rows: Vec::new(),
subagent_card,
}
}
pub(super) fn split_role_line(
text: &str,
split: usize,
prefix: DisplayRole,
suffix: DisplayRole,
) -> DisplayLine {
tool_display_line(vec![
DisplaySpan::new(&text[..split], prefix),
DisplaySpan::new(&text[split..], suffix),
])
}
pub(super) fn tool_display_line(spans: Vec<DisplaySpan>) -> DisplayLine {
use unicode_segmentation::UnicodeSegmentation;
let line = DisplayLine { spans, table: None };
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 DisplayLine::plain(text);
}
}
line
}