aboutsummaryrefslogtreecommitdiff
path: root/src/app/actions.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/actions.rs')
-rw-r--r--src/app/actions.rs225
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"]);