diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 00:19:36 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 00:19:36 +0300 |
| commit | 8b1d77acf6059e6688fb6a875c3176c95602b6c4 (patch) | |
| tree | cb9bf5e138a4cfe098726cdaf3b06e0ef458288f /src/app/api.rs | |
| parent | 1db9695eecece3ddd2bcf43ac33c961daffd6004 (diff) | |
fix: keep powershell panes alive after agent ctrl-c
refs #860
Diffstat (limited to 'src/app/api.rs')
| -rw-r--r-- | src/app/api.rs | 89 |
1 files changed, 88 insertions, 1 deletions
diff --git a/src/app/api.rs b/src/app/api.rs index b823a739..fa6d1f6d 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -17,6 +17,8 @@ use super::{api_helpers::pane_agent_status, App, Mode, OverlayPaneState, ToastKi use crate::events::AppEvent; const API_NOTIFICATION_RATE_LIMIT: Duration = Duration::from_secs(1); +#[cfg(windows)] +const WINDOWS_POWERSHELL_AGENT_EXIT_RESPAWN_GRACE: Duration = Duration::from_secs(2); #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RuntimeExitAction { @@ -235,6 +237,8 @@ impl App { self.sync_full_lifecycle_authority_detection_pauses(); if terminal_cwd_reported { self.mark_git_status_refresh_due(Instant::now()); + self.render_dirty.store(true, Ordering::Release); + self.render_notify.notify_one(); } for update in &pane_updates { self.refresh_new_herdr_toast_context_for_update(update, &previous_toast); @@ -425,13 +429,38 @@ impl App { return RuntimeExitAction::ClosePane; }; - if terminal.respawn_shell_on_exit { + if terminal.respawn_shell_on_exit || self.should_respawn_shell_after_agent_exit(terminal) { RuntimeExitAction::RespawnShell } else { RuntimeExitAction::ClosePane } } + fn should_respawn_shell_after_agent_exit( + &self, + terminal: &crate::terminal::TerminalState, + ) -> bool { + #[cfg(not(windows))] + { + let _ = terminal; + false + } + + #[cfg(windows)] + { + if !terminal.agent_process_exited_within( + Instant::now(), + WINDOWS_POWERSHELL_AGENT_EXIT_RESPAWN_GRACE, + ) { + return false; + } + + crate::pane::uses_windows_powershell_prompt_cwd_reporting( + crate::pane::PaneShellConfig::new(&self.state.default_shell, self.state.shell_mode), + ) + } + } + fn respawn_shell_for_launch_pane(&mut self, pane_id: crate::layout::PaneId) -> bool { let Some((ws_idx, pane_state)) = self.find_pane(pane_id) else { return false; @@ -1763,6 +1792,64 @@ mod tests { } } + #[cfg(windows)] + #[test] + fn windows_powershell_exit_after_agent_process_exit_respawns_shell() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &crate::config::Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + let workspace = crate::workspace::Workspace::test_new("powershell"); + let pane_id = workspace.tabs[0].root_pane; + app.state.workspaces = vec![workspace]; + app.state.ensure_test_terminals(); + app.state.default_shell = "powershell.exe".into(); + app.state.shell_mode = crate::config::ShellModeConfig::NonLogin; + + app.handle_internal_event(AppEvent::StateChanged { + pane_id, + agent: Some(crate::detect::Agent::OpenCode), + state: AgentState::Idle, + visible_blocker: false, + visible_working: false, + process_exited: true, + observed_at: std::time::Instant::now(), + }); + + assert_eq!( + app.runtime_exit_action(pane_id), + RuntimeExitAction::RespawnShell + ); + } + + #[cfg(windows)] + #[test] + fn windows_powershell_exit_without_recent_agent_process_exit_closes_pane() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &crate::config::Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + let workspace = crate::workspace::Workspace::test_new("powershell"); + let pane_id = workspace.tabs[0].root_pane; + app.state.workspaces = vec![workspace]; + app.state.ensure_test_terminals(); + app.state.default_shell = "powershell.exe".into(); + app.state.shell_mode = crate::config::ShellModeConfig::NonLogin; + + assert_eq!( + app.runtime_exit_action(pane_id), + RuntimeExitAction::ClosePane + ); + } + #[test] fn terminal_delivery_does_not_refresh_existing_targeted_toast() { let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); |
