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) line_rows: Vec<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) blocks: Vec<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.visual_rows.clear();
self.scrollbar_rows.clear();
self.visual_lines.clear();
self.history_indices.clear();
}
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) {
self.evict_cards(std::iter::once(entry_index));
}
pub(crate) fn evict_cards(&mut self, entry_indices: impl IntoIterator<Item = usize>) {
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);
}
}
self.clear_visual_aggregates();
}
pub(crate) fn evict_front(&mut self, old_base: usize, drained: usize) {
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));
}
}