From f7fe0f203e0f349df33819aab239c0aade016f79 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sun, 17 May 2026 01:41:17 +0300 Subject: fix: polish terminal-core agent semantics --- src/cli.rs | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 8 deletions(-) (limited to 'src/cli.rs') diff --git a/src/cli.rs b/src/cli.rs index ee73fb6f..a99149dc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -281,6 +281,7 @@ fn run_agent_command(args: &[String]) -> std::io::Result { "send" => agent_send(&args[1..]), "rename" => agent_rename(&args[1..]), "focus" => agent_focus(&args[1..]), + "wait" => agent_wait(&args[1..]), "attach" => agent_attach(&args[1..]), "start" => agent_start(&args[1..]), "help" | "--help" | "-h" => { @@ -934,14 +935,9 @@ fn agent_attach(args: &[String]) -> std::io::Result { Err(code) => return Ok(code), }; - let response = send_request(&Request { - id: "cli:agent:attach:resolve".into(), - method: Method::AgentGet(AgentTarget { - target: target.clone(), - }), - })?; - if let Some(error) = response.get("error") { - eprintln!("{}", serde_json::to_string(error).unwrap()); + let response = resolve_agent_target(&target, "cli:agent:attach:resolve")?; + if response.get("error").is_some() { + eprintln!("{}", serde_json::to_string(&response).unwrap()); return Ok(1); } let Some(terminal_id) = response["result"]["agent"]["terminal_id"].as_str() else { @@ -952,6 +948,107 @@ fn agent_attach(args: &[String]) -> std::io::Result { Ok(0) } +fn agent_wait(args: &[String]) -> std::io::Result { + let Some(target) = args.first() else { + eprintln!("usage: herdr agent wait --status [--timeout MS]"); + return Ok(2); + }; + + let mut timeout_ms = None; + let mut desired_status = None; + + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--status" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --status"); + return Ok(2); + }; + desired_status = Some(parse_agent_wait_status(value)?); + index += 2; + } + "--timeout" => { + let Some(value) = args.get(index + 1) else { + eprintln!("missing value for --timeout"); + return Ok(2); + }; + timeout_ms = Some(parse_u64_flag("--timeout", value)?); + index += 2; + } + "help" | "--help" | "-h" => { + eprintln!("usage: herdr agent wait --status [--timeout MS]"); + return Ok(0); + } + other => { + eprintln!("unknown option: {other}"); + return Ok(2); + } + } + } + + let Some(agent_status) = desired_status else { + eprintln!("missing required --status"); + return Ok(2); + }; + + let response = resolve_agent_target(target, "cli:agent:wait:resolve")?; + if response.get("error").is_some() { + eprintln!("{}", serde_json::to_string(&response).unwrap()); + return Ok(1); + } + if response["result"]["agent"]["agent_status"] + .as_str() + .is_some_and(|current| agent_wait_status_satisfied(agent_status, current)) + { + println!("{}", serde_json::to_string(&response).unwrap()); + return Ok(0); + } + + let Some(pane_id) = response["result"]["agent"]["pane_id"].as_str() else { + eprintln!("agent wait failed: response did not include pane_id"); + return Ok(1); + }; + + let subscriptions = if agent_status == AgentStatus::Idle { + vec![ + Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(AgentStatus::Idle), + }, + Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(AgentStatus::Done), + }, + ] + } else { + vec![Subscription::PaneAgentStatusChanged { + pane_id: pane_id.to_owned(), + agent_status: Some(agent_status), + }] + }; + + wait_for_agent_change( + Request { + id: "cli:agent:wait".into(), + method: Method::EventsSubscribe(crate::api::schema::EventsSubscribeParams { + subscriptions, + }), + }, + timeout_ms, + "timed out waiting for agent status change", + ) +} + +fn resolve_agent_target(target: &str, request_id: &str) -> std::io::Result { + send_request(&Request { + id: request_id.into(), + method: Method::AgentGet(AgentTarget { + target: target.to_owned(), + }), + }) +} + fn terminal_attach(args: &[String]) -> std::io::Result { let (terminal_id, takeover) = match parse_attach_target( args, @@ -1835,6 +1932,31 @@ fn parse_read_format(value: &str) -> std::io::Result { } } +fn agent_wait_status_satisfied(desired: AgentStatus, current: &str) -> bool { + match desired { + AgentStatus::Idle => matches!(current, "idle" | "done"), + AgentStatus::Working => current == "working", + AgentStatus::Blocked => current == "blocked", + AgentStatus::Unknown => current == "unknown", + AgentStatus::Done => false, + } +} + +fn parse_agent_wait_status(value: &str) -> std::io::Result { + match value { + "idle" => Ok(AgentStatus::Idle), + "working" => Ok(AgentStatus::Working), + "blocked" => Ok(AgentStatus::Blocked), + "unknown" => Ok(AgentStatus::Unknown), + "done" => Err(std::io::Error::other( + "done is a UI attention state; use idle for CLI agent completion waits", + )), + _ => Err(std::io::Error::other(format!( + "invalid agent status: {value} (expected idle, working, blocked, or unknown)" + ))), + } +} + fn parse_agent_status(value: &str) -> std::io::Result { match value { "idle" => Ok(AgentStatus::Idle), @@ -1978,6 +2100,7 @@ fn print_agent_help() { eprintln!(" herdr agent send "); eprintln!(" herdr agent rename |--clear"); eprintln!(" herdr agent focus "); + eprintln!(" herdr agent wait --status [--timeout MS]"); eprintln!(" herdr agent attach [--takeover]"); eprintln!(" herdr agent start [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- "); eprintln!(" targets accept terminal ids, unique agent names, and legacy pane ids"); -- cgit v1.2.3