diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index d854d10b2e..c02f26779c 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 306c3bd9fa..42bc5e1cb1 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 { 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 3164b8021b..c3e98f86f4 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,