diff options
Diffstat (limited to 'src/app/api.rs')
| -rw-r--r-- | src/app/api.rs | 115 |
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(); |
