diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-08 20:55:51 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-09 04:26:19 +0300 |
| commit | ee51c5e5fbaea694e65bb0a4908f75bcaf9707d1 (patch) | |
| tree | f566d2a5921d4112602a7abcffb5cf6eb04d5f24 /src/detect/agents | |
| parent | a7cd7806f2fa0251e2dca41ffec61ceafb6f0e18 (diff) | |
feat: use pty-first agent detection
Diffstat (limited to 'src/detect/agents')
| -rw-r--r-- | src/detect/agents/amp.rs | 23 | ||||
| -rw-r--r-- | src/detect/agents/antigravity.rs | 15 | ||||
| -rw-r--r-- | src/detect/agents/claude_code.rs | 10 | ||||
| -rw-r--r-- | src/detect/agents/cline.rs | 14 | ||||
| -rw-r--r-- | src/detect/agents/codex.rs | 4 | ||||
| -rw-r--r-- | src/detect/agents/cursor.rs | 10 | ||||
| -rw-r--r-- | src/detect/agents/droid.rs | 27 | ||||
| -rw-r--r-- | src/detect/agents/gemini.rs | 35 | ||||
| -rw-r--r-- | src/detect/agents/github_copilot.rs | 14 | ||||
| -rw-r--r-- | src/detect/agents/grok.rs | 17 | ||||
| -rw-r--r-- | src/detect/agents/hermes.rs | 22 | ||||
| -rw-r--r-- | src/detect/agents/kilo.rs | 4 | ||||
| -rw-r--r-- | src/detect/agents/kimi.rs | 50 | ||||
| -rw-r--r-- | src/detect/agents/kiro.rs | 7 | ||||
| -rw-r--r-- | src/detect/agents/mod.rs | 28 | ||||
| -rw-r--r-- | src/detect/agents/opencode.rs | 6 | ||||
| -rw-r--r-- | src/detect/agents/qodercli.rs | 14 |
17 files changed, 149 insertions, 151 deletions
diff --git a/src/detect/agents/amp.rs b/src/detect/agents/amp.rs index ad76ca27..b1fcba4d 100644 --- a/src/detect/agents/amp.rs +++ b/src/detect/agents/amp.rs @@ -17,6 +17,19 @@ use super::super::AgentState; pub(super) fn detect(content: &str) -> AgentState { let lower = content.to_lowercase(); + if has_visible_blocker(content) { + return AgentState::Blocked; + } + + if lower.contains("esc to cancel") { + return AgentState::Working; + } + + AgentState::Idle +} + +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); let has_waiting_for_approval = lower.contains("waiting for approval"); let has_approval_header = lower.contains("invoke tool") || lower.contains("run this command?") @@ -29,13 +42,5 @@ pub(super) fn detect(content: &str) -> AgentState { || lower.contains("allow file for every session") || lower.contains("deny with feedback")); - if has_approval_actions && (has_waiting_for_approval || has_approval_header) { - return AgentState::Blocked; - } - - if lower.contains("esc to cancel") { - return AgentState::Working; - } - - AgentState::Idle + has_approval_actions && (has_waiting_for_approval || has_approval_header) } diff --git a/src/detect/agents/antigravity.rs b/src/detect/agents/antigravity.rs index b504a96a..9790c5a4 100644 --- a/src/detect/agents/antigravity.rs +++ b/src/detect/agents/antigravity.rs @@ -1,12 +1,7 @@ use super::super::AgentState; pub(super) fn detect(content: &str) -> AgentState { - let lower = content.to_lowercase(); - - let has_permission_request = lower.contains("requesting permission for:"); - let has_permission_question = lower.contains("do you want to proceed?"); - let has_permission_controls = lower.contains("tab amend") && lower.contains("edit command"); - if has_permission_request && (has_permission_question || has_permission_controls) { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -17,6 +12,14 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + let has_permission_request = lower.contains("requesting permission for:"); + let has_permission_question = lower.contains("do you want to proceed?"); + let has_permission_controls = lower.contains("tab amend") && lower.contains("edit command"); + has_permission_request && (has_permission_question || has_permission_controls) +} + fn has_antigravity_spinner(content: &str) -> bool { content.lines().any(|line| { let trimmed = line.trim_start(); diff --git a/src/detect/agents/claude_code.rs b/src/detect/agents/claude_code.rs index 910eb679..a13019a4 100644 --- a/src/detect/agents/claude_code.rs +++ b/src/detect/agents/claude_code.rs @@ -27,6 +27,10 @@ pub(super) fn detect(content: &str) -> AgentState { return AgentState::Blocked; } + if has_dynamic_workflow_prompt(&lower) { + return AgentState::Blocked; + } + if has_working_chrome(content) { return AgentState::Working; } @@ -45,6 +49,7 @@ pub(super) fn detect(content: &str) -> AgentState { pub(super) fn has_visible_blocker(content: &str) -> bool { let lower = content.to_lowercase(); has_live_blocked_form(content) + || has_dynamic_workflow_prompt(&lower) || lower.contains("do you want to proceed?") && has_claude_yes_no_choice(content) && (lower.contains("bash command") @@ -96,6 +101,7 @@ fn has_claude_blocked_prompt(content: &str, lower_content: &str) -> bool { has_confirmation_prompt(lower_content) || lower_content.contains("do you want to proceed?") || lower_content.contains("would you like to proceed?") + || has_dynamic_workflow_prompt(lower_content) || lower_content.contains("waiting for permission") || lower_content.contains("do you want to allow this connection?") || lower_content.contains("tab to amend") @@ -105,6 +111,10 @@ fn has_claude_blocked_prompt(content: &str, lower_content: &str) -> bool { || (has_selection_prompt(content) && has_claude_yes_no_choice(content)) } +fn has_dynamic_workflow_prompt(lower_content: &str) -> bool { + lower_content.contains("run a dynamic workflow?") && lower_content.contains("esc to cancel") +} + fn has_live_blocked_form(content: &str) -> bool { let region = content_after_last_horizontal_rule(content); region.lines().any(|line| { diff --git a/src/detect/agents/cline.rs b/src/detect/agents/cline.rs index 86b5ce01..13509606 100644 --- a/src/detect/agents/cline.rs +++ b/src/detect/agents/cline.rs @@ -4,11 +4,7 @@ pub(super) fn detect(content: &str) -> AgentState { let lower = content.to_lowercase(); // Blocked - if lower.contains("let cline use this tool") { - return AgentState::Blocked; - } - // [act mode] or [plan mode] followed by "yes" - if (lower.contains("[act mode]") || lower.contains("[plan mode]")) && lower.contains("yes") { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -20,3 +16,11 @@ pub(super) fn detect(content: &str) -> AgentState { // Cline defaults to working (unlike most agents that default to idle) AgentState::Working } + +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + lower.contains("let cline use this tool") + || ((lower.contains("[act mode]") || lower.contains("[plan mode]")) + && (lower.contains("execute command?") || lower.contains("use this tool?")) + && lower.contains("yes")) +} diff --git a/src/detect/agents/codex.rs b/src/detect/agents/codex.rs index 0a9bdadb..b77b7d25 100644 --- a/src/detect/agents/codex.rs +++ b/src/detect/agents/codex.rs @@ -38,10 +38,6 @@ pub(super) fn has_visible_blocker(content: &str) -> bool { has_codex_strong_blocked_prompt(content) } -pub(super) fn has_prompt(content: &str) -> bool { - has_codex_current_prompt(content) || content.lines().any(codex_prompt_line) -} - pub(super) fn has_visible_working(content: &str) -> bool { has_codex_live_working_at_current_prompt(content) || (!has_codex_current_prompt(content) && has_codex_visible_working_without_prompt(content)) diff --git a/src/detect/agents/cursor.rs b/src/detect/agents/cursor.rs index a62e9382..f0f00b9b 100644 --- a/src/detect/agents/cursor.rs +++ b/src/detect/agents/cursor.rs @@ -4,7 +4,7 @@ pub(super) fn detect(content: &str) -> AgentState { let lower = content.to_lowercase(); // Blocked - if has_cursor_blocked_prompt(content, &lower) { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -19,8 +19,12 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } -fn has_cursor_blocked_prompt(content: &str, lower: &str) -> bool { - if lower.contains("waiting for approval") || lower.contains("run this command?") { +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + if lower.contains("waiting for approval") + && lower.contains("run this command?") + && (lower.contains("run (once) (y)") || lower.contains("skip (esc or n)")) + { return true; } diff --git a/src/detect/agents/droid.rs b/src/detect/agents/droid.rs index ea19da3f..52c5e087 100644 --- a/src/detect/agents/droid.rs +++ b/src/detect/agents/droid.rs @@ -9,19 +9,7 @@ use super::super::{has_braille_spinner, AgentState}; pub(super) fn detect(content: &str) -> AgentState { let lower = content.to_lowercase(); - // Blocked: EXECUTE approval prompt with selection UI chrome - // Primary (AND): structural keyword + chrome text = certain - let has_execute = content.contains("EXECUTE"); - let has_selection_chrome = lower.contains("enter to select") - || lower.contains("↑↓ to navigate") - || lower.contains("esc to cancel"); - let has_selection_options = lower.contains("> yes, allow") || lower.contains("> no, cancel"); - - if has_execute && (has_selection_chrome || has_selection_options) { - return AgentState::Blocked; - } - // Secondary: selection chrome + options together (no EXECUTE needed) - if has_selection_chrome && has_selection_options { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -37,3 +25,16 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } + +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + // Primary (AND): structural keyword + chrome text = certain + let has_execute = content.contains("EXECUTE"); + let has_selection_chrome = lower.contains("enter to select") + || lower.contains("↑↓ to navigate") + || lower.contains("esc to cancel"); + let has_selection_options = lower.contains("> yes, allow") || lower.contains("> no, cancel"); + + (has_execute && (has_selection_chrome || has_selection_options)) + || (has_selection_chrome && has_selection_options) +} diff --git a/src/detect/agents/gemini.rs b/src/detect/agents/gemini.rs index c47355c0..86cf3bca 100644 --- a/src/detect/agents/gemini.rs +++ b/src/detect/agents/gemini.rs @@ -1,24 +1,11 @@ -use super::super::{has_confirmation_prompt, AgentState}; +use super::super::AgentState; pub(super) fn detect(content: &str) -> AgentState { - let lower = content.to_lowercase(); - - // Blocked — explicit confirmation - if lower.contains("waiting for user confirmation") { - return AgentState::Blocked; - } - - // Blocked — box-drawing confirmation prompts - if content.contains("│ Apply this change") - || content.contains("│ Allow execution") - || content.contains("│ Do you want to proceed") - { - return AgentState::Blocked; - } - if has_confirmation_prompt(&lower) { + if has_visible_blocker(content) { return AgentState::Blocked; } + let lower = content.to_lowercase(); // Working if lower.contains("esc to cancel") { return AgentState::Working; @@ -26,3 +13,19 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } + +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + let has_choice = lower.contains("yes") || lower.contains("no"); + + content.contains("│ Apply this change") + || content.contains("│ Allow execution") + || (has_choice + && (lower.contains("waiting for user confirmation") + || content.contains("│ Do you want to proceed") + || lower.contains("do you want to proceed?"))) + || content.lines().any(|line| { + let line = line.trim().to_ascii_lowercase(); + line.starts_with("❯") && (line.contains("yes") || line.contains("allow")) + }) +} diff --git a/src/detect/agents/github_copilot.rs b/src/detect/agents/github_copilot.rs index eed03011..0eece95b 100644 --- a/src/detect/agents/github_copilot.rs +++ b/src/detect/agents/github_copilot.rs @@ -4,11 +4,7 @@ pub(super) fn detect(content: &str) -> AgentState { let lower = content.to_lowercase(); // Blocked - if lower.contains("esc to cancel") - && (lower.contains("enter to select") - || lower.contains("enter to confirm") - || lower.contains("enter to submit")) - { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -22,3 +18,11 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } + +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + lower.contains("esc to cancel") + && (lower.contains("enter to select") + || lower.contains("enter to confirm") + || lower.contains("enter to submit")) +} diff --git a/src/detect/agents/grok.rs b/src/detect/agents/grok.rs index 75b5cd35..8a5834a2 100644 --- a/src/detect/agents/grok.rs +++ b/src/detect/agents/grok.rs @@ -7,17 +7,11 @@ use super::super::{has_braille_spinner, AgentState}; /// status line such as "⠋ Waiting… 1.8s" plus live controls like /// "Ctrl+c:cancel" and "Ctrl+Enter:interject". pub(super) fn detect(content: &str) -> AgentState { - let lower = content.to_lowercase(); - - if lower.contains("use ← → to choose permission whitelist scope") - || lower.contains("yes, proceed") - || lower.contains("no, reject") - || lower.contains("ctrl+o:yolo") - || lower.contains(":scope") - { + if has_visible_blocker(content) { return AgentState::Blocked; } + let lower = content.to_lowercase(); if has_braille_spinner(content) && (lower.contains("waiting") || lower.contains("run ") @@ -34,3 +28,10 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } + +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + let has_scope_selector = lower.contains("use ← → to choose permission whitelist scope") + || lower.contains("←/→:scope"); + has_scope_selector && lower.contains("yes, proceed") && lower.contains("no, reject") +} diff --git a/src/detect/agents/hermes.rs b/src/detect/agents/hermes.rs index d6c4c97e..474dda5c 100644 --- a/src/detect/agents/hermes.rs +++ b/src/detect/agents/hermes.rs @@ -6,21 +6,25 @@ use super::super::AgentState; /// dialogs for dangerous terminal commands. Prefer the modal controls for /// blocked detection, then the live interrupt/status controls for working. pub(super) fn detect(content: &str) -> AgentState { - let lower = content.to_lowercase(); - - let has_approval_options = lower.contains("allow once") - && lower.contains("allow for this session") - && lower.contains("deny"); - let has_approval_controls = lower.contains("enter to confirm") - || lower.contains("↑/↓ to select") - || lower.contains("show full command"); - if (lower.contains("dangerous command") || has_approval_options) && has_approval_controls { + if has_visible_blocker(content) { return AgentState::Blocked; } + let lower = content.to_lowercase(); if lower.contains("msg=interrupt") || lower.contains("ctrl+c cancel") { return AgentState::Working; } AgentState::Idle } + +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower = content.to_lowercase(); + let has_approval_options = lower.contains("allow once") + && lower.contains("allow for this session") + && lower.contains("deny"); + let has_approval_controls = lower.contains("enter to confirm") + || lower.contains("↑/↓ to select") + || lower.contains("show full command"); + (lower.contains("dangerous command") || has_approval_options) && has_approval_controls +} diff --git a/src/detect/agents/kilo.rs b/src/detect/agents/kilo.rs index 1ab3651a..46a5601a 100644 --- a/src/detect/agents/kilo.rs +++ b/src/detect/agents/kilo.rs @@ -7,3 +7,7 @@ pub(super) fn detect(content: &str) -> AgentState { super::opencode::detect(content) } + +pub(super) fn has_visible_blocker(content: &str) -> bool { + super::opencode::has_visible_blocker(content) +} diff --git a/src/detect/agents/kimi.rs b/src/detect/agents/kimi.rs index b42bf885..9d3821ed 100644 --- a/src/detect/agents/kimi.rs +++ b/src/detect/agents/kimi.rs @@ -16,10 +16,6 @@ pub(super) fn has_visible_blocker(content: &str) -> bool { has_current_approval_panel(content) || has_question_panel(content) } -pub(super) fn has_prompt_box(content: &str) -> bool { - has_editor_prompt_box(content) && has_footer_context(content) -} - pub(super) fn has_visible_working(content: &str) -> bool { has_kimi_working_status(content) } @@ -99,49 +95,3 @@ fn has_question_panel(content: &str) -> bool { fn has_numeric_choose_hint(lower_content: &str) -> bool { lower_content.contains(" choose") && lower_content.contains('1') && lower_content.contains('2') } - -fn has_editor_prompt_box(content: &str) -> bool { - let lines: Vec<&str> = content.lines().collect(); - - for top_index in 0..lines.len() { - if !is_editor_top_border(lines[top_index]) { - continue; - } - - let mut saw_prompt = false; - for line in lines.iter().skip(top_index + 1) { - if is_editor_bottom_border(line) { - if saw_prompt { - return true; - } - break; - } - - saw_prompt |= is_editor_prompt_line(line); - } - } - - false -} - -fn is_editor_top_border(line: &str) -> bool { - let trimmed = line.trim(); - ((trimmed.starts_with('╭') && trimmed.ends_with('╮')) - || (trimmed.starts_with('├') && trimmed.ends_with('┤'))) - && trimmed.contains('─') -} - -fn is_editor_bottom_border(line: &str) -> bool { - let trimmed = line.trim(); - trimmed.starts_with('╰') && trimmed.ends_with('╯') && trimmed.contains('─') -} - -fn is_editor_prompt_line(line: &str) -> bool { - let trimmed = line.trim_start(); - let inner = trimmed.strip_prefix('│').unwrap_or(trimmed).trim_start(); - inner.starts_with('>') -} - -fn has_footer_context(content: &str) -> bool { - content.to_lowercase().contains("context: ") -} diff --git a/src/detect/agents/kiro.rs b/src/detect/agents/kiro.rs index f40b9bcb..889f5315 100644 --- a/src/detect/agents/kiro.rs +++ b/src/detect/agents/kiro.rs @@ -7,7 +7,7 @@ use super::super::AgentState; pub(super) fn detect(content: &str) -> AgentState { let lower = content.to_lowercase(); - if has_kiro_blocked_prompt(&lower) { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -20,8 +20,9 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } -fn has_kiro_blocked_prompt(lower_content: &str) -> bool { - has_tool_approval_prompt(lower_content) || has_subagent_approval_prompt(lower_content) +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower_content = content.to_lowercase(); + has_tool_approval_prompt(&lower_content) || has_subagent_approval_prompt(&lower_content) } fn has_tool_approval_prompt(lower_content: &str) -> bool { diff --git a/src/detect/agents/mod.rs b/src/detect/agents/mod.rs index 46a25f66..704350df 100644 --- a/src/detect/agents/mod.rs +++ b/src/detect/agents/mod.rs @@ -45,7 +45,6 @@ pub(super) fn detect(agent: Agent, screen_content: &str) -> AgentDetection { state, skip_state_update: true, visible_blocker: false, - visible_idle: false, visible_working: false, }; } @@ -54,7 +53,6 @@ pub(super) fn detect(agent: Agent, screen_content: &str) -> AgentDetection { state, skip_state_update: false, visible_blocker: has_visible_blocker(agent, screen_content, state), - visible_idle: has_visible_idle(agent, screen_content, state), visible_working: has_visible_working(agent, screen_content, state), } } @@ -79,20 +77,20 @@ fn has_visible_blocker(agent: Agent, content: &str, state: AgentState) -> bool { // is known to be structural and live. Agent::Claude => claude_code::has_visible_blocker(content), Agent::Codex => codex::has_visible_blocker(content), + Agent::Gemini => gemini::has_visible_blocker(content), + Agent::Cursor => cursor::has_visible_blocker(content), + Agent::Antigravity => antigravity::has_visible_blocker(content), + Agent::Cline => cline::has_visible_blocker(content), + Agent::OpenCode => opencode::has_visible_blocker(content), + Agent::GithubCopilot => github_copilot::has_visible_blocker(content), Agent::Kimi => kimi::has_visible_blocker(content), - _ => false, - } -} - -fn has_visible_idle(agent: Agent, content: &str, state: AgentState) -> bool { - if state != AgentState::Idle { - return false; - } - - match agent { - Agent::Claude => claude_code::has_prompt_box(content), - Agent::Codex => codex::has_prompt(content), - Agent::Kimi => kimi::has_prompt_box(content), + Agent::Kiro => kiro::has_visible_blocker(content), + Agent::Droid => droid::has_visible_blocker(content), + Agent::Amp => amp::has_visible_blocker(content), + Agent::Grok => grok::has_visible_blocker(content), + Agent::Hermes => hermes::has_visible_blocker(content), + Agent::Kilo => kilo::has_visible_blocker(content), + Agent::Qodercli => qodercli::has_visible_blocker(content), _ => false, } } diff --git a/src/detect/agents/opencode.rs b/src/detect/agents/opencode.rs index 219f69ed..ab8a424f 100644 --- a/src/detect/agents/opencode.rs +++ b/src/detect/agents/opencode.rs @@ -2,7 +2,7 @@ use super::super::{has_interrupt_pattern, AgentState}; pub(super) fn detect(content: &str) -> AgentState { // Blocked - if content.contains("△ Permission required") || has_opencode_question_prompt(content) { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -17,6 +17,10 @@ pub(super) fn detect(content: &str) -> AgentState { AgentState::Idle } +pub(super) fn has_visible_blocker(content: &str) -> bool { + content.contains("△ Permission required") || has_opencode_question_prompt(content) +} + fn has_opencode_question_prompt(content: &str) -> bool { let lower = content.to_lowercase(); let has_enter_action = lower.contains("enter confirm") diff --git a/src/detect/agents/qodercli.rs b/src/detect/agents/qodercli.rs index b7da356b..0a4372df 100644 --- a/src/detect/agents/qodercli.rs +++ b/src/detect/agents/qodercli.rs @@ -15,7 +15,7 @@ pub(super) fn detect(content: &str) -> AgentState { return AgentState::Idle; } - if has_qodercli_blocked_prompt(&lower) { + if has_visible_blocker(content) { return AgentState::Blocked; } @@ -87,9 +87,15 @@ fn has_qodercli_spinner_row(content: &str) -> bool { /// defensive fallbacks in case the title row scrolls off-screen. /// * The interactive shell waiting hint emitted by qodercli when an agent /// spawns a shell that is now parked for user keystrokes. -fn has_qodercli_blocked_prompt(lower_content: &str) -> bool { - lower_content.contains("waiting for user confirmation") - || lower_content.contains("awaiting approval") +pub(super) fn has_visible_blocker(content: &str) -> bool { + let lower_content = content.to_lowercase(); + (lower_content.contains("waiting for user confirmation") + && (lower_content.contains("yes") + || lower_content.contains("no") + || lower_content.contains("allow") + || lower_content.contains("reject"))) + || (lower_content.contains("awaiting approval") + && (lower_content.contains("allow") || lower_content.contains("reject"))) || lower_content.contains("permission required") || lower_content.contains("allow once or always?") || lower_content.contains("asking user") |
