vtcode_core/utils/
transcript.rs1use once_cell::sync::Lazy;
2use parking_lot::RwLock;
3use std::cell::RefCell;
4use std::collections::VecDeque;
5use std::sync::Arc;
6
7use crate::ui::{InlineHandle, InlineMessageKind, InlineSegment, InlineTextStyle};
8pub use crate::utils::message_style::MessageStyle;
9
10const MAX_LINES: usize = 4000;
11const MAX_QUEUE_SIZE: usize = 100;
12
13static TRANSCRIPT: Lazy<RwLock<Vec<String>>> = Lazy::new(|| RwLock::new(Vec::new()));
14static INLINE_HANDLE: Lazy<RwLock<Option<Arc<InlineHandle>>>> = Lazy::new(|| RwLock::new(None));
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17enum TranscriptMode {
18 #[expect(dead_code)]
19 Normal,
20 Suppressed,
21}
22
23thread_local! {
24 static MODE_STACK: RefCell<Vec<TranscriptMode>> = const { RefCell::new(Vec::new()) };
25}
26
27fn is_suppressed() -> bool {
28 MODE_STACK.with(|stack| matches!(stack.borrow().last(), Some(TranscriptMode::Suppressed)))
29}
30
31struct SuspensionGuard {
32 active: bool,
33}
34
35impl SuspensionGuard {
36 fn new() -> Self {
37 MODE_STACK.with(|stack| stack.borrow_mut().push(TranscriptMode::Suppressed));
38 Self { active: true }
39 }
40}
41
42impl Drop for SuspensionGuard {
43 fn drop(&mut self) {
44 if !self.active {
45 return;
46 }
47 MODE_STACK.with(|stack| {
48 let mut stack = stack.borrow_mut();
49 match stack.pop() {
50 Some(TranscriptMode::Suppressed) | None => {}
51 Some(TranscriptMode::Normal) => {
52 debug_assert!(
53 false,
54 "transcript suspension stack corrupted: expected Suppressed"
55 );
56 }
57 };
58 });
59 self.active = false;
60 }
61}
62
63fn suspend() -> SuspensionGuard {
64 SuspensionGuard::new()
65}
66
67pub fn with_suppressed<F, R>(operation: F) -> R
68where
69 F: FnOnce() -> R,
70{
71 let guard = suspend();
72 let result = operation();
73 drop(guard);
74 result
75}
76
77#[derive(Clone, Debug)]
79struct QueuedMessage {
80 text: String,
81 kind: InlineMessageKind,
82 style: InlineTextStyle,
83}
84
85static MESSAGE_QUEUE: Lazy<RwLock<VecDeque<QueuedMessage>>> =
86 Lazy::new(|| RwLock::new(VecDeque::new()));
87
88pub fn append(line: &str) {
89 if is_suppressed() || line.trim().is_empty() {
90 return;
91 }
92 let mut log = TRANSCRIPT.write();
93 if log.len() == MAX_LINES {
94 let drop_count = MAX_LINES / 5;
95 log.drain(0..drop_count);
96 }
97 if log.last().is_some_and(|last| last == line) {
98 return;
99 }
100 log.push(line.to_string());
101}
102
103pub fn replace_last(count: usize, lines: &[String]) {
104 if is_suppressed() {
105 return;
106 }
107 let mut log = TRANSCRIPT.write();
108 let new_len = log.len().saturating_sub(count);
109 log.truncate(new_len);
110 for line in lines {
111 if log.len() == MAX_LINES {
112 let drop_count = MAX_LINES / 5;
113 log.drain(0..drop_count);
114 }
115 log.push(line.clone());
116 }
117}
118
119pub fn tail_matches(lines: &[String]) -> bool {
120 if lines.is_empty() {
121 return false;
122 }
123
124 let log = TRANSCRIPT.read();
125 if lines.len() > log.len() {
126 return false;
127 }
128
129 log[log.len() - lines.len()..]
130 .iter()
131 .zip(lines.iter())
132 .all(|(left, right)| left == right)
133}
134
135pub fn snapshot() -> Vec<String> {
136 TRANSCRIPT.read().clone()
137}
138
139pub fn len() -> usize {
140 TRANSCRIPT.read().len()
141}
142
143pub fn clear() {
144 TRANSCRIPT.write().clear();
145}
146
147pub fn set_inline_handle(handle: Arc<InlineHandle>) {
149 *INLINE_HANDLE.write() = Some(handle);
150}
151
152pub fn clear_inline_handle() {
154 *INLINE_HANDLE.write() = None;
155}
156
157fn message_kind(style: MessageStyle) -> InlineMessageKind {
159 style.message_kind()
160}
161
162pub fn enqueue_message(message: &str, style: MessageStyle) {
164 enqueue_message_with_kind(message, message_kind(style), InlineTextStyle::default())
165}
166
167pub fn enqueue_message_with_kind(
169 message: &str,
170 kind: InlineMessageKind,
171 text_style: InlineTextStyle,
172) {
173 if message.trim().is_empty() {
174 return;
175 }
176
177 let queued = QueuedMessage {
178 text: message.to_string(),
179 kind,
180 style: text_style,
181 };
182
183 {
185 let mut queue = MESSAGE_QUEUE.write();
186 if queue.len() >= MAX_QUEUE_SIZE {
187 queue.pop_front();
188 }
189 queue.push_back(queued);
190 }
191
192 let queue_read = MESSAGE_QUEUE.read();
195 if let Some(last) = queue_read.back() {
196 display_message_now(&last.text, last.kind, &last.style);
197 }
198
199 append(message);
201}
202
203#[cold]
205pub fn display_error(message: &str) {
206 enqueue_message(message, MessageStyle::Error);
207}
208
209pub fn display_info(message: &str) {
211 enqueue_message(message, MessageStyle::Info);
212}
213
214fn display_message_now(text: &str, kind: InlineMessageKind, style: &InlineTextStyle) {
216 if let Some(handle) = INLINE_HANDLE.read().as_ref() {
217 handle.append_line(
218 kind,
219 vec![InlineSegment {
220 text: text.to_string(),
221 style: Arc::new(style.clone()),
222 }],
223 );
224 }
225}
226
227pub fn enqueue(message: &str) {
229 enqueue_message(message, MessageStyle::Output);
230}
231
232pub fn display_immediate(message: &str, style: MessageStyle) {
234 display_message_now(message, message_kind(style), &InlineTextStyle::default());
235}
236
237pub fn get_queued_messages() -> Vec<String> {
239 MESSAGE_QUEUE
240 .read()
241 .iter()
242 .map(|m| m.text.clone())
243 .collect()
244}
245
246pub fn get_queued_messages_with_metadata() -> Vec<(String, InlineMessageKind)> {
248 MESSAGE_QUEUE
249 .read()
250 .iter()
251 .map(|m| (m.text.clone(), m.kind))
252 .collect()
253}
254
255pub fn clear_queue() {
257 MESSAGE_QUEUE.write().clear();
258}
259
260pub fn queue_len() -> usize {
262 MESSAGE_QUEUE.read().len()
263}
264
265pub fn replay_queued_messages() {
267 let messages: Vec<QueuedMessage> = MESSAGE_QUEUE.read().iter().cloned().collect();
268 for msg in messages {
269 display_message_now(&msg.text, msg.kind, &msg.style);
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn append_and_snapshot_store_lines() {
279 clear();
280 append("first");
281 append("second");
282 assert_eq!(len(), 2);
283 let snap = snapshot();
284 assert_eq!(snap, vec!["first".to_owned(), "second".to_owned()]);
285 clear();
286 }
287
288 #[test]
289 fn append_skips_adjacent_duplicate_lines() {
290 clear();
291 append("duplicate");
292 append("duplicate");
293 append("different");
294 append("duplicate");
295 let snap = snapshot();
296 assert_eq!(
297 snap,
298 vec![
299 "duplicate".to_owned(),
300 "different".to_owned(),
301 "duplicate".to_owned()
302 ]
303 );
304 clear();
305 }
306
307 #[test]
308 fn transcript_drops_oldest_chunk_when_full() {
309 clear();
310 for idx in 0..MAX_LINES {
311 append(&format!("line {idx}"));
312 }
313 assert_eq!(len(), MAX_LINES);
314 for extra in 0..10 {
315 append(&format!("extra {extra}"));
316 }
317 assert_eq!(len(), MAX_LINES - (MAX_LINES / 5) + 10);
318 let snap = snapshot();
319 assert_eq!(
320 snap.first().unwrap(),
321 &format!("line {}", MAX_LINES / 5).to_owned()
322 );
323 clear();
324 }
325
326 #[test]
327 fn message_queue_enqueue_and_retrieve() {
328 clear_queue();
329 assert_eq!(queue_len(), 0);
330
331 enqueue("first message");
332 enqueue_message("second message", MessageStyle::Info);
333
334 assert_eq!(queue_len(), 2);
335 let messages = get_queued_messages();
336 assert_eq!(messages, vec!["first message", "second message"]);
337
338 clear_queue();
339 assert_eq!(queue_len(), 0);
340 }
341
342 #[test]
343 fn message_queue_preserves_metadata() {
344 clear_queue();
345
346 enqueue_message("info message", MessageStyle::Info);
347 enqueue_message("error message", MessageStyle::Error);
348 enqueue_message("user message", MessageStyle::User);
349
350 let messages = get_queued_messages_with_metadata();
351 assert_eq!(messages.len(), 3);
352 assert_eq!(messages[0].1, InlineMessageKind::Info);
353 assert_eq!(messages[1].1, InlineMessageKind::Error);
354 assert_eq!(messages[2].1, InlineMessageKind::User);
355
356 clear_queue();
357 }
358
359 #[test]
360 fn message_queue_size_limit() {
361 clear_queue();
362
363 for i in 0..MAX_QUEUE_SIZE {
365 enqueue(&format!("message {}", i));
366 }
367 assert_eq!(queue_len(), MAX_QUEUE_SIZE);
368
369 enqueue("overflow message");
371 assert_eq!(queue_len(), MAX_QUEUE_SIZE);
372
373 let messages = get_queued_messages();
374 assert_eq!(messages.first().unwrap(), "message 1"); assert_eq!(messages.last().unwrap(), "overflow message");
376
377 clear_queue();
378 }
379
380 #[test]
381 fn suppressed_scope_skips_transcript_entries() {
382 clear();
383 with_suppressed(|| {
384 append("hidden");
385 });
386 append("visible");
387 let snap = snapshot();
388 assert_eq!(snap, vec!["visible".to_owned()]);
389 clear();
390 }
391}