aboutsummaryrefslogtreecommitdiff
path: root/src/pane.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-28 01:18:03 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-28 01:18:03 +0300
commitc0fe95235f639adbc935192ed14b43631f617ac2 (patch)
tree9cc0f9fc17c462b331b0cb822bd4b063b730a64e /src/pane.rs
parente719c6517d6cc2ef2b51a056725d2558c5fb94fa (diff)
fix: stabilize claude state detection
Diffstat (limited to 'src/pane.rs')
-rw-r--r--src/pane.rs102
1 files changed, 101 insertions, 1 deletions
diff --git a/src/pane.rs b/src/pane.rs
index 5a7ae2bc..47ca79ab 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -15,6 +15,38 @@ use crate::events::AppEvent;
use crate::layout::PaneId;
use crate::pty_callbacks::PtyResponses;
+const CLAUDE_BUSY_HOLD: std::time::Duration = std::time::Duration::from_millis(1200);
+
+fn stabilize_agent_state(
+ agent: Option<Agent>,
+ previous: AgentState,
+ raw: AgentState,
+ now: std::time::Instant,
+ last_claude_busy_at: &mut Option<std::time::Instant>,
+) -> AgentState {
+ if agent != Some(Agent::Claude) {
+ return raw;
+ }
+
+ match raw {
+ AgentState::Busy => {
+ *last_claude_busy_at = Some(now);
+ AgentState::Busy
+ }
+ AgentState::Waiting => AgentState::Waiting,
+ AgentState::Idle if previous == AgentState::Busy => {
+ if last_claude_busy_at
+ .is_some_and(|last_busy| now.duration_since(last_busy) < CLAUDE_BUSY_HOLD)
+ {
+ AgentState::Busy
+ } else {
+ AgentState::Idle
+ }
+ }
+ _ => raw,
+ }
+}
+
// ---------------------------------------------------------------------------
// PaneState — pure data, constructable without PTYs, testable
// ---------------------------------------------------------------------------
@@ -210,6 +242,7 @@ impl PaneRuntime {
let mut agent: Option<Agent> = None;
let mut state = AgentState::Unknown;
let mut last_process_check = Instant::now();
+ let mut last_claude_busy_at = None;
tokio::time::sleep(Duration::from_millis(50)).await;
@@ -257,16 +290,24 @@ impl PaneRuntime {
}
}
- let new_state = if let Ok(content) = screen_content.read() {
+ let raw_state = if let Ok(content) = screen_content.read() {
detect::detect_state(agent, &content)
} else {
continue;
};
+ let new_state = stabilize_agent_state(
+ agent,
+ state,
+ raw_state,
+ now,
+ &mut last_claude_busy_at,
+ );
if new_state != state || agent_changed {
debug!(
pane = pane_id.raw(),
?state,
+ ?raw_state,
?new_state,
?agent,
"state changed"
@@ -382,3 +423,62 @@ impl PaneRuntime {
crate::platform::process_cwd(pid)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn claude_busy_is_sticky_for_short_gap() {
+ let now = std::time::Instant::now();
+ let mut last_busy = None;
+
+ let busy = stabilize_agent_state(
+ Some(Agent::Claude),
+ AgentState::Idle,
+ AgentState::Busy,
+ now,
+ &mut last_busy,
+ );
+ assert_eq!(busy, AgentState::Busy);
+
+ let still_busy = stabilize_agent_state(
+ Some(Agent::Claude),
+ AgentState::Busy,
+ AgentState::Idle,
+ now + std::time::Duration::from_millis(400),
+ &mut last_busy,
+ );
+ assert_eq!(still_busy, AgentState::Busy);
+ }
+
+ #[test]
+ fn claude_transitions_to_idle_after_hold_expires() {
+ let now = std::time::Instant::now();
+ let mut last_busy = Some(now);
+
+ let state = stabilize_agent_state(
+ Some(Agent::Claude),
+ AgentState::Busy,
+ AgentState::Idle,
+ now + CLAUDE_BUSY_HOLD + std::time::Duration::from_millis(1),
+ &mut last_busy,
+ );
+ assert_eq!(state, AgentState::Idle);
+ }
+
+ #[test]
+ fn non_claude_states_are_unchanged() {
+ let now = std::time::Instant::now();
+ let mut last_busy = None;
+
+ let state = stabilize_agent_state(
+ Some(Agent::Codex),
+ AgentState::Busy,
+ AgentState::Idle,
+ now,
+ &mut last_busy,
+ );
+ assert_eq!(state, AgentState::Idle);
+ }
+}