aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-09 03:31:37 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-09 03:31:37 +0300
commita3ecb4e3b4151e840ad237e073d8282e1181c34a (patch)
tree12c6924cc65c2b9c5da45df2e3a2fde188f07681 /src
parent05a6d89e33e722376688c679ab0d7322014814a1 (diff)
feat(api): expose done as agent status
Diffstat (limited to 'src')
-rw-r--r--src/api/mod.rs43
-rw-r--r--src/api/schema.rs58
-rw-r--r--src/app/mod.rs39
-rw-r--r--src/cli.rs65
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!(
&params.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,
}
}
diff --git a/src/cli.rs b/src/cli.rs
index 88ca5abd..8f3d3e5b 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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]"
);
}