magi-code 0.98.1

Repository-aware CLI coding agent for terminal work
Documentation
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,
    // Streaming blocks keep only their chrome plus a bounded sparse body index.
    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>,
    // Newest first; cumulative heights allow binary searches from the bottom.
    // Evicted slots keep heights/positions, not old streaming response snapshots.
    pub(crate) blocks: Vec<Option<Arc<CachedTranscriptVisualBlock>>>,
    pub(crate) entry_indices: Vec<usize>,
    pub(crate) cumulative_rows: Vec<usize>,
}

// Sparse cursor storage per retained width, independent of response size.
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>>>,
    // One active body handoff across revision eviction, shared with the live
    // block until then. Never retain a chain of historical response copies.
    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();
    }
}