use super::*;
fn append_display_body_line(
lines: &mut Vec<TranscriptVisualLine>,
card: &TranscriptCard,
line: &crate::rendering::DisplayLine,
theme: MissionControlTheme,
body_style: Style,
width: u16,
) {
if let Some(table) = &line.table {
let available = body_wrap_width(width);
let indent = available.saturating_sub(1).min(2);
for mut row in tables::layout_table(table, available - indent, theme, card.role) {
row.insert(
0,
AssistantVisualSpan::styled(" ".repeat(indent), body_style, None),
);
push_wide_assistant_spans(lines, card, row, true, width, body_style);
}
return;
}
let mut spans = Vec::with_capacity(line.spans.len().saturating_add(1));
let assistant = card.role == TranscriptCardRole::Assistant;
if assistant {
spans.push(AssistantVisualSpan::styled(
" ",
theme.pane().bg(card_body_background(card.role, theme)),
None,
));
}
let body_style = if card.role == TranscriptCardRole::CodeMode {
match line.spans.first().and_then(|span| span.text.chars().next()) {
Some('✓') => body_style.fg(theme.activity_status_color(ActivityStatus::Success)),
Some('✗') => body_style.fg(theme.activity_status_color(ActivityStatus::Failed)),
Some('⚠' | '●') => {
body_style.fg(theme.activity_status_color(ActivityStatus::Running))
}
Some('⊘') => body_style.fg(theme.activity_status_color(ActivityStatus::Canceled)),
_ => body_style,
}
} else {
body_style
};
spans.extend(line.spans.iter().enumerate().map(|(index, span)| {
AssistantVisualSpan::styled(
if !assistant && index == 0 {
format!(" {}", span.text)
} else {
span.text.clone()
},
if span.role == DisplayRole::Plain && !assistant {
body_style
} else {
theme
.display_role(span.role)
.bg(card_body_background(card.role, theme))
},
Some(span.role),
)
}));
push_wide_assistant_spans(lines, card, spans, true, width, body_style);
}
fn push_box_visual_row(
lines: &mut Vec<TranscriptVisualLine>,
display: String,
copy_text: String,
copy_continuation: bool,
style: Style,
width: u16,
body_style: Style,
) {
let copyable = !copy_text.is_empty();
lines.push(TranscriptVisualLine {
image_row: None,
status_indicators: Vec::new(),
line: wide_body_line(vec![Span::styled(display, style)], width, body_style),
copy_text,
copy_byte_range: None,
copyable,
copy_continuation,
visual_copy_column_offset: body_line_copy_offset(width, 2),
visual_copy_byte_boundaries: Vec::new(),
hit_regions: Vec::new(),
});
}
fn push_bordered_text_box(
lines: &mut Vec<TranscriptVisualLine>,
card: &TranscriptCard,
text_rows: impl IntoIterator<Item = String>,
style: Style,
width: u16,
body_style: Style,
) {
let available = body_wrap_width(width);
if available < 5 {
for text in text_rows {
push_text_line(lines, card, text, style, false, width, body_style);
}
return;
}
push_box_visual_row(
lines,
format!("╭{}╮", "─".repeat(available - 2)),
String::new(),
false,
style,
width,
body_style,
);
let inner_width = available - 4;
for text in text_rows {
for (index, wrapped) in wrap_spans_to_width(vec![Span::styled(text, style)], inner_width)
.into_iter()
.enumerate()
{
let text = spans_text(&wrapped);
let padding = inner_width.saturating_sub(UnicodeWidthStr::width(text.as_str()));
push_box_visual_row(
lines,
format!("│ {text}{} │", " ".repeat(padding)),
text,
index > 0,
style,
width,
body_style,
);
}
}
push_box_visual_row(
lines,
format!("╰{}╯", "─".repeat(available - 2)),
String::new(),
false,
style,
width,
body_style,
);
}
fn card_has_bordered_intent(state: &MissionControlState, card: &TranscriptCard) -> bool {
matches!(
card.role,
TranscriptCardRole::Bash | TranscriptCardRole::CodeMode
) && state
.transcript_absolute_index(card.id.entry_index)
.and_then(|index| state.transcript.activity_links().get(&index))
.and_then(|link| state.nodes.get(&link.activity_id))
.and_then(|node| node.tool_detail.as_ref())
.and_then(|detail| detail.params.get(crate::tools::contract::arg_key::INTENT))
.and_then(serde_json::Value::as_str)
.is_some_and(|intent| !intent.trim().is_empty())
}
pub(super) fn append_card_body_lines(
lines: &mut Vec<TranscriptVisualLine>,
state: &MissionControlState,
card: &TranscriptCard,
active: bool,
body_style: Style,
width: u16,
) {
let theme = state.theme;
if card.role == TranscriptCardRole::SubagentBatch {
append_subagent_card_body_lines(lines, state, card, active, width, body_style);
return;
}
let has_bordered_intent = card_has_bordered_intent(state, card);
let metadata_style = theme.dim(active).bg(card_body_background(card.role, theme));
let jev_metadata = card
.metadata
.iter()
.filter(|metadata| metadata.starts_with("Jev:"))
.cloned()
.collect::<Vec<_>>();
if !jev_metadata.is_empty() {
push_bordered_text_box(lines, card, jev_metadata, metadata_style, width, body_style);
if !has_bordered_intent {
push_body_blank(lines, card, width, theme);
}
}
for metadata in card
.metadata
.iter()
.filter(|metadata| !metadata.starts_with("Jev:"))
{
push_text_line(
lines,
card,
format!(" {metadata}"),
metadata_style,
false,
width,
body_style,
);
}
if has_bordered_intent && let Some(intent) = card.body.lines().next() {
let intent_style = theme
.transcript_tool_body()
.bg(card_body_background(card.role, theme));
if card.role == TranscriptCardRole::CodeMode && activity_card_content_width(width) >= 5 {
push_activity_card_text_box(
lines,
&format!("Intent: {intent}"),
intent_style,
width,
body_style,
);
} else {
push_bordered_text_box(
lines,
card,
[format!("Intent: {intent}")],
intent_style,
width,
body_style,
);
push_body_blank(lines, card, width, theme);
}
}
if streaming::is_windowed(card) {
return;
}
if !card.body_lines.is_empty() {
let skip_intent = has_bordered_intent && card.role == TranscriptCardRole::CodeMode;
for line in card.body_lines.iter().skip(usize::from(skip_intent)) {
append_display_body_line(lines, card, line, theme, body_style, width);
}
} else {
for body_line in card.body.lines().skip(usize::from(has_bordered_intent)) {
push_text_line(
lines,
card,
format!(" {body_line}"),
body_style,
true,
width,
body_style,
);
}
}
if card.role == TranscriptCardRole::CodeMode {
append_activity_feed_lines(lines, card, width, body_style, theme);
}
for child in &card.children {
let child_status = status_mark(child.status);
let label = if child_status.is_empty() {
format!(" - {}", child.label)
} else {
format!(" - {child_status} {}", child.label)
};
push_text_line(
lines,
card,
label,
theme
.transcript_tool_body()
.bg(card_body_background(card.role, theme)),
true,
width,
body_style,
);
}
}
fn append_activity_feed_lines(
lines: &mut Vec<TranscriptVisualLine>,
card: &TranscriptCard,
width: u16,
body_style: Style,
theme: MissionControlTheme,
) {
push_body_blank(lines, card, width, theme);
push_activity_card_box_edge(lines, width, body_style, theme, true, Vec::new());
for row in &card.activity_rows {
let segment = ActivityCardSegment::plain(
format!("{} {}", activity_card_status_glyph(row.status), row.label),
activity_card_status_style(row.status, theme).bg(theme.surface_background()),
)
.with_status_indicator(row.status, Some(0), Some(row.activity_id.clone()));
push_activity_card_feed_row(lines, Some(segment), width, body_style, theme);
}
for index in card.activity_rows.len()..super::TOOL_PREVIEW_LINES {
let placeholder = (index == 0)
.then(|| ActivityCardSegment::plain("No tool calls yet", theme.muted(false)));
push_activity_card_feed_row(lines, placeholder, width, body_style, theme);
}
push_activity_card_box_edge(lines, width, body_style, theme, false, Vec::new());
}