diff --git a/desktop/src/components/chat/BackgroundTasksBar.tsx b/desktop/src/components/chat/BackgroundTasksBar.tsx new file mode 100644 index 00000000..be6981e1 --- /dev/null +++ b/desktop/src/components/chat/BackgroundTasksBar.tsx @@ -0,0 +1,265 @@ +import { useEffect, useMemo, useState } from 'react' +import { CheckCircle2, CircleStop, LoaderCircle, X, XCircle } from 'lucide-react' +import { useTranslation } from '../../i18n' +import { createBackgroundTaskDismissKey, formatDurationMs } from '../../lib/backgroundTasks' +import type { BackgroundAgentTask } from '../../types/chat' + +type BackgroundTasksBarProps = { + tasks: BackgroundAgentTask[] + compact?: boolean + dismissedFinishedTaskKeys?: Set + onClearFinished?: (taskKeys: string[]) => void +} + +const EMPTY_DISMISSED_TASK_KEYS = new Set() + +export function BackgroundTasksBar({ + tasks, + compact = false, + dismissedFinishedTaskKeys, + onClearFinished, +}: BackgroundTasksBarProps) { + const t = useTranslation() + const [open, setOpen] = useState(false) + const dismissedTaskKeys = dismissedFinishedTaskKeys ?? EMPTY_DISMISSED_TASK_KEYS + + const { runningTasks, finishedTasks } = useMemo(() => { + const sorted = [...tasks].sort((a, b) => b.updatedAt - a.updatedAt) + return { + runningTasks: sorted.filter((task) => task.status === 'running'), + finishedTasks: sorted.filter((task) => task.status !== 'running'), + } + }, [tasks]) + + const visibleFinishedTasks = finishedTasks.filter((task) => + !dismissedTaskKeys.has(createBackgroundTaskDismissKey(task)) + ) + const runningCount = runningTasks.length + const visibleFinishedCount = visibleFinishedTasks.length + + useEffect(() => { + if (!open) return + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault() + setOpen(false) + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [open]) + + if (tasks.length === 0 || (runningCount === 0 && visibleFinishedCount === 0 && !open)) return null + + const taskButtonLabel = runningCount > 0 + ? t( + runningCount === 1 + ? 'chat.backgroundTasks.runningCountOne' + : 'chat.backgroundTasks.runningCountMany', + { count: runningCount }, + ) + : t( + visibleFinishedCount === 1 + ? 'chat.backgroundTasks.finishedCountOne' + : 'chat.backgroundTasks.finishedCountMany', + { count: visibleFinishedCount }, + ) + const drawerTitleId = 'background-tasks-drawer-title' + + return ( + <> + {runningCount > 0 || visibleFinishedCount > 0 ? ( +
+
+ +
+
+ ) : null} + + {open ? ( + + ) : null} + + ) +} + +function TaskSection({ title, tasks }: { title: string; tasks: BackgroundAgentTask[] }) { + if (tasks.length === 0) return null + + return ( +
+

{title}

+ +
+ ) +} + +function TaskList({ tasks }: { tasks: BackgroundAgentTask[] }) { + if (tasks.length === 0) return null + + return ( +
+ {tasks.map((task) => ( + + ))} +
+ ) +} + +function BackgroundTaskRow({ task }: { task: BackgroundAgentTask }) { + const t = useTranslation() + const title = task.description?.trim() || + task.summary?.trim() || + task.lastToolName?.trim() || + task.outputFile?.trim() || + task.taskId + const typeLabel = getTaskTypeLabel(task, t) + const duration = formatDurationMs(task.usage?.durationMs, t) + const tokenLabel = task.usage?.totalTokens + ? t('chat.backgroundAgents.tokens', { count: formatCompactNumber(task.usage.totalTokens) }) + : null + + return ( +
+
+ + {task.status === 'running' ? ( + +
+
+ {title} +
+
+ + {getTaskStatusIcon(task.status)} + {typeLabel} + + {getTaskStatusLabel(task.status, t)} + {duration ? {duration} : null} + {tokenLabel ? {tokenLabel} : null} +
+
+
+
+ ) +} + +function getTaskStatusIcon(status: BackgroundAgentTask['status']) { + if (status === 'running') { + return