From 8b1d77acf6059e6688fb6a875c3176c95602b6c4 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 30 Jun 2026 00:19:36 +0300 Subject: fix: keep powershell panes alive after agent ctrl-c refs #860 --- src/terminal/state.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) (limited to 'src/terminal') diff --git a/src/terminal/state.rs b/src/terminal/state.rs index e7923719..8fea0542 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::path::PathBuf; -#[cfg(test)] +#[cfg(any(windows, test))] use std::time::Duration; use std::time::Instant; @@ -91,6 +91,7 @@ pub struct TerminalState { pub revision: u64, pub launch_argv: Option>, pub respawn_shell_on_exit: bool, + recent_agent_process_exit_at: Option, pub pending_agent_resume_plan: Option, } @@ -117,6 +118,7 @@ impl TerminalState { revision: 0, launch_argv: None, respawn_shell_on_exit: false, + recent_agent_process_exit_at: None, pending_agent_resume_plan: None, } } @@ -131,6 +133,12 @@ impl TerminalState { self } + #[cfg(any(windows, test))] + pub(crate) fn agent_process_exited_within(&self, now: Instant, max_age: Duration) -> bool { + self.recent_agent_process_exit_at + .is_some_and(|exited_at| now.saturating_duration_since(exited_at) <= max_age) + } + pub fn with_pending_agent_resume_plan( mut self, plan: crate::agent_resume::AgentResumePlan, @@ -246,6 +254,11 @@ impl TerminalState { self.fallback_state = fallback_state; self.fallback_visible_blocker = visible_blocker && fallback_state == AgentState::Blocked; self.fallback_observed_at = Some(now); + if process_exited && agent.is_some() { + self.recent_agent_process_exit_at = Some(now); + } else if agent.is_some() { + self.recent_agent_process_exit_at = None; + } if process_exited && self.hook_authority_not_newer_than(now) && self.hook_authority.as_ref().is_some_and(|authority| { @@ -1118,6 +1131,7 @@ impl TerminalState { self.last_agent_state_change_seq = None; self.launch_argv = None; self.respawn_shell_on_exit = false; + self.recent_agent_process_exit_at = None; self.pending_agent_resume_plan = None; self.clear_agent_name(); } @@ -3656,6 +3670,39 @@ mod tests { assert!(!terminal.respawn_shell_on_exit); } + #[test] + fn agent_process_exit_tracks_recent_respawn_window() { + let mut terminal = test_terminal(); + let now = std::time::Instant::now(); + + terminal.set_detected_state_with_screen_signals_at( + Some(Agent::OpenCode), + AgentState::Idle, + false, + false, + false, + true, + now, + ); + + assert!(terminal.agent_process_exited_within(now, Duration::from_secs(2))); + assert!(!terminal + .agent_process_exited_within(now + Duration::from_secs(3), Duration::from_secs(2))); + + terminal.set_detected_state_with_screen_signals_at( + Some(Agent::OpenCode), + AgentState::Working, + false, + false, + true, + false, + now + Duration::from_secs(4), + ); + + assert!(!terminal + .agent_process_exited_within(now + Duration::from_secs(4), Duration::from_secs(2))); + } + #[test] fn detected_conflict_clears_live_hook_but_preserves_session_ref() { let mut terminal = test_terminal(); -- cgit v1.2.3