aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 20:55:51 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-09 04:26:19 +0300
commitee51c5e5fbaea694e65bb0a4908f75bcaf9707d1 (patch)
treef566d2a5921d4112602a7abcffb5cf6eb04d5f24 /src/app
parenta7cd7806f2fa0251e2dca41ffec61ceafb6f0e18 (diff)
feat: use pty-first agent detection
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs245
-rw-r--r--src/app/api.rs136
-rw-r--r--src/app/input/mouse.rs1
-rw-r--r--src/app/mod.rs2
4 files changed, 289 insertions, 95 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 371457a8..249d0e64 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -22,6 +22,28 @@ fn is_background_completion_transition(prev_state: AgentState, new_state: AgentS
&& matches!(prev_state, AgentState::Working | AgentState::Blocked)
}
+fn is_completion_transition(change: &EffectiveStateChange) -> bool {
+ is_completion_transition_parts(
+ change.previous_state,
+ change.state,
+ change.previous_agent_label.as_deref(),
+ change.agent_label.as_deref(),
+ )
+}
+
+pub fn is_completion_transition_parts(
+ previous_state: AgentState,
+ state: AgentState,
+ previous_agent_label: Option<&str>,
+ agent_label: Option<&str>,
+) -> bool {
+ is_background_completion_transition(previous_state, state)
+ || (previous_state == AgentState::Unknown
+ && state == AgentState::Idle
+ && previous_agent_label.is_some()
+ && previous_agent_label == agent_label)
+}
+
pub fn active_tab_suppresses_notifications(
is_active_tab: bool,
outer_terminal_focus: Option<bool>,
@@ -29,6 +51,7 @@ pub fn active_tab_suppresses_notifications(
is_active_tab && outer_terminal_focus != Some(false)
}
+#[cfg(test)]
pub fn notification_sound_for_state_change(
suppress_active_tab_notifications: bool,
prev_state: AgentState,
@@ -50,10 +73,58 @@ pub fn notification_sound_for_state_change(
}
}
-pub fn notification_toast_for_state_change(
+pub fn notification_sound_for_state_change_with_agent_labels(
suppress_active_tab_notifications: bool,
prev_state: AgentState,
new_state: AgentState,
+ previous_agent_label: Option<&str>,
+ agent_label: Option<&str>,
+) -> Option<crate::sound::Sound> {
+ if new_state == prev_state {
+ return None;
+ }
+
+ match new_state {
+ AgentState::Blocked => Some(crate::sound::Sound::Request),
+ AgentState::Idle
+ if is_completion_transition_parts(
+ prev_state,
+ new_state,
+ previous_agent_label,
+ agent_label,
+ ) && !suppress_active_tab_notifications =>
+ {
+ Some(crate::sound::Sound::Done)
+ }
+ _ => None,
+ }
+}
+
+fn notification_sound_for_effective_state_change(
+ suppress_active_tab_notifications: bool,
+ change: &EffectiveStateChange,
+) -> Option<crate::sound::Sound> {
+ if change.state == change.previous_state {
+ return None;
+ }
+
+ match change.state {
+ AgentState::Blocked => Some(crate::sound::Sound::Request),
+ AgentState::Idle
+ if is_completion_transition(change) && !suppress_active_tab_notifications =>
+ {
+ Some(crate::sound::Sound::Done)
+ }
+ _ => None,
+ }
+}
+
+pub fn notification_toast_for_state_change_with_agent_labels(
+ suppress_active_tab_notifications: bool,
+ prev_state: AgentState,
+ new_state: AgentState,
+ previous_agent_label: Option<&str>,
+ agent_label: Option<&str>,
) -> Option<ToastKind> {
if suppress_active_tab_notifications || new_state == prev_state {
return None;
@@ -61,13 +132,52 @@ pub fn notification_toast_for_state_change(
match new_state {
AgentState::Blocked => Some(ToastKind::NeedsAttention),
- AgentState::Idle if is_background_completion_transition(prev_state, new_state) => {
+ AgentState::Idle
+ if is_completion_transition_parts(
+ prev_state,
+ new_state,
+ previous_agent_label,
+ agent_label,
+ ) =>
+ {
Some(ToastKind::Finished)
}
_ => None,
}
}
+fn notification_toast_for_effective_state_change(
+ suppress_active_tab_notifications: bool,
+ change: &EffectiveStateChange,
+) -> Option<ToastKind> {
+ if suppress_active_tab_notifications || change.state == change.previous_state {
+ return None;
+ }
+
+ match change.state {
+ AgentState::Blocked => Some(ToastKind::NeedsAttention),
+ AgentState::Idle if is_completion_transition(change) => Some(ToastKind::Finished),
+ _ => None,
+ }
+}
+
+pub fn notification_toast_for_pane_state_update(
+ suppress_active_tab_notifications: bool,
+ update: &PaneStateUpdate,
+) -> Option<ToastKind> {
+ if suppress_active_tab_notifications || update.state == update.previous_state {
+ return None;
+ }
+
+ notification_toast_for_state_change_with_agent_labels(
+ suppress_active_tab_notifications,
+ update.previous_state,
+ update.state,
+ update.previous_agent_label.as_deref(),
+ update.agent_label.as_deref(),
+ )
+}
+
fn toast_agent_label(agent_label: &str) -> &str {
agent_label
}
@@ -2207,7 +2317,6 @@ impl AppState {
agent,
state,
visible_blocker,
- visible_idle,
visible_working,
process_exited,
observed_at,
@@ -2217,7 +2326,7 @@ impl AppState {
agent,
state,
visible_blocker,
- visible_idle,
+ false,
visible_working,
process_exited,
observed_at,
@@ -2405,6 +2514,27 @@ impl AppState {
Some(update)
}
+ pub(crate) fn publish_pane_process_exit_if_agent(
+ &mut self,
+ pane_id: PaneId,
+ ) -> Option<PaneStateUpdate> {
+ let observed_at = std::time::Instant::now();
+ self.update_terminal_state(pane_id, |terminal| {
+ let agent = terminal
+ .effective_known_agent()
+ .or(terminal.detected_agent)?;
+ Some(terminal.set_detected_state_with_screen_signals_at(
+ Some(agent),
+ AgentState::Idle,
+ false,
+ false,
+ false,
+ true,
+ observed_at,
+ ))
+ })
+ }
+
fn apply_pane_state_change(
&mut self,
ws_idx: usize,
@@ -2421,7 +2551,7 @@ impl AppState {
if change.state != AgentState::Idle {
pane.seen = true;
- } else if is_background_completion_transition(change.previous_state, change.state) {
+ } else if is_completion_transition(change) {
pane.seen = suppress_active_tab_notifications;
}
let seen = pane.seen;
@@ -2445,15 +2575,13 @@ impl AppState {
let suppress_active_tab_notifications =
active_tab_suppresses_notifications(is_active_tab, self.outer_terminal_focus);
- let client_notification_kind = notification_toast_for_state_change(
+ let client_notification_kind = notification_toast_for_effective_state_change(
suppress_active_tab_notifications,
- change.previous_state,
- change.state,
+ change,
);
- let sound = notification_sound_for_state_change(
+ let sound = notification_sound_for_effective_state_change(
suppress_active_tab_notifications,
- change.previous_state,
- change.state,
+ change,
);
if client_notification_kind.is_none() && sound.is_none() {
return None;
@@ -3834,7 +3962,6 @@ mod tests {
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(),
@@ -3854,6 +3981,7 @@ mod tests {
#[test]
fn state_changed_idle_in_background_marks_unseen() {
let mut state = app_with_workspaces(&["active", "background"]);
+ state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
state.active = Some(0);
let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap();
@@ -3872,7 +4000,6 @@ mod tests {
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(),
@@ -3880,6 +4007,10 @@ mod tests {
let pane = state.workspaces[1].panes.get(&bg_pane_id).unwrap();
assert!(!pane.seen);
+ assert!(matches!(
+ state.toast.as_ref().map(|toast| toast.kind),
+ Some(ToastKind::Finished)
+ ));
}
#[test]
@@ -3902,7 +4033,6 @@ mod tests {
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(),
@@ -3925,7 +4055,6 @@ mod tests {
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(),
@@ -3936,6 +4065,36 @@ mod tests {
}
#[test]
+ fn idle_after_known_unknown_agent_in_background_marks_done() {
+ let mut state = app_with_workspaces(&["active", "background"]);
+ state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
+ state.active = Some(0);
+ let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap();
+
+ state.handle_app_event(AppEvent::StateChanged {
+ pane_id: bg_pane_id,
+ agent: Some(Agent::Pi),
+ state: AgentState::Unknown,
+ visible_blocker: false,
+ visible_working: false,
+ process_exited: false,
+ observed_at: std::time::Instant::now(),
+ });
+ state.handle_app_event(AppEvent::StateChanged {
+ pane_id: bg_pane_id,
+ agent: Some(Agent::Pi),
+ state: AgentState::Idle,
+ visible_blocker: false,
+ visible_working: false,
+ process_exited: false,
+ observed_at: std::time::Instant::now(),
+ });
+
+ let pane = state.workspaces[1].panes.get(&bg_pane_id).unwrap();
+ assert!(!pane.seen);
+ }
+
+ #[test]
fn waiting_sound_plays_even_in_active_workspace() {
assert_eq!(
notification_sound_for_state_change(true, AgentState::Working, AgentState::Blocked),
@@ -3971,7 +4130,6 @@ mod tests {
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(),
@@ -3996,7 +4154,6 @@ mod tests {
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(),
@@ -4029,7 +4186,6 @@ mod tests {
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(),
@@ -4041,7 +4197,6 @@ mod tests {
agent: Some(Agent::Pi),
state: AgentState::Working,
visible_blocker: false,
- visible_idle: false,
visible_working: true,
process_exited: false,
observed_at: std::time::Instant::now(),
@@ -4065,7 +4220,6 @@ mod tests {
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(),
@@ -4091,7 +4245,6 @@ mod tests {
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(),
@@ -4119,7 +4272,6 @@ mod tests {
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(),
@@ -4176,7 +4328,6 @@ mod tests {
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(),
@@ -4196,7 +4347,6 @@ mod tests {
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(),
@@ -4227,7 +4377,6 @@ mod tests {
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(),
@@ -4252,7 +4401,6 @@ mod tests {
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(),
@@ -4279,7 +4427,6 @@ mod tests {
agent: Some(Agent::Claude),
state: AgentState::Working,
visible_blocker: false,
- visible_idle: false,
visible_working: true,
process_exited: false,
observed_at: std::time::Instant::now(),
@@ -4298,7 +4445,7 @@ mod tests {
}
#[test]
- fn hidden_session_ref_only_update_marks_session_dirty_without_visible_update() {
+ fn hidden_custom_session_ref_only_update_marks_session_dirty_without_visible_update() {
let mut state = app_with_workspaces(&["active"]);
let pane_id = *state.workspaces[0].panes.keys().next().unwrap();
let test_dir = std::env::current_dir().unwrap();
@@ -4307,7 +4454,7 @@ mod tests {
let first_updates = state.handle_app_event(AppEvent::HookStateReported {
pane_id,
- source: "herdr:pi".into(),
+ source: "custom:pi".into(),
agent_label: "pi".into(),
state: AgentState::Working,
message: None,
@@ -4320,7 +4467,7 @@ mod tests {
let second_updates = state.handle_app_event(AppEvent::HookStateReported {
pane_id,
- source: "herdr:pi".into(),
+ source: "custom:pi".into(),
agent_label: "pi".into(),
state: AgentState::Working,
message: None,
@@ -4377,7 +4524,6 @@ mod tests {
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(),
@@ -4407,7 +4553,6 @@ mod tests {
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(),
@@ -4434,7 +4579,6 @@ mod tests {
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(),
@@ -4458,7 +4602,6 @@ mod tests {
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(),
@@ -4480,7 +4623,6 @@ mod tests {
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(),
@@ -4678,6 +4820,39 @@ mod tests {
}
#[test]
+ fn pane_process_exit_publish_marks_agent_idle_before_pane_removal() {
+ let mut state = app_with_workspaces(&["active", "background"]);
+ state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
+ state.active = Some(1);
+ state.ensure_test_terminals();
+ let pane_id = state.workspaces[0].tabs[0].root_pane;
+ let terminal_id = state.terminal_id_for_pane(0, pane_id).unwrap();
+ state
+ .terminals
+ .get_mut(&terminal_id)
+ .unwrap()
+ .set_detected_state(Some(Agent::Pi), AgentState::Working);
+ assert_eq!(
+ state.terminals.get(&terminal_id).unwrap().state,
+ AgentState::Working
+ );
+
+ let update = state
+ .publish_pane_process_exit_if_agent(pane_id)
+ .expect("process exit update");
+
+ assert!(!state.pane_is_in_active_tab(update.ws_idx, pane_id));
+ assert_eq!(update.previous_state, AgentState::Working);
+ assert_eq!(update.state, AgentState::Idle);
+ assert_eq!(update.agent_label.as_deref(), Some("pi"));
+ assert_eq!(update.known_agent, Some(Agent::Pi));
+ assert!(matches!(
+ state.toast.as_ref().map(|toast| toast.kind),
+ Some(ToastKind::Finished)
+ ));
+ }
+
+ #[test]
fn close_pane_removes_unattached_terminal_state() {
let mut state = app_with_workspaces(&["test"]);
let pane_id = state.workspaces[0].test_split(Direction::Horizontal);
diff --git a/src/app/api.rs b/src/app/api.rs
index 166ea789..76d9c537 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -67,6 +67,12 @@ impl App {
}
if let AppEvent::PaneDied { pane_id } = &ev {
+ let previous_toast = self.state.toast.clone();
+ if let Some(update) = self.state.publish_pane_process_exit_if_agent(*pane_id) {
+ self.refresh_new_herdr_toast_context_for_update(&update, &previous_toast);
+ self.emit_pane_state_update(&update);
+ self.emit_terminal_or_system_agent_notifications(std::slice::from_ref(&update));
+ }
if self.runtime_exit_action(*pane_id) == RuntimeExitAction::RespawnShell
&& self.respawn_shell_for_launch_pane(*pane_id)
{
@@ -161,57 +167,7 @@ impl App {
let instruction = crate::update::update_install_instruction(&install_command);
let _ = notify(&format!("v{version} available"), Some(&instruction));
} else if self.state.toast_config.delay_seconds == 0 {
- for update in &pane_updates {
- let is_active_tab = self
- .state
- .pane_is_in_active_tab(update.ws_idx, update.pane_id);
- let suppress_active_tab_notifications =
- crate::app::actions::active_tab_suppresses_notifications(
- is_active_tab,
- self.state.outer_terminal_focus,
- );
- let Some(kind) = crate::app::actions::notification_toast_for_state_change(
- suppress_active_tab_notifications,
- update.previous_state,
- update.state,
- ) else {
- continue;
- };
- let Some(ws) = self.state.workspaces.get(update.ws_idx) else {
- continue;
- };
- let Some(pane) = ws
- .tabs
- .iter()
- .find_map(|tab| tab.panes.get(&update.pane_id))
- else {
- continue;
- };
- let Some(agent_label) = self
- .state
- .terminals
- .get(&pane.attached_terminal_id)
- .and_then(|terminal| terminal.effective_agent_label())
- else {
- continue;
- };
- let event_text = match kind {
- ToastKind::NeedsAttention => "needs attention",
- ToastKind::Finished => "finished",
- ToastKind::UpdateInstalled => "updated",
- };
- let workspace_label =
- ws.display_name_from(&self.state.terminals, &self.terminal_runtimes);
- let _ = notify(
- &format!("{} {}", agent_label, event_text),
- Some(&crate::app::actions::notification_context(
- ws,
- &workspace_label,
- update.ws_idx,
- update.pane_id,
- )),
- );
- }
+ self.emit_terminal_or_system_agent_notifications(&pane_updates);
}
}
@@ -407,6 +363,78 @@ impl App {
}
}
+ fn emit_terminal_or_system_agent_notifications(
+ &self,
+ pane_updates: &[crate::app::actions::PaneStateUpdate],
+ ) {
+ if !self.local_terminal_notifications
+ || self.state.toast_config.delay_seconds != 0
+ || !matches!(
+ self.state.toast_config.delivery,
+ crate::config::ToastDelivery::Terminal | crate::config::ToastDelivery::System
+ )
+ {
+ return;
+ }
+
+ let notify = match self.state.toast_config.delivery {
+ crate::config::ToastDelivery::Terminal => crate::terminal_notify::show_notification,
+ crate::config::ToastDelivery::System => crate::platform::show_desktop_notification,
+ _ => return,
+ };
+
+ for update in pane_updates {
+ let is_active_tab = self
+ .state
+ .pane_is_in_active_tab(update.ws_idx, update.pane_id);
+ let suppress_active_tab_notifications =
+ crate::app::actions::active_tab_suppresses_notifications(
+ is_active_tab,
+ self.state.outer_terminal_focus,
+ );
+ let Some(kind) = crate::app::actions::notification_toast_for_pane_state_update(
+ suppress_active_tab_notifications,
+ update,
+ ) else {
+ continue;
+ };
+ let Some(ws) = self.state.workspaces.get(update.ws_idx) else {
+ continue;
+ };
+ let Some(pane) = ws
+ .tabs
+ .iter()
+ .find_map(|tab| tab.panes.get(&update.pane_id))
+ else {
+ continue;
+ };
+ let Some(agent_label) = self
+ .state
+ .terminals
+ .get(&pane.attached_terminal_id)
+ .and_then(|terminal| terminal.effective_agent_label())
+ else {
+ continue;
+ };
+ let event_text = match kind {
+ ToastKind::NeedsAttention => "needs attention",
+ ToastKind::Finished => "finished",
+ ToastKind::UpdateInstalled => "updated",
+ };
+ let workspace_label =
+ ws.display_name_from(&self.state.terminals, &self.terminal_runtimes);
+ let _ = notify(
+ &format!("{} {}", agent_label, event_text),
+ Some(&crate::app::actions::notification_context(
+ ws,
+ &workspace_label,
+ update.ws_idx,
+ update.pane_id,
+ )),
+ );
+ }
+ }
+
pub(crate) fn sync_toast_deadline(
&mut self,
previous_toast: Option<crate::app::state::ToastNotification>,
@@ -891,7 +919,6 @@ mod tests {
agent: Some(Agent::Codex),
state: AgentState::Working,
visible_blocker: false,
- visible_idle: false,
visible_working: false,
process_exited: false,
observed_at: std::time::Instant::now(),
@@ -901,7 +928,6 @@ mod tests {
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(),
@@ -984,7 +1010,6 @@ mod tests {
agent: Some(Agent::Codex),
state: AgentState::Working,
visible_blocker: false,
- visible_idle: false,
visible_working: false,
process_exited: false,
observed_at: std::time::Instant::now(),
@@ -994,7 +1019,6 @@ mod tests {
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(),
@@ -1096,7 +1120,6 @@ mod tests {
agent: Some(Agent::Codex),
state: AgentState::Working,
visible_blocker: false,
- visible_idle: false,
visible_working: false,
process_exited: false,
observed_at: std::time::Instant::now(),
@@ -1117,7 +1140,6 @@ mod tests {
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(),
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index a682d7ac..a241dadb 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -2260,7 +2260,6 @@ mod tests {
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(),
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 3a038dee..936b082f 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -3621,7 +3621,6 @@ mod tests {
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(),
@@ -3646,7 +3645,6 @@ mod tests {
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(),