From fec9b01660c9e636491a5c71bb8322a2f8c015b5 Mon Sep 17 00:00:00 2001 From: Khugan Shanmugeswaran Date: Fri, 4 Sep 2026 14:42:21 -0400 Subject: [PATCH 1/4] Stop spamming 'Source not found' warnings for Swift helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_dependencies() now checks if the record binary exists directly instead of calling ensure_swift_binary(), which attempts recompilation on every call. In packaged builds where source .swift files don't ship, this caused hundreds of WARN-level log entries every few seconds from the onboarding status poller. Also downgrade the "Source not found" log in ensure_swift_binary from warn to debug — it's expected in packaged builds and not actionable. Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/audio/recorder.rs | 4 ++-- src-tauri/src/utils/swift_binary.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/audio/recorder.rs b/src-tauri/src/audio/recorder.rs index 0abca67..c6942e7 100644 --- a/src-tauri/src/audio/recorder.rs +++ b/src-tauri/src/audio/recorder.rs @@ -365,10 +365,10 @@ impl AudioRecorder { } pub fn check_dependencies() -> (bool, String) { - if crate::utils::swift_binary::ensure_swift_binary("record", "scripts/record.swift") { + if crate::utils::swift_binary::get_binary_path("record").exists() { (true, "native audio recorder ready".into()) } else { - (false, "Audio recorder unavailable. Install Xcode Command Line Tools: xcode-select --install".into()) + (false, "Audio recorder binary not found. Reinstall Echo or install Xcode Command Line Tools: xcode-select --install".into()) } } diff --git a/src-tauri/src/utils/swift_binary.rs b/src-tauri/src/utils/swift_binary.rs index 04b9ebd..cb0f39d 100644 --- a/src-tauri/src/utils/swift_binary.rs +++ b/src-tauri/src/utils/swift_binary.rs @@ -171,7 +171,7 @@ pub fn ensure_swift_binary(binary_name: &str, source_relative_path: &str) -> boo Some(p) => p, None => { if binary_path.exists() { return true; } - log::warn!("[swift-binary] Source not found for {}", binary_name); + log::debug!("[swift-binary] Source not found for {}", binary_name); return false; } }; From 8be0dfb255ed80f2022b4577098f0d19ab15e00f Mon Sep 17 00:00:00 2001 From: Khugan Shanmugeswaran Date: Fri, 4 Sep 2026 14:43:15 -0400 Subject: [PATCH 2/4] Check for git and cmake before attempting whisper.cpp build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_binary() now verifies git and cmake are installed before cloning or configuring. Without this, a missing cmake causes a confusing "cmake failed: No such file or directory" that the error mapper rewrites to the generic "Whisper is not set up" — hiding the actual fix. The new error messages intentionally avoid the word "whisper" so they pass through to_user_facing_error() verbatim, giving the user an actionable install command. Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/transcription/whisper.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src-tauri/src/transcription/whisper.rs b/src-tauri/src/transcription/whisper.rs index 54a621f..6682a9c 100644 --- a/src-tauri/src/transcription/whisper.rs +++ b/src-tauri/src/transcription/whisper.rs @@ -395,6 +395,13 @@ pub async fn build_binary(progress_cb: impl Fn(&str) + Send + 'static) -> Result return Ok(()); } + if !cmd_exists("git") { + return Err("git is not installed. Install Xcode Command Line Tools: xcode-select --install".into()); + } + if !cmd_exists("cmake") { + return Err("cmake is not installed. Install it with: brew install cmake".into()); + } + let tmp_dir = std::env::temp_dir().join("echo-whisper-build"); fs::create_dir_all(bin_dir()).map_err(|e| e.to_string())?; fs::create_dir_all(&tmp_dir).map_err(|e| e.to_string())?; @@ -443,6 +450,10 @@ pub async fn build_binary(progress_cb: impl Fn(&str) + Send + 'static) -> Result Ok(()) } +fn cmd_exists(name: &str) -> bool { + Command::new(name).arg("--version").output().map(|o| o.status.success()).unwrap_or(false) +} + fn run_cmd(cmd: &str, args: &[&str], cwd: &Path) -> Result<(), String> { let path_env = format!( "{}:/opt/homebrew/bin:/usr/local/bin", From 7aee1610447a37e7364d3a261158630f7ff8a70a Mon Sep 17 00:00:00 2001 From: Khugan Shanmugeswaran Date: Fri, 4 Sep 2026 14:45:08 -0400 Subject: [PATCH 3/4] Distinguish missing binary from permission errors in user-facing messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the record binary doesn't exist, the error mapper was showing "Grant microphone access..." which is misleading — the real problem is a missing binary, not a permission issue. Now errors containing "unavailable", "not found", or "command not found" correctly say "Audio recorder binary is missing" with an actionable fix. Permission- related errors still point to System Settings. Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/utils/errors.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/utils/errors.rs b/src-tauri/src/utils/errors.rs index a0268f9..df02849 100644 --- a/src-tauri/src/utils/errors.rs +++ b/src-tauri/src/utils/errors.rs @@ -2,7 +2,10 @@ pub fn to_user_facing_error(err: &str) -> String { let lower = err.to_lowercase(); if lower.contains("audio recorder") || lower.contains("record.swift") || lower.contains("rec: command not found") { - return "Audio recording is unavailable. Grant microphone access in System Settings → Privacy & Security → Microphone (and install Xcode Command Line Tools if prompted).".into(); + if lower.contains("unavailable") || lower.contains("not found") || lower.contains("command not found") { + return "Audio recorder binary is missing. Reinstall Echo or install Xcode Command Line Tools (xcode-select --install) and restart.".into(); + } + return "Audio recording failed. Grant microphone access in System Settings → Privacy & Security → Microphone.".into(); } if lower.contains("whisper") && (lower.contains("not found") || lower.contains("not ready")) { return "Whisper is not set up. Open Settings and build/download Whisper.".into(); @@ -47,8 +50,21 @@ mod tests { // Mirrors tests/errors.test.ts. The TS side wraps input in `new Error(...)` // and extracts `.message`; the Rust API takes the message string directly. #[test] - fn maps_audio_recorder_errors() { - assert!(to_user_facing_error("rec: command not found").contains("Audio recording")); + fn maps_audio_recorder_missing_binary() { + let result = to_user_facing_error("rec: command not found"); + assert!(result.contains("Audio recorder binary is missing"), "got: {}", result); + } + + #[test] + fn maps_audio_recorder_binary_unavailable() { + let result = to_user_facing_error("Native audio recorder unavailable (failed to compile record.swift)"); + assert!(result.contains("missing"), "got: {}", result); + } + + #[test] + fn maps_audio_recorder_permission_errors() { + let result = to_user_facing_error("audio recorder permission denied"); + assert!(result.contains("Microphone"), "got: {}", result); } #[test] From 282217661ef5bdca1e51cd6af5df784605cce7a8 Mon Sep 17 00:00:00 2001 From: Khugan Shanmugeswaran Date: Fri, 4 Sep 2026 15:26:28 -0400 Subject: [PATCH 4/4] Address PR review: fix PATH in cmd_exists and catch spawn ENOENT in error mapper cmd_exists now uses the same Homebrew-aware PATH as run_cmd so the pre-build check for git/cmake doesn't false-reject on Finder-launched .app bundles. The error mapper now matches "no such file" so a missing recorder binary spawn failure routes to the correct user-facing message instead of the microphone-permission one. Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/transcription/whisper.rs | 20 ++++++++++++++------ src-tauri/src/utils/errors.rs | 8 +++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/transcription/whisper.rs b/src-tauri/src/transcription/whisper.rs index 6682a9c..ef8bf00 100644 --- a/src-tauri/src/transcription/whisper.rs +++ b/src-tauri/src/transcription/whisper.rs @@ -450,19 +450,27 @@ pub async fn build_binary(progress_cb: impl Fn(&str) + Send + 'static) -> Result Ok(()) } +fn build_path_env() -> String { + format!( + "{}:/opt/homebrew/bin:/usr/local/bin", + std::env::var("PATH").unwrap_or_default() + ) +} + fn cmd_exists(name: &str) -> bool { - Command::new(name).arg("--version").output().map(|o| o.status.success()).unwrap_or(false) + Command::new(name) + .arg("--version") + .env("PATH", build_path_env()) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } fn run_cmd(cmd: &str, args: &[&str], cwd: &Path) -> Result<(), String> { - let path_env = format!( - "{}:/opt/homebrew/bin:/usr/local/bin", - std::env::var("PATH").unwrap_or_default() - ); let output = Command::new(cmd) .args(args) .current_dir(cwd) - .env("PATH", &path_env) + .env("PATH", build_path_env()) .output() .map_err(|e| format!("{} failed: {}", cmd, e))?; diff --git a/src-tauri/src/utils/errors.rs b/src-tauri/src/utils/errors.rs index df02849..60558f5 100644 --- a/src-tauri/src/utils/errors.rs +++ b/src-tauri/src/utils/errors.rs @@ -2,7 +2,7 @@ pub fn to_user_facing_error(err: &str) -> String { let lower = err.to_lowercase(); if lower.contains("audio recorder") || lower.contains("record.swift") || lower.contains("rec: command not found") { - if lower.contains("unavailable") || lower.contains("not found") || lower.contains("command not found") { + if lower.contains("unavailable") || lower.contains("not found") || lower.contains("command not found") || lower.contains("no such file") { return "Audio recorder binary is missing. Reinstall Echo or install Xcode Command Line Tools (xcode-select --install) and restart.".into(); } return "Audio recording failed. Grant microphone access in System Settings → Privacy & Security → Microphone.".into(); @@ -61,6 +61,12 @@ mod tests { assert!(result.contains("missing"), "got: {}", result); } + #[test] + fn maps_audio_recorder_spawn_no_such_file() { + let result = to_user_facing_error("Failed to start native audio recorder: No such file or directory (os error 2)"); + assert!(result.contains("missing"), "got: {}", result); + } + #[test] fn maps_audio_recorder_permission_errors() { let result = to_user_facing_error("audio recorder permission denied");