From f18bee9f5c43e56ebdcb7e5fad17f3236de4d08e Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Mon, 8 Jun 2026 21:52:48 +0300 Subject: fix: smooth agent status transitions --- src/detect/agents/claude_code.rs | 23 ++++++++++++++++++++ src/detect/mod.rs | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) (limited to 'src/detect') diff --git a/src/detect/agents/claude_code.rs b/src/detect/agents/claude_code.rs index a13019a4..1336c480 100644 --- a/src/detect/agents/claude_code.rs +++ b/src/detect/agents/claude_code.rs @@ -59,6 +59,29 @@ pub(super) fn has_visible_blocker(content: &str) -> bool { || lower.contains("ctrl+e to explain")) } +pub(in crate::detect) fn has_idle_recap_notice(content: &str) -> bool { + if !has_prompt_box(content) || has_visible_blocker(content) { + return false; + } + + let above_prompt = content_above_prompt_box(content); + let bottom_lines = bottom_non_empty_lines(above_prompt, 8); + let Some(last_line) = bottom_lines.last() else { + return false; + }; + if !last_line + .to_ascii_lowercase() + .contains("(disable recaps in /config)") + { + return false; + } + let bottom = normalize_lines(&bottom_lines).to_ascii_lowercase(); + + bottom.contains("※ recap:") + && !bottom.contains("esc to interrupt") + && !bottom.contains("ctrl+c to interrupt") +} + pub(super) fn has_working_chrome(content: &str) -> bool { let above = content_above_prompt_box(content); let above_lower = above.to_lowercase(); diff --git a/src/detect/mod.rs b/src/detect/mod.rs index 24393c59..10ecfc6b 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -35,6 +35,12 @@ pub struct AgentDetection { pub visible_working: bool, } +/// A narrow screen-derived exception that can veto PTY activity as semantic work. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AgentActivityVeto { + ClaudeIdleRecap, +} + /// Which agent we detected running in a pane. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Agent { @@ -180,6 +186,18 @@ pub fn detect_agent(agent: Option, screen_content: &str) -> AgentDetectio agents::detect(agent, screen_content) } +pub(crate) fn agent_activity_veto( + agent: Option, + screen_content: &str, +) -> Option { + match agent { + Some(Agent::Claude) if agents::claude_code::has_idle_recap_notice(screen_content) => { + Some(AgentActivityVeto::ClaudeIdleRecap) + } + _ => None, + } +} + pub fn should_skip_state_update(agent: Option, screen_content: &str) -> bool { agent.is_some_and(|agent| agents::should_skip_state_update(agent, screen_content)) } @@ -1271,6 +1289,33 @@ mod tests { assert_eq!(detect_claude(screen), AgentState::Idle); } + #[test] + fn claude_idle_recap_notice_is_activity_veto() { + let screen = "✻ Churned for 9s\n\n※ recap: We’re debugging issue 494: kitty graphics render in shell panes but not inside Neovim. Next, I’m tracing the snacks.nvim inline-image path against Herdr’s placeholder\n handling to confirm the exact failure point. (disable recaps in /config)\n\n────────────────────────────────────────────────────────────────────────────────\n❯ \n────────────────────────────────────────────────────────────────────────────────"; + + assert_eq!( + agent_activity_veto(Some(Agent::Claude), screen), + Some(AgentActivityVeto::ClaudeIdleRecap) + ); + } + + #[test] + fn wrapped_claude_idle_recap_notice_is_activity_veto() { + let screen = "※ recap: We’re debugging issue 494: kitty graphics render in shell panes but\n not inside Neovim. Next, I’m tracing the snacks.nvim inline-image path\n against Herdr’s placeholder handling to confirm the exact failure point…\n and then I will inspect the redraw cache. (disable recaps in /config)\n\n────────────────────────────────────────────────────────────────────────────────\n❯ \n────────────────────────────────────────────────────────────────────────────────"; + + assert_eq!( + agent_activity_veto(Some(Agent::Claude), screen), + Some(AgentActivityVeto::ClaudeIdleRecap) + ); + } + + #[test] + fn stale_claude_recap_above_new_work_does_not_veto_activity() { + let screen = "※ recap: Old recap. (disable recaps in /config)\n\n● Reading src/pane.rs\n\n✻ Crunching…\n\n────────────────────────────────────────────────────────────────────────────────\n❯ \n────────────────────────────────────────────────────────────────────────────────"; + + assert_eq!(agent_activity_veto(Some(Agent::Claude), screen), None); + } + #[test] fn claude_prompt_box_does_not_set_screen_idle_signal() { let screen = "Interrupted.\n─────────────\n❯ \n─────────────"; -- cgit v1.2.3