aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-22 19:54:10 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-22 19:55:07 +0300
commit062259ba3e060c7463b4896a6f761cdf66d2f8ff (patch)
treedf034626fad636e06cc558b3771c3d96914da06f /src/app
parent623a1288a7719f53f7829b9fdbab33665e2ccc57 (diff)
fix: arbitrate stale agent state evidence
refs #249
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs163
-rw-r--r--src/app/input/mouse.rs5
-rw-r--r--src/app/mod.rs10
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);