summaryrefslogtreecommitdiff
path: root/src/app/mod.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-07 16:14:46 +0300
committerGitHub <noreply@github.com>2026-09-07 15:14:46 +0200
commite7d8220788b4fa7c266a1e4457591b694478771c (patch)
tree272303d74e0b58c11f9c0c434dae8335d3d06703 /src/app/mod.rs
parent482818130407691154d1635d4029b95d0e730104 (diff)
fix: preserve sessions after interrupted pane exits (#3418)
* fix: preserve sessions when pane shells are signaled refs #3415 * fix: preserve shutdown checkpoints across platforms refs #3415 --------- Co-authored-by: Jonathan Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/app/mod.rs')
-rw-r--r--src/app/mod.rs124
1 files changed, 123 insertions, 1 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs
index a0b90023..b327b5bb 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -138,6 +138,7 @@ pub struct App {
pub(crate) pending_agent_resume_deadline: Option<Instant>,
pub(crate) session_save_deadline: Option<Instant>,
pub(crate) session_save_thread: Option<std::thread::JoinHandle<()>>,
+ pane_exit_checkpoint_pending: bool,
pub(crate) detached_process_children: Vec<std::process::Child>,
tab_bar_status_generation: u64,
tab_bar_datetimes: Vec<tab_bar_status::TabBarDatetimeRuntime>,
@@ -598,6 +599,7 @@ impl App {
pending_agent_resume_deadline: None,
session_save_deadline: None,
session_save_thread: None,
+ pane_exit_checkpoint_pending: false,
detached_process_children: Vec::new(),
tab_bar_status_generation: 0,
tab_bar_datetimes: Vec::new(),
@@ -695,9 +697,17 @@ impl App {
}
let cwd = self.resolve_new_terminal_cwd(None);
+ let preserve_checkpoint = self.pane_exit_checkpoint_pending && !self.state.session_dirty;
match self.create_workspace_with_options(cwd, true) {
- Ok(_) => true,
+ Ok(_) => {
+ if preserve_checkpoint {
+ // Automatic replacement is part of pane removal, not a new user mutation.
+ self.pane_exit_checkpoint_pending = true;
+ self.finish_checkpointed_pane_exit();
+ }
+ true
+ }
Err(err) => {
tracing::error!(err = %err, "failed to create default workspace");
self.state.mode = Mode::Navigate;
@@ -3075,6 +3085,118 @@ mod tests {
done_rx.try_recv().unwrap();
assert!(app.session_save_thread.is_none());
}
+
+ #[tokio::test]
+ async fn pane_exit_checkpoint_survives_automatic_workspace_creation_on_shutdown() {
+ let _guard = crate::config::test_config_env_lock().lock().unwrap();
+ let config_home = unique_temp_path("signaled-pane-session-checkpoint");
+ std::env::set_var("XDG_CONFIG_HOME", &config_home);
+ std::env::remove_var(crate::session::SESSION_ENV_VAR);
+
+ let mut app = test_app();
+ app.policy.persist_session = true;
+ let mut workspace = Workspace::test_new("preserved");
+ let first_pane = workspace.tabs[0].root_pane;
+ let second_pane = workspace.test_split(ratatui::layout::Direction::Horizontal);
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.ensure_test_terminals();
+
+ app.handle_internal_event(AppEvent::PaneDied {
+ pane_id: first_pane,
+ exit_reason: crate::platform::ChildExitReason::Interrupted,
+ });
+ app.handle_internal_event(AppEvent::PaneDied {
+ pane_id: second_pane,
+ exit_reason: crate::platform::ChildExitReason::Interrupted,
+ });
+ assert!(app.state.workspaces.is_empty());
+ assert!(app.ensure_default_workspace());
+
+ app.save_session_on_shutdown();
+
+ let snapshot = crate::persist::load().expect("checkpointed session should survive");
+ assert_eq!(snapshot.workspaces.len(), 1);
+ assert_eq!(snapshot.workspaces[0].tabs[0].panes.len(), 2);
+
+ std::env::remove_var("XDG_CONFIG_HOME");
+ let _ = std::fs::remove_dir_all(config_home);
+ }
+
+ #[test]
+ fn normal_autosave_replaces_a_signaled_exit_checkpoint() {
+ let _guard = crate::config::test_config_env_lock().lock().unwrap();
+ let config_home = unique_temp_path("signaled-pane-autosave");
+ std::env::set_var("XDG_CONFIG_HOME", &config_home);
+ std::env::remove_var(crate::session::SESSION_ENV_VAR);
+
+ let mut app = test_app();
+ app.policy.persist_session = true;
+ let workspace = Workspace::test_new("closed");
+ let pane_id = workspace.tabs[0].root_pane;
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.ensure_test_terminals();
+
+ app.handle_internal_event(AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Interrupted,
+ });
+ assert!(crate::persist::load().is_some());
+
+ app.start_background_session_save();
+ if let Some(thread) = app.session_save_thread.take() {
+ thread.join().unwrap();
+ }
+ app.save_session_on_shutdown();
+
+ assert!(crate::persist::load().is_none());
+
+ std::env::remove_var("XDG_CONFIG_HOME");
+ let _ = std::fs::remove_dir_all(config_home);
+ }
+
+ #[test]
+ fn durable_mutation_after_pane_exit_checkpoint_wins_on_shutdown() {
+ let _guard = crate::config::test_config_env_lock().lock().unwrap();
+ let config_home = unique_temp_path("pane-exit-newer-session-state");
+ std::env::set_var("XDG_CONFIG_HOME", &config_home);
+ std::env::remove_var(crate::session::SESSION_ENV_VAR);
+
+ for another_interrupted_exit in [false, true] {
+ let mut app = test_app();
+ app.policy.persist_session = true;
+ let workspace = Workspace::test_new("old");
+ let pane_id = workspace.tabs[0].root_pane;
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.ensure_test_terminals();
+
+ app.handle_internal_event(AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Interrupted,
+ });
+ app.state.workspaces = vec![Workspace::test_new("newer")];
+ app.state.active = Some(0);
+ app.state.ensure_test_terminals();
+ app.state.mark_session_dirty();
+ if another_interrupted_exit {
+ app.handle_internal_event(AppEvent::PaneDied {
+ pane_id: app.state.workspaces[0].tabs[0].root_pane,
+ exit_reason: crate::platform::ChildExitReason::Interrupted,
+ });
+ }
+ app.save_session_on_shutdown();
+
+ let snapshot = crate::persist::load().expect("newer session should be saved");
+ assert_eq!(snapshot.workspaces.len(), 1);
+ assert_eq!(snapshot.workspaces[0].custom_name.as_deref(), Some("newer"));
+ }
+
+ std::env::remove_var("XDG_CONFIG_HOME");
+ let _ = std::fs::remove_dir_all(config_home);
+ }
+
#[tokio::test]
async fn full_internal_event_queue_eventually_applies_working_to_idle_transition() {
let mut app = test_app();