|
1 | 1 | use anyhow::Result; |
2 | 2 | use chrono; |
| 3 | +use dirs; |
3 | 4 | use log::{debug, error, info, warn}; |
| 5 | +use regex; |
4 | 6 | use reqwest; |
5 | 7 | use rusqlite::{params, Connection, Result as SqliteResult}; |
6 | 8 | use serde::{Deserialize, Serialize}; |
7 | 9 | use serde_json::Value as JsonValue; |
| 10 | +use std::io::{BufRead, BufReader}; |
8 | 11 | use std::process::Stdio; |
9 | 12 | use std::sync::{Arc, Mutex}; |
10 | 13 | use tauri::{AppHandle, Emitter, Manager, State}; |
11 | 14 | use tauri_plugin_shell::ShellExt; |
12 | | -use tokio::io::{AsyncBufReadExt, BufReader}; |
| 15 | +use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader}; |
13 | 16 | use tokio::process::Command; |
14 | | -use regex; |
15 | 17 |
|
16 | 18 | /// Finds the full path to the claude binary |
17 | 19 | /// This is necessary because macOS apps have a limited PATH environment |
@@ -855,11 +857,15 @@ async fn spawn_agent_sidecar( |
855 | 857 |
|
856 | 858 | // Extract session ID from JSONL output |
857 | 859 | if let Ok(json) = serde_json::from_str::<JsonValue>(&line) { |
858 | | - if let Some(sid) = json.get("sessionId").and_then(|s| s.as_str()) { |
859 | | - if let Ok(mut current_session_id) = session_id_holder_clone.lock() { |
860 | | - if current_session_id.is_none() { |
861 | | - *current_session_id = Some(sid.to_string()); |
862 | | - info!("🔑 Extracted session ID: {}", sid); |
| 860 | + // Claude Code uses "session_id" (underscore), not "sessionId" |
| 861 | + if json.get("type").and_then(|t| t.as_str()) == Some("system") && |
| 862 | + json.get("subtype").and_then(|s| s.as_str()) == Some("init") { |
| 863 | + if let Some(sid) = json.get("session_id").and_then(|s| s.as_str()) { |
| 864 | + if let Ok(mut current_session_id) = session_id_holder_clone.lock() { |
| 865 | + if current_session_id.is_none() { |
| 866 | + *current_session_id = Some(sid.to_string()); |
| 867 | + info!("🔑 Extracted session ID: {}", sid); |
| 868 | + } |
863 | 869 | } |
864 | 870 | } |
865 | 871 | } |
@@ -955,10 +961,24 @@ async fn spawn_agent_sidecar( |
955 | 961 |
|
956 | 962 | // Update the run record with session ID and mark as completed |
957 | 963 | if let Ok(conn) = Connection::open(&db_path) { |
958 | | - let _ = conn.execute( |
| 964 | + info!("🔄 Updating database with extracted session ID: {}", extracted_session_id); |
| 965 | + match conn.execute( |
959 | 966 | "UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2", |
960 | 967 | params![extracted_session_id, run_id], |
961 | | - ); |
| 968 | + ) { |
| 969 | + Ok(rows_affected) => { |
| 970 | + if rows_affected > 0 { |
| 971 | + info!("✅ Successfully updated agent run {} with session ID: {}", run_id, extracted_session_id); |
| 972 | + } else { |
| 973 | + warn!("⚠️ No rows affected when updating agent run {} with session ID", run_id); |
| 974 | + } |
| 975 | + } |
| 976 | + Err(e) => { |
| 977 | + error!("❌ Failed to update agent run {} with session ID: {}", run_id, e); |
| 978 | + } |
| 979 | + } |
| 980 | + } else { |
| 981 | + error!("❌ Failed to open database to update session ID for run {}", run_id); |
962 | 982 | } |
963 | 983 |
|
964 | 984 | info!("✅ Claude sidecar execution monitoring complete"); |
@@ -1017,8 +1037,8 @@ async fn spawn_agent_system( |
1017 | 1037 | info!("📡 Set up stdout/stderr readers"); |
1018 | 1038 |
|
1019 | 1039 | // Create readers |
1020 | | - let stdout_reader = BufReader::new(stdout); |
1021 | | - let stderr_reader = BufReader::new(stderr); |
| 1040 | + let stdout_reader = TokioBufReader::new(stdout); |
| 1041 | + let stderr_reader = TokioBufReader::new(stderr); |
1022 | 1042 |
|
1023 | 1043 | // Shared state for collecting session ID and live output |
1024 | 1044 | let session_id = std::sync::Arc::new(Mutex::new(String::new())); |
@@ -1067,11 +1087,15 @@ async fn spawn_agent_system( |
1067 | 1087 |
|
1068 | 1088 | // Extract session ID from JSONL output |
1069 | 1089 | if let Ok(json) = serde_json::from_str::<JsonValue>(&line) { |
1070 | | - if let Some(sid) = json.get("sessionId").and_then(|s| s.as_str()) { |
1071 | | - if let Ok(mut current_session_id) = session_id_clone.lock() { |
1072 | | - if current_session_id.is_empty() { |
1073 | | - *current_session_id = sid.to_string(); |
1074 | | - info!("🔑 Extracted session ID: {}", sid); |
| 1090 | + // Claude Code uses "session_id" (underscore), not "sessionId" |
| 1091 | + if json.get("type").and_then(|t| t.as_str()) == Some("system") && |
| 1092 | + json.get("subtype").and_then(|s| s.as_str()) == Some("init") { |
| 1093 | + if let Some(sid) = json.get("session_id").and_then(|s| s.as_str()) { |
| 1094 | + if let Ok(mut current_session_id) = session_id_clone.lock() { |
| 1095 | + if current_session_id.is_empty() { |
| 1096 | + *current_session_id = sid.to_string(); |
| 1097 | + info!("🔑 Extracted session ID: {}", sid); |
| 1098 | + } |
1075 | 1099 | } |
1076 | 1100 | } |
1077 | 1101 | } |
@@ -1232,10 +1256,24 @@ async fn spawn_agent_system( |
1232 | 1256 |
|
1233 | 1257 | // Update the run record with session ID and mark as completed - open a new connection |
1234 | 1258 | if let Ok(conn) = Connection::open(&db_path) { |
1235 | | - let _ = conn.execute( |
| 1259 | + info!("🔄 Updating database with extracted session ID: {}", extracted_session_id); |
| 1260 | + match conn.execute( |
1236 | 1261 | "UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2", |
1237 | 1262 | params![extracted_session_id, run_id], |
1238 | | - ); |
| 1263 | + ) { |
| 1264 | + Ok(rows_affected) => { |
| 1265 | + if rows_affected > 0 { |
| 1266 | + info!("✅ Successfully updated agent run {} with session ID: {}", run_id, extracted_session_id); |
| 1267 | + } else { |
| 1268 | + warn!("⚠️ No rows affected when updating agent run {} with session ID", run_id); |
| 1269 | + } |
| 1270 | + } |
| 1271 | + Err(e) => { |
| 1272 | + error!("❌ Failed to update agent run {} with session ID: {}", run_id, e); |
| 1273 | + } |
| 1274 | + } |
| 1275 | + } else { |
| 1276 | + error!("❌ Failed to open database to update session ID for run {}", run_id); |
1239 | 1277 | } |
1240 | 1278 |
|
1241 | 1279 | // Cleanup will be handled by the cleanup_finished_processes function |
@@ -1484,13 +1522,66 @@ pub async fn get_session_output( |
1484 | 1522 | return Ok(String::new()); |
1485 | 1523 | } |
1486 | 1524 |
|
1487 | | - // Read the JSONL content |
1488 | | - match read_session_jsonl(&run.session_id, &run.project_path).await { |
1489 | | - Ok(content) => Ok(content), |
1490 | | - Err(_) => { |
1491 | | - // Fallback to live output if JSONL file doesn't exist yet |
1492 | | - let live_output = registry.0.get_live_output(run_id)?; |
1493 | | - Ok(live_output) |
| 1525 | + // Get the Claude directory |
| 1526 | + let claude_dir = dirs::home_dir() |
| 1527 | + .ok_or("Failed to get home directory")? |
| 1528 | + .join(".claude"); |
| 1529 | + |
| 1530 | + // Find the correct project directory by searching for the session file |
| 1531 | + let projects_dir = claude_dir.join("projects"); |
| 1532 | + |
| 1533 | + // Check if projects directory exists |
| 1534 | + if !projects_dir.exists() { |
| 1535 | + log::error!("Projects directory not found at: {:?}", projects_dir); |
| 1536 | + return Err("Projects directory not found".to_string()); |
| 1537 | + } |
| 1538 | + |
| 1539 | + // Search for the session file in all project directories |
| 1540 | + let mut session_file_path = None; |
| 1541 | + log::info!("Searching for session file {} in all project directories", run.session_id); |
| 1542 | + |
| 1543 | + if let Ok(entries) = std::fs::read_dir(&projects_dir) { |
| 1544 | + for entry in entries.filter_map(Result::ok) { |
| 1545 | + let path = entry.path(); |
| 1546 | + if path.is_dir() { |
| 1547 | + let dir_name = path.file_name().unwrap_or_default().to_string_lossy(); |
| 1548 | + log::debug!("Checking project directory: {}", dir_name); |
| 1549 | + |
| 1550 | + let potential_session_file = path.join(format!("{}.jsonl", run.session_id)); |
| 1551 | + if potential_session_file.exists() { |
| 1552 | + log::info!("Found session file at: {:?}", potential_session_file); |
| 1553 | + session_file_path = Some(potential_session_file); |
| 1554 | + break; |
| 1555 | + } else { |
| 1556 | + log::debug!("Session file not found in: {}", dir_name); |
| 1557 | + } |
| 1558 | + } |
| 1559 | + } |
| 1560 | + } else { |
| 1561 | + log::error!("Failed to read projects directory"); |
| 1562 | + } |
| 1563 | + |
| 1564 | + // If we found the session file, read it |
| 1565 | + if let Some(session_path) = session_file_path { |
| 1566 | + match tokio::fs::read_to_string(&session_path).await { |
| 1567 | + Ok(content) => Ok(content), |
| 1568 | + Err(e) => { |
| 1569 | + log::error!("Failed to read session file {}: {}", session_path.display(), e); |
| 1570 | + // Fallback to live output if file read fails |
| 1571 | + let live_output = registry.0.get_live_output(run_id)?; |
| 1572 | + Ok(live_output) |
| 1573 | + } |
| 1574 | + } |
| 1575 | + } else { |
| 1576 | + // If session file not found, try the old method as fallback |
| 1577 | + log::warn!("Session file not found for {}, trying legacy method", run.session_id); |
| 1578 | + match read_session_jsonl(&run.session_id, &run.project_path).await { |
| 1579 | + Ok(content) => Ok(content), |
| 1580 | + Err(_) => { |
| 1581 | + // Final fallback to live output |
| 1582 | + let live_output = registry.0.get_live_output(run_id)?; |
| 1583 | + Ok(live_output) |
| 1584 | + } |
1494 | 1585 | } |
1495 | 1586 | } |
1496 | 1587 | } |
@@ -2076,3 +2167,68 @@ pub async fn import_agent_from_github( |
2076 | 2167 | // Import using existing function |
2077 | 2168 | import_agent(db, json_data).await |
2078 | 2169 | } |
| 2170 | + |
| 2171 | +/// Load agent session history from JSONL file |
| 2172 | +/// Similar to Claude Code's load_session_history, but searches across all project directories |
| 2173 | +#[tauri::command] |
| 2174 | +pub async fn load_agent_session_history( |
| 2175 | + session_id: String, |
| 2176 | +) -> Result<Vec<serde_json::Value>, String> { |
| 2177 | + log::info!("Loading agent session history for session: {}", session_id); |
| 2178 | + |
| 2179 | + let claude_dir = dirs::home_dir() |
| 2180 | + .ok_or("Failed to get home directory")? |
| 2181 | + .join(".claude"); |
| 2182 | + |
| 2183 | + let projects_dir = claude_dir.join("projects"); |
| 2184 | + |
| 2185 | + if !projects_dir.exists() { |
| 2186 | + log::error!("Projects directory not found at: {:?}", projects_dir); |
| 2187 | + return Err("Projects directory not found".to_string()); |
| 2188 | + } |
| 2189 | + |
| 2190 | + // Search for the session file in all project directories |
| 2191 | + let mut session_file_path = None; |
| 2192 | + log::info!("Searching for session file {} in all project directories", session_id); |
| 2193 | + |
| 2194 | + if let Ok(entries) = std::fs::read_dir(&projects_dir) { |
| 2195 | + for entry in entries.filter_map(Result::ok) { |
| 2196 | + let path = entry.path(); |
| 2197 | + if path.is_dir() { |
| 2198 | + let dir_name = path.file_name().unwrap_or_default().to_string_lossy(); |
| 2199 | + log::debug!("Checking project directory: {}", dir_name); |
| 2200 | + |
| 2201 | + let potential_session_file = path.join(format!("{}.jsonl", session_id)); |
| 2202 | + if potential_session_file.exists() { |
| 2203 | + log::info!("Found session file at: {:?}", potential_session_file); |
| 2204 | + session_file_path = Some(potential_session_file); |
| 2205 | + break; |
| 2206 | + } else { |
| 2207 | + log::debug!("Session file not found in: {}", dir_name); |
| 2208 | + } |
| 2209 | + } |
| 2210 | + } |
| 2211 | + } else { |
| 2212 | + log::error!("Failed to read projects directory"); |
| 2213 | + } |
| 2214 | + |
| 2215 | + if let Some(session_path) = session_file_path { |
| 2216 | + let file = std::fs::File::open(&session_path) |
| 2217 | + .map_err(|e| format!("Failed to open session file: {}", e))?; |
| 2218 | + |
| 2219 | + let reader = BufReader::new(file); |
| 2220 | + let mut messages = Vec::new(); |
| 2221 | + |
| 2222 | + for line in reader.lines() { |
| 2223 | + if let Ok(line) = line { |
| 2224 | + if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) { |
| 2225 | + messages.push(json); |
| 2226 | + } |
| 2227 | + } |
| 2228 | + } |
| 2229 | + |
| 2230 | + Ok(messages) |
| 2231 | + } else { |
| 2232 | + Err(format!("Session file not found: {}", session_id)) |
| 2233 | + } |
| 2234 | +} |
0 commit comments