ai_session/integration/
mod.rs1use anyhow::{Context, Result};
4use async_trait::async_trait;
5use std::collections::HashMap;
6use tokio::process::Command;
7
8use crate::core::{SessionConfig, SessionId};
9
10pub struct TmuxCompatLayer {
12 tmux_path: String,
14 session_prefix: String,
16}
17
18impl Default for TmuxCompatLayer {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl TmuxCompatLayer {
25 pub fn new() -> Self {
27 Self {
28 tmux_path: "tmux".to_string(),
29 session_prefix: "ai-session".to_string(),
30 }
31 }
32
33 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 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 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 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 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#[derive(Debug, Clone)]
137pub struct TmuxSession {
138 pub name: String,
140 pub created: i64,
142 pub attached: bool,
144}
145
146pub struct ScreenCompatLayer {
148 screen_path: String,
150}
151
152impl Default for ScreenCompatLayer {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158impl ScreenCompatLayer {
159 pub fn new() -> Self {
161 Self {
162 screen_path: "screen".to_string(),
163 }
164 }
165
166 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
186pub struct MigrationHelper {
188 tmux: TmuxCompatLayer,
190}
191
192impl Default for MigrationHelper {
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198impl MigrationHelper {
199 pub fn new() -> Self {
201 Self {
202 tmux: TmuxCompatLayer::new(),
203 }
204 }
205
206 pub async fn migrate_tmux_session(&self, tmux_name: &str) -> Result<MigrationResult> {
208 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 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 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#[derive(Debug)]
262pub struct MigrationResult {
263 pub session_name: String,
265 pub captured_output: String,
267 pub environment: HashMap<String, String>,
269 pub working_directory: String,
271}
272
273#[async_trait]
275pub trait ExternalIntegration: Send + Sync {
276 fn name(&self) -> &str;
278
279 async fn initialize(&mut self) -> Result<()>;
281
282 async fn on_session_created(&self, session_id: &SessionId) -> Result<()>;
284
285 async fn on_session_terminated(&self, session_id: &SessionId) -> Result<()>;
287
288 async fn export_session_data(&self, session_id: &SessionId) -> Result<serde_json::Value>;
290}
291
292pub struct VSCodeIntegration {
294 port: u16,
296}
297
298impl VSCodeIntegration {
299 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 Ok(())
314 }
315
316 async fn on_session_created(&self, session_id: &SessionId) -> Result<()> {
317 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 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}