aboutsummaryrefslogtreecommitdiff
path: root/src/detect/agents
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-03 04:38:46 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-03 04:38:46 +0300
commit58af4902fc51b6be8fec99a3cec576c98e0c6075 (patch)
tree8dbb2501a61904b4c90d368150b5370b5f9e90e6 /src/detect/agents
parent816cb88cd9df3cba305a50842ebf78db19279dd5 (diff)
fix: preserve status in transcript viewers
Diffstat (limited to 'src/detect/agents')
-rw-r--r--src/detect/agents/claude_code.rs42
-rw-r--r--src/detect/agents/codex.rs47
-rw-r--r--src/detect/agents/mod.rs20
3 files changed, 109 insertions, 0 deletions
diff --git a/src/detect/agents/claude_code.rs b/src/detect/agents/claude_code.rs
index ace946da..77db856c 100644
--- a/src/detect/agents/claude_code.rs
+++ b/src/detect/agents/claude_code.rs
@@ -63,6 +63,20 @@ pub(super) fn has_working_chrome(content: &str) -> bool {
|| has_spinner_activity(above)
}
+pub(super) fn is_transcript_viewer(content: &str) -> bool {
+ let bottom_lines = bottom_non_empty_lines(content, 3);
+ let Some(last_line) = bottom_lines.last() else {
+ return false;
+ };
+ let bottom_text = normalize_lines(&bottom_lines);
+
+ bottom_text.contains("showing detailed transcript")
+ && bottom_text.contains("ctrl+o to toggle")
+ && (bottom_text.contains("ctrl+e to show all")
+ || bottom_text.contains("ctrl+e to collapse"))
+ && transcript_control_tail(last_line)
+}
+
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 {
@@ -232,3 +246,31 @@ fn is_horizontal_rule(line: &str) -> bool {
let trimmed = line.trim();
!trimmed.is_empty() && trimmed.chars().all(|c| c == '─')
}
+
+fn bottom_non_empty_lines(content: &str, max_lines: usize) -> Vec<&str> {
+ let mut lines: Vec<&str> = content
+ .lines()
+ .rev()
+ .filter(|line| !line.trim().is_empty())
+ .take(max_lines)
+ .collect();
+ lines.reverse();
+ lines
+}
+
+fn normalize_lines(lines: &[&str]) -> String {
+ lines
+ .iter()
+ .flat_map(|line| line.split_whitespace())
+ .collect::<Vec<_>>()
+ .join(" ")
+ .to_lowercase()
+}
+
+fn transcript_control_tail(line: &str) -> bool {
+ let lower = line.to_lowercase();
+ lower.contains("ctrl+e")
+ || lower.contains("show all")
+ || lower.contains("collapse")
+ || lower.contains("verbose")
+}
diff --git a/src/detect/agents/codex.rs b/src/detect/agents/codex.rs
index 50430aa4..e00d0a28 100644
--- a/src/detect/agents/codex.rs
+++ b/src/detect/agents/codex.rs
@@ -48,6 +48,25 @@ pub(super) fn has_visible_working(content: &str) -> bool {
|| (!has_codex_current_prompt(content) && has_codex_visible_working_without_prompt(content))
}
+pub(super) fn is_transcript_viewer(content: &str) -> bool {
+ let bottom_lines = bottom_non_empty_lines(content, 3);
+ let Some(last_line) = bottom_lines.last() else {
+ return false;
+ };
+ let bottom_text = normalize_lines(&bottom_lines);
+
+ bottom_text.contains("↑/↓ to scroll")
+ && bottom_text.contains("pgup/pgdn to page")
+ && bottom_text.contains("home/end to jump")
+ && bottom_text.contains("q to quit")
+ && has_codex_edit_prev_controls(&bottom_text)
+ && transcript_control_tail(last_line)
+}
+
+fn has_codex_edit_prev_controls(bottom_text: &str) -> bool {
+ bottom_text.contains("esc to edit prev") || bottom_text.contains("esc/← to edit prev")
+}
+
fn has_codex_visible_working_without_prompt(content: &str) -> bool {
let mut recent_lines = content.lines().rev().filter(|line| !line.trim().is_empty());
let Some(last_line) = recent_lines.next() else {
@@ -171,3 +190,31 @@ fn codex_queued_input_header_line(line: &str) -> bool {
lower.starts_with("• queued follow-up inputs")
|| lower.starts_with("• messages to be submitted after next tool call")
}
+
+fn bottom_non_empty_lines(content: &str, max_lines: usize) -> Vec<&str> {
+ let mut lines: Vec<&str> = content
+ .lines()
+ .rev()
+ .filter(|line| !line.trim().is_empty())
+ .take(max_lines)
+ .collect();
+ lines.reverse();
+ lines
+}
+
+fn normalize_lines(lines: &[&str]) -> String {
+ lines
+ .iter()
+ .flat_map(|line| line.split_whitespace())
+ .collect::<Vec<_>>()
+ .join(" ")
+ .to_lowercase()
+}
+
+fn transcript_control_tail(line: &str) -> bool {
+ let lower = line.to_lowercase();
+ lower.contains("q to quit")
+ || lower.contains("esc to edit")
+ || lower.contains("esc/← to edit")
+ || lower.contains("edit message")
+}
diff --git a/src/detect/agents/mod.rs b/src/detect/agents/mod.rs
index 4597c122..a89b2f37 100644
--- a/src/detect/agents/mod.rs
+++ b/src/detect/agents/mod.rs
@@ -39,14 +39,34 @@ pub(super) fn detect(agent: Agent, screen_content: &str) -> AgentDetection {
Agent::Qodercli => qodercli::detect(screen_content),
};
+ let skip_state_update = should_skip_state_update(agent, screen_content);
+ if skip_state_update {
+ return AgentDetection {
+ state,
+ skip_state_update: true,
+ visible_blocker: false,
+ visible_idle: false,
+ visible_working: false,
+ };
+ }
+
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),
}
}
+pub(super) fn should_skip_state_update(agent: Agent, content: &str) -> bool {
+ match agent {
+ Agent::Claude => claude_code::is_transcript_viewer(content),
+ Agent::Codex => codex::is_transcript_viewer(content),
+ _ => false,
+ }
+}
+
fn has_visible_blocker(agent: Agent, content: &str, state: AgentState) -> bool {
if state != AgentState::Blocked {
return false;