aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs3
-rw-r--r--src/app/api.rs41
-rw-r--r--src/app/api/plugins/mod.rs6
-rw-r--r--src/app/api/worktrees.rs6
-rw-r--r--src/app/mod.rs124
-rw-r--r--src/app/session.rs29
-rw-r--r--src/events.rs5
-rw-r--r--src/pane.rs23
-rw-r--r--src/platform/mod.rs49
-rw-r--r--src/platform/unix_common.rs8
-rw-r--r--src/platform/windows.rs9
-rw-r--r--src/server/headless.rs2
-rw-r--r--src/server/headless/notifications.rs2
-rw-r--r--src/server/headless/tests/mod.rs21
14 files changed, 305 insertions, 23 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 69b459d2..ca6f6704 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -1575,7 +1575,7 @@ impl AppState {
pub fn handle_app_event(&mut self, event: AppEvent) -> Vec<PaneStateUpdate> {
match event {
- AppEvent::PaneDied { pane_id } => {
+ AppEvent::PaneDied { pane_id, .. } => {
self.handle_pane_died(pane_id);
Vec::new()
}
@@ -3298,6 +3298,7 @@ mod tests {
let deadline = state.next_pending_agent_notification_deadline().unwrap();
state.handle_app_event(AppEvent::PaneDied {
pane_id: bg_pane_id,
+ exit_reason: crate::platform::ChildExitReason::Exited,
});
assert!(state.pending_agent_notifications.is_empty());
diff --git a/src/app/api.rs b/src/app/api.rs
index c504b4c0..8470818e 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -93,7 +93,10 @@ impl App {
return Vec::new();
}
worktree_restore_failed = true;
- AppEvent::PaneDied { pane_id }
+ AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Exited,
+ }
}
ev => ev,
};
@@ -164,7 +167,7 @@ impl App {
}
let mut worktree_restore_updates = Vec::new();
- if let AppEvent::PaneDied { pane_id } = &ev {
+ if let AppEvent::PaneDied { pane_id, .. } = &ev {
if self
.state
.popup_pane
@@ -224,7 +227,18 @@ impl App {
}
}
- let overlay_state = if let AppEvent::PaneDied { pane_id } = &ev {
+ let checkpointed_pane_exit = matches!(
+ &ev,
+ AppEvent::PaneDied {
+ pane_id,
+ exit_reason,
+ } if exit_reason.requires_session_checkpoint() && self.find_pane(*pane_id).is_some() && !self.overlay_panes.contains_key(pane_id)
+ );
+ if checkpointed_pane_exit {
+ self.checkpoint_session_before_pane_exit();
+ }
+
+ let overlay_state = if let AppEvent::PaneDied { pane_id, .. } = &ev {
self.overlay_panes.remove(pane_id).map(|overlay| {
let was_overlay_active =
self.state
@@ -248,7 +262,7 @@ impl App {
None
};
- if let AppEvent::PaneDied { pane_id } = &ev {
+ if let AppEvent::PaneDied { pane_id, .. } = &ev {
if let Some((ws_idx, _)) = self.find_pane(*pane_id) {
if let Some(public_pane_id) = self.public_pane_id(ws_idx, *pane_id) {
self.emit_event(crate::api::schema::EventEnvelope {
@@ -261,7 +275,7 @@ impl App {
}
}
}
- let pane_exit_layout_target = if let AppEvent::PaneDied { pane_id } = &ev {
+ let pane_exit_layout_target = if let AppEvent::PaneDied { pane_id, .. } = &ev {
self.find_pane(*pane_id).and_then(|(ws_idx, _)| {
self.layout_update_target_after_pane_removal(ws_idx, *pane_id)
})
@@ -301,6 +315,9 @@ impl App {
if update_ready.is_some() {
self.state.latest_release_notes = crate::release_notes::load_latest();
}
+ if checkpointed_pane_exit {
+ self.finish_checkpointed_pane_exit();
+ }
if let Some(agents) = manifest_update_agents {
self.reset_agent_detection_for_agents(&agents);
}
@@ -1968,6 +1985,7 @@ mod tests {
app.handle_internal_event(AppEvent::PaneDied {
pane_id: overlay_pane,
+ exit_reason: crate::platform::ChildExitReason::Exited,
});
let overlay_tab = &app.state.workspaces[0].tabs[0];
@@ -1994,7 +2012,10 @@ mod tests {
app.state.ensure_test_terminals();
let tab_id = app.public_tab_id(0, 0).unwrap();
- app.handle_internal_event(AppEvent::PaneDied { pane_id: dead_pane });
+ app.handle_internal_event(AppEvent::PaneDied {
+ pane_id: dead_pane,
+ exit_reason: crate::platform::ChildExitReason::Exited,
+ });
let events = event_hub.events_after(0);
let pane_exited = events
@@ -2143,6 +2164,7 @@ mod tests {
app.handle_internal_event(AppEvent::PaneDied {
pane_id: overlay_pane,
+ exit_reason: crate::platform::ChildExitReason::Exited,
});
let events = event_hub.events_after(0);
@@ -2168,6 +2190,7 @@ mod tests {
app.handle_internal_event(AppEvent::PaneDied {
pane_id: overlay_pane,
+ exit_reason: crate::platform::ChildExitReason::Exited,
});
let tab = &app.state.workspaces[0].tabs[0];
@@ -2187,6 +2210,7 @@ mod tests {
app.handle_internal_event(AppEvent::PaneDied {
pane_id: overlay_pane,
+ exit_reason: crate::platform::ChildExitReason::Exited,
});
let tab = &app.state.workspaces[0].tabs[0];
@@ -2226,7 +2250,10 @@ mod tests {
.expect("test session id should be valid"),
});
- app.handle_internal_event(AppEvent::PaneDied { pane_id });
+ app.handle_internal_event(AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Exited,
+ });
assert!(
app.find_pane(pane_id).is_some(),
diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs
index f4b6329b..8ad083d4 100644
--- a/src/app/api/plugins/mod.rs
+++ b/src/app/api/plugins/mod.rs
@@ -2210,6 +2210,7 @@ command = ["sh", "-c", "printf %s ${{HERDR_PANE_ID-unset}} > '{}'; sleep 1"]
app.handle_internal_event(crate::events::AppEvent::PaneDied {
pane_id: opened_pane_id,
+ exit_reason: crate::platform::ChildExitReason::Exited,
});
assert!(app.state.popup_pane.is_none());
assert!(event_hub.events_after(0).is_empty());
@@ -3448,7 +3449,10 @@ command = ["sh", "-c", "echo ok"]
},
);
- app.handle_internal_event(crate::events::AppEvent::PaneDied { pane_id });
+ app.handle_internal_event(crate::events::AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Exited,
+ });
assert!(!app.state.plugin_panes.contains_key(&pane_id));
}
diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs
index 3c8407b9..71f2e03a 100644
--- a/src/app/api/worktrees.rs
+++ b/src/app/api/worktrees.rs
@@ -2319,8 +2319,10 @@ mod tests {
.pending_worktree_remove_runtime_restores
.contains_key(&pane_id));
- let pane_updates =
- app.handle_internal_event_with_pane_updates(AppEvent::PaneDied { pane_id });
+ let pane_updates = app.handle_internal_event_with_pane_updates(AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Exited,
+ });
assert!(matches!(
pane_updates.as_slice(),
[update] if update.agent_released && update.suppress_completion
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();
diff --git a/src/app/session.rs b/src/app/session.rs
index 07bc80a9..49dca914 100644
--- a/src/app/session.rs
+++ b/src/app/session.rs
@@ -13,6 +13,7 @@ enum SessionSaveJob {
impl App {
pub(super) fn schedule_session_save(&mut self) {
if self.policy.persist_session {
+ self.pane_exit_checkpoint_pending = false;
self.session_save_deadline = Some(Instant::now() + SESSION_SAVE_DEBOUNCE);
}
}
@@ -67,6 +68,7 @@ impl App {
}
let job = self.capture_session_save_job();
+ self.pane_exit_checkpoint_pending = false;
self.session_save_deadline = None;
match std::thread::Builder::new()
.name("herdr-session-save".into())
@@ -91,8 +93,35 @@ impl App {
}
run_session_save_job(self.capture_session_save_job());
+ self.pane_exit_checkpoint_pending = false;
self.session_save_deadline = None;
}
+
+ pub(crate) fn checkpoint_session_before_pane_exit(&mut self) {
+ if !self.policy.persist_session
+ || (self.pane_exit_checkpoint_pending && !self.state.session_dirty)
+ {
+ return;
+ }
+ self.save_session_now();
+ self.pane_exit_checkpoint_pending = true;
+ self.state.session_dirty = false;
+ }
+
+ pub(crate) fn finish_checkpointed_pane_exit(&mut self) {
+ if self.pane_exit_checkpoint_pending {
+ self.state.session_dirty = false;
+ self.session_save_deadline = Some(Instant::now() + SESSION_SAVE_DEBOUNCE);
+ }
+ }
+
+ pub(crate) fn save_session_on_shutdown(&mut self) {
+ if self.pane_exit_checkpoint_pending && !self.state.session_dirty {
+ self.session_save_deadline = None;
+ return;
+ }
+ self.save_session_now();
+ }
}
fn run_session_save_job(job: SessionSaveJob) {
diff --git a/src/events.rs b/src/events.rs
index 2901e493..95638991 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -56,7 +56,10 @@ pub struct WorktreeRemoveResult {
#[derive(Debug)]
pub enum AppEvent {
/// A pane's child process exited.
- PaneDied { pane_id: PaneId },
+ PaneDied {
+ pane_id: PaneId,
+ exit_reason: crate::platform::ChildExitReason,
+ },
/// A worktree-removal runtime could not be restored normally.
WorktreeRuntimeRestoreFailed { pane_id: PaneId, operation_id: u64 },
/// Process detection identified an agent before its screen state was confirmed.
diff --git a/src/pane.rs b/src/pane.rs
index 83bad900..aa6acb57 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -2194,7 +2194,12 @@ impl PaneRuntime {
});
let exit_events = events.clone();
let on_reader_exit = Box::new(move || {
- let _ = rt.block_on(exit_events.send(AppEvent::PaneDied { pane_id }));
+ // Imported handoff panes have no child wait handle, so their exit cause is
+ // unknowable. Checkpoint conservatively; normal autosave settles clean exits.
+ let _ = rt.block_on(exit_events.send(AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Handoff,
+ }));
debug!(pane = pane_id.raw(), "handoff PTY actor exiting");
});
PaneRuntimeIo::Actor(PtyIoActor::spawn(PtyIoActorConfig {
@@ -2299,16 +2304,24 @@ impl PaneRuntime {
crate::logging::pane_spawned(pane_id.raw(), pid);
}
tokio::task::spawn_blocking(move || {
- match child.wait() {
+ let exit_reason = match child.wait() {
Ok(status) => {
+ let exit_reason = crate::platform::classify_child_exit(&status);
let status_text = format!("{status:?}");
crate::logging::pane_exited(pane_id.raw(), &status_text);
+ exit_reason
}
- Err(e) => crate::logging::pane_exit_failed(pane_id.raw(), &e.to_string()),
- }
+ Err(e) => {
+ crate::logging::pane_exit_failed(pane_id.raw(), &e.to_string());
+ crate::platform::ChildExitReason::WaitFailed
+ }
+ };
child_wait_completed.store(true, Ordering::Release);
// Use blocking send — PaneDied is critical, must not be dropped
- if let Err(e) = rt.block_on(events.send(AppEvent::PaneDied { pane_id })) {
+ if let Err(e) = rt.block_on(events.send(AppEvent::PaneDied {
+ pane_id,
+ exit_reason,
+ })) {
error!(pane = pane_id.raw(), err = %e, "failed to send PaneDied event");
}
});
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 86a431aa..d48ad4fa 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -25,6 +25,36 @@ pub enum Signal {
Kill,
}
+/// Why a pane runtime ended, before application persistence policy is applied.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ChildExitReason {
+ Exited,
+ Interrupted,
+ /// Imported runtimes have no child wait handle in the replacement server.
+ #[cfg(unix)]
+ Handoff,
+ WaitFailed,
+}
+
+impl ChildExitReason {
+ pub(crate) fn requires_session_checkpoint(self) -> bool {
+ match self {
+ Self::Interrupted => true,
+ #[cfg(unix)]
+ Self::Handoff => true,
+ _ => false,
+ }
+ }
+}
+
+#[cfg(unix)]
+pub(crate) use unix_common::classify_child_exit;
+
+#[cfg(not(any(unix, windows)))]
+pub(crate) fn classify_child_exit(_status: &portable_pty::ExitStatus) -> ChildExitReason {
+ ChildExitReason::Exited
+}
+
pub(crate) fn detached_custom_command_process(command: &str) -> std::process::Command {
let mut process = detached_custom_command_process_platform(command);
configure_background_command(&mut process);
@@ -415,6 +445,25 @@ impl PrefixInputSource for RealPrefixInputSource {
}
}
+#[cfg(all(test, any(unix, windows)))]
+#[test]
+fn child_exit_classification_only_checkpoints_interruptions() {
+ for code in [0, 1, 130, 255, 0xC0000005] {
+ let reason = classify_child_exit(&portable_pty::ExitStatus::with_exit_code(code));
+ assert_eq!(reason, ChildExitReason::Exited, "exit code {code:#x}");
+ assert!(!reason.requires_session_checkpoint());
+ }
+ #[cfg(windows)]
+ let status = portable_pty::ExitStatus::with_exit_code(0xC000013A);
+ #[cfg(not(windows))]
+ let status = portable_pty::ExitStatus::with_signal("Terminated: 15");
+ assert_eq!(classify_child_exit(&status), ChildExitReason::Interrupted);
+ assert!(classify_child_exit(&status).requires_session_checkpoint());
+ #[cfg(unix)]
+ assert!(ChildExitReason::Handoff.requires_session_checkpoint());
+ assert!(!ChildExitReason::WaitFailed.requires_session_checkpoint());
+}
+
#[cfg(all(test, unix))]
mod tests {
use super::*;
diff --git a/src/platform/unix_common.rs b/src/platform/unix_common.rs
index dff67357..104f6b69 100644
--- a/src/platform/unix_common.rs
+++ b/src/platform/unix_common.rs
@@ -1,5 +1,13 @@
use std::path::{Path, PathBuf};
+pub(crate) fn classify_child_exit(status: &portable_pty::ExitStatus) -> super::ChildExitReason {
+ if status.signal().is_some() {
+ super::ChildExitReason::Interrupted
+ } else {
+ super::ChildExitReason::Exited
+ }
+}
+
pub(crate) fn wait_client_stream_readable(stream: &crate::ipc::LocalStream) -> std::io::Result<()> {
use std::os::fd::{AsFd as _, AsRawFd as _};
let crate::ipc::LocalStream::UdSocket(stream) = stream;
diff --git a/src/platform/windows.rs b/src/platform/windows.rs
index ce0afedb..4deb1864 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -15,6 +15,15 @@ use std::{
mod clipboard_image;
+pub(crate) fn classify_child_exit(status: &portable_pty::ExitStatus) -> super::ChildExitReason {
+ // STATUS_CONTROL_C_EXIT is reported without a Unix signal by portable-pty.
+ if status.exit_code() == 0xC000013A {
+ super::ChildExitReason::Interrupted
+ } else {
+ super::ChildExitReason::Exited
+ }
+}
+
pub(crate) fn wait_client_stream_readable(
_stream: &crate::ipc::LocalStream,
) -> std::io::Result<()> {
diff --git a/src/server/headless.rs b/src/server/headless.rs
index abaca0aa..2d652378 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -721,7 +721,7 @@ impl HeadlessServer {
// Save session on exit.
if self.app.policy.persist_session {
- self.app.save_session_now();
+ self.app.save_session_on_shutdown();
}
info!("headless server exiting");
diff --git a/src/server/headless/notifications.rs b/src/server/headless/notifications.rs
index dd661809..ddf9a9b3 100644
--- a/src/server/headless/notifications.rs
+++ b/src/server/headless/notifications.rs
@@ -663,7 +663,7 @@ impl HeadlessServer {
}
true
}
- AppEvent::PaneDied { pane_id }
+ AppEvent::PaneDied { pane_id, .. }
| AppEvent::WorktreeRuntimeRestoreFailed { pane_id, .. } => {
let focus_before = self.shell_focus_targets();
let focused_tabs_before = self.focused_shell_tabs();
diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs
index 5e0e5d01..63f7264d 100644
--- a/src/server/headless/tests/mod.rs
+++ b/src/server/headless/tests/mod.rs
@@ -3614,7 +3614,12 @@ async fn pane_death_reconciles_each_client_view_and_focus() {
server.clients.get_mut(&71).unwrap().outer_terminal_focus = Some(true);
server.clients.get_mut(&72).unwrap().outer_terminal_focus = Some(false);
- assert!(server.handle_internal_event_with_forwarding(AppEvent::PaneDied { pane_id: dead_pane }));
+ assert!(
+ server.handle_internal_event_with_forwarding(AppEvent::PaneDied {
+ pane_id: dead_pane,
+ exit_reason: crate::platform::ChildExitReason::Exited
+ })
+ );
assert_eq!(
server.shell_tab_id_for_client(71).as_deref(),
@@ -3678,7 +3683,12 @@ async fn pane_death_reapplies_controller_geometry() {
let shrunk = server.app.state.workspaces[0].test_runtimes[&first_pane].current_size();
assert!(shrunk.0 < 46);
- assert!(server.handle_internal_event_with_forwarding(AppEvent::PaneDied { pane_id: dead_pane }));
+ assert!(
+ server.handle_internal_event_with_forwarding(AppEvent::PaneDied {
+ pane_id: dead_pane,
+ exit_reason: crate::platform::ChildExitReason::Exited
+ })
+ );
let runtime = &server.app.state.workspaces[0].test_runtimes[&first_pane];
let grown = runtime.current_size();
@@ -3858,7 +3868,12 @@ fn expected_worktree_runtime_exit_does_not_release_agent() {
.pending_worktree_remove_runtime_exits
.insert(pane_id, 1);
- assert!(server.handle_internal_event_with_forwarding(AppEvent::PaneDied { pane_id }));
+ assert!(
+ server.handle_internal_event_with_forwarding(AppEvent::PaneDied {
+ pane_id,
+ exit_reason: crate::platform::ChildExitReason::Exited
+ })
+ );
assert_eq!(
server.app.state.terminals[&terminal_id].state,