aboutsummaryrefslogtreecommitdiff
path: root/src/detect
diff options
context:
space:
mode:
Diffstat (limited to 'src/detect')
-rw-r--r--src/detect/agents/claude_code.rs23
-rw-r--r--src/detect/mod.rs45
2 files changed, 68 insertions, 0 deletions
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<Agent>, screen_content: &str) -> AgentDetectio
agents::detect(agent, screen_content)
}
+pub(crate) fn agent_activity_veto(
+ agent: Option<Agent>,
+ screen_content: &str,
+) -> Option<AgentActivityVeto> {
+ 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<Agent>, screen_content: &str) -> bool {
agent.is_some_and(|agent| agents::should_skip_state_update(agent, screen_content))
}
@@ -1272,6 +1290,33 @@ mod tests {
}
#[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─────────────";
let detection = detect_agent(Some(Agent::Claude), screen);