diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 32925d5..9a49220 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -6,8 +6,9 @@ use rusqlite::{params, Connection, Result as SqliteResult}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use std::process::Stdio; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter, Manager, State}; +use tauri_plugin_shell::ShellExt; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; @@ -691,7 +692,7 @@ pub async fn execute_agent( conn.last_insert_rowid() }; - // Build the command + // Find Claude binary info!("Running agent '{}'", agent.name); let claude_path = match find_claude_binary(&app) { Ok(path) => path, @@ -700,24 +701,293 @@ pub async fn execute_agent( return Err(e); } }; - let mut cmd = create_command_with_env(&claude_path); - cmd.arg("-p") - .arg(&task) - .arg("--system-prompt") - .arg(&agent.system_prompt) - .arg("--model") - .arg(&execution_model) - .arg("--output-format") - .arg("stream-json") - .arg("--verbose") - .arg("--dangerously-skip-permissions") - .current_dir(&project_path) - .stdin(Stdio::null()) // Don't pipe stdin - we have no input to send + + // Build arguments + let args = vec![ + "-p".to_string(), + task.clone(), + "--system-prompt".to_string(), + agent.system_prompt.clone(), + "--model".to_string(), + execution_model.clone(), + "--output-format".to_string(), + "stream-json".to_string(), + "--verbose".to_string(), + "--dangerously-skip-permissions".to_string(), + ]; + + // Execute based on whether we should use sidecar or system binary + if should_use_sidecar(&claude_path) { + spawn_agent_sidecar(app, run_id, agent_id, agent.name.clone(), args, project_path, task, execution_model, db, registry).await + } else { + spawn_agent_system(app, run_id, agent_id, agent.name.clone(), claude_path, args, project_path, task, execution_model, db, registry).await + } +} + +/// Determines whether to use sidecar or system binary execution for agents +fn should_use_sidecar(claude_path: &str) -> bool { + claude_path == "claude-code" +} + +/// Creates a sidecar command for agent execution +fn create_agent_sidecar_command( + app: &AppHandle, + args: Vec, + project_path: &str, +) -> Result { + let mut sidecar_cmd = app + .shell() + .sidecar("claude-code") + .map_err(|e| format!("Failed to create sidecar command: {}", e))?; + + // Add all arguments + sidecar_cmd = sidecar_cmd.args(args); + + // Set working directory + sidecar_cmd = sidecar_cmd.current_dir(project_path); + + Ok(sidecar_cmd) +} + +/// Creates a system binary command for agent execution +fn create_agent_system_command( + claude_path: &str, + args: Vec, + project_path: &str, +) -> Command { + let mut cmd = create_command_with_env(claude_path); + + // Add all arguments + for arg in args { + cmd.arg(arg); + } + + cmd.current_dir(project_path) + .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + + cmd +} + +/// Spawn agent using sidecar command +async fn spawn_agent_sidecar( + app: AppHandle, + run_id: i64, + _agent_id: i64, + _agent_name: String, + args: Vec, + project_path: String, + _task: String, + _execution_model: String, + db: State<'_, AgentDb>, + registry: State<'_, crate::process::ProcessRegistryState>, +) -> Result { + use std::sync::Mutex; + + // Create the sidecar command + let sidecar_cmd = create_agent_sidecar_command(&app, args, &project_path)?; + + // Spawn the sidecar process + let (mut rx, child) = sidecar_cmd + .spawn() + .map_err(|e| format!("Failed to spawn Claude sidecar: {}", e))?; + + // Get the child PID for logging + let pid = child.pid(); + info!("✅ Spawned Claude sidecar process with PID: {:?}", pid); + + // Update the database with PID and status + let now = chrono::Utc::now().to_rfc3339(); + { + let conn = db.0.lock().map_err(|e| e.to_string())?; + conn.execute( + "UPDATE agent_runs SET status = 'running', pid = ?1, process_started_at = ?2 WHERE id = ?3", + params![pid as i64, now, run_id], + ).map_err(|e| e.to_string())?; + info!("📝 Updated database with running status and PID"); + } + + // We'll extract the session ID from Claude's init message + let session_id_holder: Arc>> = Arc::new(Mutex::new(None)); + + // Spawn task to read events from sidecar + let app_handle = app.clone(); + let session_id_holder_clone = session_id_holder.clone(); + let live_output = std::sync::Arc::new(Mutex::new(String::new())); + let live_output_clone = live_output.clone(); + let registry_clone = registry.0.clone(); + let first_output = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let first_output_clone = first_output.clone(); + + let sidecar_task = tokio::spawn(async move { + info!("📖 Starting to read Claude sidecar events..."); + let mut line_count = 0; + + while let Some(event) = rx.recv().await { + match event { + tauri_plugin_shell::process::CommandEvent::Stdout(data) => { + let line = String::from_utf8_lossy(&data).trim().to_string(); + if !line.is_empty() { + line_count += 1; + + // Log first output + if !first_output_clone.load(std::sync::atomic::Ordering::Relaxed) { + info!("🎉 First output received from Claude sidecar! Line: {}", line); + first_output_clone.store(true, std::sync::atomic::Ordering::Relaxed); + } + + if line_count <= 5 { + info!("sidecar stdout[{}]: {}", line_count, line); + } else { + debug!("sidecar stdout[{}]: {}", line_count, line); + } + + // Store live output + if let Ok(mut output) = live_output_clone.lock() { + output.push_str(&line); + output.push('\n'); + } + + // Also store in process registry for cross-session access + let _ = registry_clone.append_live_output(run_id, &line); + + // Extract session ID from JSONL output + if let Ok(json) = serde_json::from_str::(&line) { + if let Some(sid) = json.get("sessionId").and_then(|s| s.as_str()) { + if let Ok(mut current_session_id) = session_id_holder_clone.lock() { + if current_session_id.is_none() { + *current_session_id = Some(sid.to_string()); + info!("🔑 Extracted session ID: {}", sid); + } + } + } + } + + // 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); + } + } + tauri_plugin_shell::process::CommandEvent::Stderr(data) => { + let line = String::from_utf8_lossy(&data).trim().to_string(); + if !line.is_empty() { + error!("sidecar stderr: {}", line); + // Emit error lines to the frontend with run_id for isolation + let _ = app_handle.emit(&format!("agent-error:{}", run_id), &line); + // Also emit to the generic event for backward compatibility + let _ = app_handle.emit("agent-error", &line); + } + } + tauri_plugin_shell::process::CommandEvent::Terminated { .. } => { + info!("📖 Claude sidecar process terminated"); + break; + } + tauri_plugin_shell::process::CommandEvent::Error(e) => { + error!("🔥 Claude sidecar error: {}", e); + break; + } + _ => { + // Handle any other event types we might not know about + debug!("Received unknown sidecar event type"); + } + } + } + + info!("📖 Finished reading Claude sidecar events. Total lines: {}", line_count); + }); + + // Create variables we need for the spawned task + let app_dir = app + .path() + .app_data_dir() + .expect("Failed to get app data dir"); + let db_path = app_dir.join("agents.db"); + + // Monitor process status and wait for completion + tokio::spawn(async move { + info!("🕐 Starting sidecar process monitoring..."); + + // Wait for first output with timeout + for i in 0..300 { + // 30 seconds (300 * 100ms) + if first_output.load(std::sync::atomic::Ordering::Relaxed) { + info!("✅ Output detected after {}ms, continuing normal execution", i * 100); + break; + } + + if i == 299 { + warn!("⏰ TIMEOUT: No output from Claude sidecar after 30 seconds"); + warn!("💡 This usually means:"); + warn!(" 1. Claude sidecar is waiting for user input"); + warn!(" 2. Authentication issues (API key not found/invalid)"); + warn!(" 3. Network connectivity issues"); + warn!(" 4. Claude failed to initialize but didn't report an error"); + + // Update database with failed status + if let Ok(conn) = Connection::open(&db_path) { + let _ = conn.execute( + "UPDATE agent_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP WHERE id = ?1", + params![run_id], + ); + } + + let _ = app.emit("agent-complete", false); + let _ = app.emit(&format!("agent-complete:{}", run_id), false); + return; + } + + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + + // Wait for sidecar task to complete + info!("⏳ Waiting for sidecar reading to complete..."); + let _ = sidecar_task.await; + + // Get the session ID that was extracted + let extracted_session_id = if let Ok(Some(sid)) = session_id_holder.lock().map(|s| s.clone()) { + sid + } else { + String::new() + }; + + // Update the run record with session ID and mark as completed + if let Ok(conn) = Connection::open(&db_path) { + let _ = conn.execute( + "UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2", + params![extracted_session_id, run_id], + ); + } + + info!("✅ Claude sidecar execution monitoring complete"); + + let _ = app.emit("agent-complete", true); + let _ = app.emit(&format!("agent-complete:{}", run_id), true); + }); + + Ok(run_id) +} + +/// Spawn agent using system binary command +async fn spawn_agent_system( + app: AppHandle, + run_id: i64, + agent_id: i64, + agent_name: String, + claude_path: String, + args: Vec, + project_path: String, + task: String, + execution_model: String, + db: State<'_, AgentDb>, + registry: State<'_, crate::process::ProcessRegistryState>, +) -> Result { + // Build the command + let mut cmd = create_agent_system_command(&claude_path, args, &project_path); // Spawn the process - info!("🚀 Spawning Claude process..."); + info!("🚀 Spawning Claude system process..."); let mut child = cmd.spawn().map_err(|e| { error!("❌ Failed to spawn Claude process: {}", e); format!("Failed to spawn Claude: {}", e) @@ -859,7 +1129,7 @@ pub async fn execute_agent( .register_process( run_id, agent_id, - agent.name.clone(), + agent_name, pid, project_path.clone(), task.clone(), @@ -891,64 +1161,54 @@ pub async fn execute_agent( break; } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + if i == 299 { + warn!("⏰ TIMEOUT: No output from Claude process after 30 seconds"); + warn!("💡 This usually means:"); + warn!(" 1. Claude process is waiting for user input"); + warn!(" 3. Claude failed to initialize but didn't report an error"); + warn!(" 4. Network connectivity issues"); + warn!(" 5. Authentication issues (API key not found/invalid)"); - // Log progress every 5 seconds - if i > 0 && i % 50 == 0 { - info!( - "⏳ Still waiting for Claude output... ({}s elapsed)", - i / 10 + // Process timed out - kill it via PID + warn!( + "🔍 Process likely stuck waiting for input, attempting to kill PID: {}", + pid ); - } - } + let kill_result = std::process::Command::new("kill") + .arg("-TERM") + .arg(pid.to_string()) + .output(); - // Check if we timed out - if !first_output.load(std::sync::atomic::Ordering::Relaxed) { - warn!("⏰ TIMEOUT: No output from Claude process after 30 seconds"); - warn!("💡 This usually means:"); - warn!(" 1. Claude process is waiting for user input"); - - warn!(" 3. Claude failed to initialize but didn't report an error"); - warn!(" 4. Network connectivity issues"); - warn!(" 5. Authentication issues (API key not found/invalid)"); - - // Process timed out - kill it via PID - warn!( - "🔍 Process likely stuck waiting for input, attempting to kill PID: {}", - pid - ); - let kill_result = std::process::Command::new("kill") - .arg("-TERM") - .arg(pid.to_string()) - .output(); - - match kill_result { - Ok(output) if output.status.success() => { - warn!("🔍 Successfully sent TERM signal to process"); + match kill_result { + Ok(output) if output.status.success() => { + warn!("🔍 Successfully sent TERM signal to process"); + } + Ok(_) => { + warn!("🔍 Failed to kill process with TERM, trying KILL"); + let _ = std::process::Command::new("kill") + .arg("-KILL") + .arg(pid.to_string()) + .output(); + } + Err(e) => { + warn!("🔍 Error killing process: {}", e); + } } - Ok(_) => { - warn!("🔍 Failed to kill process with TERM, trying KILL"); - let _ = std::process::Command::new("kill") - .arg("-KILL") - .arg(pid.to_string()) - .output(); - } - Err(e) => { - warn!("🔍 Error killing process: {}", e); + + // Update database + if let Ok(conn) = Connection::open(&db_path) { + let _ = conn.execute( + "UPDATE agent_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP WHERE id = ?1", + params![run_id], + ); } + + let _ = app.emit("agent-complete", false); + let _ = app.emit(&format!("agent-complete:{}", run_id), false); + return; } - // Update database - if let Ok(conn) = Connection::open(&db_path) { - let _ = conn.execute( - "UPDATE agent_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP WHERE id = ?1", - params![run_id], - ); - } - - let _ = app.emit("agent-complete", false); - let _ = app.emit(&format!("agent-complete:{}", run_id), false); - return; + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } // Wait for reading tasks to complete