From a0aa6cd5262971b0d65719db73a69a95fef61cf3 Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 27 May 2026 08:29:18 -0700 Subject: [PATCH 1/2] fix(desktop): strip ANSI escapes from agent log tail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent harness (sprout-acp) emits colorized tracing output. When its stdout/stderr is redirected to a log file by the desktop spawn path, the ANSI escape codes get baked into the file and render as `ESC[2m`-style gunk in the log viewer's
.

Strip the escapes in `read_log_tail` (desktop-only) using the
`strip-ansi-escapes` crate, which delegates to `vte` — the same
terminal parser Alacritty uses. Handles CSI, OSC, DCS, and C1 codes
correctly, where a hand-rolled scrubber would silently mangle them.

Leaves sprout-acp's logging untouched so terminals, `tail -f`, and
CI remain colorized.

Signed-off-by: Wes 
---
 desktop/src-tauri/Cargo.lock                  | 19 +++++++++++++++++++
 desktop/src-tauri/Cargo.toml                  |  1 +
 .../src-tauri/src/managed_agents/storage.rs   | 19 +++++++++++++++++--
 3 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index 28461d551..f759beb7b 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -5193,6 +5193,7 @@ dependencies = [
  "sprout-core",
  "sprout-persona",
  "sprout-sdk",
+ "strip-ansi-escapes",
  "tar",
  "tauri",
  "tauri-build",
@@ -5300,6 +5301,15 @@ dependencies = [
  "quote",
 ]
 
+[[package]]
+name = "strip-ansi-escapes"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
+dependencies = [
+ "vte",
+]
+
 [[package]]
 name = "strsim"
 version = "0.11.1"
@@ -6793,6 +6803,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "vte"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "walkdir"
 version = "2.5.0"
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index 855e75462..695c4f18f 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -72,5 +72,6 @@ earshot = "1.0"
 rubato = "3.0"
 audioadapter-buffers = "3.0"
 tempfile = "3"
+strip-ansi-escapes = "0.2"
 
 [dev-dependencies]
diff --git a/desktop/src-tauri/src/managed_agents/storage.rs b/desktop/src-tauri/src/managed_agents/storage.rs
index ae3ac6204..265224d66 100644
--- a/desktop/src-tauri/src/managed_agents/storage.rs
+++ b/desktop/src-tauri/src/managed_agents/storage.rs
@@ -181,8 +181,11 @@ pub fn read_log_tail(path: &Path, max_lines: usize) -> Result {
         newline_count = bytecount_newlines(&buf);
     }
 
-    let text = String::from_utf8_lossy(&buf);
-    let lines: Vec<&str> = text.lines().collect();
+    // Strip ANSI escapes here (not in the harness) so the desktop log view
+    // renders cleanly while terminals and other tools still get the colors
+    // sprout-acp emits.
+    let cleaned = strip_ansi_escapes::strip_str(&String::from_utf8_lossy(&buf));
+    let lines: Vec<&str> = cleaned.lines().collect();
     let start = lines.len().saturating_sub(max_lines);
     Ok(lines[start..].join("\n"))
 }
@@ -190,3 +193,15 @@ pub fn read_log_tail(path: &Path, max_lines: usize) -> Result {
 fn bytecount_newlines(buf: &[u8]) -> usize {
     buf.iter().filter(|&&b| b == b'\n').count()
 }
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn strips_ansi_from_typical_tracing_line() {
+        let input = "\x1b[2m2026-05-27T15:16:32\x1b[0m \x1b[32m INFO\x1b[0m \x1b[2msprout_acp\x1b[0m\x1b[2m:\x1b[0m starting";
+        assert_eq!(
+            strip_ansi_escapes::strip_str(input),
+            "2026-05-27T15:16:32  INFO sprout_acp: starting"
+        );
+    }
+}

From 3d1ef4b1167f21ab55a765e0d7c77456862631f7 Mon Sep 17 00:00:00 2001
From: Wes 
Date: Wed, 27 May 2026 08:54:54 -0700
Subject: [PATCH 2/2] refactor(desktop): drop unused AppHandle plumbing in
 command discovery

The `app: Option<&AppHandle>` parameter was threaded through five
functions in `managed_agents/discovery.rs` (`command_search_dirs`,
`resolve_workspace_command`, `resolve_command`, `resolve_command_uncached`,
`command_availability`) and never read by any of them. This produced an
unused-variable warning and carried ghost intent across the public API.

