aboutsummaryrefslogtreecommitdiff
path: root/src/app/api.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-19 23:52:20 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-19 23:56:07 +0300
commit3f80947678a9723434aa7fddfe23750b5393e24c (patch)
tree6f463797132acfc51dfb6e135820a052954b4f13 /src/app/api.rs
parentf76f895efe5b9f19aa9db909354c6c0f2195630b (diff)
feat: make agent automation a first-class cli primitive
Diffstat (limited to 'src/app/api.rs')
-rw-r--r--src/app/api.rs115
1 files changed, 113 insertions, 2 deletions
diff --git a/src/app/api.rs b/src/app/api.rs
index 79402e47..7c963ef4 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -556,13 +556,19 @@ impl App {
};
let workspace_id = self.public_workspace_id(update.ws_idx);
- if update.previous_agent_label != update.agent_label {
+ if update.agent_name_changed {
+ self.emit_pane_updated(update.ws_idx, update.pane_id);
+ }
+
+ if update.previous_agent_label != update.agent_label || update.agent_released {
self.emit_event(crate::api::schema::EventEnvelope {
event: crate::api::schema::EventKind::PaneAgentDetected,
data: crate::api::schema::EventData::PaneAgentDetected {
pane_id: pane_id.clone(),
workspace_id: workspace_id.clone(),
agent: update.agent_label.clone(),
+ released: update.agent_released,
+ final_status: update.agent_release_status,
},
});
}
@@ -996,9 +1002,18 @@ impl App {
Method::AgentRename(params) => return self.handle_agent_rename(request.id, params),
Method::AgentStart(params) => return self.handle_agent_start(request.id, params),
Method::AgentPrompt(params) => return self.handle_agent_prompt(request.id, params),
+ Method::AgentWait(_) => {
+ return responses::encode_error(
+ request.id,
+ "invalid_request",
+ "agent.wait is handled by the api server",
+ );
+ }
Method::AgentRead(params) => return self.handle_agent_read(request.id, params),
Method::AgentExplain(target) => return self.handle_agent_explain(request.id, target),
- Method::AgentSend(params) => return self.handle_agent_send(request.id, params),
+ Method::AgentSendKeys(params) => {
+ return self.handle_agent_send_keys(request.id, params)
+ }
Method::PaneSplit(params) => return self.handle_pane_split(request.id, params),
Method::PaneSwap(params) => return self.handle_pane_swap(request.id, params),
Method::PaneMove(params) => return self.handle_pane_move(request.id, params),
@@ -1876,6 +1891,102 @@ mod tests {
}
#[test]
+ fn idle_agent_exit_emits_release_event_without_a_state_change() {
+ for agent_name in [None, Some("reviewer")] {
+ let event_hub = crate::api::EventHub::default();
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(
+ &crate::config::Config::default(),
+ true,
+ None,
+ api_rx,
+ event_hub.clone(),
+ );
+ let workspace = crate::workspace::Workspace::test_new("idle-agent-exit");
+ let pane_id = workspace.tabs[0].root_pane;
+ let terminal_id = workspace.terminal_id(pane_id).cloned().unwrap();
+ app.state.workspaces = vec![workspace];
+ app.state.ensure_test_terminals();
+ let terminal = app.state.terminals.get_mut(&terminal_id).unwrap();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ if let Some(agent_name) = agent_name {
+ terminal.set_agent_name(agent_name.into());
+ }
+
+ app.handle_internal_event(AppEvent::StateChanged {
+ pane_id,
+ agent: Some(Agent::Pi),
+ state: AgentState::Idle,
+ visible_blocker: false,
+ visible_working: false,
+ process_exited: true,
+ observed_at: std::time::Instant::now(),
+ });
+
+ assert!(app.state.terminals[&terminal_id].agent_name.is_none());
+ assert!(event_hub.events_after(0).iter().any(|(_, event)| matches!(
+ &event.data,
+ crate::api::schema::EventData::PaneAgentDetected {
+ released: true,
+ final_status: Some(crate::api::schema::AgentStatus::Idle),
+ ..
+ }
+ )));
+ }
+ }
+
+ #[test]
+ fn stale_detector_exit_does_not_release_a_newer_hook_owned_agent() {
+ let event_hub = crate::api::EventHub::default();
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(
+ &crate::config::Config::default(),
+ true,
+ None,
+ api_rx,
+ event_hub.clone(),
+ );
+ let workspace = crate::workspace::Workspace::test_new("stale-agent-exit");
+ let pane_id = workspace.tabs[0].root_pane;
+ let terminal_id = workspace.terminal_id(pane_id).cloned().unwrap();
+ app.state.workspaces = vec![workspace];
+ app.state.ensure_test_terminals();
+ let observed_at = std::time::Instant::now();
+ let terminal = app.state.terminals.get_mut(&terminal_id).unwrap();
+ terminal.set_detected_state(Some(Agent::Codex), AgentState::Working);
+ terminal
+ .set_hook_authority_at(
+ "herdr:codex".into(),
+ "codex".into(),
+ AgentState::Working,
+ None,
+ None,
+ Some(1),
+ observed_at + std::time::Duration::from_secs(1),
+ )
+ .unwrap();
+ terminal.set_agent_name("reviewer".into());
+
+ app.handle_internal_event(AppEvent::StateChanged {
+ pane_id,
+ agent: Some(Agent::Codex),
+ state: AgentState::Idle,
+ visible_blocker: false,
+ visible_working: false,
+ process_exited: true,
+ observed_at,
+ });
+
+ let terminal = &app.state.terminals[&terminal_id];
+ assert_eq!(terminal.state, AgentState::Working);
+ assert_eq!(terminal.agent_name.as_deref(), Some("reviewer"));
+ assert!(!event_hub.events_after(0).iter().any(|(_, event)| matches!(
+ event.data,
+ crate::api::schema::EventData::PaneAgentDetected { released: true, .. }
+ )));
+ }
+
+ #[test]
fn overlay_exit_layout_updated_uses_restored_zoom_state() {
let event_hub = crate::api::EventHub::default();
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();