diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-09 03:31:37 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-09 03:31:37 +0300 |
| commit | a3ecb4e3b4151e840ad237e073d8282e1181c34a (patch) | |
| tree | 12c6924cc65c2b9c5da45df2e3a2fde188f07681 /src | |
| parent | 05a6d89e33e722376688c679ab0d7322014814a1 (diff) | |
feat(api): expose done as agent status
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/mod.rs | 43 | ||||
| -rw-r--r-- | src/api/schema.rs | 58 | ||||
| -rw-r--r-- | src/app/mod.rs | 39 | ||||
| -rw-r--r-- | src/cli.rs | 65 |
4 files changed, 120 insertions, 85 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index 28a80602..1b81e09c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -15,7 +15,7 @@ use regex::Regex; use tokio::sync::mpsc; use crate::api::schema::{ - ErrorBody, ErrorResponse, Method, PaneAgentStateChangedEvent, PaneOutputMatchedEvent, Request, + ErrorBody, ErrorResponse, Method, PaneAgentStatusChangedEvent, PaneOutputMatchedEvent, Request, ResponseResult, Subscription, SubscriptionEventData, SubscriptionEventEnvelope, SubscriptionEventKind, SuccessResponse, }; @@ -553,10 +553,10 @@ struct ActiveOutputMatchedSubscription { request_prefix: String, } -struct ActiveAgentStateChangedSubscription { +struct ActiveAgentStatusChangedSubscription { pane_id: String, - state_filter: Option<crate::api::schema::PaneAgentState>, - last_state: Option<crate::api::schema::PaneAgentState>, + status_filter: Option<crate::api::schema::AgentStatus>, + last_status: Option<crate::api::schema::AgentStatus>, request_prefix: String, } @@ -568,7 +568,7 @@ struct ActiveEventSubscription { enum ActiveSubscription { Event(ActiveEventSubscription), OutputMatched(ActiveOutputMatchedSubscription), - AgentStateChanged(ActiveAgentStateChangedSubscription), + AgentStatusChanged(ActiveAgentStatusChangedSubscription), } impl ActiveSubscription { @@ -674,18 +674,21 @@ impl ActiveSubscription { request_prefix: format!("{request_id}:sub:{index}"), })) } - Subscription::PaneAgentStateChanged { pane_id, state } => { + Subscription::PaneAgentStatusChanged { + pane_id, + agent_status, + } => { let probe = match pane_get(format!("{request_id}:sub:{index}:probe"), &pane_id, api_tx) { Ok(probe) => probe, Err(error) => return Err(error), }; - Ok(Self::AgentStateChanged( - ActiveAgentStateChangedSubscription { + Ok(Self::AgentStatusChanged( + ActiveAgentStatusChangedSubscription { pane_id, - state_filter: state, - last_state: Some(probe.agent_state), + status_filter: agent_status, + last_status: Some(probe.agent_status), request_prefix: format!("{request_id}:sub:{index}"), }, )) @@ -703,7 +706,7 @@ impl ActiveSubscription { Self::OutputMatched(subscription) => { serde_json::to_value(subscription.poll(api_tx)?).ok() } - Self::AgentStateChanged(subscription) => { + Self::AgentStatusChanged(subscription) => { serde_json::to_value(subscription.poll(api_tx)?).ok() } } @@ -758,7 +761,7 @@ impl ActiveOutputMatchedSubscription { } } -impl ActiveAgentStateChangedSubscription { +impl ActiveAgentStatusChangedSubscription { fn poll(&mut self, api_tx: &ApiRequestSender) -> Option<SubscriptionEventEnvelope> { let pane = pane_get( format!("{}:pane", self.request_prefix), @@ -766,24 +769,24 @@ impl ActiveAgentStateChangedSubscription { api_tx, ) .ok()?; - let current_state = pane.agent_state; - let previous_state = self.last_state.replace(current_state); - if previous_state.is_none() || previous_state == Some(current_state) { + let current_status = pane.agent_status; + let previous_status = self.last_status.replace(current_status); + if previous_status.is_none() || previous_status == Some(current_status) { return None; } if self - .state_filter - .is_some_and(|wanted| wanted != current_state) + .status_filter + .is_some_and(|wanted| wanted != current_status) { return None; } Some(SubscriptionEventEnvelope { - event: SubscriptionEventKind::PaneAgentStateChanged, - data: SubscriptionEventData::PaneAgentStateChanged(PaneAgentStateChangedEvent { + event: SubscriptionEventKind::PaneAgentStatusChanged, + data: SubscriptionEventData::PaneAgentStatusChanged(PaneAgentStatusChangedEvent { pane_id: pane.pane_id, workspace_id: pane.workspace_id, - state: current_state, + agent_status: current_status, agent: pane.agent, }), }) diff --git a/src/api/schema.rs b/src/api/schema.rs index 063663fa..568a276b 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -253,11 +253,11 @@ pub enum Subscription { #[serde(default = "default_true")] strip_ansi: bool, }, - #[serde(rename = "pane.agent_state_changed")] - PaneAgentStateChanged { + #[serde(rename = "pane.agent_status_changed")] + PaneAgentStatusChanged { pane_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] - state: Option<PaneAgentState>, + agent_status: Option<AgentStatus>, }, } @@ -348,9 +348,9 @@ pub enum EventMatch { #[serde(default, skip_serializing_if = "Option::is_none")] agent: Option<String>, }, - PaneAgentStateChanged { + PaneAgentStatusChanged { pane_id: String, - state: PaneAgentState, + agent_status: AgentStatus, }, } @@ -371,7 +371,7 @@ pub enum EventKind { PaneOutputChanged, PaneExited, PaneAgentDetected, - PaneAgentStateChanged, + PaneAgentStatusChanged, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -441,7 +441,7 @@ pub struct WorkspaceInfo { pub pane_count: usize, pub tab_count: usize, pub active_tab_id: String, - pub agent_state: PaneAgentState, + pub agent_status: AgentStatus, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -452,7 +452,7 @@ pub struct TabInfo { pub label: String, pub focused: bool, pub pane_count: usize, - pub agent_state: PaneAgentState, + pub agent_status: AgentStatus, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -465,7 +465,7 @@ pub struct PaneInfo { pub cwd: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option<String>, - pub agent_state: PaneAgentState, + pub agent_status: AgentStatus, pub revision: u64, } @@ -490,8 +490,8 @@ pub struct EventEnvelope { pub enum SubscriptionEventKind { #[serde(rename = "pane.output_matched")] PaneOutputMatched, - #[serde(rename = "pane.agent_state_changed")] - PaneAgentStateChanged, + #[serde(rename = "pane.agent_status_changed")] + PaneAgentStatusChanged, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -504,7 +504,7 @@ pub struct SubscriptionEventEnvelope { #[serde(untagged)] pub enum SubscriptionEventData { PaneOutputMatched(PaneOutputMatchedEvent), - PaneAgentStateChanged(PaneAgentStateChangedEvent), + PaneAgentStatusChanged(PaneAgentStatusChangedEvent), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -515,10 +515,10 @@ pub struct PaneOutputMatchedEvent { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PaneAgentStateChangedEvent { +pub struct PaneAgentStatusChangedEvent { pub pane_id: String, pub workspace_id: String, - pub state: PaneAgentState, + pub agent_status: AgentStatus, #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option<String>, } @@ -581,10 +581,10 @@ pub enum EventData { #[serde(default, skip_serializing_if = "Option::is_none")] agent: Option<String>, }, - PaneAgentStateChanged { + PaneAgentStatusChanged { pane_id: String, workspace_id: String, - state: PaneAgentState, + agent_status: AgentStatus, }, } @@ -597,6 +597,16 @@ pub enum PaneAgentState { Unknown, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AgentStatus { + Idle, + Working, + Blocked, + Done, + Unknown, +} + fn default_true() -> bool { true } @@ -777,9 +787,9 @@ mod tests { "match": { "type": "substring", "value": "auth: received" } }, { - "type": "pane.agent_state_changed", + "type": "pane.agent_status_changed", "pane_id": "p_1_1", - "state": "blocked" + "agent_status": "done" } ] } @@ -803,9 +813,9 @@ mod tests { )); assert!(matches!( ¶ms.subscriptions[1], - Subscription::PaneAgentStateChanged { + Subscription::PaneAgentStatusChanged { pane_id, - state: Some(PaneAgentState::Blocked), + agent_status: Some(AgentStatus::Done), } if pane_id == "p_1_1" )); } @@ -872,9 +882,9 @@ mod tests { "method": "events.wait", "params": { "match_event": { - "event": "pane_agent_state_changed", + "event": "pane_agent_status_changed", "pane_id": "p_1", - "state": "blocked" + "agent_status": "done" }, "timeout_ms": 30000 } @@ -887,9 +897,9 @@ mod tests { }; assert_eq!( params.match_event, - EventMatch::PaneAgentStateChanged { + EventMatch::PaneAgentStatusChanged { pane_id: "p_1".into(), - state: PaneAgentState::Blocked, + agent_status: AgentStatus::Done, } ); } diff --git a/src/app/mod.rs b/src/app/mod.rs index 22e4eef6..87af63e5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -827,12 +827,19 @@ impl App { } if update.previous_state != update.state { + let agent_status = self + .state + .workspaces + .get(update.ws_idx) + .and_then(|ws| ws.pane_state(update.pane_id)) + .map(|pane| pane_agent_status(pane.state, pane.seen)) + .unwrap_or_else(|| pane_agent_status(update.state, true)); self.emit_event(crate::api::schema::EventEnvelope { - event: crate::api::schema::EventKind::PaneAgentStateChanged, - data: crate::api::schema::EventData::PaneAgentStateChanged { + event: crate::api::schema::EventKind::PaneAgentStatusChanged, + data: crate::api::schema::EventData::PaneAgentStatusChanged { pane_id, workspace_id, - state: pane_agent_state(update.state), + agent_status, }, }); } @@ -2200,7 +2207,7 @@ impl App { fn tab_info(&self, ws_idx: usize, tab_idx: usize) -> Option<crate::api::schema::TabInfo> { let ws = self.state.workspaces.get(ws_idx)?; let tab = ws.tabs.get(tab_idx)?; - let (agg_state, _) = tab + let (agg_state, seen) = tab .panes .values() .map(|pane| (pane.state, pane.seen)) @@ -2213,7 +2220,7 @@ impl App { label: tab.display_name(), focused: self.state.active == Some(ws_idx) && ws.active_tab == tab_idx, pane_count: tab.panes.len(), - agent_state: pane_agent_state(agg_state), + agent_status: pane_agent_status(agg_state, seen), }) } @@ -2240,7 +2247,7 @@ impl App { .and_then(|rt| rt.cwd()) .map(|cwd| cwd.display().to_string()), agent: pane.detected_agent.map(agent_name), - agent_state: pane_agent_state(pane.state), + agent_status: pane_agent_status(pane.state, pane.seen), revision: 0, }) } @@ -2266,7 +2273,7 @@ impl App { fn workspace_info(&self, index: usize) -> crate::api::schema::WorkspaceInfo { let ws = &self.state.workspaces[index]; - let (agg_state, _) = ws.aggregate_state(); + let (agg_state, seen) = ws.aggregate_state(); crate::api::schema::WorkspaceInfo { workspace_id: self.public_workspace_id(index), number: index + 1, @@ -2277,7 +2284,7 @@ impl App { active_tab_id: self .public_tab_id(index, ws.active_tab) .unwrap_or_else(|| format!("{}:{}", ws.id, ws.active_tab + 1)), - agent_state: pane_agent_state(agg_state), + agent_status: pane_agent_status(agg_state, seen), } } } @@ -2349,12 +2356,16 @@ fn detect_state_from_api(state: crate::api::schema::PaneAgentState) -> crate::de } } -fn pane_agent_state(state: crate::detect::AgentState) -> crate::api::schema::PaneAgentState { - match state { - crate::detect::AgentState::Idle => crate::api::schema::PaneAgentState::Idle, - crate::detect::AgentState::Working => crate::api::schema::PaneAgentState::Working, - crate::detect::AgentState::Blocked => crate::api::schema::PaneAgentState::Blocked, - crate::detect::AgentState::Unknown => crate::api::schema::PaneAgentState::Unknown, +fn pane_agent_status( + state: crate::detect::AgentState, + seen: bool, +) -> crate::api::schema::AgentStatus { + match (state, seen) { + (crate::detect::AgentState::Idle, false) => crate::api::schema::AgentStatus::Done, + (crate::detect::AgentState::Idle, true) => crate::api::schema::AgentStatus::Idle, + (crate::detect::AgentState::Working, _) => crate::api::schema::AgentStatus::Working, + (crate::detect::AgentState::Blocked, _) => crate::api::schema::AgentStatus::Blocked, + (crate::detect::AgentState::Unknown, _) => crate::api::schema::AgentStatus::Unknown, } } @@ -6,7 +6,7 @@ use serde::Serialize; use crate::api; use crate::api::schema::{ - EmptyParams, Method, OutputMatch, PaneAgentState, PaneListParams, PaneReadParams, + AgentStatus, EmptyParams, Method, OutputMatch, PaneListParams, PaneReadParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, PaneWaitForOutputParams, ReadSource, Request, SplitDirection, Subscription, TabCreateParams, TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams, @@ -105,7 +105,7 @@ fn run_wait_command(args: &[String]) -> std::io::Result<i32> { match subcommand { "output" => wait_output(&args[1..]), - "agent-state" => wait_agent_state(&args[1..]), + "agent-status" => wait_agent_status(&args[1..]), _ => { print_wait_help(); Ok(2) @@ -851,25 +851,25 @@ fn wait_output(args: &[String]) -> std::io::Result<i32> { Ok(0) } -fn wait_agent_state(args: &[String]) -> std::io::Result<i32> { +fn wait_agent_status(args: &[String]) -> std::io::Result<i32> { let Some(raw_pane_id) = args.first() else { - eprintln!("usage: herdr wait agent-state <pane_id> --state <idle|working|blocked|unknown> [--timeout MS]"); + eprintln!("usage: herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]"); return Ok(2); }; let pane_id = normalize_pane_id(raw_pane_id); let mut timeout_ms = None; - let mut desired_state = None; + let mut desired_status = None; let mut index = 1; while index < args.len() { match args[index].as_str() { - "--state" => { + "--status" => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for --state"); + eprintln!("missing value for --status"); return Ok(2); }; - desired_state = Some(parse_agent_state(value)?); + desired_status = Some(parse_agent_status(value)?); index += 2; } "--timeout" => { @@ -887,21 +887,31 @@ fn wait_agent_state(args: &[String]) -> std::io::Result<i32> { } } - let Some(state) = desired_state else { - eprintln!("missing required --state"); + let Some(agent_status) = desired_status else { + eprintln!("missing required --status"); return Ok(2); }; - let request = Request { - id: "cli:wait:agent-state".into(), - method: Method::EventsSubscribe(crate::api::schema::EventsSubscribeParams { - subscriptions: vec![Subscription::PaneAgentStateChanged { - pane_id, - state: Some(state), - }], - }), - }; + wait_for_agent_change( + Request { + id: "cli:wait:agent-status".into(), + method: Method::EventsSubscribe(crate::api::schema::EventsSubscribeParams { + subscriptions: vec![Subscription::PaneAgentStatusChanged { + pane_id, + agent_status: Some(agent_status), + }], + }), + }, + timeout_ms, + "timed out waiting for agent status change", + ) +} +fn wait_for_agent_change( + request: Request, + timeout_ms: Option<u64>, + timeout_message: &str, +) -> std::io::Result<i32> { let mut stream = UnixStream::connect(api::socket_path())?; stream.write_all(serde_json::to_string(&request)?.as_bytes())?; stream.write_all(b"\n")?; @@ -941,7 +951,7 @@ fn wait_agent_state(args: &[String]) -> std::io::Result<i32> { std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock ) => { - eprintln!("timed out waiting for agent state change"); + eprintln!("{timeout_message}"); Ok(1) } Err(err) => Err(err), @@ -1017,14 +1027,15 @@ fn parse_read_source(value: &str) -> std::io::Result<ReadSource> { } } -fn parse_agent_state(value: &str) -> std::io::Result<PaneAgentState> { +fn parse_agent_status(value: &str) -> std::io::Result<AgentStatus> { match value { - "idle" => Ok(PaneAgentState::Idle), - "working" => Ok(PaneAgentState::Working), - "blocked" => Ok(PaneAgentState::Blocked), - "unknown" => Ok(PaneAgentState::Unknown), + "idle" => Ok(AgentStatus::Idle), + "working" => Ok(AgentStatus::Working), + "blocked" => Ok(AgentStatus::Blocked), + "done" => Ok(AgentStatus::Done), + "unknown" => Ok(AgentStatus::Unknown), _ => Err(std::io::Error::other(format!( - "invalid agent state: {value} (expected idle, working, blocked, or unknown)" + "invalid agent status: {value} (expected idle, working, blocked, done, or unknown)" ))), } } @@ -1077,7 +1088,7 @@ fn print_wait_help() { eprintln!("herdr wait commands:"); eprintln!(" herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex]"); eprintln!( - " herdr wait agent-state <pane_id> --state <idle|working|blocked|unknown> [--timeout MS]" + " herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]" ); } |
