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