use super::*;
#[derive(Debug, Clone)]
pub(crate) struct CachedTranscriptCard {
pub(crate) content_marker: u64,
pub(crate) card: Arc<crate::tui::transcript_cards::TranscriptCard>,
}
#[derive(Debug)]
pub(crate) struct CachedTranscriptVisualBlock {
pub(crate) content_marker: u64,
pub(crate) theme_revision: u64,
pub(crate) active: bool,
pub(crate) prepend_blank: bool,
pub(crate) subagent_card_rows: usize,
pub(crate) lines: Vec<crate::tui::transcript_cards::TranscriptVisualLine>,
pub(crate) cumulative_line_rows: Vec<usize>,
pub(crate) copy_bytes: usize,
pub(crate) rows: usize,
pub(crate) streaming: Option<crate::tui::transcript_cards::StreamingBody>,
}
#[derive(Debug, Clone)]
pub(crate) struct CachedTranscriptVisualLines {
pub(crate) generation: u64,
pub(crate) theme_revision: u64,
pub(crate) active: bool,
pub(crate) lines: Vec<crate::tui::transcript_cards::TranscriptVisualLine>,
pub(crate) rows: usize,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CachedTranscriptScrollbarRows {
pub(crate) rows: usize,
pub(crate) exact: bool,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct TranscriptScrollAnchor {
pub(crate) entry_index: usize,
pub(crate) row: usize,
pub(crate) scrollback_rows: usize,
}
#[derive(Debug, Default)]
pub(crate) struct CachedTranscriptHistoryIndex {
pub(crate) generation: u64,
pub(crate) source_start: Option<usize>,
pub(crate) source_len: usize,
pub(crate) theme_revision: u64,
pub(crate) active: bool,
pub(crate) subagent_card_rows: usize,
pub(crate) next_entry: usize,
pub(crate) dirty_entry: Option<usize>,
pub(crate) blocks: Vec<Option<Arc<CachedTranscriptVisualBlock>>>,
pub(crate) entry_indices: Vec<usize>,
pub(crate) cumulative_rows: Vec<usize>,
}
pub(crate) const MAX_STREAMING_CACHE_BYTES: usize = 256 * 1024;
pub(super) const MAX_TRANSCRIPT_WIDTH_CACHES: usize = 4;
#[derive(Debug, Default)]
pub(crate) struct TranscriptRenderCache {
pub(crate) visual_rows: HashMap<u16, usize>,
pub(crate) scrollbar_rows: HashMap<u16, CachedTranscriptScrollbarRows>,
pub(crate) cards: BTreeMap<usize, CachedTranscriptCard>,
pub(crate) visual_lines: HashMap<u16, CachedTranscriptVisualLines>,
pub(crate) visual_blocks: HashMap<u16, BTreeMap<usize, Arc<CachedTranscriptVisualBlock>>>,
pub(crate) streaming_reuse: Option<(u16, usize, Arc<CachedTranscriptVisualBlock>)>,
pub(crate) history_indices: HashMap<u16, CachedTranscriptHistoryIndex>,
visual_widths: VecDeque<u16>,
}
impl TranscriptRenderCache {
pub(crate) fn clear(&mut self) {
self.clear_visual_projection();
self.cards.clear();
}
pub(crate) fn clear_visual_projection(&mut self) {
self.clear_visual_aggregates();
self.visual_blocks.clear();
self.streaming_reuse = None;
self.visual_widths.clear();
}
pub(crate) fn clear_visual_aggregates(&mut self) {
self.clear_visual_measurements();
self.history_indices.clear();
}
fn clear_visual_measurements(&mut self) {
self.visual_rows.clear();
self.scrollbar_rows.clear();
self.visual_lines.clear();
}
pub(crate) fn invalidate_append(&mut self, generation: u64) {
self.clear_visual_measurements();
for index in self.history_indices.values_mut() {
index.generation = generation;
let entry = index.source_len.saturating_sub(1);
index.dirty_entry = Some(index.dirty_entry.map_or(entry, |dirty| dirty.min(entry)));
}
}
pub(crate) fn touch_visual_width(&mut self, width: u16) {
let width = width.max(1);
if let Some(index) = self
.visual_widths
.iter()
.position(|cached| *cached == width)
{
let width = self.visual_widths.remove(index).expect("cached width");
self.visual_widths.push_back(width);
return;
}
if self.visual_widths.len() >= MAX_TRANSCRIPT_WIDTH_CACHES
&& let Some(evicted_width) = self.visual_widths.pop_front()
{
self.visual_rows.remove(&evicted_width);
self.scrollbar_rows.remove(&evicted_width);
self.visual_lines.remove(&evicted_width);
self.visual_blocks.remove(&evicted_width);
self.history_indices.remove(&evicted_width);
if self
.streaming_reuse
.as_ref()
.is_some_and(|(width, _, _)| *width == evicted_width)
{
self.streaming_reuse = None;
}
}
self.visual_widths.push_back(width);
}
pub(crate) fn insert_visual_block(
&mut self,
width: u16,
entry_index: usize,
block: Arc<CachedTranscriptVisualBlock>,
) {
let width = width.max(1);
self.touch_visual_width(width);
if block.streaming.is_some() {
self.streaming_reuse = Some((width, entry_index, Arc::clone(&block)));
} else if self
.streaming_reuse
.as_ref()
.is_some_and(|(_, index, _)| *index == entry_index)
{
self.streaming_reuse = None;
}
self.visual_blocks
.entry(width)
.or_default()
.insert(entry_index, block);
}
pub(crate) fn evict_card(&mut self, entry_index: usize, generation: u64) {
self.evict_cards(std::iter::once(entry_index), generation);
}
pub(crate) fn evict_cards(
&mut self,
entry_indices: impl IntoIterator<Item = usize>,
generation: u64,
) {
let entry_indices = entry_indices.into_iter().collect::<Vec<_>>();
if entry_indices.is_empty() {
return;
}
for entry_index in entry_indices {
self.cards.remove(&entry_index);
for blocks in self.visual_blocks.values_mut() {
blocks.remove(&entry_index);
}
for index in self.history_indices.values_mut() {
index.generation = generation;
if let Some(entry) = index
.source_start
.and_then(|base| entry_index.checked_sub(base))
&& entry < index.source_len
&& entry.saturating_add(1) >= index.next_entry
{
index.dirty_entry =
Some(index.dirty_entry.map_or(entry, |dirty| dirty.min(entry)));
let block_index = index.entry_indices.partition_point(|&first| first > entry);
if let Some(block) = index.blocks.get_mut(block_index) {
*block = None;
}
}
}
}
self.clear_visual_measurements();
}
pub(crate) fn evict_front(&mut self, old_base: usize, drained: usize, generation: u64) {
if self.streaming_reuse.as_ref().is_some_and(|(_, index, _)| {
(old_base..old_base.saturating_add(drained)).contains(index)
}) {
self.streaming_reuse = None;
}
self.evict_cards(old_base..old_base.saturating_add(drained), generation);
self.history_indices.clear();
}
}