Skip to main content

ai_session/integration/
mod.rs

1//! Integration layers for compatibility with existing systems
2
3use anyhow::{Context, Result};
4use async_trait::async_trait;
5use std::collections::HashMap;
6use tokio::process::Command;
7
8use crate::core::{SessionConfig, SessionId};
9
10/// Tmux compatibility layer for migration support
11pub struct TmuxCompatLayer {
12    /// Tmux command path
13    tmux_path: String,
14    /// Session name prefix
15    session_prefix: String,
16}
17
18impl Default for TmuxCompatLayer {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl TmuxCompatLayer {
25    /// Create new tmux compatibility layer
26    pub fn new() -> Self {
27        Self {
28            tmux_path: "tmux".to_string(),
29            session_prefix: "ai-session".to_string(),
30        }
31    }
32
33    /// Convert AI session to tmux session
34    pub async fn create_tmux_session(
35        &self,
36        session_id: &SessionId,
37        config: &SessionConfig,
38    ) -> Result<String> {
39        let tmux_name = format!(
40            "{}-{}",
41            self.session_prefix,
42            session_id
43                .to_string()
44                .split('-')
45                .next()
46                .unwrap_or("unknown")
47        );
48
49        let mut cmd = Command::new(&self.tmux_path);
50        cmd.args(["new-session", "-d", "-s", &tmux_name]);
51
52        if let Some(shell) = &config.shell_command {
53            cmd.arg("-c")
54                .arg(config.working_directory.display().to_string());
55            cmd.arg(shell);
56        }
57
58        cmd.output()
59            .await
60            .context("Failed to create tmux session")?;
61
62        Ok(tmux_name)
63    }
64
65    /// List existing tmux sessions
66    pub async fn list_tmux_sessions(&self) -> Result<Vec<TmuxSession>> {
67        let output = Command::new(&self.tmux_path)
68            .args([
69                "list-sessions",
70                "-F",
71                "#{session_name}:#{session_created}:#{session_attached}",
72            ])
73            .output()
74            .await?;
75
76        if !output.status.success() {
77            return Ok(Vec::new());
78        }
79
80        let mut sessions = Vec::new();
81        let stdout = String::from_utf8_lossy(&output.stdout);
82
83        for line in stdout.lines() {
84            let parts: Vec<&str> = line.split(':').collect();
85            if parts.len() >= 3 {
86                sessions.push(TmuxSession {
87                    name: parts[0].to_string(),
88                    created: parts[1].parse().unwrap_or(0),
89                    attached: parts[2] == "1",
90                });
91            }
92        }
93
94        Ok(sessions)
95    }
96
97    /// Send command to tmux session
98    pub async fn send_command(&self, session_name: &str, command: &str) -> Result<()> {
99        Command::new(&self.tmux_path)
100            .args(["send-keys", "-t", session_name, command, "Enter"])
101            .output()
102            .await
103            .context("Failed to send command to tmux")?;
104
105        Ok(())
106    }
107
108    /// Capture tmux pane output
109    pub async fn capture_output(&self, session_name: &str, lines: Option<usize>) -> Result<String> {
110        let mut args = vec!["capture-pane", "-t", session_name, "-p"];
111
112        let line_arg;
113        if let Some(n) = lines {
114            args.push("-S");
115            line_arg = format!("-{}", n);
116            args.push(&line_arg);
117        }
118
119        let output = Command::new(&self.tmux_path).args(&args).output().await?;
120
121        Ok(String::from_utf8_lossy(&output.stdout).to_string())
122    }
123
124    /// Kill tmux session
125    pub async fn kill_session(&self, session_name: &str) -> Result<()> {
126        Command::new(&self.tmux_path)
127            .args(["kill-session", "-t", session_name])
128            .output()
129            .await?;
130
131        Ok(())
132    }
133}
134
135/// Tmux session information
136#[derive(Debug, Clone)]
137pub struct TmuxSession {
138    /// Session name
139    pub name: String,
140    /// Creation timestamp
141    pub created: i64,
142    /// Whether session is attached
143    pub attached: bool,
144}
145
146/// Screen compatibility layer (for legacy systems)
147pub struct ScreenCompatLayer {
148    /// Screen command path
149    screen_path: String,
150}
151
152impl Default for ScreenCompatLayer {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl ScreenCompatLayer {
159    /// Create new screen compatibility layer
160    pub fn new() -> Self {
161        Self {
162            screen_path: "screen".to_string(),
163        }
164    }
165
166    /// Create screen session
167    pub async fn create_screen_session(&self, session_id: &SessionId) -> Result<String> {
168        let screen_name = format!(
169            "ai-{}",
170            session_id
171                .to_string()
172                .split('-')
173                .next()
174                .unwrap_or("unknown")
175        );
176
177        Command::new(&self.screen_path)
178            .args(["-dmS", &screen_name])
179            .output()
180            .await?;
181
182        Ok(screen_name)
183    }
184}
185
186/// Migration helper for transitioning from tmux to native sessions
187pub struct MigrationHelper {
188    /// Tmux compatibility layer
189    tmux: TmuxCompatLayer,
190}
191
192impl Default for MigrationHelper {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198impl MigrationHelper {
199    /// Create new migration helper
200    pub fn new() -> Self {
201        Self {
202            tmux: TmuxCompatLayer::new(),
203        }
204    }
205
206    /// Migrate tmux session to AI session
207    pub async fn migrate_tmux_session(&self, tmux_name: &str) -> Result<MigrationResult> {
208        // Capture current state
209        let output = self.tmux.capture_output(tmux_name, Some(1000)).await?;
210        let env_vars = self.capture_tmux_environment(tmux_name).await?;
211        let working_dir = self.get_tmux_working_directory(tmux_name).await?;
212
213        Ok(MigrationResult {
214            session_name: tmux_name.to_string(),
215            captured_output: output,
216            environment: env_vars,
217            working_directory: working_dir,
218        })
219    }
220
221    /// Capture tmux environment variables
222    async fn capture_tmux_environment(
223        &self,
224        session_name: &str,
225    ) -> Result<HashMap<String, String>> {
226        let output = Command::new("tmux")
227            .args(["show-environment", "-t", session_name])
228            .output()
229            .await?;
230
231        let mut env_vars = HashMap::new();
232        let stdout = String::from_utf8_lossy(&output.stdout);
233
234        for line in stdout.lines() {
235            if let Some((key, value)) = line.split_once('=') {
236                env_vars.insert(key.to_string(), value.to_string());
237            }
238        }
239
240        Ok(env_vars)
241    }
242
243    /// Get tmux working directory
244    async fn get_tmux_working_directory(&self, session_name: &str) -> Result<String> {
245        let output = Command::new("tmux")
246            .args([
247                "display-message",
248                "-t",
249                session_name,
250                "-p",
251                "#{pane_current_path}",
252            ])
253            .output()
254            .await?;
255
256        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
257    }
258}
259
260/// Result of migrating a tmux session
261#[derive(Debug)]
262pub struct MigrationResult {
263    /// Original session name
264    pub session_name: String,
265    /// Captured output
266    pub captured_output: String,
267    /// Environment variables
268    pub environment: HashMap<String, String>,
269    /// Working directory
270    pub working_directory: String,
271}
272
273/// Integration with external tools
274#[async_trait]
275pub trait ExternalIntegration: Send + Sync {
276    /// Name of the integration
277    fn name(&self) -> &str;
278
279    /// Initialize the integration
280    async fn initialize(&mut self) -> Result<()>;
281
282    /// Handle session creation
283    async fn on_session_created(&self, session_id: &SessionId) -> Result<()>;
284
285    /// Handle session termination
286    async fn on_session_terminated(&self, session_id: &SessionId) -> Result<()>;
287
288    /// Export session data
289    async fn export_session_data(&self, session_id: &SessionId) -> Result<serde_json::Value>;
290}
291
292/// VS Code integration
293pub struct VSCodeIntegration {
294    /// Extension communication port
295    port: u16,
296}
297
298impl VSCodeIntegration {
299    /// Create new VS Code integration
300    pub fn new(port: u16) -> Self {
301        Self { port }
302    }
303}
304
305#[async_trait]
306impl ExternalIntegration for VSCodeIntegration {
307    fn name(&self) -> &str {
308        "vscode"
309    }
310
311    async fn initialize(&mut self) -> Result<()> {
312        // Initialize VS Code extension communication
313        Ok(())
314    }
315
316    async fn on_session_created(&self, session_id: &SessionId) -> Result<()> {
317        // Notify VS Code extension
318        tracing::info!("VS Code integration: session {} created", session_id);
319        Ok(())
320    }
321
322    async fn on_session_terminated(&self, session_id: &SessionId) -> Result<()> {
323        // Notify VS Code extension
324        tracing::info!("VS Code integration: session {} terminated", session_id);
325        Ok(())
326    }
327
328    async fn export_session_data(&self, session_id: &SessionId) -> Result<serde_json::Value> {
329        Ok(serde_json::json!({
330            "session_id": session_id.to_string(),
331            "integration": "vscode",
332            "port": self.port,
333        }))
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_tmux_compat_layer() {
343        let tmux = TmuxCompatLayer::new();
344        assert_eq!(tmux.session_prefix, "ai-session");
345    }
346
347    #[tokio::test]
348    async fn test_vscode_integration() {
349        let mut vscode = VSCodeIntegration::new(3000);
350        assert_eq!(vscode.name(), "vscode");
351        vscode.initialize().await.unwrap();
352    }
353}