diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-06 20:17:52 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-06 20:17:52 +0300 |
| commit | 94b90043f98499a1ed3594dd9ea42b2aef62b93a (patch) | |
| tree | 2f19157650b5010d91d815ee4afd4d54d2519fe2 /src | |
| parent | f7b0ee2f758440db3a3ff4cc727f879b420255d7 (diff) | |
fix: clear native agent detection after shell returns
fixes #66
Diffstat (limited to 'src')
| -rw-r--r-- | src/detect.rs | 6 | ||||
| -rw-r--r-- | src/pane.rs | 89 | ||||
| -rw-r--r-- | src/platform/fallback.rs | 5 | ||||
| -rw-r--r-- | src/platform/linux.rs | 6 | ||||
| -rw-r--r-- | src/platform/macos.rs | 4 |
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; |