Remove the parameter at every layer, drop the now-unused `AppHandle`
imports, and update the six call sites in `agent_discovery`,
`agent_models`, `media`, and `runtime`. The `discover_managed_agent_prereqs`
Tauri command no longer needs the injected `AppHandle` either.

If a future change needs `AppHandle` (e.g. for `resolve_resource` to
locate bundled sidecars), threading it back in will be a self-documenting
diff rather than dead plumbing.

No behavior change.

Signed-off-by: Wes 
---
 .../src-tauri/src/commands/agent_discovery.rs | 11 +++++-----
 .../src-tauri/src/commands/agent_models.rs    |  4 ++--
 desktop/src-tauri/src/commands/media.rs       |  2 +-
 .../src-tauri/src/managed_agents/discovery.rs | 22 +++++++++----------
 .../src-tauri/src/managed_agents/runtime.rs   | 21 +++++++++---------
 5 files changed, 28 insertions(+), 32 deletions(-)

diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs
index 7c27d73bc..efc4ed308 100644
--- a/desktop/src-tauri/src/commands/agent_discovery.rs
+++ b/desktop/src-tauri/src/commands/agent_discovery.rs
@@ -1,5 +1,5 @@
 use std::io::Read;
-use tauri::{AppHandle, State};
+use tauri::State;
 
 use crate::{
     app_state::AppState,
@@ -64,7 +64,7 @@ fn install_acp_runtime_blocking(provider_id: &str) -> Result Result String {
 #[tauri::command]
 pub fn discover_managed_agent_prereqs(
     input: DiscoverManagedAgentPrereqsRequest,
-    app: AppHandle,
 ) -> ManagedAgentPrereqsInfo {
     let acp_command = input
         .acp_command
@@ -302,8 +301,8 @@ pub fn discover_managed_agent_prereqs(
         .unwrap_or(DEFAULT_MCP_COMMAND);
 
     ManagedAgentPrereqsInfo {
-        acp: command_availability(acp_command, Some(&app)),
-        mcp: command_availability(mcp_command, Some(&app)),
+        acp: command_availability(acp_command),
+        mcp: command_availability(mcp_command),
     }
 }
 
diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs
index 3d7bc12f2..dc1ab6059 100644
--- a/desktop/src-tauri/src/commands/agent_models.rs
+++ b/desktop/src-tauri/src/commands/agent_models.rs
@@ -45,12 +45,12 @@ pub async fn get_agent_models(
             .find(|r| r.pubkey == pubkey)
             .ok_or_else(|| format!("agent {pubkey} not found"))?;
 
-        let resolved = resolve_command(&record.acp_command, Some(&app))
+        let resolved = resolve_command(&record.acp_command)
             .ok_or_else(|| missing_command_message(&record.acp_command, "ACP harness command"))?;
 
         let args = normalize_agent_args(&record.agent_command, record.agent_args.clone());
 
-        let resolved_agent = resolve_command(&record.agent_command, Some(&app))
+        let resolved_agent = resolve_command(&record.agent_command)
             .map(|p| p.display().to_string())
             .unwrap_or_else(|| record.agent_command.clone());
 
diff --git a/desktop/src-tauri/src/commands/media.rs b/desktop/src-tauri/src/commands/media.rs
index f27fdb015..4635c7f83 100644
--- a/desktop/src-tauri/src/commands/media.rs
+++ b/desktop/src-tauri/src/commands/media.rs
@@ -239,7 +239,7 @@ pub async fn upload_media(
 /// (login shell PATH, /opt/homebrew/bin, /usr/local/bin, etc.).
 /// Returns the resolved absolute path on success.
 fn find_ffmpeg() -> Result {
-    let ffmpeg_path = resolve_command("ffmpeg", None).ok_or_else(|| {
+    let ffmpeg_path = resolve_command("ffmpeg").ok_or_else(|| {
         "ffmpeg is required for video uploads but was not found.\n\n\
          Install it:\n  \
          macOS:   brew install ffmpeg\n  \
diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs
index 0fc8a42f9..acb38e9c0 100644
--- a/desktop/src-tauri/src/managed_agents/discovery.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery.rs
@@ -1,8 +1,6 @@
 use std::path::{Path, PathBuf};
 use std::process::Command;
 
-use tauri::AppHandle;
-
 use crate::managed_agents::{
     AcpAvailabilityStatus, AcpProviderCatalogEntry, CommandAvailabilityInfo,
 };
@@ -234,7 +232,7 @@ pub fn normalize_agent_args(command: &str, agent_args: Vec) -> Vec) -> Vec {
+fn command_search_dirs() -> Vec {
     let mut dirs = vec![
         workspace_root_dir().join("target/release"),
         workspace_root_dir().join("target/debug"),
@@ -262,14 +260,14 @@ fn command_search_dirs(app: Option<&AppHandle>) -> Vec {
     unique
 }
 
-fn resolve_workspace_command(command: &str, app: Option<&AppHandle>) -> Option {
+fn resolve_workspace_command(command: &str) -> Option {
     if command_looks_like_path(command) {
         let path = PathBuf::from(command);
         return path.exists().then_some(path);
     }
 
     let file_name = executable_basename(command);
-    command_search_dirs(app)
+    command_search_dirs()
         .into_iter()
         .map(|dir| dir.join(&file_name))
         .find(|candidate| candidate.exists())
@@ -286,7 +284,7 @@ fn resolve_cache() -> &'static std::sync::Mutex) -> Option {
+pub fn resolve_command(command: &str) -> Option {
     let cache = resolve_cache();
 
     // Fast path: return cached result without allocating a key.
@@ -297,7 +295,7 @@ pub fn resolve_command(command: &str, app: Option<&AppHandle>) -> Option) -> Option {
-    if let Some(path) = resolve_workspace_command(command, app) {
+fn resolve_command_uncached(command: &str) -> Option {
+    if let Some(path) = resolve_workspace_command(command) {
         return Some(path);
     }
 
@@ -393,11 +391,11 @@ pub fn login_shell_path() -> Option {
 }
 
 fn find_command(command: &str) -> Option {
-    resolve_command(command, None)
+    resolve_command(command)
 }
 
-pub fn command_availability(command: &str, app: Option<&AppHandle>) -> CommandAvailabilityInfo {
-    let resolved_path = resolve_command(command, app).map(|path| path.display().to_string());
+pub fn command_availability(command: &str) -> CommandAvailabilityInfo {
+    let resolved_path = resolve_command(command).map(|path| path.display().to_string());
     CommandAvailabilityInfo {
         command: command.to_string(),
         available: resolved_path.is_some(),
diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs
index 2cf295154..7818522bd 100644
--- a/desktop/src-tauri/src/managed_agents/runtime.rs
+++ b/desktop/src-tauri/src/managed_agents/runtime.rs
@@ -527,19 +527,18 @@ pub fn spawn_agent_child(
         .try_clone()
         .map_err(|error| format!("failed to clone log handle: {error}"))?;
     let agent_args = normalize_agent_args(&record.agent_command, record.agent_args.clone());
-    let resolved_acp_command = resolve_command(&record.acp_command, Some(app))
+    let resolved_acp_command = resolve_command(&record.acp_command)
         .ok_or_else(|| missing_command_message(&record.acp_command, "ACP harness command"))?;
-    let resolved_mcp_command: Option = if record.mcp_command.is_empty() {
-        None
-    } else {
-        Some(
-            resolve_command(&record.mcp_command, Some(app)).ok_or_else(|| {
+    let resolved_mcp_command: Option =
+        if record.mcp_command.is_empty() {
+            None
+        } else {
+            Some(resolve_command(&record.mcp_command).ok_or_else(|| {
                 missing_command_message(&record.mcp_command, "MCP server command")
-            })?,
-        )
-    };
+            })?)
+        };
     // Resolve agent command to a full path (DMG launches have minimal PATH).
-    let resolved_agent_command = resolve_command(&record.agent_command, Some(app))
+    let resolved_agent_command = resolve_command(&record.agent_command)
         .map(|p| p.display().to_string())
         .unwrap_or_else(|| record.agent_command.clone());
 
@@ -698,7 +697,7 @@ pub fn spawn_agent_child(
     // interfere with other remotes (e.g. GitHub).
     //
     // NOSTR_PRIVATE_KEY mirrors SPROUT_PRIVATE_KEY — keep in sync.
-    if let Some(cred_helper) = resolve_command("git-credential-nostr", Some(app)) {
+    if let Some(cred_helper) = resolve_command("git-credential-nostr") {
         let relay_http_url = crate::relay::relay_http_base_url(&record.relay_url);
 
         command.env("NOSTR_PRIVATE_KEY", &record.private_key_nsec);