1use super::{Channel, IncomingMessage, OutgoingMessage};
12use crate::cmd::CommandErrorContext;
13use async_trait::async_trait;
14use std::sync::Arc;
15
16pub struct TmuxChannel {
17 transport: Option<Arc<crate::channels::transport::Transport>>,
19}
20
21impl TmuxChannel {
22 pub fn with_transport(transport: Arc<crate::channels::transport::Transport>) -> Self {
23 Self {
24 transport: Some(transport),
25 }
26 }
27}
28
29#[async_trait]
30impl Channel for TmuxChannel {
31 fn name(&self) -> &str {
32 "tmux"
33 }
34
35 async fn start(&self) -> anyhow::Result<tokio::sync::mpsc::Receiver<IncomingMessage>> {
36 let (_tx, rx) = tokio::sync::mpsc::channel(64);
37 tracing::info!("tmux channel started");
38
39 if self.transport.is_none() {
42 tracing::warn!("tmux channel started without transport - output streaming disabled");
43 }
44
45 Ok(rx)
46 }
47
48 async fn send(&self, msg: &OutgoingMessage) -> anyhow::Result<()> {
49 let session = &msg.thread_id; send_keys(session, &msg.body).await
52 }
53
54 async fn health_check(&self) -> anyhow::Result<()> {
55 let output = tokio::process::Command::new("tmux")
57 .args(["list-sessions"])
58 .output_with_context()
59 .await?;
60 if !output.status.success() {
61 anyhow::bail!("tmux server not running");
62 }
63 Ok(())
64 }
65}
66
67pub async fn send_keys(session: &str, text: &str) -> anyhow::Result<()> {
69 let output = tokio::process::Command::new("tmux")
70 .args(["send-keys", "-t", session, text, "Enter"])
71 .output_with_context()
72 .await?;
73 if !output.status.success() {
74 let stderr = String::from_utf8_lossy(&output.stderr);
75 anyhow::bail!("tmux send-keys failed: {stderr}");
76 }
77 Ok(())
78}
79
80pub async fn capture_pane(session: &str) -> anyhow::Result<String> {
82 let output = tokio::process::Command::new("tmux")
83 .args(["capture-pane", "-t", session, "-p", "-S", "-"])
84 .output_with_context()
85 .await?;
86 if !output.status.success() {
87 let stderr = String::from_utf8_lossy(&output.stderr);
88 anyhow::bail!("tmux capture-pane failed: {stderr}");
89 }
90 Ok(String::from_utf8_lossy(&output.stdout).to_string())
91}
92
93pub async fn list_orch_sessions() -> anyhow::Result<Vec<String>> {
95 let output = tokio::process::Command::new("tmux")
96 .args(["list-sessions", "-F", "#{session_name}"])
97 .output_with_context()
98 .await?;
99 if !output.status.success() {
100 return Ok(vec![]);
101 }
102 Ok(String::from_utf8_lossy(&output.stdout)
103 .lines()
104 .filter(|s| s.starts_with("orch-"))
105 .map(String::from)
106 .collect())
107}
108
109pub async fn is_session_dead(session: &str) -> bool {
115 let output = tokio::process::Command::new("tmux")
116 .args(["has-session", "-t", session])
117 .output_with_context()
118 .await;
119
120 match output {
121 Ok(output) => !output.status.success(),
122 Err(_) => true,
123 }
124}
125
126