1#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum PtyCloseMode {
61 Graceful,
62 Immediate,
63}
64
65#[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 #[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::*;