aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-06 20:17:52 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-06 20:17:52 +0300
commit94b90043f98499a1ed3594dd9ea42b2aef62b93a (patch)
tree2f19157650b5010d91d815ee4afd4d54d2519fe2 /src
parentf7b0ee2f758440db3a3ff4cc727f879b420255d7 (diff)
fix: clear native agent detection after shell returns
fixes #66
Diffstat (limited to 'src')
-rw-r--r--src/detect.rs6
-rw-r--r--src/pane.rs89
-rw-r--r--src/platform/fallback.rs5
-rw-r--r--src/platform/linux.rs6
-rw-r--r--src/platform/macos.rs4
5 files changed, 103 insertions, 7 deletions
diff --git a/src/detect.rs b/src/detect.rs
index d6d48343..ce06211c 100644
--- a/src/detect.rs
+++ b/src/detect.rs
@@ -589,6 +589,12 @@ pub fn foreground_job(child_pid: u32) -> Option<crate::platform::ForegroundJob>
crate::platform::foreground_job(child_pid)
}
+/// Get the foreground process group for a pane shell PID.
+/// This is cheaper than collecting every process in the foreground job.
+pub fn foreground_process_group_id(child_pid: u32) -> Option<u32> {
+ crate::platform::foreground_process_group_id(child_pid)
+}
+
fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> String {
let effective = process.argv0.as_deref().unwrap_or(&process.name);
let lower_effective = effective.to_lowercase();
diff --git a/src/pane.rs b/src/pane.rs
index 51760ecf..71f35ca9 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -96,6 +96,14 @@ struct AgentDetectionPresence {
consecutive_misses: u8,
}
+fn should_clear_agent_for_foreground_shell(
+ previous_agent: Option<Agent>,
+ new_agent: Option<Agent>,
+ foreground_is_pane_shell: bool,
+) -> bool {
+ previous_agent.is_some() && new_agent.is_none() && foreground_is_pane_shell
+}
+
impl AgentDetectionPresence {
fn from_agent(current_agent: Option<Agent>) -> Self {
Self {
@@ -108,6 +116,16 @@ impl AgentDetectionPresence {
self.current_agent
}
+ fn clear_current_agent(&mut self) -> bool {
+ if self.current_agent.is_none() {
+ self.consecutive_misses = 0;
+ return false;
+ }
+ self.current_agent = None;
+ self.consecutive_misses = 0;
+ true
+ }
+
fn observe_process_probe(&mut self, identified_agent: Option<Agent>) -> bool {
match identified_agent {
Some(agent) => {
@@ -458,6 +476,8 @@ impl PaneRuntime {
let mut agent_presence = AgentDetectionPresence::from_agent(None);
let mut state = AgentState::Unknown;
let mut last_process_check = Instant::now();
+ let mut last_foreground_pgid = None;
+ let mut pending_foreground_shell_clear = false;
let mut last_claude_working_at = None;
tokio::time::sleep(Duration::from_millis(50)).await;
@@ -478,33 +498,50 @@ impl PaneRuntime {
_ = detect_reset.notified() => {
agent_presence = AgentDetectionPresence::from_agent(None);
state = AgentState::Unknown;
+ last_foreground_pgid = None;
+ pending_foreground_shell_clear = false;
last_claude_working_at = None;
}
}
let now = Instant::now();
let suppressed_agent = active_pending_release(&pending_release_for_task, now);
+ let pid = child_pid.load(Ordering::Acquire);
+ let foreground_pgid = (pid > 0 && agent_presence.current_agent().is_some())
+ .then(|| detect::foreground_process_group_id(pid))
+ .flatten();
+ let foreground_group_changed = foreground_pgid.is_some()
+ && last_foreground_pgid.is_some()
+ && foreground_pgid != last_foreground_pgid;
let should_check_process = suppressed_agent.is_some()
|| agent_presence.current_agent().is_none()
+ || foreground_group_changed
+ || pending_foreground_shell_clear
|| now.duration_since(last_process_check) >= PROCESS_RECHECK;
let mut agent_changed = false;
let mut agent = agent_presence.current_agent();
if should_check_process {
last_process_check = now;
- let pid = child_pid.load(Ordering::Acquire);
if pid > 0 {
let mut process_name = None;
let mut process_group_id = None;
+ let mut foreground_is_pane_shell = false;
let mut new_agent = None;
if let Some(job) = detect::foreground_job(pid) {
process_group_id = Some(job.process_group_id);
+ last_foreground_pgid = Some(job.process_group_id);
+ foreground_is_pane_shell =
+ job.processes.iter().any(|p| p.pid == pid);
let identified = detect::identify_agent_in_job(&job);
process_name = identified
.as_ref()
.map(|(_, process_name)| process_name.clone());
new_agent = identified.as_ref().map(|(agent, _)| *agent);
+ } else if foreground_pgid.is_some() {
+ process_group_id = foreground_pgid;
+ last_foreground_pgid = foreground_pgid;
}
if let Some(suppressed_agent) = suppressed_agent {
@@ -518,7 +555,28 @@ impl PaneRuntime {
}
let previous_agent = agent_presence.current_agent();
- if agent_presence.observe_process_probe(new_agent) {
+ let changed = if should_clear_agent_for_foreground_shell(
+ previous_agent,
+ new_agent,
+ foreground_is_pane_shell,
+ ) {
+ if state == AgentState::Idle {
+ pending_foreground_shell_clear = false;
+ agent_presence.clear_current_agent()
+ } else {
+ pending_foreground_shell_clear = true;
+ false
+ }
+ } else {
+ pending_foreground_shell_clear = false;
+ agent_presence.observe_process_probe(new_agent)
+ };
+ if new_agent.is_some() {
+ last_foreground_pgid = process_group_id;
+ } else if agent_presence.current_agent().is_none() {
+ last_foreground_pgid = None;
+ }
+ if changed {
agent = agent_presence.current_agent();
if let Some(process_name) = process_name {
info!(
@@ -995,6 +1053,33 @@ mod tests {
}
#[test]
+ fn foreground_shell_without_agent_is_immediate_clear_signal() {
+ assert!(should_clear_agent_for_foreground_shell(
+ Some(Agent::Claude),
+ None,
+ true
+ ));
+ }
+
+ #[test]
+ fn unknown_non_shell_foreground_job_is_not_immediate_clear_signal() {
+ assert!(!should_clear_agent_for_foreground_shell(
+ Some(Agent::Claude),
+ None,
+ false
+ ));
+ }
+
+ #[test]
+ fn foreground_agent_job_is_not_clear_signal() {
+ assert!(!should_clear_agent_for_foreground_shell(
+ Some(Agent::Claude),
+ Some(Agent::OpenCode),
+ true
+ ));
+ }
+
+ #[test]
fn transient_process_miss_keeps_current_agent_detected() {
let mut presence = AgentDetectionPresence::from_agent(Some(Agent::Pi));
diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs
index e90cdaf0..7addbc5d 100644
--- a/src/platform/fallback.rs
+++ b/src/platform/fallback.rs
@@ -8,6 +8,11 @@ pub fn foreground_job(_child_pid: u32) -> Option<ForegroundJob> {
}
/// Unsupported platform stub.
+pub fn foreground_process_group_id(_child_pid: u32) -> Option<u32> {
+ None
+}
+
+/// Unsupported platform stub.
pub fn process_cwd(_pid: u32) -> Option<PathBuf> {
None
}
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
index dc944cd9..13aa6ec8 100644
--- a/src/platform/linux.rs
+++ b/src/platform/linux.rs
@@ -8,7 +8,7 @@ use super::{ClipboardCommand, ForegroundJob, ForegroundProcess, Signal};
/// Collect the foreground terminal job for a given child PID.
pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
- let tpgid = foreground_pgid(child_pid)? as u32;
+ let tpgid = foreground_process_group_id(child_pid)?;
let mut processes = Vec::new();
for entry in std::fs::read_dir("/proc").ok()? {
@@ -49,7 +49,7 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
})
}
-fn foreground_pgid(child_pid: u32) -> Option<i32> {
+pub fn foreground_process_group_id(child_pid: u32) -> Option<u32> {
// /proc/<pid>/stat format: "pid (comm) state ppid pgrp session tty_nr tpgid ..."
// The (comm) field can contain spaces and parens, so we find the last ')' first.
let stat = std::fs::read_to_string(format!("/proc/{child_pid}/stat")).ok()?;
@@ -57,7 +57,7 @@ fn foreground_pgid(child_pid: u32) -> Option<i32> {
let fields: Vec<&str> = rest.split_whitespace().collect();
// After (comm): state(0) ppid(1) pgrp(2) session(3) tty_nr(4) tpgid(5)
let tpgid: i32 = fields.get(5)?.parse().ok()?;
- (tpgid > 0).then_some(tpgid)
+ (tpgid > 0).then_some(tpgid as u32)
}
fn process_pgrp_and_comm(pid: u32) -> Option<(i32, String)> {
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index f5de67ce..43c24386 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -12,7 +12,7 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
return None;
}
- let fg_pgid = foreground_pgid(child_pid)?;
+ let fg_pgid = foreground_process_group_id(child_pid)?;
let mut pids = vec![0i32; 4096];
let bytes = unsafe {
libc::proc_listallpids(
@@ -62,7 +62,7 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
/// Read `e_tpgid` (foreground process group of the controlling terminal)
/// for the given PID.
-fn foreground_pgid(pid: u32) -> Option<u32> {
+pub fn foreground_process_group_id(pid: u32) -> Option<u32> {
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;