tmai_core/runtime/mod.rs
1//! Runtime adapter abstraction for tmux decoupling.
2//!
3//! Provides [`RuntimeAdapter`] trait that abstracts agent discovery, observation,
4//! and control. Two implementations:
5//! - [`TmuxAdapter`]: wraps `TmuxClient` for traditional tmux-based operation
6//! - [`StandaloneAdapter`]: hook/IPC-only mode, no tmux required
7
8mod standalone;
9mod tmux_adapter;
10
11pub use standalone::StandaloneAdapter;
12pub use tmux_adapter::TmuxAdapter;
13
14use anyhow::Result;
15
16use crate::tmux::PaneInfo;
17
18/// Abstraction over the terminal multiplexer / process runtime.
19///
20/// Implementors provide agent discovery, screen observation, key control,
21/// and optional session management. Methods are sync because `TmuxClient`
22/// is subprocess-based (Poller already runs in a tokio blocking task).
23pub trait RuntimeAdapter: Send + Sync {
24 // =========================================================
25 // Discovery
26 // =========================================================
27
28 /// List all panes/agents across all sessions (including detached).
29 fn list_all_panes(&self) -> Result<Vec<PaneInfo>>;
30
31 /// List panes from attached sessions only.
32 fn list_panes(&self) -> Result<Vec<PaneInfo>>;
33
34 /// List session names.
35 fn list_sessions(&self) -> Result<Vec<String>>;
36
37 /// Check if the runtime backend is available and operational.
38 fn is_available(&self) -> bool;
39
40 // =========================================================
41 // Observation
42 // =========================================================
43
44 /// Capture pane content with ANSI escape codes (for preview rendering).
45 fn capture_pane(&self, target: &str) -> Result<String>;
46
47 /// Capture full scrollback with ANSI escape codes (all history).
48 fn capture_pane_full(&self, target: &str) -> Result<String> {
49 self.capture_pane(target)
50 }
51
52 /// Capture pane content as plain text (for detection analysis).
53 fn capture_pane_plain(&self, target: &str) -> Result<String>;
54
55 /// Get the title of a pane.
56 fn get_pane_title(&self, target: &str) -> Result<String>;
57
58 /// Get terminal cursor position (col, row) for a pane, both 0-indexed.
59 /// Returns None if the runtime does not support cursor queries.
60 fn get_cursor_position(&self, _target: &str) -> Result<Option<(u32, u32)>> {
61 Ok(None)
62 }
63
64 // =========================================================
65 // Control
66 // =========================================================
67
68 /// Send interpreted keys to a pane (e.g., "Enter", "C-c").
69 fn send_keys(&self, target: &str, keys: &str) -> Result<()>;
70
71 /// Send literal text to a pane (no key interpretation).
72 fn send_keys_literal(&self, target: &str, keys: &str) -> Result<()>;
73
74 /// Send literal text followed by Enter.
75 fn send_text_and_enter(&self, target: &str, text: &str) -> Result<()>;
76
77 // =========================================================
78 // Focus / Lifecycle
79 // =========================================================
80
81 /// Focus on a pane (bring to foreground in the runtime).
82 fn focus_pane(&self, target: &str) -> Result<()>;
83
84 /// Terminate a pane / agent process.
85 fn kill_pane(&self, target: &str) -> Result<()>;
86
87 // =========================================================
88 // Session Management (optional capabilities)
89 // =========================================================
90
91 /// Create a new session. Not all runtimes support this.
92 fn create_session(&self, _name: &str, _cwd: &str, _window_name: Option<&str>) -> Result<()> {
93 anyhow::bail!("session creation not supported by {} runtime", self.name())
94 }
95
96 /// Create a new window in a session. Returns the new pane target.
97 fn new_window(&self, _session: &str, _cwd: &str, _window_name: Option<&str>) -> Result<String> {
98 anyhow::bail!("window creation not supported by {} runtime", self.name())
99 }
100
101 /// Split a window to create a new pane. Returns the new pane target.
102 fn split_window(&self, _session: &str, _cwd: &str) -> Result<String> {
103 anyhow::bail!("window splitting not supported by {} runtime", self.name())
104 }
105
106 /// Split a window and apply tiled layout for balanced pane sizes.
107 fn split_window_tiled(&self, session: &str, cwd: &str) -> Result<String> {
108 // Default: fall back to regular split_window
109 self.split_window(session, cwd)
110 }
111
112 /// Apply a layout to the window containing the target (e.g. "tiled").
113 fn select_layout(&self, _target: &str, _layout: &str) -> Result<()> {
114 Ok(()) // no-op for runtimes that don't support layouts
115 }
116
117 /// Count panes in the window containing the target.
118 fn count_panes(&self, _target: &str) -> Result<usize> {
119 Ok(0)
120 }
121
122 /// Run a command in a pane (send text + Enter).
123 fn run_command(&self, _target: &str, _command: &str) -> Result<()> {
124 anyhow::bail!("command execution not supported by {} runtime", self.name())
125 }
126
127 /// Run a command wrapped with `tmai wrap` for PTY monitoring.
128 fn run_command_wrapped(&self, _target: &str, _command: &str) -> Result<()> {
129 anyhow::bail!("wrapped command not supported by {} runtime", self.name())
130 }
131
132 /// Get the user's current location (session name, window index).
133 fn get_current_location(&self) -> Result<(String, u32)> {
134 anyhow::bail!("current location not available in {} runtime", self.name())
135 }
136
137 // =========================================================
138 // Metadata
139 // =========================================================
140
141 /// Runtime name for logging and display (e.g., "tmux", "standalone").
142 fn name(&self) -> &str;
143}