vtcode_core/tools/exec_session/
output_buffer.rs1use super::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum BackgroundShortcutResult {
8 Requested,
10 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 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}