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 std::fmt::Debug for TmuxChannel {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 f.debug_struct("TmuxChannel")
24 .field("has_transport", &self.transport.is_some())
25 .finish()
26 }
27}
28
29impl TmuxChannel {
30 pub fn with_transport(transport: Arc<crate::channels::transport::Transport>) -> Self {
31 Self {
32 transport: Some(transport),
33 }
34 }
35}
36
37#[async_trait]
38impl Channel for TmuxChannel {
39 fn name(&self) -> &'static str {
40 "tmux"
41 }
42
43 async fn start(&self) -> anyhow::Result<tokio::sync::mpsc::Receiver<IncomingMessage>> {
44 let (_tx, rx) = tokio::sync::mpsc::channel(64);
45 tracing::info!("tmux channel started");
46
47 if self.transport.is_none() {
50 tracing::warn!("tmux channel started without transport - output streaming disabled");
51 }
52
53 Ok(rx)
54 }
55
56 async fn send(&self, msg: &OutgoingMessage) -> anyhow::Result<()> {
57 let session = &msg.thread_id; send_keys(session, &msg.body).await
60 }
61
62 async fn health_check(&self) -> anyhow::Result<()> {
63 let output = tokio::process::Command::new("tmux")
65 .args(["list-sessions"])
66 .output_with_context()
67 .await?;
68 if !output.status.success() {
69 anyhow::bail!("tmux server not running");
70 }
71 Ok(())
72 }
73}
74
75pub async fn send_keys(session: &str, text: &str) -> anyhow::Result<()> {
77 let output = tokio::process::Command::new("tmux")
78 .args(["send-keys", "-t", session, text, "Enter"])
79 .output_with_context()
80 .await?;
81 if !output.status.success() {
82 let stderr = String::from_utf8_lossy(&output.stderr);
83 anyhow::bail!("tmux send-keys failed: {stderr}");
84 }
85 Ok(())
86}
87
88pub async fn capture_pane(session: &str) -> anyhow::Result<String> {
90 let output = tokio::process::Command::new("tmux")
91 .args(["capture-pane", "-t", session, "-p", "-S", "-"])
92 .output_with_context()
93 .await?;
94 if !output.status.success() {
95 let stderr = String::from_utf8_lossy(&output.stderr);
96 anyhow::bail!("tmux capture-pane failed: {stderr}");
97 }
98 Ok(String::from_utf8_lossy(&output.stdout).to_string())
99}
100
101pub async fn list_orch_sessions() -> anyhow::Result<Vec<String>> {
103 let output = tokio::process::Command::new("tmux")
104 .args(["list-sessions", "-F", "#{session_name}"])
105 .output_with_context()
106 .await?;
107 if !output.status.success() {
108 return Ok(vec![]);
109 }
110 Ok(String::from_utf8_lossy(&output.stdout)
111 .lines()
112 .filter(|s| s.starts_with("orch-"))
113 .map(String::from)
114 .collect())
115}
116
117pub async fn is_session_dead(session: &str) -> bool {
123 let output = tokio::process::Command::new("tmux")
124 .args(["has-session", "-t", session])
125 .output_with_context()
126 .await;
127
128 match output {
129 Ok(output) => !output.status.success(),
130 Err(_) => true,
131 }
132}
133
134