aboutsummaryrefslogtreecommitdiff
path: root/src/detect/agents/claude_code.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-09 04:24:12 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-09 04:26:19 +0300
commiteeacb45b4b6ab4446c5bea1331f743fee5e5b9f8 (patch)
tree063e99ff0a40ce7ef00002ac00130aeb3f30f6d7 /src/detect/agents/claude_code.rs
parent120001b591e64d80a557d81e3b112460b9987fc6 (diff)
fix: remove idle screen patterns from agent detection
Diffstat (limited to 'src/detect/agents/claude_code.rs')
-rw-r--r--src/detect/agents/claude_code.rs77
1 files changed, 54 insertions, 23 deletions
diff --git a/src/detect/agents/claude_code.rs b/src/detect/agents/claude_code.rs
index 1336c480..b159fede 100644
--- a/src/detect/agents/claude_code.rs
+++ b/src/detect/agents/claude_code.rs
@@ -12,17 +12,6 @@ use super::super::{has_confirmation_prompt, has_selection_prompt, AgentState};
pub(super) fn detect(content: &str) -> AgentState {
let lower = content.to_lowercase();
- // Search prompt is always idle
- if content.contains("⌕ Search…") {
- return AgentState::Idle;
- }
-
- // ctrl+r toggle — don't change state
- // (we return Idle as a safe default since we don't have previous state here)
- if lower.contains("ctrl+r to toggle") {
- return AgentState::Idle;
- }
-
if has_live_blocked_form(content) {
return AgentState::Blocked;
}
@@ -39,14 +28,14 @@ pub(super) fn detect(content: &str) -> AgentState {
return AgentState::Blocked;
}
- if has_prompt_box(content) {
- return AgentState::Idle;
- }
-
AgentState::Idle
}
pub(super) fn has_visible_blocker(content: &str) -> bool {
+ if has_live_input_prompt_box(content) {
+ return false;
+ }
+
let lower = content.to_lowercase();
has_live_blocked_form(content)
|| has_dynamic_workflow_prompt(&lower)
@@ -106,15 +95,57 @@ pub(super) fn is_transcript_viewer(content: &str) -> bool {
}
pub(super) fn has_prompt_box(content: &str) -> bool {
- let lines: Vec<&str> = content.lines().collect();
- let Some(top_border_index) = claude_prompt_box_top_border_index(&lines) else {
- return false;
- };
+ prompt_box_body_lines(content)
+ .is_some_and(|lines| lines.iter().any(|line| line.trim_start().starts_with('❯')))
+}
- lines[top_border_index + 1..]
- .iter()
- .take_while(|line| !is_horizontal_rule(line))
- .any(|line| line.trim_start().starts_with('❯'))
+fn has_live_input_prompt_box(content: &str) -> bool {
+ prompt_box_body_lines(content).is_some_and(|lines| {
+ let non_empty: Vec<&str> = lines
+ .iter()
+ .map(|line| line.trim())
+ .filter(|line| !line.is_empty())
+ .collect();
+ let Some(first) = non_empty.first() else {
+ return false;
+ };
+ first.trim_start().starts_with('❯')
+ && !non_empty
+ .iter()
+ .any(|line| claude_prompt_box_line_is_selector_chrome(line))
+ })
+}
+
+fn claude_prompt_box_line_is_selector_chrome(line: &str) -> bool {
+ let trimmed = line.trim().trim_start_matches('❯').trim_start();
+ let lower = trimmed.to_ascii_lowercase();
+
+ lower.contains("enter to select")
+ || lower.contains("enter to confirm")
+ || lower.contains("enter to submit")
+ || lower.contains("esc to cancel")
+ || lower.contains("tab/arrow")
+ || lower.contains("arrow keys")
+ || lower.contains("↑/↓")
+ || lower.contains("↑↓")
+ || lower.contains("ctrl+g to edit")
+ || lower.contains("ctrl+e to explain")
+ || trimmed
+ .split_once('.')
+ .is_some_and(|(prefix, _)| prefix.trim().parse::<u32>().is_ok())
+}
+
+fn prompt_box_body_lines(content: &str) -> Option<Vec<&str>> {
+ let lines: Vec<&str> = content.lines().collect();
+ let top_border_index = claude_prompt_box_top_border_index(&lines)?;
+
+ Some(
+ lines[top_border_index + 1..]
+ .iter()
+ .take_while(|line| !is_horizontal_rule(line))
+ .copied()
+ .collect(),
+ )
}
/// Claude uses the same generic Select and Dialog widgets for both