fix(kill): resume polling for the details view that resurfaces from a chain
Reported: kill still orphaned a window via Enter (child details) -> P (parent details) -> t (terminate parent). The parent's view closed correctly, but the child's view underneath resurfaced with no selection (forget_process_row had cleared it — it pointed at the parent) and wiped data; the selection-gated details poll never refilled it. close_details_for_gone_process now retargets the selection to the uppermost remaining ProcessDetails view (looking through stacked Info/Confirmation modals) and makes the poll due immediately — the same retarget SwitchToParentProcess performs on the way down the chain. Same class of hole in plain navigation, fixed alongside: Esc-ing back from a parent view left the selection on the PARENT, so the resurfaced child-titled view refilled with the parent's data. The dismiss arm now retargets the selection to whatever details view it lands on. Both verified live: child -> P -> kill parent -> child view resurfaces populated and updating; child -> P -> Esc -> child view shows the child. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+69
-4
@@ -527,9 +527,25 @@ impl App {
|
||||
|
||||
/// Close the details view for a process that no longer exists, and drop the
|
||||
/// data collected for it.
|
||||
///
|
||||
/// A parent-navigation chain can leave another details view underneath
|
||||
/// (child → P → parent killed): the resurfacing view must resume polling,
|
||||
/// so retarget the selection to it — the same thing SwitchToParentProcess
|
||||
/// does on the way down. Without this the child view came back with no
|
||||
/// selection (forget_process_row had just cleared it) and wiped data, and
|
||||
/// the selection-gated details poll never refilled it: a frozen, orphaned
|
||||
/// window.
|
||||
fn close_details_for_gone_process(&mut self, pid: u32) {
|
||||
if self.modal_manager.close_process_details(pid) {
|
||||
self.clear_process_details();
|
||||
if let Some(next_pid) = self.modal_manager.topmost_process_details() {
|
||||
self.selected_process_pid = Some(next_pid);
|
||||
// Fire the details poll on the next tick rather than waiting
|
||||
// out the interval.
|
||||
self.last_process_details_poll = Instant::now()
|
||||
.checked_sub(self.process_details_interval)
|
||||
.unwrap_or_else(Instant::now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,12 +1025,20 @@ impl App {
|
||||
continue; // Skip normal key processing
|
||||
}
|
||||
ModalAction::Cancel | ModalAction::Dismiss => {
|
||||
// If ProcessDetails modal was dismissed, clear the data to save resources
|
||||
if let Some(crate::ui::modal::ModalType::ProcessDetails {
|
||||
..
|
||||
}) = self.modal_manager.current_modal()
|
||||
// If a ProcessDetails view is what we landed on,
|
||||
// clear the stale data AND point the poll at it —
|
||||
// Esc-ing back from a parent view otherwise left
|
||||
// the selection on the parent, refilling the
|
||||
// child-titled view with the parent's data.
|
||||
if let Some(crate::ui::modal::ModalType::ProcessDetails { pid }) =
|
||||
self.modal_manager.current_modal()
|
||||
{
|
||||
let pid = *pid;
|
||||
self.clear_process_details();
|
||||
self.selected_process_pid = Some(pid);
|
||||
self.last_process_details_poll = Instant::now()
|
||||
.checked_sub(self.process_details_interval)
|
||||
.unwrap_or_else(Instant::now);
|
||||
}
|
||||
// Abandon any pending kill the user backed out of.
|
||||
self.pending_kill = None;
|
||||
@@ -2104,6 +2128,47 @@ mod kill_refresh_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod parent_chain_tests {
|
||||
use super::*;
|
||||
use crate::ui::modal::ModalType;
|
||||
|
||||
/// Kill a parent reached via P-navigation: the child's view resurfaces and
|
||||
/// must resume polling. Reported as: "I can still see the orphaned window
|
||||
/// if I open a process, hit P, then terminate that process with t".
|
||||
#[test]
|
||||
fn killing_a_navigated_to_parent_retargets_the_child_view() {
|
||||
let mut app = App::new();
|
||||
let (child, parent) = (200u32, 100u32);
|
||||
app.modal_manager
|
||||
.push_modal(ModalType::ProcessDetails { pid: child });
|
||||
app.modal_manager
|
||||
.push_modal(ModalType::ProcessDetails { pid: parent });
|
||||
// The kill flow stacks the "Signal sent" Info on top, and the watch
|
||||
// usually confirms the death while it is still up.
|
||||
app.modal_manager.push_modal(ModalType::Info {
|
||||
title: "Signal sent".into(),
|
||||
message: "Sent SIGTERM".into(),
|
||||
});
|
||||
// forget_process_row has already cleared the selection by this point.
|
||||
app.selected_process_pid = None;
|
||||
|
||||
app.close_details_for_gone_process(parent);
|
||||
|
||||
assert_eq!(
|
||||
app.selected_process_pid,
|
||||
Some(child),
|
||||
"resurfaced child view has no selection: its poll never runs and \
|
||||
the window sits frozen"
|
||||
);
|
||||
assert_eq!(app.modal_manager.topmost_process_details(), Some(child));
|
||||
assert!(
|
||||
app.last_process_details_poll.elapsed() >= app.process_details_interval,
|
||||
"poll should be due immediately"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod details_close_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -127,6 +127,16 @@ impl ModalManager {
|
||||
true
|
||||
}
|
||||
|
||||
/// PID of the uppermost ProcessDetails view, looking through any
|
||||
/// Info/Confirmation stacked above it. What the user will land on when
|
||||
/// transient modals are dismissed.
|
||||
pub fn topmost_process_details(&self) -> Option<u32> {
|
||||
self.stack.iter().rev().find_map(|m| match m {
|
||||
ModalType::ProcessDetails { pid } => Some(*pid),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_connection_error_countdown(&mut self, new_countdown: Option<u64>) {
|
||||
if let Some(ModalType::ConnectionError {
|
||||
auto_retry_countdown,
|
||||
|
||||
Reference in New Issue
Block a user