diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-22 19:54:10 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-22 19:55:07 +0300 |
| commit | 062259ba3e060c7463b4896a6f761cdf66d2f8ff (patch) | |
| tree | df034626fad636e06cc558b3771c3d96914da06f /src/app | |
| parent | 623a1288a7719f53f7829b9fdbab33665e2ccc57 (diff) | |
fix: arbitrate stale agent state evidence
refs #249
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/actions.rs | 163 | ||||
| -rw-r--r-- | src/app/input/mouse.rs | 5 | ||||
| -rw-r--r-- | src/app/mod.rs | 10 |
3 files changed, 177 insertions, 1 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index d6b3ae36..ee2e210a 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -876,9 +876,22 @@ impl AppState { pane_id, agent, state, + visible_blocker, + visible_idle, + visible_working, + process_exited, + observed_at, } => self .update_terminal_state(pane_id, |terminal| { - terminal.set_detected_state(agent, state) + terminal.set_detected_state_with_screen_signals_at( + agent, + state, + visible_blocker, + visible_idle, + visible_working, + process_exited, + observed_at, + ) }) .into_iter() .collect(), @@ -1610,6 +1623,11 @@ mod tests { pane_id, agent: Some(Agent::Pi), state: AgentState::Working, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let terminal_id = state.workspaces[0] @@ -1643,6 +1661,11 @@ mod tests { pane_id: bg_pane_id, agent: Some(Agent::Pi), state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let pane = state.workspaces[1].panes.get(&bg_pane_id).unwrap(); @@ -1668,6 +1691,11 @@ mod tests { pane_id, agent: Some(Agent::Pi), state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let terminal = state.terminals.get(&terminal_id).unwrap(); @@ -1686,6 +1714,11 @@ mod tests { pane_id: bg_pane_id, agent: Some(Agent::Pi), state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let pane = state.workspaces[1].panes.get(&bg_pane_id).unwrap(); @@ -1727,6 +1760,11 @@ mod tests { pane_id: bg_pane_id, agent: Some(Agent::Pi), state: AgentState::Blocked, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let toast = state.toast.as_ref().unwrap(); @@ -1759,6 +1797,104 @@ mod tests { } #[test] + fn visible_blocker_overrides_hook_working_and_notifies() { + let mut state = app_with_workspaces(&["active", "background"]); + state.active = Some(0); + state.toast_config.delivery = crate::config::ToastDelivery::Herdr; + let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap(); + let bg_terminal_id = state.workspaces[1] + .panes + .get(&bg_pane_id) + .unwrap() + .attached_terminal_id + .clone(); + + state.handle_app_event(AppEvent::StateChanged { + pane_id: bg_pane_id, + agent: Some(Agent::Codex), + state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + state.handle_app_event(AppEvent::HookStateReported { + pane_id: bg_pane_id, + source: "herdr:codex".into(), + agent_label: "codex".into(), + state: AgentState::Working, + message: None, + custom_status: None, + seq: Some(1), + }); + state.handle_app_event(AppEvent::StateChanged { + pane_id: bg_pane_id, + agent: Some(Agent::Codex), + state: AgentState::Blocked, + visible_blocker: true, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + + let terminal = state.terminals.get(&bg_terminal_id).unwrap(); + assert_eq!(terminal.state, AgentState::Blocked); + let toast = state.toast.as_ref().unwrap(); + assert_eq!(toast.kind, ToastKind::NeedsAttention); + assert_eq!(toast.title, "codex needs attention"); + } + + #[test] + fn visible_idle_waits_before_overriding_claude_hook_working() { + let mut state = app_with_workspaces(&["active", "background"]); + state.active = Some(0); + state.toast_config.delivery = crate::config::ToastDelivery::Herdr; + let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap(); + let bg_terminal_id = state.workspaces[1] + .panes + .get(&bg_pane_id) + .unwrap() + .attached_terminal_id + .clone(); + + state.handle_app_event(AppEvent::StateChanged { + pane_id: bg_pane_id, + agent: Some(Agent::Claude), + state: AgentState::Working, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + state.handle_app_event(AppEvent::HookStateReported { + pane_id: bg_pane_id, + source: "herdr:claude".into(), + agent_label: "claude".into(), + state: AgentState::Working, + message: None, + custom_status: None, + seq: Some(1), + }); + state.handle_app_event(AppEvent::StateChanged { + pane_id: bg_pane_id, + agent: Some(Agent::Claude), + state: AgentState::Idle, + visible_blocker: false, + visible_idle: true, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + + let terminal = state.terminals.get(&bg_terminal_id).unwrap(); + assert_eq!(terminal.state, AgentState::Working); + assert!(state.toast.is_none()); + } + + #[test] fn background_idle_sets_finished_toast() { let mut state = app_with_workspaces(&["active", "background"]); state.active = Some(0); @@ -1776,6 +1912,11 @@ mod tests { pane_id: bg_pane_id, agent: Some(Agent::Droid), state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let toast = state.toast.as_ref().unwrap(); @@ -1801,6 +1942,11 @@ mod tests { pane_id: bg_pane_id, agent: Some(Agent::Pi), state: AgentState::Blocked, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let toast = state.toast.as_ref().unwrap(); @@ -1823,6 +1969,11 @@ mod tests { pane_id: bg_pane_id, agent: Some(Agent::Pi), state: AgentState::Blocked, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); let toast = state.toast.as_ref().unwrap(); @@ -1842,6 +1993,11 @@ mod tests { pane_id, agent: Some(Agent::Pi), state: AgentState::Blocked, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); assert!(state.toast.is_none()); @@ -1859,6 +2015,11 @@ mod tests { pane_id, agent: Some(Agent::Pi), state: AgentState::Blocked, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); assert!(state.toast.is_none()); diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs index 691026de..87c14756 100644 --- a/src/app/input/mouse.rs +++ b/src/app/input/mouse.rs @@ -1723,6 +1723,11 @@ mod tests { pane_id: target_pane, agent: Some(Agent::Pi), state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); diff --git a/src/app/mod.rs b/src/app/mod.rs index a1ae4a74..1eeba25b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2566,6 +2566,11 @@ mod tests { pane_id, agent: Some(Agent::Pi), state: AgentState::Working, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); assert_eq!( app.state.terminals.get(&terminal_id).unwrap().state, @@ -2586,6 +2591,11 @@ mod tests { pane_id, agent: Some(Agent::Pi), state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), }); tokio::pin!(send); |
