feat(core): implement session isolation for agent and claude executions
- Add run_id/session_id based event isolation for concurrent executions - Enhance process registry with graceful shutdown and fallback kill methods - Implement session-specific event listeners in React components - Add proper process cleanup with timeout handling - Support both isolated and backward-compatible event emissions - Improve error handling and logging for process management This change prevents event crosstalk between multiple concurrent agent/claude sessions running simultaneously, ensuring proper isolation and user experience.
This commit is contained in:
@@ -1251,7 +1251,9 @@ pub async fn execute_agent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit the line to the frontend
|
// Emit the line to the frontend with run_id for isolation
|
||||||
|
let _ = app_handle.emit(&format!("agent-output:{}", run_id), &line);
|
||||||
|
// Also emit to the generic event for backward compatibility
|
||||||
let _ = app_handle.emit("agent-output", &line);
|
let _ = app_handle.emit("agent-output", &line);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1277,7 +1279,9 @@ pub async fn execute_agent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
error!("stderr[{}]: {}", error_count, line);
|
error!("stderr[{}]: {}", error_count, line);
|
||||||
// Emit error lines to the frontend
|
// Emit error lines to the frontend with run_id for isolation
|
||||||
|
let _ = app_handle_stderr.emit(&format!("agent-error:{}", run_id), &line);
|
||||||
|
// Also emit to the generic event for backward compatibility
|
||||||
let _ = app_handle_stderr.emit("agent-error", &line);
|
let _ = app_handle_stderr.emit("agent-error", &line);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1366,6 +1370,7 @@ pub async fn execute_agent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _ = app.emit("agent-complete", false);
|
let _ = app.emit("agent-complete", false);
|
||||||
|
let _ = app.emit(&format!("agent-complete:{}", run_id), false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1398,6 +1403,7 @@ pub async fn execute_agent(
|
|||||||
// Cleanup will be handled by the cleanup_finished_processes function
|
// Cleanup will be handled by the cleanup_finished_processes function
|
||||||
|
|
||||||
let _ = app.emit("agent-complete", true);
|
let _ = app.emit("agent-complete", true);
|
||||||
|
let _ = app.emit(&format!("agent-complete:{}", run_id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(run_id)
|
Ok(run_id)
|
||||||
@@ -1442,43 +1448,45 @@ pub async fn list_running_sessions(
|
|||||||
/// Kill a running agent session
|
/// Kill a running agent session
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn kill_agent_session(
|
pub async fn kill_agent_session(
|
||||||
|
app: AppHandle,
|
||||||
db: State<'_, AgentDb>,
|
db: State<'_, AgentDb>,
|
||||||
|
registry: State<'_, crate::process::ProcessRegistryState>,
|
||||||
run_id: i64,
|
run_id: i64,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
// First try to kill the process using system kill
|
info!("Attempting to kill agent session {}", run_id);
|
||||||
let pid_result = {
|
|
||||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
// First try to kill using the process registry
|
||||||
conn.query_row(
|
let killed_via_registry = match registry.0.kill_process(run_id).await {
|
||||||
"SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'",
|
Ok(success) => {
|
||||||
params![run_id],
|
if success {
|
||||||
|row| row.get::<_, Option<i64>>(0)
|
info!("Successfully killed process {} via registry", run_id);
|
||||||
)
|
true
|
||||||
.map_err(|e| e.to_string())?
|
} else {
|
||||||
|
warn!("Process {} not found in registry", run_id);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to kill process {} via registry: {}", run_id, e);
|
||||||
|
false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(pid) = pid_result {
|
// If registry kill didn't work, try fallback with PID from database
|
||||||
// Try to kill the process
|
if !killed_via_registry {
|
||||||
let kill_result = if cfg!(target_os = "windows") {
|
let pid_result = {
|
||||||
std::process::Command::new("taskkill")
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||||
.args(["/F", "/PID", &pid.to_string()])
|
conn.query_row(
|
||||||
.output()
|
"SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'",
|
||||||
} else {
|
params![run_id],
|
||||||
std::process::Command::new("kill")
|
|row| row.get::<_, Option<i64>>(0)
|
||||||
.args(["-TERM", &pid.to_string()])
|
)
|
||||||
.output()
|
.map_err(|e| e.to_string())?
|
||||||
};
|
};
|
||||||
|
|
||||||
match kill_result {
|
if let Some(pid) = pid_result {
|
||||||
Ok(output) => {
|
info!("Attempting fallback kill for PID {} from database", pid);
|
||||||
if output.status.success() {
|
let _ = registry.0.kill_process_by_pid(run_id, pid as u32)?;
|
||||||
info!("Successfully killed process {}", pid);
|
|
||||||
} else {
|
|
||||||
warn!("Kill command failed for PID {}: {}", pid, String::from_utf8_lossy(&output.stderr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to execute kill command for PID {}: {}", pid, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,7 +1497,10 @@ pub async fn kill_agent_session(
|
|||||||
params![run_id],
|
params![run_id],
|
||||||
).map_err(|e| e.to_string())?;
|
).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(updated > 0)
|
// Emit cancellation event with run_id for proper isolation
|
||||||
|
let _ = app.emit(&format!("agent-cancelled:{}", run_id), true);
|
||||||
|
|
||||||
|
Ok(updated > 0 || killed_via_registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the status of a specific agent session
|
/// Get the status of a specific agent session
|
||||||
|
@@ -9,6 +9,7 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||||||
use tokio::process::{Command, Child};
|
use tokio::process::{Command, Child};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid;
|
||||||
|
|
||||||
/// Global state to track current Claude process
|
/// Global state to track current Claude process
|
||||||
pub struct ClaudeProcessState {
|
pub struct ClaudeProcessState {
|
||||||
@@ -857,8 +858,8 @@ pub async fn resume_claude_code(
|
|||||||
|
|
||||||
/// Cancel the currently running Claude Code execution
|
/// Cancel the currently running Claude Code execution
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn cancel_claude_execution(app: AppHandle) -> Result<(), String> {
|
pub async fn cancel_claude_execution(app: AppHandle, session_id: Option<String>) -> Result<(), String> {
|
||||||
log::info!("Cancelling Claude Code execution");
|
log::info!("Cancelling Claude Code execution for session: {:?}", session_id);
|
||||||
|
|
||||||
let claude_state = app.state::<ClaudeProcessState>();
|
let claude_state = app.state::<ClaudeProcessState>();
|
||||||
let mut current_process = claude_state.current_process.lock().await;
|
let mut current_process = claude_state.current_process.lock().await;
|
||||||
@@ -872,9 +873,16 @@ pub async fn cancel_claude_execution(app: AppHandle) -> Result<(), String> {
|
|||||||
match child.kill().await {
|
match child.kill().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!("Successfully killed Claude process");
|
log::info!("Successfully killed Claude process");
|
||||||
// Emit cancellation event
|
|
||||||
|
// If we have a session ID, emit session-specific events
|
||||||
|
if let Some(sid) = session_id {
|
||||||
|
let _ = app.emit(&format!("claude-cancelled:{}", sid), true);
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
let _ = app.emit(&format!("claude-complete:{}", sid), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also emit generic events for backward compatibility
|
||||||
let _ = app.emit("claude-cancelled", true);
|
let _ = app.emit("claude-cancelled", true);
|
||||||
// Also emit complete with false to indicate failure
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
let _ = app.emit("claude-complete", false);
|
let _ = app.emit("claude-complete", false);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1055,6 +1063,15 @@ fn get_claude_settings_sync(_app: &AppHandle) -> Result<ClaudeSettings, String>
|
|||||||
async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> {
|
async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> {
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
|
|
||||||
|
// Generate a unique session ID for this Claude Code session
|
||||||
|
let session_id = format!("claude-{}-{}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis(),
|
||||||
|
uuid::Uuid::new_v4().to_string()
|
||||||
|
);
|
||||||
|
|
||||||
// Spawn the process
|
// Spawn the process
|
||||||
let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?;
|
let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?;
|
||||||
|
|
||||||
@@ -1064,36 +1081,47 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), St
|
|||||||
|
|
||||||
// Get the child PID for logging
|
// Get the child PID for logging
|
||||||
let pid = child.id();
|
let pid = child.id();
|
||||||
log::info!("Spawned Claude process with PID: {:?}", pid);
|
log::info!("Spawned Claude process with PID: {:?} and session ID: {}", pid, session_id);
|
||||||
|
|
||||||
// Create readers
|
// Create readers
|
||||||
let stdout_reader = BufReader::new(stdout);
|
let stdout_reader = BufReader::new(stdout);
|
||||||
let stderr_reader = BufReader::new(stderr);
|
let stderr_reader = BufReader::new(stderr);
|
||||||
|
|
||||||
// Store the child process in the global state
|
// Store the child process in the global state (for backward compatibility)
|
||||||
let claude_state = app.state::<ClaudeProcessState>();
|
let claude_state = app.state::<ClaudeProcessState>();
|
||||||
{
|
{
|
||||||
let mut current_process = claude_state.current_process.lock().await;
|
let mut current_process = claude_state.current_process.lock().await;
|
||||||
|
// If there's already a process running, kill it first
|
||||||
|
if let Some(mut existing_child) = current_process.take() {
|
||||||
|
log::warn!("Killing existing Claude process before starting new one");
|
||||||
|
let _ = existing_child.kill().await;
|
||||||
|
}
|
||||||
*current_process = Some(child);
|
*current_process = Some(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn tasks to read stdout and stderr
|
// Spawn tasks to read stdout and stderr
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
let session_id_clone = session_id.clone();
|
||||||
let stdout_task = tokio::spawn(async move {
|
let stdout_task = tokio::spawn(async move {
|
||||||
let mut lines = stdout_reader.lines();
|
let mut lines = stdout_reader.lines();
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
while let Ok(Some(line)) = lines.next_line().await {
|
||||||
log::debug!("Claude stdout: {}", line);
|
log::debug!("Claude stdout: {}", line);
|
||||||
// Emit the line to the frontend
|
// Emit the line to the frontend with session isolation
|
||||||
|
let _ = app_handle.emit(&format!("claude-output:{}", session_id_clone), &line);
|
||||||
|
// Also emit to the generic event for backward compatibility
|
||||||
let _ = app_handle.emit("claude-output", &line);
|
let _ = app_handle.emit("claude-output", &line);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let app_handle_stderr = app.clone();
|
let app_handle_stderr = app.clone();
|
||||||
|
let session_id_clone2 = session_id.clone();
|
||||||
let stderr_task = tokio::spawn(async move {
|
let stderr_task = tokio::spawn(async move {
|
||||||
let mut lines = stderr_reader.lines();
|
let mut lines = stderr_reader.lines();
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
while let Ok(Some(line)) = lines.next_line().await {
|
||||||
log::error!("Claude stderr: {}", line);
|
log::error!("Claude stderr: {}", line);
|
||||||
// Emit error lines to the frontend
|
// Emit error lines to the frontend with session isolation
|
||||||
|
let _ = app_handle_stderr.emit(&format!("claude-error:{}", session_id_clone2), &line);
|
||||||
|
// Also emit to the generic event for backward compatibility
|
||||||
let _ = app_handle_stderr.emit("claude-error", &line);
|
let _ = app_handle_stderr.emit("claude-error", &line);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1101,6 +1129,7 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), St
|
|||||||
// Wait for the process to complete
|
// Wait for the process to complete
|
||||||
let app_handle_wait = app.clone();
|
let app_handle_wait = app.clone();
|
||||||
let claude_state_wait = claude_state.current_process.clone();
|
let claude_state_wait = claude_state.current_process.clone();
|
||||||
|
let session_id_clone3 = session_id.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = stdout_task.await;
|
let _ = stdout_task.await;
|
||||||
let _ = stderr_task.await;
|
let _ = stderr_task.await;
|
||||||
@@ -1113,12 +1142,16 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), St
|
|||||||
log::info!("Claude process exited with status: {}", status);
|
log::info!("Claude process exited with status: {}", status);
|
||||||
// Add a small delay to ensure all messages are processed
|
// Add a small delay to ensure all messages are processed
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
let _ = app_handle_wait.emit(&format!("claude-complete:{}", session_id_clone3), status.success());
|
||||||
|
// Also emit to the generic event for backward compatibility
|
||||||
let _ = app_handle_wait.emit("claude-complete", status.success());
|
let _ = app_handle_wait.emit("claude-complete", status.success());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to wait for Claude process: {}", e);
|
log::error!("Failed to wait for Claude process: {}", e);
|
||||||
// Add a small delay to ensure all messages are processed
|
// Add a small delay to ensure all messages are processed
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
let _ = app_handle_wait.emit(&format!("claude-complete:{}", session_id_clone3), false);
|
||||||
|
// Also emit to the generic event for backward compatibility
|
||||||
let _ = app_handle_wait.emit("claude-complete", false);
|
let _ = app_handle_wait.emit("claude-complete", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1128,6 +1161,9 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), St
|
|||||||
*current_process = None;
|
*current_process = None;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Return the session ID to the frontend
|
||||||
|
let _ = app.emit(&format!("claude-session-started:{}", session_id), session_id.clone());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -94,29 +94,176 @@ impl ProcessRegistry {
|
|||||||
Ok(processes.get(&run_id).map(|handle| handle.info.clone()))
|
Ok(processes.get(&run_id).map(|handle| handle.info.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kill a running process
|
/// Kill a running process with proper cleanup
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn kill_process(&self, run_id: i64) -> Result<bool, String> {
|
pub async fn kill_process(&self, run_id: i64) -> Result<bool, String> {
|
||||||
let processes = self.processes.lock().map_err(|e| e.to_string())?;
|
use log::{info, warn, error};
|
||||||
|
|
||||||
if let Some(handle) = processes.get(&run_id) {
|
// First check if the process exists and get its PID
|
||||||
let child_arc = handle.child.clone();
|
let (pid, child_arc) = {
|
||||||
drop(processes); // Release the lock before async operation
|
let processes = self.processes.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(handle) = processes.get(&run_id) {
|
||||||
|
(handle.info.pid, handle.child.clone())
|
||||||
|
} else {
|
||||||
|
return Ok(false); // Process not found
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Attempting graceful shutdown of process {} (PID: {})", run_id, pid);
|
||||||
|
|
||||||
|
// Send kill signal to the process
|
||||||
|
let kill_sent = {
|
||||||
let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?;
|
let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?;
|
||||||
if let Some(ref mut child) = child_guard.as_mut() {
|
if let Some(child) = child_guard.as_mut() {
|
||||||
match child.kill().await {
|
match child.start_kill() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
*child_guard = None; // Clear the child handle
|
info!("Successfully sent kill signal to process {}", run_id);
|
||||||
Ok(true)
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to send kill signal to process {}: {}", run_id, e);
|
||||||
|
return Err(format!("Failed to kill process: {}", e));
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!("Failed to kill process: {}", e)),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(false) // Process was already killed or completed
|
false // Process already killed
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !kill_sent {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the process to exit (with timeout)
|
||||||
|
let wait_result = tokio::time::timeout(
|
||||||
|
tokio::time::Duration::from_secs(5),
|
||||||
|
async {
|
||||||
|
loop {
|
||||||
|
// Check if process has exited
|
||||||
|
let status = {
|
||||||
|
let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(child) = child_guard.as_mut() {
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(status)) => {
|
||||||
|
info!("Process {} exited with status: {:?}", run_id, status);
|
||||||
|
*child_guard = None; // Clear the child handle
|
||||||
|
Some(Ok::<(), String>(()))
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Still running
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error checking process status: {}", e);
|
||||||
|
Some(Err(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Process already gone
|
||||||
|
Some(Ok(()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Some(result) => return result,
|
||||||
|
None => {
|
||||||
|
// Still running, wait a bit
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match wait_result {
|
||||||
|
Ok(Ok(_)) => {
|
||||||
|
info!("Process {} exited gracefully", run_id);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
error!("Error waiting for process {}: {}", run_id, e);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Process {} didn't exit within 5 seconds after kill", run_id);
|
||||||
|
// Force clear the handle
|
||||||
|
if let Ok(mut child_guard) = child_arc.lock() {
|
||||||
|
*child_guard = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from registry after killing
|
||||||
|
self.unregister_process(run_id)?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kill a process by PID using system commands (fallback method)
|
||||||
|
pub fn kill_process_by_pid(&self, run_id: i64, pid: u32) -> Result<bool, String> {
|
||||||
|
use log::{info, warn, error};
|
||||||
|
|
||||||
|
info!("Attempting to kill process {} by PID {}", run_id, pid);
|
||||||
|
|
||||||
|
let kill_result = if cfg!(target_os = "windows") {
|
||||||
|
std::process::Command::new("taskkill")
|
||||||
|
.args(["/F", "/PID", &pid.to_string()])
|
||||||
|
.output()
|
||||||
} else {
|
} else {
|
||||||
Ok(false) // Process not found
|
// First try SIGTERM
|
||||||
|
let term_result = std::process::Command::new("kill")
|
||||||
|
.args(["-TERM", &pid.to_string()])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match &term_result {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
info!("Sent SIGTERM to PID {}", pid);
|
||||||
|
// Give it 2 seconds to exit gracefully
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
|
|
||||||
|
// Check if still running
|
||||||
|
let check_result = std::process::Command::new("kill")
|
||||||
|
.args(["-0", &pid.to_string()])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if let Ok(output) = check_result {
|
||||||
|
if output.status.success() {
|
||||||
|
// Still running, send SIGKILL
|
||||||
|
warn!("Process {} still running after SIGTERM, sending SIGKILL", pid);
|
||||||
|
std::process::Command::new("kill")
|
||||||
|
.args(["-KILL", &pid.to_string()])
|
||||||
|
.output()
|
||||||
|
} else {
|
||||||
|
term_result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
term_result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// SIGTERM failed, try SIGKILL directly
|
||||||
|
warn!("SIGTERM failed for PID {}, trying SIGKILL", pid);
|
||||||
|
std::process::Command::new("kill")
|
||||||
|
.args(["-KILL", &pid.to_string()])
|
||||||
|
.output()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match kill_result {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
info!("Successfully killed process with PID {}", pid);
|
||||||
|
// Remove from registry
|
||||||
|
self.unregister_process(run_id)?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
let error_msg = String::from_utf8_lossy(&output.stderr);
|
||||||
|
warn!("Failed to kill PID {}: {}", pid, error_msg);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to execute kill command for PID {}: {}", pid, e);
|
||||||
|
Err(format!("Failed to execute kill command: {}", e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -268,6 +268,8 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
|||||||
const handleExecute = async () => {
|
const handleExecute = async () => {
|
||||||
if (!projectPath || !task.trim()) return;
|
if (!projectPath || !task.trim()) return;
|
||||||
|
|
||||||
|
let runId: number | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -277,8 +279,11 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
|||||||
setElapsedTime(0);
|
setElapsedTime(0);
|
||||||
setTotalTokens(0);
|
setTotalTokens(0);
|
||||||
|
|
||||||
// Set up event listeners
|
// Execute the agent with model override and get run ID
|
||||||
const outputUnlisten = await listen<string>("agent-output", (event) => {
|
runId = await api.executeAgent(agent.id!, projectPath, task, model);
|
||||||
|
|
||||||
|
// Set up event listeners with run ID isolation
|
||||||
|
const outputUnlisten = await listen<string>(`agent-output:${runId}`, (event) => {
|
||||||
try {
|
try {
|
||||||
// Store raw JSONL
|
// Store raw JSONL
|
||||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||||
@@ -291,12 +296,12 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorUnlisten = await listen<string>("agent-error", (event) => {
|
const errorUnlisten = await listen<string>(`agent-error:${runId}`, (event) => {
|
||||||
console.error("Agent error:", event.payload);
|
console.error("Agent error:", event.payload);
|
||||||
setError(event.payload);
|
setError(event.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const completeUnlisten = await listen<boolean>("agent-complete", (event) => {
|
const completeUnlisten = await listen<boolean>(`agent-complete:${runId}`, (event) => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
setExecutionStartTime(null);
|
setExecutionStartTime(null);
|
||||||
if (!event.payload) {
|
if (!event.payload) {
|
||||||
@@ -304,10 +309,13 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${runId}`, () => {
|
||||||
|
setIsRunning(false);
|
||||||
|
setExecutionStartTime(null);
|
||||||
|
setError("Agent execution was cancelled");
|
||||||
|
});
|
||||||
|
|
||||||
// Execute the agent with model override
|
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
|
||||||
await api.executeAgent(agent.id!, projectPath, task, model);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to execute agent:", err);
|
console.error("Failed to execute agent:", err);
|
||||||
setError("Failed to execute agent");
|
setError("Failed to execute agent");
|
||||||
|
@@ -176,13 +176,15 @@ export function AgentRunOutputViewer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setupLiveEventListeners = async () => {
|
const setupLiveEventListeners = async () => {
|
||||||
|
if (!run.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clean up existing listeners
|
// Clean up existing listeners
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
unlistenRefs.current = [];
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
// Set up live event listeners
|
// Set up live event listeners with run ID isolation
|
||||||
const outputUnlisten = await listen<string>("agent-output", (event) => {
|
const outputUnlisten = await listen<string>(`agent-output:${run.id}`, (event) => {
|
||||||
try {
|
try {
|
||||||
// Store raw JSONL
|
// Store raw JSONL
|
||||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||||
@@ -195,16 +197,20 @@ export function AgentRunOutputViewer({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorUnlisten = await listen<string>("agent-error", (event) => {
|
const errorUnlisten = await listen<string>(`agent-error:${run.id}`, (event) => {
|
||||||
console.error("Agent error:", event.payload);
|
console.error("Agent error:", event.payload);
|
||||||
setToast({ message: event.payload, type: 'error' });
|
setToast({ message: event.payload, type: 'error' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const completeUnlisten = await listen<boolean>("agent-complete", () => {
|
const completeUnlisten = await listen<boolean>(`agent-complete:${run.id}`, () => {
|
||||||
setToast({ message: 'Agent execution completed', type: 'success' });
|
setToast({ message: 'Agent execution completed', type: 'success' });
|
||||||
});
|
});
|
||||||
|
|
||||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${run.id}`, () => {
|
||||||
|
setToast({ message: 'Agent execution was cancelled', type: 'error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set up live event listeners:', error);
|
console.error('Failed to set up live event listeners:', error);
|
||||||
}
|
}
|
||||||
|
@@ -71,10 +71,8 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);
|
const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);
|
||||||
const [isFirstPrompt, setIsFirstPrompt] = useState(!session);
|
const [isFirstPrompt, setIsFirstPrompt] = useState(!session);
|
||||||
const [totalTokens, setTotalTokens] = useState(0);
|
const [totalTokens, setTotalTokens] = useState(0);
|
||||||
const [extractedSessionInfo, setExtractedSessionInfo] = useState<{
|
const [extractedSessionInfo, setExtractedSessionInfo] = useState<{ sessionId: string; projectId: string } | null>(null);
|
||||||
sessionId: string;
|
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);
|
||||||
projectId: string;
|
|
||||||
} | null>(null);
|
|
||||||
const [showTimeline, setShowTimeline] = useState(false);
|
const [showTimeline, setShowTimeline] = useState(false);
|
||||||
const [timelineVersion, setTimelineVersion] = useState(0);
|
const [timelineVersion, setTimelineVersion] = useState(0);
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
@@ -268,33 +266,39 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => {
|
const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => {
|
||||||
if (!projectPath || !prompt.trim() || isLoading) return;
|
console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath });
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
setError("Please select a project directory first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
hasActiveSessionRef.current = true;
|
hasActiveSessionRef.current = true;
|
||||||
|
|
||||||
// Add the user message immediately to the UI
|
// Clean up previous listeners
|
||||||
const userMessage: ClaudeStreamMessage = {
|
|
||||||
type: "user",
|
|
||||||
message: {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: prompt
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, userMessage]);
|
|
||||||
|
|
||||||
// Clean up any existing listeners before creating new ones
|
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
unlistenRefs.current = [];
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
// Set up event listeners
|
// Set up event listeners before executing
|
||||||
const outputUnlisten = await listen<string>("claude-output", async (event) => {
|
console.log('[ClaudeCodeSession] Setting up event listeners...');
|
||||||
|
|
||||||
|
// Listen for the session started event to get the Claude session ID
|
||||||
|
const sessionStartedUnlisten = await listen<string>(`claude-session-started:*`, (event) => {
|
||||||
|
const eventName = event.event;
|
||||||
|
const sessionId = eventName.split(':')[1];
|
||||||
|
if (sessionId && !claudeSessionId) {
|
||||||
|
console.log('[ClaudeCodeSession] Received Claude session ID:', sessionId);
|
||||||
|
setClaudeSessionId(sessionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we already have a Claude session ID, use isolated listeners
|
||||||
|
const eventSuffix = claudeSessionId ? `:${claudeSessionId}` : '';
|
||||||
|
|
||||||
|
const outputUnlisten = await listen<string>(`claude-output${eventSuffix}`, async (event) => {
|
||||||
try {
|
try {
|
||||||
console.log('[ClaudeCodeSession] Received claude-output:', event.payload);
|
console.log('[ClaudeCodeSession] Received claude-output:', event.payload);
|
||||||
|
|
||||||
@@ -325,84 +329,69 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorUnlisten = await listen<string>("claude-error", (event) => {
|
const errorUnlisten = await listen<string>(`claude-error${eventSuffix}`, (event) => {
|
||||||
console.error("Claude error:", event.payload);
|
console.error("Claude error:", event.payload);
|
||||||
setError(event.payload);
|
setError(event.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const completeUnlisten = await listen<boolean>("claude-complete", async (event) => {
|
const completeUnlisten = await listen<boolean>(`claude-complete${eventSuffix}`, async (event) => {
|
||||||
console.log('[ClaudeCodeSession] Received claude-complete:', event.payload);
|
console.log('[ClaudeCodeSession] Received claude-complete:', event.payload);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsCancelling(false);
|
|
||||||
hasActiveSessionRef.current = false;
|
hasActiveSessionRef.current = false;
|
||||||
if (!event.payload) {
|
|
||||||
setError("Claude execution failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track all messages at once after completion (batch operation)
|
// Check if we should create an auto checkpoint after completion
|
||||||
if (effectiveSession && rawJsonlOutput.length > 0) {
|
if (effectiveSession && event.payload) {
|
||||||
console.log('[ClaudeCodeSession] Tracking all messages in batch:', rawJsonlOutput.length);
|
|
||||||
api.trackSessionMessages(
|
|
||||||
effectiveSession.id,
|
|
||||||
effectiveSession.project_id,
|
|
||||||
projectPath,
|
|
||||||
rawJsonlOutput
|
|
||||||
).catch(err => {
|
|
||||||
console.error("Failed to track session messages:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should auto-checkpoint
|
|
||||||
if (effectiveSession && messages.length > 0) {
|
|
||||||
try {
|
try {
|
||||||
const lastMessage = messages[messages.length - 1];
|
const settings = await api.getCheckpointSettings(
|
||||||
const shouldCheckpoint = await api.checkAutoCheckpoint(
|
|
||||||
effectiveSession.id,
|
effectiveSession.id,
|
||||||
effectiveSession.project_id,
|
effectiveSession.project_id,
|
||||||
projectPath,
|
projectPath
|
||||||
JSON.stringify(lastMessage)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldCheckpoint) {
|
if (settings.auto_checkpoint_enabled) {
|
||||||
await api.createCheckpoint(
|
await api.checkAutoCheckpoint(
|
||||||
effectiveSession.id,
|
effectiveSession.id,
|
||||||
effectiveSession.project_id,
|
effectiveSession.project_id,
|
||||||
projectPath,
|
projectPath,
|
||||||
messages.length - 1,
|
prompt
|
||||||
"Auto-checkpoint after tool use"
|
|
||||||
);
|
);
|
||||||
console.log("Auto-checkpoint created");
|
// Reload timeline to show new checkpoint
|
||||||
// Trigger timeline reload if it's currently visible
|
|
||||||
setTimelineVersion((v) => v + 1);
|
setTimelineVersion((v) => v + 1);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to check/create auto-checkpoint:", err);
|
console.error('Failed to check auto checkpoint:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up listeners after completion
|
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
|
||||||
unlistenRefs.current = [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
unlistenRefs.current = [sessionStartedUnlisten, outputUnlisten, errorUnlisten, completeUnlisten];
|
||||||
|
|
||||||
|
// Add the user message immediately to the UI (after setting up listeners)
|
||||||
|
const userMessage: ClaudeStreamMessage = {
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: prompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, userMessage]);
|
||||||
|
|
||||||
// Execute the appropriate command
|
// Execute the appropriate command
|
||||||
if (isFirstPrompt && !session) {
|
if (effectiveSession && !isFirstPrompt) {
|
||||||
// New session
|
console.log('[ClaudeCodeSession] Resuming session:', effectiveSession.id);
|
||||||
await api.executeClaudeCode(projectPath, prompt, model);
|
await api.resumeClaudeCode(projectPath, effectiveSession.id, prompt, model);
|
||||||
setIsFirstPrompt(false);
|
|
||||||
} else if (session && isFirstPrompt) {
|
|
||||||
// Resuming a session
|
|
||||||
await api.resumeClaudeCode(projectPath, session.id, prompt, model);
|
|
||||||
setIsFirstPrompt(false);
|
|
||||||
} else {
|
} else {
|
||||||
// Continuing conversation
|
console.log('[ClaudeCodeSession] Starting new session');
|
||||||
await api.continueClaudeCode(projectPath, prompt, model);
|
setIsFirstPrompt(false);
|
||||||
|
await api.executeClaudeCode(projectPath, prompt, model);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to send prompt:", err);
|
console.error("Failed to send prompt:", err);
|
||||||
setError("Failed to execute Claude Code");
|
setError("Failed to send prompt");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
hasActiveSessionRef.current = false;
|
hasActiveSessionRef.current = false;
|
||||||
}
|
}
|
||||||
@@ -499,8 +488,8 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
try {
|
try {
|
||||||
setIsCancelling(true);
|
setIsCancelling(true);
|
||||||
|
|
||||||
// Cancel the Claude execution
|
// Cancel the Claude execution with session ID if available
|
||||||
await api.cancelClaudeExecution();
|
await api.cancelClaudeExecution(claudeSessionId || undefined);
|
||||||
|
|
||||||
// Clean up listeners
|
// Clean up listeners
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
|
@@ -153,13 +153,15 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setupLiveEventListeners = async () => {
|
const setupLiveEventListeners = async () => {
|
||||||
|
if (!session.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clean up existing listeners
|
// Clean up existing listeners
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
unlistenRefs.current = [];
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
// Set up live event listeners similar to AgentExecution
|
// Set up live event listeners with run ID isolation
|
||||||
const outputUnlisten = await listen<string>("agent-output", (event) => {
|
const outputUnlisten = await listen<string>(`agent-output:${session.id}`, (event) => {
|
||||||
try {
|
try {
|
||||||
// Store raw JSONL
|
// Store raw JSONL
|
||||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||||
@@ -172,17 +174,21 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorUnlisten = await listen<string>("agent-error", (event) => {
|
const errorUnlisten = await listen<string>(`agent-error:${session.id}`, (event) => {
|
||||||
console.error("Agent error:", event.payload);
|
console.error("Agent error:", event.payload);
|
||||||
setToast({ message: event.payload, type: 'error' });
|
setToast({ message: event.payload, type: 'error' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const completeUnlisten = await listen<boolean>("agent-complete", () => {
|
const completeUnlisten = await listen<boolean>(`agent-complete:${session.id}`, () => {
|
||||||
setToast({ message: 'Agent execution completed', type: 'success' });
|
setToast({ message: 'Agent execution completed', type: 'success' });
|
||||||
// Don't set status here as the parent component should handle it
|
// Don't set status here as the parent component should handle it
|
||||||
});
|
});
|
||||||
|
|
||||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${session.id}`, () => {
|
||||||
|
setToast({ message: 'Agent execution was cancelled', type: 'error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set up live event listeners:', error);
|
console.error('Failed to set up live event listeners:', error);
|
||||||
}
|
}
|
||||||
|
@@ -1027,9 +1027,10 @@ export const api = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the currently running Claude Code execution
|
* Cancels the currently running Claude Code execution
|
||||||
|
* @param sessionId - Optional session ID to cancel a specific session
|
||||||
*/
|
*/
|
||||||
async cancelClaudeExecution(): Promise<void> {
|
async cancelClaudeExecution(sessionId?: string): Promise<void> {
|
||||||
return invoke("cancel_claude_execution");
|
return invoke("cancel_claude_execution", { sessionId });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user