Файли
openidea/src/workbench/StatusBar.tsx
T
l0ng-ai 1c46a1cd04 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
2026-10-29 09:05:12 +08:00

149 рядки
5.4 KiB
TypeScript

import { useMemo, useRef, useState } from "react";
import clsx from "clsx";
import { BarButton, IconButton, Popup, ProgressBar, showContextMenu, T, tooltipAttrs } from "@/ui";
import { useContributions, useStatusInfo, type StatusItemContribution } from "./contributions";
import { ErrorBoundary } from "./ErrorBoundary";
import { isStatusItemVisible, statusBarMenu, useStatusWidgetOverrides } from "./statusBarWidgets";
import { cancelTask, canCancel, useTasks, type BackgroundTask } from "./tasks";
import styles from "./StatusBar.module.css";
import { tx } from "@/platform/i18n";
/**
* Orders status items: on both sides a higher priority sits closer to the
* window edge, so left items sort descending and right items ascending.
*/
export function orderStatusItems(items: Iterable<StatusItemContribution>) {
const all = [...items];
const left = all
.filter((i) => i.align === "left")
.sort((a, b) => b.priority - a.priority || a.id.localeCompare(b.id));
const right = all
.filter((i) => i.align === "right")
.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
return { left, right };
}
/** `lightEdit`: a LightEdit window's bar, with the items that apply there only. */
export function StatusBar({ lightEdit = false }: { lightEdit?: boolean }) {
const map = useContributions((s) => s.statusItems);
const overrides = useStatusWidgetOverrides();
const { left, right } = useMemo(
() => orderStatusItems([...map.values()].filter((i) => !lightEdit || i.lightEdit)),
[map, lightEdit],
);
const visible = (i: StatusItemContribution) => isStatusItemVisible(i, overrides);
const render = (item: StatusItemContribution) => {
const C = item.component;
const shrink = item.align === "left" || item.id === "workbench.tasks";
return (
<div key={item.id} className={clsx(styles.item, shrink && styles.shrink)} data-status-item={item.id}>
<ErrorBoundary name={`status item ${item.id}`}>
<C />
</ErrorBoundary>
</div>
);
};
return (
<footer
className={styles.bar}
aria-label={tx("Status bar")}
onContextMenu={(e) => {
if (e.defaultPrevented) return;
e.preventDefault();
showContextMenu(e, statusBarMenu([...left, ...right], overrides));
}}
>
{left.filter(visible).map(render)}
<div className={styles.spacer} />
{right.filter(visible).map(render)}
</footer>
);
}
/** Built-in: the transient status message (`showStatusInfo`). */
export function StatusInfo() {
const text = useStatusInfo((s) => s.text);
if (!text) return null;
return (
<span className={styles.info} role="status" title={text}>
{text}
</span>
);
}
/** A task's cancel button: working, "Cancelling…", or disabled with the reason it can't be stopped. */
function TaskCancel({ task: t }: { task: BackgroundTask }) {
if (t.done) return null;
if (canCancel(t))
return <IconButton icon="stop" size="small" title={tx`Cancel ${t.title}`} onClick={() => cancelTask(t.id)} />;
const reason = t.cancelling ? tx("Cancelling…") : t.uncancellable;
if (!reason) return null;
// A disabled button gets no pointer events; the wrapper shows why.
return (
<span className={styles.taskCancelOff} {...tooltipAttrs(reason)}>
<IconButton icon="stop" size="small" title={reason} disabled />
</span>
);
}
/** Built-in: background task progress; click lists all running tasks. */
export function TaskStatus() {
const tasks = useTasks((s) => s.tasks);
const [open, setOpen] = useState(false);
const ref = useRef<HTMLButtonElement>(null);
const running = tasks.filter((t) => !t.done);
// A task that is working wins over the queued ones waiting behind it.
const current = running.findLast((t) => !t.queued) ?? running[running.length - 1] ?? tasks[tasks.length - 1];
if (!current) return null;
const more = running.length - 1;
return (
<>
<BarButton
ref={ref}
aria-expanded={open}
aria-label={tx`Background tasks: ${current.title}`}
tooltip={current.message ?? current.title}
onClick={() => setOpen((v) => !v)}
className={styles.task}
>
<span className={styles.taskTitle}>
<T>{current.title}</T>
</span>
<ProgressBar
value={current.done ? 1 : current.fraction}
className={styles.taskBar}
aria-label={current.title}
/>
{more > 0 && <span className={styles.taskMore}>+{more}</span>}
</BarButton>
<Popup
open={open && tasks.length > 0}
anchor={ref}
placement="top-end"
offset={6}
onClose={() => setOpen(false)}
aria-label="Background tasks"
>
<div className={styles.taskList}>
<div className={styles.taskListHeader}>
<T>Background Tasks</T>
</div>
{tasks.map((t) => (
<div key={t.id} className={styles.taskRow}>
<div className={styles.taskRowHead}>
<span className={styles.taskRowTitle}>
<T>{t.title}</T>
</span>
{t.fraction != null && <span className={styles.taskPct}>{Math.round(t.fraction * 100)}%</span>}
<TaskCancel task={t} />
</div>
<ProgressBar value={t.done ? 1 : t.fraction} aria-label={t.title} />
{t.message && <div className={styles.taskMessage}>{t.message}</div>}
</div>
))}
</div>
</Popup>
</>
);
}