Skip to main content

lattice_runtime/
messages.rs

1//! `*messages*` transcript types -- one historical record per
2//! editor echo, a bounded ring of them, and a typed
3//! `MessagePushed` event published whenever a new record
4//! lands.
5//!
6//! Lives in `lattice-runtime` so any host-side crate that
7//! cares about message stream events (TUI, GPU renderer,
8//! plugin host, telemetry) can subscribe without taking a
9//! dependency on the renderer crate. The wire-typed
10//! `lattice_grammar::EchoLevel` rides through unchanged so
11//! grammar / runtime / plugin code all read the same severity
12//! enum without round-tripping through a display-typed clone.
13//!
14//! Plugins via the WASM Component Model subscribe through the
15//! plugin host's WIT bridge -- the host wires
16//! `event_bus.subscribe_typed::<MessagePushed>(...)` and
17//! marshals each record into the plugin's WIT struct. The
18//! Rust type's location here is purely for in-process Rust
19//! consumers; plugins never import this module directly.
20
21use lattice_grammar::EchoLevel;
22
23/// One historical minibuffer echo. The renderer's
24/// `*messages*` buffer renders these in chronological order;
25/// each `set_message` call appends one record. Cheap to clone
26/// (bounded text) -- producers fan the same record out to
27/// every subscriber on the typed event bus.
28#[derive(Debug, Clone)]
29pub struct MessageRecord {
30    pub timestamp: std::time::SystemTime,
31    pub level: EchoLevel,
32    pub text: String,
33}
34
35/// Bounded chronological ring of every echo the editor has
36/// emitted. Push on every `set_message`; snapshot on demand
37/// for `:messages` open and live refresh. Capacity is fixed
38/// at construction; once full, the oldest entry drops to make
39/// room for the newest.
40#[derive(Debug, Clone)]
41pub struct MessagesRing {
42    records: std::collections::VecDeque<MessageRecord>,
43    capacity: usize,
44}
45
46impl MessagesRing {
47    /// Construct an empty ring with the given capacity. `0`
48    /// is normalised via `max(1)` so push never silently
49    /// drops every record.
50    pub fn with_capacity(capacity: usize) -> Self {
51        Self {
52            records: std::collections::VecDeque::new(),
53            capacity: capacity.max(1),
54        }
55    }
56
57    /// Append a record. Evicts the oldest entry when at
58    /// capacity. O(1) amortized; the ring is bounded so a
59    /// flood of messages stays at a fixed memory ceiling.
60    pub fn push(&mut self, record: MessageRecord) {
61        if self.records.len() == self.capacity {
62            self.records.pop_front();
63        }
64        self.records.push_back(record);
65    }
66
67    /// Borrow the records in chronological order (oldest
68    /// first). Renderers walk this for the transcript view.
69    pub fn records(&self) -> &std::collections::VecDeque<MessageRecord> {
70        &self.records
71    }
72
73    pub fn len(&self) -> usize {
74        self.records.len()
75    }
76
77    pub fn is_empty(&self) -> bool {
78        self.records.is_empty()
79    }
80
81    pub fn capacity(&self) -> usize {
82        self.capacity
83    }
84}
85
86impl Default for MessagesRing {
87    fn default() -> Self {
88        // 2000 records is plenty for typical sessions: at an
89        // average ~200 bytes per record the worst-case
90        // footprint is ~400 KB. Users can review a morning's
91        // worth of LSP progress + diagnostics without
92        // hitting the wrap.
93        Self::with_capacity(2000)
94    }
95}
96
97/// Typed event published on the editor's event bus whenever
98/// `set_message` runs. Carries the appended record (cloned
99/// from the ring). Subscribers see every echo in arrival
100/// order; the renderer's `*messages*` buffer live tail, the
101/// plugin host's WIT bridge, future telemetry hooks, etc.
102/// are all peer subscribers with no privileged path.
103#[derive(Debug, Clone)]
104pub struct MessagePushed {
105    pub record: MessageRecord,
106}
107
108lattice_protocol::register_event!(
109    MessagePushed,
110    "ui.message-pushed",
111    "Fired when the editor pushes a new minibuffer echo / notification \
112     onto the messages ring. Subscribers receive one event per \
113     `set_message` call regardless of renderer; the bounded ring on \
114     `App.messages` is the source of truth for replay.",
115    "lattice-runtime",
116);
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn ring_push_appends_in_order_and_caps_capacity() {
124        let mut ring = MessagesRing::with_capacity(3);
125        for i in 0..5 {
126            ring.push(MessageRecord {
127                timestamp: std::time::SystemTime::now(),
128                level: EchoLevel::Info,
129                text: format!("m{i}"),
130            });
131        }
132        assert_eq!(ring.len(), 3);
133        let texts: Vec<&str> = ring.records().iter().map(|r| r.text.as_str()).collect();
134        assert_eq!(texts, vec!["m2", "m3", "m4"]);
135    }
136
137    #[test]
138    fn ring_default_has_nonzero_capacity() {
139        let r = MessagesRing::default();
140        assert!(r.capacity() >= 2000);
141        assert!(r.is_empty());
142    }
143
144    #[test]
145    fn ring_with_zero_capacity_normalises_to_one() {
146        let mut r = MessagesRing::with_capacity(0);
147        assert_eq!(r.capacity(), 1);
148        r.push(MessageRecord {
149            timestamp: std::time::SystemTime::now(),
150            level: EchoLevel::Warn,
151            text: "x".into(),
152        });
153        assert_eq!(r.len(), 1);
154    }
155}