diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-09 16:53:00 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-09 17:28:02 +0300 |
| commit | 1cbd3129363d2ad867d2753ef176a944554591f6 (patch) | |
| tree | 1febffab2d6b6033625127fb8b20d285ce35350c | |
| parent | 8ae7ccc5b28b4c0eb46a659841d31b488bd3f7bc (diff) | |
fix: detect agent state from terminal tail instead of viewport
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | src/pane.rs | 43 |
2 files changed, 43 insertions, 2 deletions
@@ -294,7 +294,7 @@ herdr does not require hooks or agent-side configuration for its built-in detect 1. identifying the foreground process of each pane's pty via `/proc` on linux or `proc_pidinfo` on macos 2. matching the process name against known agents -3. reading terminal screen content and applying per-agent heuristics to determine state +3. reading the live bottom of the terminal buffer and applying per-agent heuristics to determine state this means detection works with any supported agent, installed any way, with zero setup. if it runs in a terminal, herdr can see it. diff --git a/src/pane.rs b/src/pane.rs index 9552e1c6..a578eb33 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -19,6 +19,7 @@ use crate::layout::PaneId; const CLAUDE_WORKING_HOLD: std::time::Duration = std::time::Duration::from_millis(1200); const RELEASE_REACQUIRE_SUPPRESSION: std::time::Duration = std::time::Duration::from_secs(1); +const DEFAULT_DETECTION_ROWS: usize = 24; #[derive(Debug, Clone, Copy)] struct PendingAgentRelease { @@ -342,6 +343,10 @@ impl PaneTerminal { self.ghostty.visible_text() } + fn detection_text(&self) -> String { + self.ghostty.detection_text() + } + fn recent_text(&self, lines: usize) -> String { self.ghostty.recent_text(lines) } @@ -646,6 +651,14 @@ impl GhosttyPaneTerminal { .unwrap_or_default() } + fn detection_text(&self) -> String { + self.core + .lock() + .ok() + .and_then(|core| ghostty_detection_text(&core).ok()) + .unwrap_or_default() + } + fn recent_text(&self, lines: usize) -> String { self.core .lock() @@ -1054,6 +1067,16 @@ fn ghostty_visible_text(core: &mut GhosttyPaneCore) -> Result<String, crate::gho Ok(lines_to_text(lines)) } +fn ghostty_detection_text(core: &GhosttyPaneCore) -> Result<String, crate::ghostty::Error> { + let lines = core + .terminal + .rows() + .ok() + .map(|rows| usize::from(rows).max(1)) + .unwrap_or(DEFAULT_DETECTION_ROWS); + ghostty_recent_text(core, lines) +} + fn ghostty_recent_text( core: &GhosttyPaneCore, lines: usize, @@ -1572,7 +1595,7 @@ impl PaneRuntime { } } - let content = terminal.visible_text(); + let content = terminal.detection_text(); let raw_state = detect::detect_state(agent, &content); let new_state = stabilize_agent_state( agent, @@ -2126,6 +2149,24 @@ mod tests { } #[test] + fn detection_text_stays_at_bottom_when_viewport_is_scrolled() { + let (tx, _rx) = mpsc::channel(4); + let mut terminal = crate::ghostty::Terminal::new(80, 3, 100).unwrap(); + write_numbered_lines(&mut terminal, 10); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + + let bottom_snapshot = pane.detection_text(); + assert_eq!(bottom_snapshot, pane.recent_text(3)); + assert!(bottom_snapshot.contains("000009")); + + let before = pane.scroll_metrics().expect("scroll metrics before scroll"); + pane.set_scroll_offset_from_bottom(before.max_offset_from_bottom); + + assert!(pane.visible_text().contains("000000")); + assert_eq!(pane.detection_text(), bottom_snapshot); + } + + #[test] fn recent_unwrapped_text_ignores_soft_wraps() { let (tx, _rx) = mpsc::channel(4); let mut terminal = crate::ghostty::Terminal::new(5, 3, 100).unwrap(); |
