aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-25 01:44:10 +0300
committerGitHub <noreply@github.com>2026-08-25 01:44:10 +0300
commit0e548bcf72671e6e6e661e90192bcb7f2f442032 (patch)
treebe689fcc2ace48cd6e288998f0bd470307a313a5
parentcafd9e5dd0e59ecc7551ae066c737f23573460a7 (diff)
fix: report agent explain file errors as json (#3023)
refs #3022 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/cli/agent.rs20
-rw-r--r--tests/cli/agents.rs29
3 files changed, 47 insertions, 3 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index d854d10b..c02f2677 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -17,6 +17,7 @@
- Windows clients now preserve layout-generated text for Shift-only keys, so characters such as `/` on German keyboards reach shell panes and pasted input. (#3045)
- Windows panes now keep bare `cursor-agent` launches detected after Cursor hands off to its bundled Node process. (#3032)
- Oversized Kitty images no longer prevent smaller images shown later in the same pane from rendering. (#3033)
+- `herdr agent explain --file` now reports fixture read failures as structured JSON instead of exposing Rust I/O debug output. (#3022)
- Claude Code panes now use visible turn, background shell, and background agent activity as working-state fallbacks when OSC titles are unavailable or disabled. (#1630, #2241)
- Claude Code panes now remain working while MCP tasks continue in the background after a turn ends. (#3090)
- Tab bar status commands now remove ESC-prefixed terminal control sequences instead of displaying their sequence bodies as text. (#3001)
diff --git a/src/cli/agent.rs b/src/cli/agent.rs
index 306c3bd9..42bc5e1c 100644
--- a/src/cli/agent.rs
+++ b/src/cli/agent.rs
@@ -2,8 +2,8 @@ use std::time::{Duration, Instant};
use crate::api::schema::{
AgentPromptParams, AgentPromptWaitOptions, AgentReadParams, AgentRenameParams,
- AgentSendKeysParams, AgentStartParams, AgentTarget, AgentWaitParams, EmptyParams, Method,
- PaneProcessInfoParams, PaneTarget, ReadFormat, ReadSource, Request,
+ AgentSendKeysParams, AgentStartParams, AgentTarget, AgentWaitParams, EmptyParams, ErrorBody,
+ ErrorResponse, Method, PaneProcessInfoParams, PaneTarget, ReadFormat, ReadSource, Request,
};
const AGENT_START_POLL_INTERVAL: Duration = Duration::from_millis(100);
@@ -118,7 +118,21 @@ fn agent_explain(args: &[String]) -> std::io::Result<i32> {
eprintln!("herdr agent explain --file requires --agent LABEL");
return Ok(2);
};
- let content = std::fs::read_to_string(path)?;
+ let content = match std::fs::read_to_string(&path) {
+ Ok(content) => content,
+ Err(err) => {
+ let response = ErrorResponse {
+ id: "cli:agent:explain".into(),
+ error: ErrorBody {
+ code: "agent_explain_file_read_failed".into(),
+ message: format!("failed to read agent explain file {path}: {err}"),
+ },
+ };
+ let response = serde_json::to_string(&response).map_err(std::io::Error::other)?;
+ eprintln!("{response}");
+ return Ok(1);
+ }
+ };
crate::detect::manifest::explain_to_json_value(&crate::detect::manifest::explain_for_label(
&agent_label,
&content,
diff --git a/tests/cli/agents.rs b/tests/cli/agents.rs
index 3164b802..c3e98f86 100644
--- a/tests/cli/agents.rs
+++ b/tests/cli/agents.rs
@@ -1,5 +1,34 @@
use super::harness::*;
+#[test]
+fn agent_explain_missing_file_reports_json_error() {
+ let base = unique_test_dir();
+ let missing = base.join("missing-screen.txt");
+ let output = run_named_cli(
+ &base.join("config"),
+ &base.join("runtime"),
+ &[
+ "agent",
+ "explain",
+ "--file",
+ missing.to_str().unwrap(),
+ "--agent",
+ "claude",
+ "--json",
+ ],
+ );
+
+ assert_eq!(output.status.code(), Some(1));
+ assert!(output.stdout.is_empty());
+ let error: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
+ assert_eq!(error["id"], "cli:agent:explain");
+ assert_eq!(error["error"]["code"], "agent_explain_file_read_failed");
+ assert!(error["error"]["message"]
+ .as_str()
+ .unwrap()
+ .contains(missing.to_str().unwrap()));
+}
+
fn write_delayed_shell_and_fake_pi(
base: &Path,
shell_delay_seconds: &str,