feat: implement resumable Claude sessions with background execution (#93)

This comprehensive update adds support for resumable Claude Code sessions that can run in the background and be resumed later. Key improvements include:

Backend enhancements:
- Enhanced ProcessRegistry to track both agent runs and Claude sessions
- Added new Tauri commands: list_running_claude_sessions, get_claude_session_output
- Improved Claude process management with proper session ID extraction and lifecycle handling
- Enhanced cancellation logic with registry-based process killing and fallback mechanisms
- Added live output capture and storage for session persistence

Frontend improvements:
- New RunningClaudeSessions component to display and manage active sessions
- Added streaming state management and session tracking in App component
- Implemented navigation protection when Claude is actively streaming
- Enhanced ClaudeCodeSession component with streaming callbacks and session management

Configuration updates:
- Updated .gitignore to exclude documentation files (AGENTS.md, CLAUDE.md, *_TASK.md)

This feature enables users to start Claude sessions, navigate away while Claude continues processing, and resume sessions later from the Projects view, significantly improving the user experience for long-running AI interactions.
This commit is contained in:
Mufeed VH
2025-07-02 18:07:14 +05:30
committed by GitHub
parent 4fb6fd5159
commit e8c54d7fad
9 changed files with 637 additions and 100 deletions

View File

@@ -4,12 +4,23 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::process::Child;
/// Type of process being tracked
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProcessType {
AgentRun {
agent_id: i64,
agent_name: String,
},
ClaudeSession {
session_id: String,
},
}
/// Information about a running agent process
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessInfo {
pub run_id: i64,
pub agent_id: i64,
pub agent_name: String,
pub process_type: ProcessType,
pub pid: u32,
pub started_at: DateTime<Utc>,
pub project_path: String,
@@ -28,16 +39,26 @@ pub struct ProcessHandle {
/// Registry for tracking active agent processes
pub struct ProcessRegistry {
processes: Arc<Mutex<HashMap<i64, ProcessHandle>>>, // run_id -> ProcessHandle
next_id: Arc<Mutex<i64>>, // Auto-incrementing ID for non-agent processes
}
impl ProcessRegistry {
pub fn new() -> Self {
Self {
processes: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(Mutex::new(1000000)), // Start at high number to avoid conflicts
}
}
/// Register a new running process
/// Generate a unique ID for non-agent processes
pub fn generate_id(&self) -> Result<i64, String> {
let mut next_id = self.next_id.lock().map_err(|e| e.to_string())?;
let id = *next_id;
*next_id += 1;
Ok(id)
}
/// Register a new running agent process
pub fn register_process(
&self,
run_id: i64,
@@ -49,12 +70,9 @@ impl ProcessRegistry {
model: String,
child: Child,
) -> Result<(), String> {
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
let process_info = ProcessInfo {
run_id,
agent_id,
agent_name,
process_type: ProcessType::AgentRun { agent_id, agent_name },
pid,
started_at: Utc::now(),
project_path,
@@ -62,6 +80,52 @@ impl ProcessRegistry {
model,
};
self.register_process_internal(run_id, process_info, child)
}
/// Register a new Claude session (without child process - handled separately)
pub fn register_claude_session(
&self,
session_id: String,
pid: u32,
project_path: String,
task: String,
model: String,
) -> Result<i64, String> {
let run_id = self.generate_id()?;
let process_info = ProcessInfo {
run_id,
process_type: ProcessType::ClaudeSession { session_id },
pid,
started_at: Utc::now(),
project_path,
task,
model,
};
// Register without child - Claude sessions use ClaudeProcessState for process management
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
let process_handle = ProcessHandle {
info: process_info,
child: Arc::new(Mutex::new(None)), // No child handle for Claude sessions
live_output: Arc::new(Mutex::new(String::new())),
};
processes.insert(run_id, process_handle);
Ok(run_id)
}
/// Internal method to register any process
fn register_process_internal(
&self,
run_id: i64,
process_info: ProcessInfo,
child: Child,
) -> Result<(), String> {
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
let process_handle = ProcessHandle {
info: process_info,
child: Arc::new(Mutex::new(Some(child))),
@@ -72,6 +136,34 @@ impl ProcessRegistry {
Ok(())
}
/// Get all running Claude sessions
pub fn get_running_claude_sessions(&self) -> Result<Vec<ProcessInfo>, String> {
let processes = self.processes.lock().map_err(|e| e.to_string())?;
Ok(processes
.values()
.filter_map(|handle| {
match &handle.info.process_type {
ProcessType::ClaudeSession { .. } => Some(handle.info.clone()),
_ => None,
}
})
.collect())
}
/// Get a specific Claude session by session ID
pub fn get_claude_session_by_id(&self, session_id: &str) -> Result<Option<ProcessInfo>, String> {
let processes = self.processes.lock().map_err(|e| e.to_string())?;
Ok(processes
.values()
.find(|handle| {
match &handle.info.process_type {
ProcessType::ClaudeSession { session_id: sid } => sid == session_id,
_ => false,
}
})
.map(|handle| handle.info.clone()))
}
/// Unregister a process (called when it completes)
#[allow(dead_code)]
pub fn unregister_process(&self, run_id: i64) -> Result<(), String> {
@@ -90,6 +182,20 @@ impl ProcessRegistry {
.collect())
}
/// Get all running agent processes
pub fn get_running_agent_processes(&self) -> Result<Vec<ProcessInfo>, String> {
let processes = self.processes.lock().map_err(|e| e.to_string())?;
Ok(processes
.values()
.filter_map(|handle| {
match &handle.info.process_type {
ProcessType::AgentRun { .. } => Some(handle.info.clone()),
_ => None,
}
})
.collect())
}
/// Get a specific running process
#[allow(dead_code)]
pub fn get_process(&self, run_id: i64) -> Result<Option<ProcessInfo>, String> {