feat(jobs): add cancel-all shell job action

Harvested from PR #1536 by @jieshu666.

Co-authored-by: jieshu666 <jieshu666@users.noreply.github.com>
This commit is contained in:
Hunter Bown
2026-05-12 23:36:31 -05:00
parent b816234fbc
commit 3e93962bdd
6 changed files with 56 additions and 4 deletions
+10 -1
View File
@@ -54,8 +54,11 @@ pub fn jobs(_app: &mut App, args: Option<&str>) -> CommandResult {
})),
None => CommandResult::error("Usage: /jobs cancel <id>"),
},
"cancel-all" | "kill-all" | "stop-all" => {
CommandResult::action(AppAction::ShellJob(ShellJobAction::CancelAll))
}
_ => CommandResult::error(
"Usage: /jobs [list|show <id>|poll <id>|wait <id>|stdin <id> <input>|close-stdin <id>|cancel <id>]",
"Usage: /jobs [list|show <id>|poll <id>|wait <id>|stdin <id> <input>|close-stdin <id>|cancel <id>|cancel-all]",
),
}
}
@@ -109,5 +112,11 @@ mod tests {
Some(AppAction::ShellJob(ShellJobAction::SendStdin { id, input, close: false }))
if id == "shell_abcd" && input == "y"
));
let cancel_all = jobs(&mut app, Some("cancel-all"));
assert!(matches!(
cancel_all.action,
Some(AppAction::ShellJob(ShellJobAction::CancelAll))
));
}
}
-2
View File
@@ -427,7 +427,6 @@ impl BackgroundShell {
}
/// Kill the process
#[allow(dead_code)]
fn kill(&mut self) -> Result<()> {
if let Some(ref mut child) = self.child {
child.kill().context("Failed to kill process")?;
@@ -1250,7 +1249,6 @@ impl ShellManager {
}
/// Kill a running background process
#[allow(dead_code)]
pub fn kill(&mut self, task_id: &str) -> Result<ShellResult> {
let shell = self
.processes
+1
View File
@@ -4069,6 +4069,7 @@ pub enum ShellJobAction {
Cancel {
id: String,
},
CancelAll,
}
#[derive(Debug, Clone, PartialEq, Eq)]
+1 -1
View File
@@ -64,7 +64,7 @@ pub(super) fn format_shell_job_list(jobs: &[ShellJobSnapshot]) -> String {
}
}
lines.push(
"Controls: /jobs show <id>, /jobs poll <id>, /jobs wait <id>, /jobs stdin <id> <input>, /jobs cancel <id>."
"Controls: /jobs show <id>, /jobs poll <id>, /jobs wait <id>, /jobs stdin <id> <input>, /jobs cancel <id>, /jobs cancel-all."
.to_string(),
);
lines.join("\n")
+13
View File
@@ -611,6 +611,19 @@ fn task_panel_lines(app: &App, content_width: usize, max_rows: usize) -> Vec<Lin
Style::default().fg(palette::TEXT_DIM),
)));
}
if lines.len() < max_rows
&& background_rows
.iter()
.any(|task| task.id.starts_with("shell_") && task.status == "running")
{
lines.push(Line::from(Span::styled(
truncate_line_to_width("Ctrl+K -> /jobs cancel-all", content_width.max(1)),
Style::default()
.fg(palette::TEXT_MUTED)
.add_modifier(ratatui::style::Modifier::ITALIC),
)));
}
}
if lines.len() < max_rows {
+31
View File
@@ -2287,6 +2287,19 @@ async fn run_event_loop(
}
if key.code == KeyCode::Char('k') && key.modifiers.contains(KeyModifiers::CONTROL) {
if app.view_stack.is_empty()
&& app.sidebar_focus == SidebarFocus::Tasks
&& app
.task_panel
.iter()
.any(|task| task.id.starts_with("shell_") && task.status == "running")
{
app.input = "/jobs cancel-all".to_string();
app.cursor_position = app.input.len();
app.status_message =
Some("Press Enter to kill all running shell jobs".to_string());
continue;
}
// When the composer is the active input target (no modal/pager
// intercepting keys), Ctrl+K performs an emacs-style kill to
// end-of-line. If the kill is a no-op (cursor at end of empty
@@ -5698,6 +5711,24 @@ fn handle_shell_job_action(app: &mut App, action: crate::tui::app::ShellJobActio
Ok(result) => add_shell_job_message(app, format_shell_poll(&result)),
Err(err) => add_shell_job_message(app, format!("Shell job cancel failed: {err}")),
},
crate::tui::app::ShellJobAction::CancelAll => match manager.kill_running() {
Ok(results) => {
let count = results.len();
if count == 0 {
add_shell_job_message(app, "No running shell jobs to cancel.".to_string());
} else {
let tasks: Vec<String> = results
.iter()
.filter_map(|result| result.task_id.clone())
.collect();
add_shell_job_message(
app,
format!("Killed {count} shell job(s): {}", tasks.join(", ")),
);
}
}
Err(err) => add_shell_job_message(app, format!("Shell job cancel-all failed: {err}")),
},
}
}