Skip to main content

vtcode_core/tools/
pty.rs

1//! PTY session management.
2//!
3//! When the `tui` feature is enabled, the full PTY implementation is compiled
4//! in from submodules.  When disabled, a no-op `PtyManager` stub and shared
5//! types are provided so callers compile without feature-gating every call.
6
7// ── Submodules (TUI only) ───────────────────────────────────────────────────
8
9#[cfg(feature = "tui")]
10mod command_utils;
11#[cfg(feature = "tui")]
12mod formatting;
13#[cfg(feature = "tui")]
14mod manager;
15#[cfg(feature = "tui")]
16mod manager_utils;
17#[cfg(feature = "tui")]
18mod preview;
19#[cfg(feature = "tui")]
20mod screen_backend;
21#[cfg(feature = "tui")]
22mod scrollback;
23#[cfg(feature = "tui")]
24mod session;
25#[cfg(feature = "tui")]
26mod types;
27
28// ── Re-exports (TUI) ───────────────────────────────────────────────────────
29
30#[cfg(feature = "tui")]
31pub use command_utils::{is_cargo_command, is_cargo_command_string, is_development_toolchain_command};
32#[cfg(feature = "tui")]
33pub use manager::PtyManager;
34#[cfg(feature = "tui")]
35pub use portable_pty::PtySize;
36#[cfg(feature = "tui")]
37pub use preview::PtyPreviewRenderer;
38#[cfg(feature = "tui")]
39pub use types::{PtyCommandRequest, PtyCommandResult, PtyOutputCallback};
40
41/// Bounded-memory metadata for a PTY session's complete output spool.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub(crate) struct PtyOutputStats {
44    pub total_bytes: u64,
45    pub truncated: bool,
46    pub spool_path: String,
47    pub spool_available: bool,
48    pub spool_complete: bool,
49    pub spool_integrity: Option<crate::tools::output_spooler::SpoolIntegrity>,
50}
51
52/// Termination behavior used when closing a PTY session.
53///
54/// `Graceful` writes `exit\n` and gives the child a SIGTERM grace window so
55/// in-session closes let it clean up. `Immediate` goes straight to a process-
56/// group SIGKILL: user-initiated program exit must not park the terminal on a
57/// 500 ms grace window per live session when the OS reaps any remainder at
58/// process exit anyway.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum PtyCloseMode {
61    Graceful,
62    Immediate,
63}
64
65// ── Shared types (headless) ─────────────────────────────────────────────────
66
67#[cfg(not(feature = "tui"))]
68mod headless_pty {
69    use std::path::PathBuf;
70    use std::sync::Arc;
71    use std::time::Duration;
72
73    use anyhow::{Context, Result, anyhow};
74    use hashbrown::HashMap;
75
76    use crate::config::CommandsConfig;
77    use crate::config::PtyConfig;
78    use crate::tools::types::VTCodePtySession;
79    use crate::utils::path::ensure_path_within_workspace;
80    use crate::zsh_exec_bridge::ZshExecBridgeSession;
81
82    use super::PtyCloseMode;
83
84    pub use portable_pty::PtySize;
85
86    pub type PtyOutputCallback = Arc<dyn Fn(&str) + Send + Sync>;
87
88    pub struct PtyCommandRequest {
89        pub command: Vec<String>,
90        pub working_dir: PathBuf,
91        pub timeout: Duration,
92        pub size: PtySize,
93        pub max_tokens: Option<usize>,
94        pub output_callback: Option<PtyOutputCallback>,
95    }
96
97    impl PtyCommandRequest {
98        pub fn with_streaming(
99            command: Vec<String>,
100            working_dir: PathBuf,
101            timeout: Duration,
102            callback: PtyOutputCallback,
103        ) -> Self {
104            Self {
105                command,
106                working_dir,
107                timeout,
108                size: PtySize {
109                    rows: 24,
110                    cols: 80,
111                    pixel_width: 0,
112                    pixel_height: 0,
113                },
114                max_tokens: None,
115                output_callback: Some(callback),
116            }
117        }
118    }
119
120    pub struct PtyCommandResult {
121        pub exit_code: i32,
122        pub output: String,
123        pub duration: Duration,
124        pub size: PtySize,
125        pub applied_max_tokens: Option<usize>,
126    }
127
128    /// No-op PTY manager for headless builds.
129    #[derive(Clone, Default)]
130    pub struct PtyManager {
131        workspace_root: PathBuf,
132        _config: PtyConfig,
133    }
134
135    impl PtyManager {
136        pub fn new(workspace_root: PathBuf, config: PtyConfig) -> Self {
137            Self { workspace_root, _config: config }
138        }
139
140        pub async fn resolve_working_dir(&self, working_dir: Option<&str>) -> Result<PathBuf> {
141            let requested = match working_dir {
142                Some(dir) if !dir.trim().is_empty() => dir.trim(),
143                _ => return Ok(self.workspace_root.clone()),
144            };
145
146            let candidate = self.workspace_root.join(requested);
147            let normalized = ensure_path_within_workspace(&candidate, &self.workspace_root)
148                .map_err(|_| anyhow!("Working directory '{}' escapes the workspace root", candidate.display()))?;
149            let metadata = tokio::fs::metadata(&normalized)
150                .await
151                .with_context(|| format!("Working directory '{}' does not exist", normalized.display()))?;
152            if !metadata.is_dir() {
153                return Err(anyhow!("Working directory '{}' is not a directory", normalized.display()));
154            }
155            Ok(normalized)
156        }
157
158        pub fn apply_commands_config(&self, _commands_config: &CommandsConfig) {}
159
160        pub(crate) fn create_session_with_bridge(
161            &self,
162            _session_id: String,
163            _command: Vec<String>,
164            _working_dir: PathBuf,
165            _size: PtySize,
166            _extra_env: HashMap<String, String>,
167            _zsh_exec_bridge: Option<ZshExecBridgeSession>,
168        ) -> Result<VTCodePtySession> {
169            Err(anyhow!("PTY support disabled in headless build"))
170        }
171
172        pub fn snapshot_session(&self, _session_id: &str) -> Result<VTCodePtySession> {
173            Err(anyhow!("PTY support disabled in headless build"))
174        }
175
176        pub fn read_session_output(&self, _session_id: &str, _drain: bool) -> Result<Option<String>> {
177            Err(anyhow!("PTY support disabled in headless build"))
178        }
179
180        pub(crate) fn output_stats(&self, _session_id: &str) -> Result<Option<super::PtyOutputStats>> {
181            Ok(None)
182        }
183
184        pub fn send_input_to_session(&self, _session_id: &str, _data: &[u8], _append_newline: bool) -> Result<usize> {
185            Err(anyhow!("PTY support disabled in headless build"))
186        }
187
188        pub fn is_session_completed(&self, _session_id: &str) -> Result<Option<i32>> {
189            Err(anyhow!("PTY support disabled in headless build"))
190        }
191
192        pub fn terminate_session(&self, _session_id: &str) -> Result<()> {
193            Err(anyhow!("PTY support disabled in headless build"))
194        }
195
196        pub fn close_session(&self, _session_id: &str) -> Result<VTCodePtySession> {
197            Err(anyhow!("PTY support disabled in headless build"))
198        }
199
200        pub fn close_session_with_mode(&self, _session_id: &str, _mode: PtyCloseMode) -> Result<VTCodePtySession> {
201            Err(anyhow!("PTY support disabled in headless build"))
202        }
203
204        pub fn terminate_all_sessions(&self) {}
205
206        pub fn terminate_all_sessions_with_mode(&self, _mode: PtyCloseMode) {}
207    }
208
209    #[derive(Clone, Default)]
210    pub struct PtyPreviewRenderer;
211
212    pub fn is_cargo_command(_command: &PtyCommandRequest) -> bool {
213        false
214    }
215
216    pub fn is_cargo_command_string(_command: &str) -> bool {
217        false
218    }
219
220    pub fn is_development_toolchain_command(_command: &str) -> bool {
221        false
222    }
223}
224
225#[cfg(not(feature = "tui"))]
226pub use headless_pty::*;