fix(tasks): Make every background task cancellable, queued or running (#177)

Background tasks often couldn't be cancelled, or cancelling didn't stop them:
- A cancel that arrived before the backend registered the request was dropped (build while saving files, multi-repository fetch/pull between repositories). The host now remembers early cancels (bounded, 2 min), and request ids start at a random value per page.
- There was no queue and no common cancel path: backend-reported tasks (LSP progress, deployment transfers) couldn't be cancelled, and tool installs, VCS operations and update checks had no cancel button in Background Tasks.
- Only the direct child process was killed, and JDK / tool archives were extracted without checking for cancellation.

Tool installs and JDK downloads now share a downloads queue; cancelling a queued task removes it without starting any work. Every task cancels through one path that shows "Cancelling…" and then removes it; tasks that can't be interrupted show a disabled cancel button with the reason. IdeEvent::Task carries `cancellable` with a cancel_task command, cancellable LSP progress sends window/workDoneProgress/cancel, installers, curl and tar run in their own process group and are killed as a group, and extraction checks for cancellation between entries.

Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-29 09:05:12 +08:00
1 джерело c6621c5c36
коміт 1c46a1cd04
40 files changed
+888 -148

No files matched your search

+5 -5
Переглянути файл
@@ -8,7 +8,7 @@ import { notify } from "./notificationStore";
import { NOTIFICATIONS_TOOL_WINDOW } from "./Notifications";
import { activateToolWindow } from "./toolwindows/store";
import { toggleTheme } from "./theme";
import { useTasks } from "./tasks";
import { cancelTask, canCancel, useTasks } from "./tasks";
import { showStatusInfo } from "./contributions";
export async function openProjectDialog() {
@@ -39,13 +39,13 @@ export async function openProjectDialog() {
/** Stop Background Processes (⇧⌘F2): the one running cancellable task, else a choice of them. */
export function stopBackgroundProcesses() {
const running = useTasks.getState().tasks.filter((t) => t.cancel && !t.done);
const running = useTasks.getState().tasks.filter(canCancel);
if (!running.length) return showStatusInfo("No background processes to stop");
if (running.length === 1) return running[0]!.cancel!();
if (running.length === 1) return cancelTask(running[0]!.id);
showContextMenu({ x: window.innerWidth / 2 - 160, y: 96 }, [
...running.map((t) => ({ id: String(t.id), label: t.title, icon: "stop", run: () => t.cancel!() })),
...running.map((t) => ({ id: String(t.id), label: t.title, icon: "stop", run: () => cancelTask(t.id) })),
{ kind: "separator" as const },
{ id: "all", label: "Stop All", run: () => running.forEach((t) => t.cancel!()) },
{ id: "all", label: "Stop All", run: () => running.forEach((t) => cancelTask(t.id)) },
]);
}