use super::*;
pub(super) fn is_windowed(card: &TranscriptCard) -> bool {
card.role == TranscriptCardRole::Assistant
&& card.status == TranscriptCardStatus::Running
&& card.id.entry_index != usize::MAX
&& card.body_lines.is_empty()
&& card.metadata.is_empty()
&& card.children.is_empty()
}
#[derive(Debug)]
pub(crate) struct StreamingBody {
card: Arc<TranscriptCard>,
checkpoints: Vec<Checkpoint>,
checkpoint_stride: usize,
pub(super) rows: usize,
copy_bytes: usize,
width: u16,
theme: MissionControlTheme,
body_style: Style,
}
#[derive(Debug, Clone, Copy)]
struct Checkpoint {
row: usize,
cursor: Cursor,
}
#[derive(Debug, Clone, Copy)]
struct Cursor {
byte: usize,
line_end: usize,
indent: usize,
continuation: bool,
copy_byte: usize,
finished: bool,
}
struct PlainRow {
source: Range<usize>,
indent: usize,
continuation: bool,
copy: Range<usize>,
}
impl Cursor {
fn new(body: &str) -> Self {
Self {
byte: 0,
line_end: body.find('\n').unwrap_or(body.len()),
indent: 2,
continuation: false,
copy_byte: 0,
finished: false,
}
}
fn next_line(&mut self, body: &str) {
if self.line_end == body.len() {
self.finished = true;
} else {
self.byte = self.line_end + 1;
self.line_end = body[self.byte..]
.find('\n')
.map_or(body.len(), |end| self.byte + end);
self.indent = 2;
self.continuation = false;
self.copy_byte += 1;
}
}
fn next(&mut self, body: &str, max_width: usize) -> Option<PlainRow> {
while !self.finished {
let mut row = PlainRow {
source: self.byte..self.byte,
indent: 0,
continuation: self.continuation,
copy: self.copy_byte..self.copy_byte,
};
let mut row_width = 0usize;
let mut has_units = false;
while self.indent > 0 {
self.indent -= 1;
row.indent += 1;
self.copy_byte += 1;
row_width += 1;
has_units = true;
if row_width >= max_width {
row.copy.end = self.copy_byte;
self.continuation = true;
return Some(row);
}
}
while let Some(unit) = next_display_width_unit(&body[..self.line_end], self.byte) {
if has_units && row_width.saturating_add(unit.width) > max_width {
row.copy.end = self.copy_byte;
self.continuation = true;
return Some(row);
}
self.byte = unit.end;
row.source.end = self.byte;
self.copy_byte += unit.text.len();
row_width = row_width.saturating_add(unit.width);
has_units = true;
if row_width >= max_width {
row.copy.end = self.copy_byte;
self.continuation = true;
return Some(row);
}
}
row.copy.end = self.copy_byte;
self.next_line(body);
if has_units {
return Some(row);
}
}
None
}
}
impl StreamingBody {
pub(super) fn new(
card: Arc<TranscriptCard>,
width: u16,
theme: MissionControlTheme,
active: bool,
previous: Option<&Self>,
) -> Self {
let max_checkpoints = transcript::MAX_STREAMING_CACHE_BYTES / size_of::<Checkpoint>();
let max_lines = card.body.len().saturating_mul(3).saturating_add(2);
let stride = max_lines.div_ceil(max_checkpoints).max(1);
let previous = previous.and_then(|old| {
if old.checkpoints.first()?.cursor.line_end == old.card.body.len() {
return None;
}
let prefix_end = old.card.body.rfind('\n')? + 1;
(old.width == width
&& card.body.get(..prefix_end) == Some(&old.card.body[..prefix_end]))
.then_some((old, prefix_end))
});
let stride = previous.map_or(stride, |(old, _)| {
old.checkpoint_stride * stride.div_ceil(old.checkpoint_stride)
});
let body_style =
role_body_style(card.role, active, theme).bg(card_body_background(card.role, theme));
let mut body = Self {
card,
checkpoints: Vec::with_capacity(32.min(max_lines).min(max_checkpoints)),
checkpoint_stride: stride,
rows: 0,
copy_bytes: 0,
width,
theme,
body_style,
};
let mut cursor = Cursor::new(&body.card.body);
let mut generated_lines = 0usize;
if let Some((old, prefix_end)) = previous {
let reusable = old
.checkpoints
.partition_point(|point| point.cursor.line_end < prefix_end);
if reusable > 0 {
let thinning = stride / old.checkpoint_stride;
let resume_index = (reusable - 1) / thinning * thinning;
let retained = resume_index / thinning;
if retained > body.checkpoints.capacity() {
body.checkpoints.reserve_exact(retained);
}
body.checkpoints.extend(
old.checkpoints[..resume_index]
.iter()
.step_by(thinning)
.copied(),
);
let resume = old.checkpoints[resume_index];
cursor = resume.cursor;
body.rows = resume.row;
body.copy_bytes = cursor.copy_byte;
generated_lines = resume_index * old.checkpoint_stride;
}
}
loop {
let start = cursor;
let Some(row) = cursor.next(&body.card.body, body_wrap_width(width)) else {
break;
};
if generated_lines.is_multiple_of(stride) {
let checkpoints = &mut body.checkpoints;
if checkpoints.len() == checkpoints.capacity() {
let capacity = checkpoints.capacity();
checkpoints.reserve_exact((capacity * 2).min(max_checkpoints) - capacity);
}
checkpoints.push(Checkpoint {
row: body.rows,
cursor: start,
});
}
body.copy_bytes = row.copy.end;
body.rows += body.measured_line(&row).map_or(1, |(_, rows)| rows);
generated_lines += 1;
}
debug_assert!(
body.checkpoints.capacity() * size_of::<Checkpoint>()
<= transcript::MAX_STREAMING_CACHE_BYTES
);
body
}
fn measured_line(&self, row: &PlainRow) -> Option<(TranscriptVisualLine, usize)> {
if self.card.body[row.source.clone()].is_ascii() {
return None;
}
let line = self.visual_line(row);
let rows = visual_line_rows(&line, self.width);
Some((line, rows))
}
fn visual_line(&self, row: &PlainRow) -> TranscriptVisualLine {
let mut spans = Vec::with_capacity(2);
if row.indent > 0 {
spans.push(Span::styled(
" ".repeat(row.indent),
self.theme
.pane()
.bg(card_body_background(self.card.role, self.theme)),
));
}
let text: String = self.card.body[row.source.clone()]
.graphemes(true)
.filter(|unit| !unit.chars().any(char::is_control))
.collect();
let copy_text = format!("{}{text}", " ".repeat(row.indent));
if !text.is_empty() {
spans.push(Span::styled(
text,
self.theme
.display_role(DisplayRole::Plain)
.bg(card_body_background(self.card.role, self.theme)),
));
}
TranscriptVisualLine {
image_row: None,
status_indicators: Vec::new(),
line: wide_body_line(spans, self.width, self.body_style),
copy_text,
copy_byte_range: None,
copyable: true,
hit_regions: Vec::new(),
copy_continuation: row.continuation,
visual_copy_column_offset: body_line_copy_offset(self.width, 0),
visual_copy_byte_boundaries: Vec::new(),
}
}
fn visit<T>(
&self,
range: Range<usize>,
mut visit: impl FnMut(&TranscriptVisualLine, usize, usize, Range<usize>) -> Option<T>,
) -> Option<T> {
if range.start >= range.end || range.start >= self.rows {
return None;
}
let checkpoint = &self.checkpoints[self
.checkpoints
.partition_point(|point| point.row <= range.start)
- 1];
let mut cursor = checkpoint.cursor;
let mut row_index = checkpoint.row;
while row_index < range.end.min(self.rows) {
let row = cursor.next(&self.card.body, body_wrap_width(self.width))?;
let measured = self.measured_line(&row);
let rows = measured.as_ref().map_or(1, |(_, rows)| *rows);
let next_row = row_index + rows;
if next_row > range.start {
let line = measured.map_or_else(|| self.visual_line(&row), |(line, _)| line);
if let Some(result) = visit(&line, row_index, rows, row.copy) {
return Some(result);
}
}
row_index = next_row;
}
None
}
}
impl transcript::CachedTranscriptVisualBlock {
pub(super) fn copy_bytes(&self) -> usize {
let mut bytes = 0;
let mut seen = false;
for line in &self.lines {
advance_copy_byte_range(line, &mut bytes, &mut seen);
}
bytes
+ self
.streaming
.as_ref()
.map_or(0, |body| 1 + body.copy_bytes)
}
pub(super) fn visit_lines<T>(
&self,
range: Range<usize>,
mut visit: impl FnMut(&TranscriptVisualLine, usize, usize, Option<(usize, usize)>) -> Option<T>,
) -> Option<T> {
if range.start >= range.end || range.start >= self.rows {
return None;
}
let mut current_row = 0usize;
let mut bytes = 0;
let mut seen = false;
for (index, (line, &rows)) in self.lines.iter().zip(&self.line_rows).enumerate() {
if index + 1 == self.lines.len()
&& let Some(body) = &self.streaming
{
let byte_base = bytes + usize::from(seen);
let body_range =
range.start.saturating_sub(current_row)..range.end.saturating_sub(current_row);
if let Some(result) = body.visit(body_range, |line, row, rows, copy| {
visit(
line,
current_row + row,
rows,
Some((byte_base + copy.start, byte_base + copy.end)),
)
}) {
return Some(result);
}
current_row += body.rows;
bytes = byte_base + body.copy_bytes;
seen = true;
}
if current_row >= range.end {
break;
}
let copy = advance_copy_byte_range(line, &mut bytes, &mut seen);
if current_row.saturating_add(rows) > range.start
&& let Some(result) = visit(line, current_row, rows, copy)
{
return Some(result);
}
current_row += rows;
}
None
}
}