diff options
Diffstat (limited to 'src/app/actions.rs')
| -rw-r--r-- | src/app/actions.rs | 225 |
1 files changed, 224 insertions, 1 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index 6a6e99d2..e1ae2ffc 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -1500,6 +1500,28 @@ impl AppState { .collect() } } + AppEvent::AgentResumeReported { + pane_id, + source, + agent_label, + seq, + argv, + } => self + .update_terminal_state(pane_id, |terminal| { + terminal.record_reported_resume(&source, &agent_label, seq, argv); + None + }) + .into_iter() + .collect(), + AppEvent::ReportedAgentShellReturned { + pane_id, + observed_at, + } => self + .update_terminal_state(pane_id, |terminal| { + terminal.clear_self_reported_agent(observed_at) + }) + .into_iter() + .collect(), AppEvent::AgentSessionReported { pane_id, source, @@ -1653,7 +1675,14 @@ impl AppState { let terminal = self.terminals.get_mut(&terminal_id)?; let previous_agent_name = terminal.agent_name.clone(); let had_completion = terminal.last_agent_completion_seq.is_some() || !previous_seen; - let mutation = update(terminal)?; + let resume_revision = terminal.reported_resume_revision(); + let mutation = update(terminal); + terminal.reconcile_reported_resume(); + // Resume-only changes return no mutation but must still be saved. + if terminal.reported_resume_revision() != resume_revision { + self.session_dirty = true; + } + let mutation = mutation?; let completion_reset = mutation.session_ref_changed || mutation .effective_state_change @@ -3541,6 +3570,200 @@ mod tests { assert!(state.toast.is_none()); } + fn report_custom_agent_with_resume(state: &mut AppState, pane_id: PaneId, argv: &[&str]) { + state.handle_app_event(AppEvent::HookStateReported { + pane_id, + source: "prime-agent".into(), + agent_label: "prime-agent".into(), + state: AgentState::Idle, + message: None, + seq: Some(1), + session_ref: None, + }); + state.handle_app_event(AppEvent::AgentResumeReported { + pane_id, + source: "prime-agent".into(), + agent_label: "prime-agent".into(), + seq: Some(1), + argv: argv.iter().map(|part| part.to_string()).collect(), + }); + } + + fn first_pane_terminal(state: &AppState) -> (PaneId, crate::terminal::TerminalId) { + let pane_id = *state.workspaces[0].panes.keys().next().unwrap(); + let terminal_id = state.workspaces[0].panes[&pane_id] + .attached_terminal_id + .clone(); + (pane_id, terminal_id) + } + + #[test] + fn reported_resume_follows_the_reporting_agent_until_release() { + let mut state = app_with_workspaces(&["one"]); + let (pane_id, terminal_id) = first_pane_terminal(&state); + report_custom_agent_with_resume(&mut state, pane_id, &["prime-agent", "--resume", "a"]); + + let resume = state.terminals[&terminal_id].reported_resume().unwrap(); + assert_eq!(resume.agent, "prime-agent"); + assert_eq!(resume.argv, vec!["prime-agent", "--resume", "a"]); + + state.handle_app_event(AppEvent::AgentResumeReported { + pane_id, + source: "someone-else".into(), + agent_label: "other".into(), + seq: None, + argv: vec!["other".into()], + }); + assert_eq!( + state.terminals[&terminal_id] + .reported_resume() + .unwrap() + .argv, + vec!["prime-agent", "--resume", "a"] + ); + + state.handle_app_event(AppEvent::HookAgentReleased { + pane_id, + source: "prime-agent".into(), + agent_label: "prime-agent".into(), + known_agent: None, + seq: Some(2), + }); + assert!(state.terminals[&terminal_id].reported_resume().is_none()); + } + + #[test] + fn reported_resume_is_dropped_and_saved_when_another_agent_takes_the_pane() { + let mut state = app_with_workspaces(&["one"]); + let (pane_id, terminal_id) = first_pane_terminal(&state); + report_custom_agent_with_resume(&mut state, pane_id, &["prime-agent", "--resume", "a"]); + state.session_dirty = false; + + state.handle_app_event(AppEvent::HookStateReported { + pane_id, + source: "custom:other".into(), + agent_label: "other".into(), + state: AgentState::Working, + message: None, + seq: None, + session_ref: None, + }); + + assert!(state.terminals[&terminal_id].reported_resume().is_none()); + assert!(state.session_dirty); + } + + #[test] + fn reported_resume_of_recognized_agent_is_dropped_when_its_process_exits() { + let mut state = app_with_workspaces(&["one"]); + let (pane_id, terminal_id) = first_pane_terminal(&state); + state.handle_app_event(AppEvent::AgentProcessDetected { + pane_id, + agent: Agent::Pi, + observed_at: std::time::Instant::now(), + }); + state.handle_app_event(AppEvent::AgentResumeReported { + pane_id, + source: "herdr:pi".into(), + agent_label: "pi".into(), + seq: None, + argv: vec!["pi".into(), "--continue".into()], + }); + assert!(state.terminals[&terminal_id].reported_resume().is_some()); + state.session_dirty = false; + + state.handle_app_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!(state.terminals[&terminal_id].reported_resume().is_none()); + assert!(state.session_dirty); + + state.handle_app_event(AppEvent::AgentResumeReported { + pane_id, + source: "herdr:pi".into(), + agent_label: "pi".into(), + seq: None, + argv: vec!["pi".into(), "--continue".into()], + }); + assert!( + state.terminals[&terminal_id].reported_resume().is_none(), + "a late report must not revive an exited agent" + ); + } + + #[test] + fn shell_return_drops_self_reported_agent_and_its_resume() { + let mut state = app_with_workspaces(&["one"]); + let (pane_id, terminal_id) = first_pane_terminal(&state); + report_custom_agent_with_resume(&mut state, pane_id, &["prime-agent", "--resume", "a"]); + assert!(state.terminals[&terminal_id].self_reported_agent_active()); + + state.handle_app_event(AppEvent::ReportedAgentShellReturned { + pane_id, + observed_at: std::time::Instant::now(), + }); + + let terminal = &state.terminals[&terminal_id]; + assert_eq!(terminal.effective_agent_label(), None); + assert!(terminal.reported_resume().is_none()); + assert!(!terminal.self_reported_agent_active()); + } + + #[test] + fn delayed_shell_return_keeps_an_agent_that_claimed_the_pane_afterwards() { + let mut state = app_with_workspaces(&["one"]); + let (pane_id, terminal_id) = first_pane_terminal(&state); + let shell_seen_idle_at = std::time::Instant::now(); + report_custom_agent_with_resume(&mut state, pane_id, &["prime-agent", "--resume", "b"]); + + state.handle_app_event(AppEvent::ReportedAgentShellReturned { + pane_id, + observed_at: shell_seen_idle_at, + }); + + let terminal = &state.terminals[&terminal_id]; + assert_eq!(terminal.effective_agent_label(), Some("prime-agent")); + assert!(terminal.reported_resume().is_some()); + } + + #[test] + fn shell_return_keeps_agents_herdr_recognizes_by_process() { + let mut state = app_with_workspaces(&["one"]); + let (pane_id, terminal_id) = first_pane_terminal(&state); + state.handle_app_event(AppEvent::AgentProcessDetected { + pane_id, + agent: Agent::Pi, + observed_at: std::time::Instant::now(), + }); + state.handle_app_event(AppEvent::HookStateReported { + pane_id, + source: "herdr:pi".into(), + agent_label: "pi".into(), + state: AgentState::Working, + message: None, + seq: Some(1), + session_ref: None, + }); + assert!(!state.terminals[&terminal_id].self_reported_agent_active()); + + state.handle_app_event(AppEvent::ReportedAgentShellReturned { + pane_id, + observed_at: std::time::Instant::now(), + }); + + assert_eq!( + state.terminals[&terminal_id].effective_agent_label(), + Some("pi") + ); + } + #[test] fn hook_reported_unknown_agent_sets_toast_title_from_label() { let mut state = app_with_workspaces(&["active", "background"]); |
