Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 95 additions & 197 deletions src/agents/claude.rs

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

mod claude;
mod opencode;
pub mod pty_manager;
pub mod pty_session;
mod session;
mod traits;
mod worktree;

pub use claude::{
attach_tmux_command, create_pr, dispatch_to_agent, dispatch_to_claude, is_tmux_session_running,
kill_agent, kill_tmux_session, launch_agent_interactive, list_all_tmux_sessions,
list_tmux_sessions, resume_monitoring_for_running_sessions, tmux_session_name, ClaudeCodeAgent,
create_pr, dispatch_to_agent, dispatch_to_claude, get_pty_session, is_pty_session_running,
kill_agent, launch_agent_interactive, launch_agent_pty, list_all_pty_sessions,
list_pty_sessions, pty_session_name, resume_monitoring_for_running_sessions, ClaudeCodeAgent,
};
pub use opencode::OpencodeAgent;
pub use pty_manager::{pty_manager, PtyManager};
pub use pty_session::PtySession;
pub use session::{AgentSession, AgentStats, AgentStatus, SessionManager};
pub use traits::{get_agent, CodingAgent};
pub use worktree::{
Expand Down
159 changes: 159 additions & 0 deletions src/agents/pty_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! PTY Manager for handling multiple PTY sessions.

use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, RwLock};

use super::pty_session::PtySession;

/// Global PTY manager singleton.
static PTY_MANAGER: std::sync::LazyLock<PtyManager> =
std::sync::LazyLock::new(PtyManager::new);

/// Get the global PTY manager instance.
pub fn pty_manager() -> &'static PtyManager {
&PTY_MANAGER
}

/// Manager for multiple PTY sessions.
pub struct PtyManager {
sessions: Arc<RwLock<HashMap<String, Arc<PtySession>>>>,
}

impl PtyManager {
/// Create a new PTY manager.
pub fn new() -> Self {
Self {
sessions: Arc::new(RwLock::new(HashMap::new())),
}
}

/// Spawn a new PTY session.
///
/// # Arguments
/// * `id` - Unique session identifier (e.g., "project-issue-42")
/// * `program` - The program to run (e.g., "claude" or "opencode")
/// * `args` - Arguments to pass to the program
/// * `working_dir` - Working directory for the command
/// * `rows` - Terminal height in rows
/// * `cols` - Terminal width in columns
pub fn spawn_session(
&self,
id: &str,
program: &str,
args: &[&str],
working_dir: &Path,
rows: u16,
cols: u16,
) -> Result<Arc<PtySession>, String> {
// Check if session already exists
{
let sessions = self.sessions.read().unwrap();
if let Some(existing) = sessions.get(id) {
if existing.is_running() {
return Err(format!("Session '{}' already exists and is running", id));
}
}
}

// Spawn the new session
let session = PtySession::spawn(id, program, args, working_dir, rows, cols)?;
let session = Arc::new(session);

// Store in map
{
let mut sessions = self.sessions.write().unwrap();
sessions.insert(id.to_string(), Arc::clone(&session));
}

Ok(session)
}

/// Get a session by ID.
pub fn get_session(&self, id: &str) -> Option<Arc<PtySession>> {
let sessions = self.sessions.read().unwrap();
sessions.get(id).cloned()
}

/// List all session IDs.
pub fn list_sessions(&self) -> Vec<String> {
let sessions = self.sessions.read().unwrap();
sessions.keys().cloned().collect()
}

/// List all running session IDs.
pub fn list_running_sessions(&self) -> Vec<String> {
let sessions = self.sessions.read().unwrap();
sessions
.iter()
.filter(|(_, session)| session.is_running())
.map(|(id, _)| id.clone())
.collect()
}

/// Check if a session exists and is running.
pub fn is_session_running(&self, id: &str) -> bool {
let sessions = self.sessions.read().unwrap();
sessions
.get(id)
.is_some_and(|session| session.is_running())
}

/// Kill a session by ID.
pub fn kill_session(&self, id: &str) -> bool {
let sessions = self.sessions.read().unwrap();
if let Some(session) = sessions.get(id) {
session.kill();
true
} else {
false
}
}

/// Remove a session from the manager (cleanup).
pub fn remove_session(&self, id: &str) -> Option<Arc<PtySession>> {
let mut sessions = self.sessions.write().unwrap();
sessions.remove(id)
}

/// Cleanup dead sessions from the manager.
pub fn cleanup_dead_sessions(&self) {
let mut sessions = self.sessions.write().unwrap();
sessions.retain(|_, session| session.is_running());
}

/// Get the number of active sessions.
pub fn session_count(&self) -> usize {
let sessions = self.sessions.read().unwrap();
sessions.len()
}

/// Get the number of running sessions.
pub fn running_session_count(&self) -> usize {
let sessions = self.sessions.read().unwrap();
sessions.values().filter(|s| s.is_running()).count()
}
}

impl Default for PtyManager {
fn default() -> Self {
Self::new()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_manager_creation() {
let manager = PtyManager::new();
assert_eq!(manager.session_count(), 0);
}

#[test]
fn test_list_sessions_empty() {
let manager = PtyManager::new();
assert!(manager.list_sessions().is_empty());
}
}
Loading