Skip to main content

codetether_agent/tui/ui/chat_view/
format_cache.rs

1//! Per-message formatted-line cache.
2//!
3//! Thread-local cache keyed by `(timestamp_nanos, content_len, max_width,
4//! role)`. `ChatMessage` content is immutable post-construction, so formatted
5//! lines can be reused across frames until width or content changes.
6
7use std::cell::RefCell;
8use std::collections::HashMap;
9
10use ratatui::text::Line;
11
12use crate::tui::chat::message::ChatMessage;
13use crate::tui::message_formatter::MessageFormatter;
14
15type Key = (u128, usize, usize, u8);
16const CACHE_CAP: usize = 512;
17
18thread_local! {
19    static FORMAT_CACHE: RefCell<HashMap<Key, Vec<Line<'static>>>> =
20        RefCell::new(HashMap::with_capacity(128));
21}
22
23fn role_discrim(r: &str) -> u8 {
24    match r {
25        "user" => 1,
26        "assistant" => 2,
27        "system" => 3,
28        "error" => 4,
29        _ => 0,
30    }
31}
32
33fn ts_nanos(m: &ChatMessage) -> u128 {
34    m.timestamp
35        .duration_since(std::time::UNIX_EPOCH)
36        .map(|d| d.as_nanos())
37        .unwrap_or(0)
38}
39
40/// Format `message.content` for `role` at `max_width`, reusing prior parses.
41pub fn format_message_cached(
42    message: &ChatMessage,
43    role: &str,
44    formatter: &MessageFormatter,
45    max_width: usize,
46) -> Vec<Line<'static>> {
47    let key = (
48        ts_nanos(message),
49        message.content.len(),
50        max_width,
51        role_discrim(role),
52    );
53    FORMAT_CACHE.with(|c| {
54        if let Some(l) = c.borrow().get(&key) {
55            return l.clone();
56        }
57        let lines = formatter.format_content(&message.content, role);
58        let mut m = c.borrow_mut();
59        if m.len() >= CACHE_CAP {
60            m.clear();
61        }
62        m.insert(key, lines.clone());
63        lines
64    })
65}
66
67/// Clear the entire cache.
68pub fn reset_format_cache() {
69    FORMAT_CACHE.with(|c| c.borrow_mut().clear());
70}