Skip to content

Commit f4e491e

Browse files
ShahNewazKhanclaude
andcommitted
Fix: project paths with hyphens incorrectly parsed
Projects with hyphens in their paths (e.g., data-discovery) were being incorrectly displayed with the hyphen replaced by a slash (data/discovery). Root cause: The get_project_path_from_sessions() function only checked the first line of session JSONL files for the 'cwd' field. Some session files have null cwd on the first line, causing the fallback to the buggy decode_project_path() which blindly replaces all hyphens with slashes. Fix: Check up to the first 10 lines of session files to find a valid, non-empty cwd value before falling back to path decoding. Also fixes compilation error in claude_binary.rs where ClaudeInstallation was missing the installation_type field for NVM installations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 246e0c8 commit f4e491e

9 files changed

Lines changed: 491 additions & 392 deletions

File tree

src-tauri/src/claude_binary.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
4747
|row| row.get::<_, String>(0),
4848
) {
4949
info!("Found stored claude path in database: {}", stored_path);
50-
50+
5151
// Check if the path still exists
5252
let path_buf = PathBuf::from(&stored_path);
5353
if path_buf.exists() && path_buf.is_file() {
@@ -56,14 +56,14 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
5656
warn!("Stored claude path no longer exists: {}", stored_path);
5757
}
5858
}
59-
59+
6060
// Check user preference
6161
let preference = conn.query_row(
6262
"SELECT value FROM app_settings WHERE key = 'claude_installation_preference'",
6363
[],
6464
|row| row.get::<_, String>(0),
6565
).unwrap_or_else(|_| "system".to_string());
66-
66+
6767
info!("User preference for Claude installation: {}", preference);
6868
}
6969
}
@@ -218,11 +218,14 @@ fn find_nvm_installations() -> Vec<ClaudeInstallation> {
218218
let claude_path = PathBuf::from(&nvm_bin).join("claude");
219219
if claude_path.exists() && claude_path.is_file() {
220220
debug!("Found Claude via NVM_BIN: {:?}", claude_path);
221-
let version = get_claude_version(&claude_path.to_string_lossy()).ok().flatten();
221+
let version = get_claude_version(&claude_path.to_string_lossy())
222+
.ok()
223+
.flatten();
222224
installations.push(ClaudeInstallation {
223225
path: claude_path.to_string_lossy().to_string(),
224226
version,
225227
source: "nvm-active".to_string(),
228+
installation_type: InstallationType::System,
226229
});
227230
}
228231
}
@@ -366,10 +369,10 @@ fn get_claude_version(path: &str) -> Result<Option<String>, String> {
366369
/// Extract version string from command output
367370
fn extract_version_from_output(stdout: &[u8]) -> Option<String> {
368371
let output_str = String::from_utf8_lossy(stdout);
369-
372+
370373
// Debug log the raw output
371374
debug!("Raw version output: {:?}", output_str);
372-
375+
373376
// Use regex to directly extract version pattern (e.g., "1.0.41")
374377
// This pattern matches:
375378
// - One or more digits, followed by
@@ -378,16 +381,17 @@ fn extract_version_from_output(stdout: &[u8]) -> Option<String> {
378381
// - A dot, followed by
379382
// - One or more digits
380383
// - Optionally followed by pre-release/build metadata
381-
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok()?;
382-
384+
let version_regex =
385+
regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok()?;
386+
383387
if let Some(captures) = version_regex.captures(&output_str) {
384388
if let Some(version_match) = captures.get(1) {
385389
let version = version_match.as_str().to_string();
386390
debug!("Extracted version: {:?}", version);
387391
return Some(version);
388392
}
389393
}
390-
394+
391395
debug!("No version found in output");
392396
None
393397
}
@@ -467,7 +471,7 @@ fn compare_versions(a: &str, b: &str) -> Ordering {
467471
/// This ensures commands like Claude can find Node.js and other dependencies
468472
pub fn create_command_with_env(program: &str) -> Command {
469473
let mut cmd = Command::new(program);
470-
474+
471475
info!("Creating command for: {}", program);
472476

473477
// Inherit essential environment variables from parent process
@@ -495,7 +499,7 @@ pub fn create_command_with_env(program: &str) -> Command {
495499
cmd.env(&key, &value);
496500
}
497501
}
498-
502+
499503
// Log proxy-related environment variables for debugging
500504
info!("Command will use proxy settings:");
501505
if let Ok(http_proxy) = std::env::var("HTTP_PROXY") {
@@ -518,7 +522,7 @@ pub fn create_command_with_env(program: &str) -> Command {
518522
}
519523
}
520524
}
521-
525+
522526
// Add Homebrew support if the program is in a Homebrew directory
523527
if program.contains("/homebrew/") || program.contains("/opt/homebrew/") {
524528
if let Some(program_dir) = std::path::Path::new(program).parent() {
@@ -527,7 +531,10 @@ pub fn create_command_with_env(program: &str) -> Command {
527531
let homebrew_bin_str = program_dir.to_string_lossy();
528532
if !current_path.contains(&homebrew_bin_str.as_ref()) {
529533
let new_path = format!("{}:{}", homebrew_bin_str, current_path);
530-
debug!("Adding Homebrew bin directory to PATH: {}", homebrew_bin_str);
534+
debug!(
535+
"Adding Homebrew bin directory to PATH: {}",
536+
homebrew_bin_str
537+
);
531538
cmd.env("PATH", new_path);
532539
}
533540
}

src-tauri/src/commands/agents.rs

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ pub async fn read_session_jsonl(session_id: &str, project_path: &str) -> Result<
179179
let session_file = project_dir.join(format!("{}.jsonl", session_id));
180180

181181
if !session_file.exists() {
182-
return Err(format!("Session file not found: {}", session_file.display()));
182+
return Err(format!(
183+
"Session file not found: {}",
184+
session_file.display()
185+
));
183186
}
184187

185188
match tokio::fs::read_to_string(&session_file).await {
@@ -317,7 +320,6 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
317320
[],
318321
)?;
319322

320-
321323
// Create settings table for app-wide settings
322324
conn.execute(
323325
"CREATE TABLE IF NOT EXISTS app_settings (
@@ -690,38 +692,41 @@ pub async fn execute_agent(
690692
// Get the agent from database
691693
let agent = get_agent(db.clone(), agent_id).await?;
692694
let execution_model = model.unwrap_or(agent.model.clone());
693-
695+
694696
// Create .claude/settings.json with agent hooks if it doesn't exist
695697
if let Some(hooks_json) = &agent.hooks {
696698
let claude_dir = std::path::Path::new(&project_path).join(".claude");
697699
let settings_path = claude_dir.join("settings.json");
698-
700+
699701
// Create .claude directory if it doesn't exist
700702
if !claude_dir.exists() {
701703
std::fs::create_dir_all(&claude_dir)
702704
.map_err(|e| format!("Failed to create .claude directory: {}", e))?;
703705
info!("Created .claude directory at: {:?}", claude_dir);
704706
}
705-
707+
706708
// Check if settings.json already exists
707709
if !settings_path.exists() {
708710
// Parse the hooks JSON
709711
let hooks: serde_json::Value = serde_json::from_str(hooks_json)
710712
.map_err(|e| format!("Failed to parse agent hooks: {}", e))?;
711-
713+
712714
// Create a settings object with just the hooks
713715
let settings = serde_json::json!({
714716
"hooks": hooks
715717
});
716-
718+
717719
// Write the settings file
718720
let settings_content = serde_json::to_string_pretty(&settings)
719721
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
720-
722+
721723
std::fs::write(&settings_path, settings_content)
722724
.map_err(|e| format!("Failed to write settings.json: {}", e))?;
723-
724-
info!("Created settings.json with agent hooks at: {:?}", settings_path);
725+
726+
info!(
727+
"Created settings.json with agent hooks at: {:?}",
728+
settings_path
729+
);
725730
} else {
726731
info!("settings.json already exists at: {:?}", settings_path);
727732
}
@@ -775,7 +780,8 @@ pub async fn execute_agent(
775780
execution_model,
776781
db,
777782
registry,
778-
).await
783+
)
784+
.await
779785
}
780786

781787
/// Creates a system binary command for agent execution
@@ -785,17 +791,17 @@ fn create_agent_system_command(
785791
project_path: &str,
786792
) -> Command {
787793
let mut cmd = create_command_with_env(claude_path);
788-
794+
789795
// Add all arguments
790796
for arg in args {
791797
cmd.arg(arg);
792798
}
793-
799+
794800
cmd.current_dir(project_path)
795801
.stdin(Stdio::null())
796802
.stdout(Stdio::piped())
797803
.stderr(Stdio::piped());
798-
804+
799805
cmd
800806
}
801807

@@ -905,14 +911,15 @@ async fn spawn_agent_system(
905911
// Extract session ID from JSONL output
906912
if let Ok(json) = serde_json::from_str::<JsonValue>(&line) {
907913
// Claude Code uses "session_id" (underscore), not "sessionId"
908-
if json.get("type").and_then(|t| t.as_str()) == Some("system") &&
909-
json.get("subtype").and_then(|s| s.as_str()) == Some("init") {
914+
if json.get("type").and_then(|t| t.as_str()) == Some("system")
915+
&& json.get("subtype").and_then(|s| s.as_str()) == Some("init")
916+
{
910917
if let Some(sid) = json.get("session_id").and_then(|s| s.as_str()) {
911918
if let Ok(mut current_session_id) = session_id_clone.lock() {
912919
if current_session_id.is_empty() {
913920
*current_session_id = sid.to_string();
914921
info!("🔑 Extracted session ID: {}", sid);
915-
922+
916923
// Update database immediately with session ID
917924
if let Ok(conn) = Connection::open(&db_path_for_stdout) {
918925
match conn.execute(
@@ -925,7 +932,10 @@ async fn spawn_agent_system(
925932
}
926933
}
927934
Err(e) => {
928-
error!("❌ Failed to update session ID immediately: {}", e);
935+
error!(
936+
"❌ Failed to update session ID immediately: {}",
937+
e
938+
);
929939
}
930940
}
931941
}
@@ -1085,7 +1095,10 @@ async fn spawn_agent_system(
10851095

10861096
// Update the run record with session ID and mark as completed - open a new connection
10871097
if let Ok(conn) = Connection::open(&db_path_for_monitor) {
1088-
info!("🔄 Updating database with extracted session ID: {}", extracted_session_id);
1098+
info!(
1099+
"🔄 Updating database with extracted session ID: {}",
1100+
extracted_session_id
1101+
);
10891102
match conn.execute(
10901103
"UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2",
10911104
params![extracted_session_id, run_id],
@@ -1102,7 +1115,10 @@ async fn spawn_agent_system(
11021115
}
11031116
}
11041117
} else {
1105-
error!("❌ Failed to open database to update session ID for run {}", run_id);
1118+
error!(
1119+
"❌ Failed to open database to update session ID for run {}",
1120+
run_id
1121+
);
11061122
}
11071123

11081124
// Cleanup will be handled by the cleanup_finished_processes function
@@ -1162,10 +1178,8 @@ pub async fn list_running_sessions(
11621178
// Cross-check with the process registry to ensure accuracy
11631179
// Get actually running processes from the registry
11641180
let registry_processes = registry.0.get_running_agent_processes()?;
1165-
let registry_run_ids: std::collections::HashSet<i64> = registry_processes
1166-
.iter()
1167-
.map(|p| p.run_id)
1168-
.collect();
1181+
let registry_run_ids: std::collections::HashSet<i64> =
1182+
registry_processes.iter().map(|p| p.run_id).collect();
11691183

11701184
// Filter out any database entries that aren't actually running in the registry
11711185
// This handles cases where processes crashed without updating the database
@@ -1358,7 +1372,7 @@ pub async fn get_session_output(
13581372

13591373
// Find the correct project directory by searching for the session file
13601374
let projects_dir = claude_dir.join("projects");
1361-
1375+
13621376
// Check if projects directory exists
13631377
if !projects_dir.exists() {
13641378
log::error!("Projects directory not found at: {:?}", projects_dir);
@@ -1367,15 +1381,18 @@ pub async fn get_session_output(
13671381

13681382
// Search for the session file in all project directories
13691383
let mut session_file_path = None;
1370-
log::info!("Searching for session file {} in all project directories", run.session_id);
1371-
1384+
log::info!(
1385+
"Searching for session file {} in all project directories",
1386+
run.session_id
1387+
);
1388+
13721389
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
13731390
for entry in entries.filter_map(Result::ok) {
13741391
let path = entry.path();
13751392
if path.is_dir() {
13761393
let dir_name = path.file_name().unwrap_or_default().to_string_lossy();
13771394
log::debug!("Checking project directory: {}", dir_name);
1378-
1395+
13791396
let potential_session_file = path.join(format!("{}.jsonl", run.session_id));
13801397
if potential_session_file.exists() {
13811398
log::info!("Found session file at: {:?}", potential_session_file);
@@ -1395,15 +1412,22 @@ pub async fn get_session_output(
13951412
match tokio::fs::read_to_string(&session_path).await {
13961413
Ok(content) => Ok(content),
13971414
Err(e) => {
1398-
log::error!("Failed to read session file {}: {}", session_path.display(), e);
1415+
log::error!(
1416+
"Failed to read session file {}: {}",
1417+
session_path.display(),
1418+
e
1419+
);
13991420
// Fallback to live output if file read fails
14001421
let live_output = registry.0.get_live_output(run_id)?;
14011422
Ok(live_output)
14021423
}
14031424
}
14041425
} else {
14051426
// If session file not found, try the old method as fallback
1406-
log::warn!("Session file not found for {}, trying legacy method", run.session_id);
1427+
log::warn!(
1428+
"Session file not found for {}, trying legacy method",
1429+
run.session_id
1430+
);
14071431
match read_session_jsonl(&run.session_id, &run.project_path).await {
14081432
Ok(content) => Ok(content),
14091433
Err(_) => {
@@ -1916,23 +1940,26 @@ pub async fn load_agent_session_history(
19161940
.join(".claude");
19171941

19181942
let projects_dir = claude_dir.join("projects");
1919-
1943+
19201944
if !projects_dir.exists() {
19211945
log::error!("Projects directory not found at: {:?}", projects_dir);
19221946
return Err("Projects directory not found".to_string());
19231947
}
19241948

19251949
// Search for the session file in all project directories
19261950
let mut session_file_path = None;
1927-
log::info!("Searching for session file {} in all project directories", session_id);
1928-
1951+
log::info!(
1952+
"Searching for session file {} in all project directories",
1953+
session_id
1954+
);
1955+
19291956
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
19301957
for entry in entries.filter_map(Result::ok) {
19311958
let path = entry.path();
19321959
if path.is_dir() {
19331960
let dir_name = path.file_name().unwrap_or_default().to_string_lossy();
19341961
log::debug!("Checking project directory: {}", dir_name);
1935-
1962+
19361963
let potential_session_file = path.join(format!("{}.jsonl", session_id));
19371964
if potential_session_file.exists() {
19381965
log::info!("Found session file at: {:?}", potential_session_file);

0 commit comments

Comments
 (0)