vtcode_core/tools/pty/manager/
session_ops.rs1use super::super::PtyCloseMode;
2use super::super::formatting::{format_terminal_file, sanitize_session_id};
3use super::super::manager_utils::exit_status_code;
4use super::super::session::{CommandEchoState, PtySessionHandle};
5use super::PtyManager;
6use crate::tools::types::VTCodePtySession;
7use crate::utils::file_utils::{ensure_dir_exists, write_file_with_context};
8use anyhow::{Context, Result, anyhow};
9use portable_pty::PtySize;
10use std::io::Write;
11use std::sync::Arc;
12use tracing::warn;
13
14impl PtyManager {
15 pub fn list_sessions(&self) -> Vec<VTCodePtySession> {
16 let sessions = self.inner.sessions.lock();
17 sessions.values().map(|handle| handle.snapshot_metadata()).collect()
18 }
19
20 pub fn snapshot_session(&self, session_id: &str) -> Result<VTCodePtySession> {
21 let handle = self.session_handle(session_id)?;
22 Ok(handle.snapshot_metadata())
23 }
24
25 pub fn read_session_output(&self, session_id: &str, drain: bool) -> Result<Option<String>> {
26 let handle = self.session_handle(session_id)?;
27 Ok(handle.read_output(drain))
28 }
29
30 pub(crate) fn output_stats(&self, session_id: &str) -> Result<Option<super::super::PtyOutputStats>> {
31 let handle = self.session_handle(session_id)?;
32 let scrollback_truncated = handle.scrollback.lock().output_truncated();
33 let spool_ready = handle.output_spool_ready.load(std::sync::atomic::Ordering::Acquire);
34 let spool_finished = handle.output_spool_finished.load(std::sync::atomic::Ordering::Acquire);
35 let spool_failed = handle.output_spool_failed.load(std::sync::atomic::Ordering::Acquire);
36 Ok(Some(super::super::PtyOutputStats {
37 total_bytes: handle.output_total_bytes.load(std::sync::atomic::Ordering::Relaxed),
38 truncated: scrollback_truncated || spool_failed,
39 spool_path: handle.output_spool_path.clone(),
40 spool_available: spool_ready && !spool_failed,
41 spool_complete: spool_ready && spool_finished && !spool_failed,
42 spool_integrity: handle.output_spool_integrity.lock().clone(),
43 }))
44 }
45
46 pub fn is_output_drained(&self, session_id: &str) -> Result<bool> {
48 let handle = self.session_handle(session_id)?;
49 Ok(handle.is_output_drained())
50 }
51
52 pub fn send_input_to_session(&self, session_id: &str, data: &[u8], append_newline: bool) -> Result<usize> {
53 let handle = self.session_handle(session_id)?;
54
55 {
57 let mut last_input = handle.last_input.lock();
58 *last_input = if let Ok(input_text) = std::str::from_utf8(data) {
59 CommandEchoState::new(input_text, append_newline)
60 } else {
61 None
62 };
63 }
64
65 {
67 let mut writer_guard = handle.writer.lock();
68 let writer = writer_guard
69 .as_mut()
70 .ok_or_else(|| anyhow!("PTY session '{session_id}' is no longer writable"))?;
71
72 writer.write_all(data).context("failed to write input to PTY session")?;
73
74 if append_newline {
75 writer.write_all(b"\n").context("failed to write newline to PTY session")?;
76 }
77
78 writer.flush().context("failed to flush PTY session input")?;
79 }
80
81 let written = data.len() + if append_newline { 1 } else { 0 };
82 Ok(written)
83 }
84
85 pub fn resize_session(&self, session_id: &str, size: PtySize) -> Result<VTCodePtySession> {
86 let handle = self.session_handle(session_id)?;
87
88 {
89 let master = handle.master.lock();
90 master.resize(size).context("failed to resize PTY session")?;
91 }
92
93 {
94 let mut screen_state = handle.screen_state.lock();
95 screen_state.resize(size);
96 }
97
98 Ok(handle.snapshot_metadata())
99 }
100
101 pub fn is_session_completed(&self, session_id: &str) -> Result<Option<i32>> {
102 let handle = self.session_handle(session_id)?;
103 let mut child = handle.child.lock();
104 child
105 .try_wait()
106 .context("failed to poll PTY session status")
107 .map(|opt| opt.map(exit_status_code))
108 }
109
110 pub fn terminate_session(&self, session_id: &str) -> Result<()> {
111 let handle = self.session_handle(session_id)?;
112
113 {
114 let mut writer_guard = handle.writer.lock();
115 if let Some(mut writer) = writer_guard.take() {
116 let _ = writer.write_all(b"exit\n");
117 let _ = writer.flush();
118 }
119 }
120
121 handle.graceful_terminate();
122 Ok(())
123 }
124
125 pub fn force_terminate_session(&self, session_id: &str) -> Result<()> {
126 let handle = self.session_handle(session_id)?;
127 handle.force_terminate();
128 Ok(())
129 }
130
131 pub async fn sync_sessions_to_files(&self) -> Result<Vec<std::path::PathBuf>> {
138 let terminals_dir = self.workspace_root.join(".vtcode").join("terminals");
139 ensure_dir_exists(&terminals_dir)
140 .await
141 .with_context(|| format!("Failed to create terminals directory: {}", terminals_dir.display()))?;
142
143 let sessions = self.list_sessions();
144 let mut written_files = Vec::with_capacity(sessions.len());
145
146 for session in &sessions {
147 let output = match self.read_session_output(&session.id, false) {
148 Ok(Some(output)) => output,
149 Ok(None) => String::new(),
150 Err(_) => continue,
151 };
152
153 let content = format_terminal_file(session, &output);
154 let file_path = terminals_dir.join(format!("{}.txt", sanitize_session_id(&session.id)));
155
156 if let Err(e) = write_file_with_context(&file_path, &content, "terminal session file").await {
157 warn!(
158 session_id = %session.id,
159 error = %e,
160 "Failed to sync terminal session to file"
161 );
162 continue;
163 }
164
165 written_files.push(file_path);
166 }
167
168 let index_content = self.generate_terminals_index(&sessions);
170 let index_path = terminals_dir.join("INDEX.md");
171 write_file_with_context(&index_path, &index_content, "terminals index")
172 .await
173 .with_context(|| format!("Failed to write terminals index: {}", index_path.display()))?;
174
175 tracing::info!(sessions = sessions.len(), files = written_files.len(), "Synced terminal sessions to files");
176
177 Ok(written_files)
178 }
179
180 fn generate_terminals_index(&self, sessions: &[VTCodePtySession]) -> String {
182 let mut content = String::new();
183 content.push_str("# Terminal Sessions Index\n\n");
184 content.push_str("This file lists all active terminal sessions for dynamic discovery.\n");
185 content.push_str("Use `read_file` on individual session files for full output.\n\n");
186
187 if sessions.is_empty() {
188 content.push_str("*No active terminal sessions.*\n");
189 } else {
190 content.push_str(&format!("**Active Sessions**: {}\n\n", sessions.len()));
191 content.push_str("| Session ID | Command | Working Dir | Size |\n");
192 content.push_str("|------------|---------|-------------|------|\n");
193
194 for session in sessions {
195 let cwd = session.working_dir.as_deref().unwrap_or("-");
196 let cmd_truncated = vtcode_commons::formatting::truncate_byte_budget(&session.command, 22, "...");
197
198 content.push_str(&format!(
199 "| `{}` | {} | {} | {}x{} |\n",
200 session.id,
201 cmd_truncated.replace('|', "\\|"),
202 cwd.replace('|', "\\|"),
203 session.cols,
204 session.rows
205 ));
206 }
207
208 content.push_str("\n## Session Details\n\n");
209 for session in sessions {
210 content.push_str(&format!("### {}\n\n", session.id));
211 content.push_str(&format!("- **Command**: `{}`\n", session.command));
212 if !session.args.is_empty() {
213 content.push_str(&format!("- **Args**: {}\n", session.args.join(" ")));
214 }
215 if let Some(cwd) = &session.working_dir {
216 content.push_str(&format!("- **Working Dir**: {cwd}\n"));
217 }
218 content.push_str(&format!("- **Terminal Size**: {}x{}\n", session.cols, session.rows));
219 content
220 .push_str(&format!("- **File**: `.vtcode/terminals/{}.txt`\n\n", sanitize_session_id(&session.id)));
221 }
222 }
223
224 content.push_str("---\n");
225 content.push_str("*Generated automatically. Do not edit manually.*\n");
226
227 content
228 }
229
230 pub fn terminals_dir(&self) -> std::path::PathBuf {
232 self.workspace_root.join(".vtcode").join("terminals")
233 }
234
235 pub fn close_session(&self, session_id: &str) -> Result<VTCodePtySession> {
236 self.close_session_with_mode(session_id, PtyCloseMode::Graceful)
237 }
238
239 pub fn close_session_with_mode(&self, session_id: &str, mode: PtyCloseMode) -> Result<VTCodePtySession> {
240 let handle = {
242 let mut sessions = self.inner.sessions.lock();
243 sessions
244 .remove(session_id)
245 .ok_or_else(|| anyhow!("PTY session '{session_id}' not found"))?
246 };
247
248 if mode == PtyCloseMode::Graceful {
254 let mut writer_guard = handle.writer.lock();
255 if let Some(mut writer) = writer_guard.take() {
256 let _ = writer.write_all(b"exit\n");
257 let _ = writer.flush();
258 }
259 } else {
260 drop(handle.writer.lock().take());
261 }
262
263 match mode {
267 PtyCloseMode::Graceful => handle.graceful_terminate(),
268 PtyCloseMode::Immediate => handle.force_terminate(),
269 }
270
271 {
275 let mut thread_guard = handle.reader_thread.lock();
276 if let Some(reader_thread) = thread_guard.take() {
277 super::super::session::join_reader_thread_bounded(reader_thread);
278 }
279 }
280
281 Ok(handle.snapshot_metadata())
283 }
284
285 pub fn terminate_all_sessions(&self) {
286 self.terminate_all_sessions_with_mode(PtyCloseMode::Graceful);
287 }
288
289 pub fn terminate_all_sessions_with_mode(&self, mode: PtyCloseMode) {
290 let session_ids: Vec<String> = {
291 let sessions = self.inner.sessions.lock();
292 sessions.keys().cloned().collect()
293 };
294 for id in session_ids {
295 if let Err(e) = self.close_session_with_mode(&id, mode) {
296 warn!("Failed to close PTY session {}: {}", id, e);
297 }
298 }
299 }
300
301 fn session_handle(&self, session_id: &str) -> Result<Arc<PtySessionHandle>> {
302 let sessions = self.inner.sessions.lock();
303 sessions
304 .get(session_id)
305 .cloned()
306 .ok_or_else(|| anyhow!("PTY session '{session_id}' not found"))
307 }
308}