Skip to main content

vtcode_core/tools/exec_session/
output_buffer.rs

1//! Bounded pipe-output buffering: head/tail windows, stats, retained previews.
2
3use super::*;
4
5/// Result of the Ctrl+B foreground-session handoff request.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum BackgroundShortcutResult {
8    /// A live foreground session was reserved for background promotion.
9    Requested,
10    /// The runtime already owns the maximum number of background processes.
11    AtCapacity,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub(crate) struct PipeOutputStats {
16    pub total_bytes: u64,
17    pub truncated: bool,
18    pub spool_path: String,
19    pub spool_available: bool,
20    pub spool_complete: bool,
21    pub spool_integrity: Option<SpoolIntegrity>,
22}
23
24#[derive(Default)]
25pub(crate) struct PipeOutputBuffer {
26    pub(crate) pending: Mutex<PipeOutputWindow>,
27    total_bytes: AtomicU64,
28    pub(crate) truncated: AtomicBool,
29}
30
31#[derive(Default)]
32pub(crate) struct PipeOutputWindow {
33    head: String,
34    tail: String,
35    total_bytes: u64,
36    pub(crate) truncated: bool,
37}
38
39impl PipeOutputBuffer {
40    pub(crate) async fn append(&self, chunk: &str, raw_byte_count: usize) {
41        let mut pending = self.pending.lock().await;
42        pending.total_bytes = pending.total_bytes.saturating_add(raw_byte_count as u64);
43        self.total_bytes.fetch_add(raw_byte_count as u64, Ordering::Relaxed);
44        if pending.head.len() < PIPE_OUTPUT_HEAD_BYTES {
45            let remaining = PIPE_OUTPUT_HEAD_BYTES - pending.head.len();
46            let end = chunk.floor_char_boundary(remaining.min(chunk.len()));
47            pending.head.push_str(&chunk[..end]);
48            if end < chunk.len() {
49                pending.truncated = true;
50                self.truncated.store(true, Ordering::Relaxed);
51            }
52        }
53
54        // Operate on `pending.tail` in place. The previous code cloned the tail,
55        // mutated the clone, then assigned it back — an O(tail) copy on every
56        // output chunk. Since `pending` is already a &mut MutexGuard, in-place
57        // mutation is equivalent and avoids the per-chunk allocation.
58        pending.tail.push_str(chunk);
59        if pending.tail.len() > PIPE_OUTPUT_TAIL_BYTES {
60            let start = pending.tail.ceil_char_boundary(pending.tail.len() - PIPE_OUTPUT_TAIL_BYTES);
61            pending.tail.drain(..start);
62            pending.truncated = true;
63            self.truncated.store(true, Ordering::Relaxed);
64        }
65    }
66
67    pub(crate) async fn peek_pending(&self) -> Option<String> {
68        let pending = self.pending.lock().await;
69        if pending.total_bytes == 0 {
70            None
71        } else {
72            Some(pending.preview())
73        }
74    }
75
76    pub(crate) async fn drain_pending(&self) -> Option<String> {
77        let mut pending = self.pending.lock().await;
78        if pending.total_bytes == 0 {
79            None
80        } else {
81            Some(std::mem::take(&mut *pending).preview())
82        }
83    }
84
85    pub(crate) async fn stats(&self) -> (u64, bool) {
86        (self.total_bytes.load(Ordering::Relaxed), self.truncated.load(Ordering::Relaxed))
87    }
88}
89
90impl PipeOutputWindow {
91    pub(crate) fn preview(&self) -> String {
92        if !self.truncated {
93            return self.head.clone();
94        }
95        if self.head == self.tail {
96            return format!("{}\n[output preview truncated]", self.head);
97        }
98        format!("{}\n[output preview truncated]\n{}", self.head, self.tail)
99    }
100}
101
102#[derive(Clone, Default)]
103pub(crate) struct RetainedSessionPreview {
104    head: String,
105    tail: String,
106    total_bytes: u64,
107    pub(crate) truncated: bool,
108}
109
110impl RetainedSessionPreview {
111    pub(crate) fn append(&mut self, chunk: &str) {
112        if chunk.is_empty() {
113            return;
114        }
115
116        self.total_bytes = self.total_bytes.saturating_add(chunk.len() as u64);
117        if self.head.len() < EXEC_SESSION_PREVIEW_HEAD_BYTES {
118            let remaining = EXEC_SESSION_PREVIEW_HEAD_BYTES - self.head.len();
119            let end = chunk.floor_char_boundary(remaining.min(chunk.len()));
120            self.head.push_str(&chunk[..end]);
121            if end < chunk.len() {
122                self.truncated = true;
123            }
124        }
125
126        self.tail.push_str(chunk);
127        if self.tail.len() > EXEC_SESSION_PREVIEW_TAIL_BYTES {
128            let start = self.tail.ceil_char_boundary(self.tail.len() - EXEC_SESSION_PREVIEW_TAIL_BYTES);
129            self.tail.drain(..start);
130            self.truncated = true;
131        }
132    }
133
134    pub(crate) fn render(&self) -> String {
135        if self.total_bytes == 0 {
136            return String::new();
137        }
138        if !self.truncated {
139            return self.head.clone();
140        }
141        if self.head == self.tail {
142            return format!("{}\n[output preview truncated]", self.head);
143        }
144        format!("{}\n[output preview truncated]\n{}", self.head, self.tail)
145    }
146}