diff --git a/src/components/layout/activity-panel.tsx b/src/components/layout/activity-panel.tsx
index 37a9d6d05..a63982398 100644
--- a/src/components/layout/activity-panel.tsx
+++ b/src/components/layout/activity-panel.tsx
@@ -3,7 +3,7 @@ import {
ChevronUp, ChevronDown, Loader2, CheckCircle2, AlertCircle,
FileText, Users, Lightbulb, BookOpen, GitMerge, BarChart3, HelpCircle, Layout,
RotateCcw, X, Clock, TrendingUp, Target, Pause, Play,
- ArrowUp, ArrowDown,
+ ArrowUp, ArrowDown, Trash2,
} from "lucide-react"
import { useTranslation } from "react-i18next"
import { useAppDialog } from "@/stores/app-dialog-store"
@@ -20,6 +20,10 @@ import {
cancelTask,
cancelTasks,
cancelAllTasks,
+ removeTask,
+ removeTasks,
+ hasActiveRun,
+ retainQueuedSelection,
movePendingTask,
pauseProcessing,
resumeProcessing,
@@ -154,6 +158,26 @@ export function ActivityPanel() {
})
}, [project, selectedTaskIds])
+ const syncAfterRemoval = useCallback(() => {
+ const remaining = [...getQueue()]
+ setSelectedTaskIds((current) => retainQueuedSelection(current, remaining))
+ setQueueTasks(remaining)
+ }, [])
+
+ const handleRemoveSelected = useCallback(async () => {
+ if (!project) return
+ const removable = queueTasks.filter(
+ (task) => selectedTaskIds.has(task.id) && isRemovable(task),
+ )
+ if (removable.length === 0) return
+ if (!(await appDialog.confirm({
+ message: t("activity.removeSelectedConfirm", { count: removable.length }),
+ variant: "destructive",
+ }))) return
+ await removeTasks(removable.map((task) => task.id))
+ syncAfterRemoval()
+ }, [appDialog, project, queueTasks, selectedTaskIds, syncAfterRemoval, t])
+
const handleMoveTask = useCallback((taskId: string, direction: "up" | "down") => {
void movePendingTask(taskId, direction).then(() => setQueueTasks([...getQueue()]))
}, [])
@@ -172,6 +196,11 @@ export function ActivityPanel() {
cancelTask(taskId)
}, [project])
+ const handleIngestRemove = useCallback((taskId: string) => {
+ if (!project) return
+ void removeTask(taskId).then(syncAfterRemoval)
+ }, [project, syncAfterRemoval])
+
const handleCancelAll = useCallback(async () => {
if (!project) return
const activeCount = queueSummary.pending + queueSummary.processing
@@ -346,6 +375,12 @@ export function ActivityPanel() {
>
{t("activity.cancelSelected")}
+
>
)}
@@ -435,7 +470,7 @@ export function ActivityPanel() {
{/* Queue tasks */}
{visibleQueueTasks.map((task) => (
-
+
))}
{orderedQueueTasks.length > visibleQueueTasks.length && (
@@ -471,12 +506,19 @@ export function ActivityPanel() {
)
}
-function QueueRow({ task, selected, onSelect, onRetry, onCancel, onMove }: {
+/** Stopped, and its run has already released the worker slot. */
+function isRemovable(task: IngestTask): boolean {
+ return (task.status === "failed" || task.status === "cancelled") &&
+ !hasActiveRun(task.id)
+}
+
+function QueueRow({ task, selected, onSelect, onRetry, onCancel, onRemove, onMove }: {
task: IngestTask
selected: boolean
onSelect: (id: string) => void
onRetry: (id: string) => void
onCancel: (id: string) => void
+ onRemove: (id: string) => void
onMove: (id: string, direction: "up" | "down") => void
}) {
const { t } = useTranslation()
@@ -517,6 +559,15 @@ function QueueRow({ task, selected, onSelect, onRetry, onCancel, onMove }: {
)}
+ {isRemovable(task) && (
+
+ )}
{task.status === "pending" && (
<>