Skip to main content

codetether_agent/tui/app/state/
message_cache.rs

1//! Message line cache for render performance.
2//!
3//! Caches rendered `Line<'static>` so the TUI can skip full rebuilds
4//! when only the streaming preview changed.
5
6use ratatui::text::Line;
7
8impl super::AppState {
9    /// Check whether the cached message lines are still valid for the given
10    /// width. Returns `true` when the cache can be reused.
11    pub(crate) fn is_message_cache_valid(&self, max_width: usize) -> bool {
12        self.cached_messages_len == self.messages.len()
13            && self.cached_max_width == max_width
14            && self.cached_streaming_snapshot.as_deref() == Some(&self.streaming_text)
15            && self.cached_processing == self.processing
16    }
17
18    /// Returns cached lines when still valid for `max_width`, or `None`.
19    pub fn get_or_build_message_lines(&mut self, max_width: usize) -> Option<Vec<Line<'static>>> {
20        if self.is_message_cache_valid(max_width) && !self.cached_message_lines.is_empty() {
21            Some(self.cached_message_lines.clone())
22        } else {
23            None
24        }
25    }
26
27    /// Take ownership of the cached lines, clearing the cache.
28    #[allow(dead_code)]
29    pub(crate) fn take_cached_message_lines(&mut self) -> Vec<Line<'static>> {
30        self.cached_message_lines.drain(..).collect()
31    }
32
33    /// Store rebuilt message lines in the cache.
34    pub(crate) fn store_message_lines(&mut self, lines: Vec<Line<'static>>, max_width: usize) {
35        self.cached_message_lines = lines;
36        self.cached_messages_len = self.messages.len();
37        self.cached_max_width = max_width;
38        self.cached_streaming_snapshot = if self.processing {
39            Some(self.streaming_text.clone())
40        } else {
41            None
42        };
43        self.cached_processing = self.processing;
44    }
45
46    /// Clone only the frozen (non-streaming) prefix of cached lines.
47    ///
48    /// Returns `None` if there is no cache or the frozen prefix is empty.
49    /// Used by the fast-path render that reuses the frozen prefix and
50    /// only rebuilds the streaming suffix.
51    pub(crate) fn clone_frozen_prefix(&self, max_width: usize) -> Option<Vec<Line<'static>>> {
52        if self.cached_frozen_len == 0
53            || self.cached_messages_len != self.messages.len()
54            || self.cached_max_width != max_width
55        {
56            return None;
57        }
58        let frozen: Vec<Line<'static>> = self
59            .cached_message_lines
60            .iter()
61            .take(self.cached_frozen_len)
62            .cloned()
63            .collect();
64        if frozen.is_empty() {
65            None
66        } else {
67            Some(frozen)
68        }
69    }
70
71    /// Store rebuilt message lines along with the frozen-prefix length.
72    pub(crate) fn store_message_lines_with_frozen(
73        &mut self,
74        lines: Vec<Line<'static>>,
75        max_width: usize,
76        frozen_len: usize,
77    ) {
78        self.cached_frozen_len = frozen_len;
79        self.store_message_lines(lines, max_width);
80    }
81}