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::{
32    is_cargo_command, is_cargo_command_string, is_development_toolchain_command,
33};
34#[cfg(feature = "tui")]
35pub use manager::PtyManager;
36#[cfg(feature = "tui")]
37pub use portable_pty::PtySize;
38#[cfg(feature = "tui")]
39pub use preview::PtyPreviewRenderer;
40#[cfg(feature = "tui")]
41pub use types::{PtyCommandRequest, PtyCommandResult, PtyOutputCallback};
42
43// ── Shared types (headless) ─────────────────────────────────────────────────
44
45#[cfg(not(feature = "tui"))]
46mod headless_pty {
47    use std::path::PathBuf;
48    use std::sync::Arc;
49    use std::time::Duration;
50
51    use anyhow::{Context, Result, anyhow};
52    use hashbrown::HashMap;
53
54    use crate::config::CommandsConfig;
55    use crate::config::PtyConfig;
56    use crate::tools::types::VTCodePtySession;
57    use crate::utils::path::ensure_path_within_workspace;
58    use crate::zsh_exec_bridge::ZshExecBridgeSession;
59
60    pub use portable_pty::PtySize;
61
62    pub type PtyOutputCallback = Arc<dyn Fn(&str) + Send + Sync>;
63
64    pub struct PtyCommandRequest {
65        pub command: Vec<String>,
66        pub working_dir: PathBuf,
67        pub timeout: Duration,
68        pub size: PtySize,
69        pub max_tokens: Option<usize>,
70        pub output_callback: Option<PtyOutputCallback>,
71    }
72
73    impl PtyCommandRequest {
74        pub fn with_streaming(
75            command: Vec<String>,
76            working_dir: PathBuf,
77            timeout: Duration,
78            callback: PtyOutputCallback,
79        ) -> Self {
80            Self {
81                command,
82                working_dir,
83                timeout,
84                size: PtySize {
85                    rows: 24,
86                    cols: 80,
87                    pixel_width: 0,
88                    pixel_height: 0,
89                },
90                max_tokens: None,
91                output_callback: Some(callback),
92            }
93        }
94    }
95
96    pub struct PtyCommandResult {
97        pub exit_code: i32,
98        pub output: String,
99        pub duration: Duration,
100        pub size: PtySize,
101        pub applied_max_tokens: Option<usize>,
102    }
103
104    /// No-op PTY manager for headless builds.
105    #[derive(Clone, Default)]
106    pub struct PtyManager {
107        workspace_root: PathBuf,
108        _config: PtyConfig,
109    }
110
111    impl PtyManager {
112        pub fn new(workspace_root: PathBuf, config: PtyConfig) -> Self {
113            Self {
114                workspace_root,
115                _config: config,
116            }
117        }
118
119        pub async fn resolve_working_dir(&self, working_dir: Option<&str>) -> Result<PathBuf> {
120            let requested = match working_dir {
121                Some(dir) if !dir.trim().is_empty() => dir.trim(),
122                _ => return Ok(self.workspace_root.clone()),
123            };
124
125            let candidate = self.workspace_root.join(requested);
126            let normalized = ensure_path_within_workspace(&candidate, &self.workspace_root)
127                .map_err(|_| {
128                    anyhow!(
129                        "Working directory '{}' escapes the workspace root",
130                        candidate.display()
131                    )
132                })?;
133            let metadata = tokio::fs::metadata(&normalized).await.with_context(|| {
134                format!(
135                    "Working directory '{}' does not exist",
136                    normalized.display()
137                )
138            })?;
139            if !metadata.is_dir() {
140                return Err(anyhow!(
141                    "Working directory '{}' is not a directory",
142                    normalized.display()
143                ));
144            }
145            Ok(normalized)
146        }
147
148        pub fn apply_commands_config(&self, _commands_config: &CommandsConfig) {}
149
150        pub(crate) fn create_session_with_bridge(
151            &self,
152            _session_id: String,
153            _command: Vec<String>,
154            _working_dir: PathBuf,
155            _size: PtySize,
156            _extra_env: HashMap<String, String>,
157            _zsh_exec_bridge: Option<ZshExecBridgeSession>,
158        ) -> Result<VTCodePtySession> {
159            Err(anyhow!("PTY support disabled in headless build"))
160        }
161
162        pub fn snapshot_session(&self, _session_id: &str) -> Result<VTCodePtySession> {
163            Err(anyhow!("PTY support disabled in headless build"))
164        }
165
166        pub fn read_session_output(
167            &self,
168            _session_id: &str,
169            _drain: bool,
170        ) -> Result<Option<String>> {
171            Err(anyhow!("PTY support disabled in headless build"))
172        }
173
174        pub fn send_input_to_session(
175            &self,
176            _session_id: &str,
177            _data: &[u8],
178            _append_newline: bool,
179        ) -> Result<usize> {
180            Err(anyhow!("PTY support disabled in headless build"))
181        }
182
183        pub fn is_session_completed(&self, _session_id: &str) -> Result<Option<i32>> {
184            Err(anyhow!("PTY support disabled in headless build"))
185        }
186
187        pub fn terminate_session(&self, _session_id: &str) -> Result<()> {
188            Err(anyhow!("PTY support disabled in headless build"))
189        }
190
191        pub fn close_session(&self, _session_id: &str) -> Result<VTCodePtySession> {
192            Err(anyhow!("PTY support disabled in headless build"))
193        }
194
195        pub fn terminate_all_sessions(&self) {}
196    }
197
198    #[derive(Clone, Default)]
199    pub struct PtyPreviewRenderer;
200
201    pub fn is_cargo_command(_command: &PtyCommandRequest) -> bool {
202        false
203    }
204
205    pub fn is_cargo_command_string(_command: &str) -> bool {
206        false
207    }
208
209    pub fn is_development_toolchain_command(_command: &str) -> bool {
210        false
211    }
212}
213
214#[cfg(not(feature = "tui"))]
215pub use headless_pty::*;