use super::*;
pub(super) fn append_user_prompt_lines(
lines: &mut Vec<TranscriptVisualLine>,
state: &MissionControlState,
card: &TranscriptCard,
body_style: Style,
width: u16,
timestamp: Option<&str>,
active: bool,
) {
let theme = state.theme;
push_body_blank(lines, card, width, theme);
let timestamp_style = theme
.muted(active)
.bg(card_body_background(card.role, theme))
.add_modifier(Modifier::DIM);
let body_lines = if card.body.is_empty() {
vec![""]
} else {
card.body.lines().collect::<Vec<_>>()
};
for (line_index, body_line) in body_lines.into_iter().enumerate() {
let first = line_index == 0;
let mut spans = Vec::new();
if first {
if let Some(timestamp) = timestamp {
spans.push(UserPromptVisualSpan::styled(
timestamp,
timestamp_style,
false,
));
spans.push(UserPromptVisualSpan::styled(" | ", timestamp_style, false));
}
let label = if card.title == "Steering injected" {
"Steering injected: "
} else {
"User: "
};
spans.push(UserPromptVisualSpan::styled(label, body_style, true));
}
if !body_line.is_empty() {
spans.push(UserPromptVisualSpan::styled(body_line, body_style, true));
}
let padding = user_prompt_horizontal_padding(width);
let wrap_width = usize::from(width).saturating_sub(2 * padding).max(1);
let wrapped = wrap_user_prompt_spans_to_width(spans, wrap_width);
let mut copy_line_seen = false;
for row in wrapped {
let row_has_copy = row.iter().any(|span| span.copyable);
let force_copyable = !first && !row_has_copy;
let copyable = row_has_copy || force_copyable;
push_user_prompt_row(
lines,
row,
width,
body_style,
force_copyable,
copy_line_seen,
);
if copyable {
copy_line_seen = true;
}
}
}
push_card_token_footer(lines, state, card, active, width, theme, None);
}
struct UserPromptVisualSpan {
pub(super) display_text: String,
pub(super) copy_text: String,
pub(super) style: Style,
pub(super) copyable: bool,
}
impl UserPromptVisualSpan {
pub(super) fn styled(text: impl Into<String>, style: Style, copyable: bool) -> Self {
let text = text.into();
Self {
copy_text: if copyable {
text.clone()
} else {
String::new()
},
display_text: text,
style,
copyable,
}
}
fn with_copy_text(
display_text: impl Into<String>,
copy_text: impl Into<String>,
style: Style,
copyable: bool,
) -> Self {
Self {
display_text: display_text.into(),
copy_text: copy_text.into(),
style,
copyable,
}
}
}
fn push_user_prompt_row(
lines: &mut Vec<TranscriptVisualLine>,
row: Vec<UserPromptVisualSpan>,
width: u16,
body_style: Style,
force_copyable: bool,
copy_continuation: bool,
) {
let mut spans = Vec::with_capacity(row.len());
let mut copy_text = String::new();
let mut copy_byte_boundaries = Vec::new();
let mut copy_column = None;
let mut display_width = 0usize;
for span in row {
if span.copyable {
copy_column.get_or_insert(display_width);
let copy_byte_base = copy_text.len();
copy_text.push_str(&span.copy_text);
append_copy_byte_boundaries(
&mut copy_byte_boundaries,
©_byte_boundaries_for_span(&span.display_text, &span.copy_text),
copy_byte_base,
);
}
display_width =
display_width.saturating_add(UnicodeWidthStr::width(span.display_text.as_str()));
spans.push(Span::styled(span.display_text, span.style));
}
let copyable = copy_column.is_some() || force_copyable;
let left = user_prompt_horizontal_padding(width);
lines.push(TranscriptVisualLine {
image_row: None,
status_indicators: Vec::new(),
line: padded_body_line(spans, width, body_style, (left, left)),
copy_text,
copy_byte_range: None,
copyable,
copy_continuation: copyable && copy_continuation,
visual_copy_column_offset: copy_column
.map(|column| display_width_to_u16(left.saturating_add(column)))
.unwrap_or_default(),
visual_copy_byte_boundaries: copy_byte_boundaries,
hit_regions: Vec::new(),
});
}
fn wrap_user_prompt_spans_to_width(
spans: Vec<UserPromptVisualSpan>,
max_width: usize,
) -> Vec<Vec<UserPromptVisualSpan>> {
let max_width = max_width.max(1);
let mut rows: Vec<Vec<UserPromptVisualSpan>> = Vec::new();
let mut row = Vec::new();
let mut row_width = 0usize;
for span in spans {
let mut segment = String::new();
let mut copy_segment = String::new();
let mut segment_has_positive_width = false;
let mut trailing_zero_width_after_full_row = false;
let mut index = 0usize;
while let Some(unit) = next_display_width_unit(&span.display_text, index) {
index = unit.end;
let unit_width = unit.width;
if unit_width == 0
&& trailing_zero_width_after_full_row
&& segment.is_empty()
&& row.is_empty()
&& row_width == 0
&& let Some(last_row) = rows.last_mut()
{
last_row.push(UserPromptVisualSpan::with_copy_text(
unit.text,
if span.copyable { unit.text } else { "" },
span.style,
span.copyable,
));
continue;
}
trailing_zero_width_after_full_row = false;
if unit_width > max_width {
let carries_leading_zero_width = !segment.is_empty() && !segment_has_positive_width;
if segment_has_positive_width && !segment.is_empty() {
row.push(UserPromptVisualSpan::with_copy_text(
std::mem::take(&mut segment),
std::mem::take(&mut copy_segment),
span.style,
span.copyable,
));
}
if !row.is_empty() {
rows.push(std::mem::take(&mut row));
}
row_width = 0;
let mut replacement_display = if carries_leading_zero_width {
std::mem::take(&mut segment)
} else {
String::new()
};
replacement_display.push('�');
let mut replacement_copy = if carries_leading_zero_width {
std::mem::take(&mut copy_segment)
} else {
copy_segment.clear();
String::new()
};
if span.copyable {
replacement_copy.push_str(unit.text);
} else {
replacement_copy.clear();
}
row.push(UserPromptVisualSpan::with_copy_text(
replacement_display,
replacement_copy,
span.style,
span.copyable,
));
rows.push(std::mem::take(&mut row));
trailing_zero_width_after_full_row = true;
segment_has_positive_width = false;
continue;
}
if row_width.saturating_add(unit_width) > max_width
&& (!row.is_empty() || !segment.is_empty())
{
if segment_has_positive_width && !segment.is_empty() {
row.push(UserPromptVisualSpan::with_copy_text(
std::mem::take(&mut segment),
std::mem::take(&mut copy_segment),
span.style,
span.copyable,
));
}
rows.push(std::mem::take(&mut row));
row_width = 0;
}
segment.push_str(unit.text);
if span.copyable {
copy_segment.push_str(unit.text);
}
if unit_width > 0 {
segment_has_positive_width = true;
}
row_width = row_width.saturating_add(unit_width);
if row_width >= max_width {
row.push(UserPromptVisualSpan::with_copy_text(
std::mem::take(&mut segment),
std::mem::take(&mut copy_segment),
span.style,
span.copyable,
));
rows.push(std::mem::take(&mut row));
row_width = 0;
segment_has_positive_width = false;
trailing_zero_width_after_full_row = true;
}
}
if !segment.is_empty() {
row.push(UserPromptVisualSpan::with_copy_text(
segment,
copy_segment,
span.style,
span.copyable,
));
}
}
if rows.is_empty() || !row.is_empty() {
rows.push(row);
}
rows
}