1use anyhow::Result;
9use parking_lot::RwLock;
10use std::collections::HashMap;
11use std::sync::atomic::{AtomicU32, Ordering};
12use std::sync::Arc;
13
14use super::RuntimeAdapter;
15use crate::tmux::PaneInfo;
16
17pub struct StandaloneAdapter {
23 agents: Arc<RwLock<HashMap<String, PaneInfo>>>,
25 next_id: AtomicU32,
27}
28
29impl StandaloneAdapter {
30 pub fn new() -> Self {
32 Self {
33 agents: Arc::new(RwLock::new(HashMap::new())),
34 next_id: AtomicU32::new(1),
35 }
36 }
37
38 pub fn register_agent(
43 &self,
44 session_id: &str,
45 cwd: &str,
46 title: &str,
47 command: &str,
48 pid: u32,
49 ) -> String {
50 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
51 let target = format!("standalone:0.{}", id);
52 let pane_id = id.to_string();
53
54 let pane = PaneInfo {
55 target: target.clone(),
56 session: "standalone".to_string(),
57 window_index: 0,
58 pane_index: id,
59 pane_id,
60 window_name: command.to_string(),
61 command: command.to_string(),
62 pid,
63 title: title.to_string(),
64 cwd: cwd.to_string(),
65 };
66
67 let mut agents = self.agents.write();
68 agents.insert(session_id.to_string(), pane);
69 target
70 }
71
72 pub fn unregister_agent(&self, session_id: &str) {
74 let mut agents = self.agents.write();
75 agents.remove(session_id);
76 }
77
78 pub fn update_agent(&self, session_id: &str, cwd: Option<&str>, title: Option<&str>) {
80 let mut agents = self.agents.write();
81 if let Some(pane) = agents.get_mut(session_id) {
82 if let Some(cwd) = cwd {
83 pane.cwd = cwd.to_string();
84 }
85 if let Some(title) = title {
86 pane.title = title.to_string();
87 }
88 }
89 }
90
91 pub fn target_for_session(&self, session_id: &str) -> Option<String> {
93 let agents = self.agents.read();
94 agents.get(session_id).map(|p| p.target.clone())
95 }
96
97 pub fn agent_count(&self) -> usize {
99 self.agents.read().len()
100 }
101}
102
103impl Default for StandaloneAdapter {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl RuntimeAdapter for StandaloneAdapter {
110 fn list_all_panes(&self) -> Result<Vec<PaneInfo>> {
113 let agents = self.agents.read();
114 Ok(agents.values().cloned().collect())
115 }
116
117 fn list_panes(&self) -> Result<Vec<PaneInfo>> {
118 self.list_all_panes()
120 }
121
122 fn list_sessions(&self) -> Result<Vec<String>> {
123 let agents = self.agents.read();
124 if agents.is_empty() {
125 Ok(vec![])
126 } else {
127 Ok(vec!["standalone".to_string()])
128 }
129 }
130
131 fn is_available(&self) -> bool {
132 true
134 }
135
136 fn capture_pane(&self, _target: &str) -> Result<String> {
139 Ok(String::new())
141 }
142
143 fn capture_pane_plain(&self, _target: &str) -> Result<String> {
144 Ok(String::new())
145 }
146
147 fn get_pane_title(&self, target: &str) -> Result<String> {
148 let agents = self.agents.read();
149 for pane in agents.values() {
150 if pane.target == target {
151 return Ok(pane.title.clone());
152 }
153 }
154 Ok(String::new())
155 }
156
157 fn send_keys(&self, _target: &str, _keys: &str) -> Result<()> {
160 anyhow::bail!("send_keys not available in standalone mode (use IPC)")
163 }
164
165 fn send_keys_literal(&self, _target: &str, _keys: &str) -> Result<()> {
166 anyhow::bail!("send_keys_literal not available in standalone mode (use IPC)")
167 }
168
169 fn send_text_and_enter(&self, _target: &str, _text: &str) -> Result<()> {
170 anyhow::bail!("send_text_and_enter not available in standalone mode (use IPC)")
171 }
172
173 fn focus_pane(&self, _target: &str) -> Result<()> {
176 anyhow::bail!("focus_pane not available in standalone mode")
177 }
178
179 fn kill_pane(&self, _target: &str) -> Result<()> {
180 anyhow::bail!("kill_pane not available in standalone mode")
181 }
182
183 fn name(&self) -> &str {
186 "standalone"
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_standalone_creation() {
196 let adapter = StandaloneAdapter::new();
197 assert_eq!(adapter.name(), "standalone");
198 assert!(adapter.is_available());
199 }
200
201 #[test]
202 fn test_register_and_list() {
203 let adapter = StandaloneAdapter::new();
204
205 let target =
206 adapter.register_agent("sess-1", "/home/user/project", "Working", "claude", 1234);
207 assert!(target.starts_with("standalone:0."));
208
209 let panes = adapter.list_all_panes().unwrap();
210 assert_eq!(panes.len(), 1);
211 assert_eq!(panes[0].cwd, "/home/user/project");
212 assert_eq!(panes[0].command, "claude");
213 assert_eq!(panes[0].pid, 1234);
214 }
215
216 #[test]
217 fn test_register_multiple_and_unregister() {
218 let adapter = StandaloneAdapter::new();
219
220 adapter.register_agent("sess-1", "/path/a", "A", "claude", 100);
221 adapter.register_agent("sess-2", "/path/b", "B", "codex", 200);
222 assert_eq!(adapter.agent_count(), 2);
223
224 adapter.unregister_agent("sess-1");
225 assert_eq!(adapter.agent_count(), 1);
226
227 let panes = adapter.list_all_panes().unwrap();
228 assert_eq!(panes[0].command, "codex");
229 }
230
231 #[test]
232 fn test_update_agent() {
233 let adapter = StandaloneAdapter::new();
234 adapter.register_agent("sess-1", "/old/path", "Old Title", "claude", 100);
235
236 adapter.update_agent("sess-1", Some("/new/path"), Some("New Title"));
237
238 let panes = adapter.list_all_panes().unwrap();
239 assert_eq!(panes[0].cwd, "/new/path");
240 assert_eq!(panes[0].title, "New Title");
241 }
242
243 #[test]
244 fn test_target_for_session() {
245 let adapter = StandaloneAdapter::new();
246 assert!(adapter.target_for_session("nonexistent").is_none());
247
248 let target = adapter.register_agent("sess-1", "/path", "T", "claude", 100);
249 assert_eq!(adapter.target_for_session("sess-1"), Some(target));
250 }
251
252 #[test]
253 fn test_observation_returns_empty() {
254 let adapter = StandaloneAdapter::new();
255 assert_eq!(adapter.capture_pane("standalone:0.1").unwrap(), "");
256 assert_eq!(adapter.capture_pane_plain("standalone:0.1").unwrap(), "");
257 }
258
259 #[test]
260 fn test_control_methods_error() {
261 let adapter = StandaloneAdapter::new();
262 assert!(adapter.send_keys("standalone:0.1", "Enter").is_err());
263 assert!(adapter
264 .send_keys_literal("standalone:0.1", "hello")
265 .is_err());
266 assert!(adapter
267 .send_text_and_enter("standalone:0.1", "hello")
268 .is_err());
269 assert!(adapter.focus_pane("standalone:0.1").is_err());
270 assert!(adapter.kill_pane("standalone:0.1").is_err());
271 }
272
273 #[test]
274 fn test_session_management_not_supported() {
275 let adapter = StandaloneAdapter::new();
276 assert!(adapter.create_session("test", "/tmp", None).is_err());
277 assert!(adapter.new_window("test", "/tmp", None).is_err());
278 assert!(adapter.split_window("test", "/tmp").is_err());
279 assert!(adapter.get_current_location().is_err());
280 }
281
282 #[test]
283 fn test_list_sessions_empty_and_nonempty() {
284 let adapter = StandaloneAdapter::new();
285 assert!(adapter.list_sessions().unwrap().is_empty());
286
287 adapter.register_agent("sess-1", "/path", "T", "claude", 100);
288 let sessions = adapter.list_sessions().unwrap();
289 assert_eq!(sessions, vec!["standalone"]);
290 }
291
292 #[test]
293 fn test_get_pane_title_from_registry() {
294 let adapter = StandaloneAdapter::new();
295 let target = adapter.register_agent("sess-1", "/path", "My Title", "claude", 100);
296 assert_eq!(adapter.get_pane_title(&target).unwrap(), "My Title");
297 assert_eq!(adapter.get_pane_title("nonexistent:0.99").unwrap(), "");
298 }
299}