From 8d12fdbcff163117b5f0679e5542966b5ec1e173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sun, 28 Jun 2026 10:50:33 +0800 Subject: [PATCH] fix: keep background tasks visible after turn completion Preserve background task visibility after the foreground turn completes and move task progress into an official-style background task drawer. Keep tabs/sidebar active while background tasks run, delay foreground completion rows until background work is settled, and avoid hiding resumed agents after clearing prior finished entries. Tested: bun run check:desktop Confidence: high Scope-risk: moderate --- .../components/chat/BackgroundTasksBar.tsx | 265 +++++++ .../src/components/chat/MessageList.test.tsx | 131 ++++ desktop/src/components/chat/MessageList.tsx | 41 +- .../src/components/layout/Sidebar.test.tsx | 50 ++ desktop/src/components/layout/Sidebar.tsx | 5 +- desktop/src/components/layout/TabBar.test.tsx | 18 +- desktop/src/components/layout/TabBar.tsx | 10 +- desktop/src/i18n/locales/en.ts | 16 + desktop/src/i18n/locales/jp.ts | 16 + desktop/src/i18n/locales/kr.ts | 16 + desktop/src/i18n/locales/zh-TW.ts | 16 + desktop/src/i18n/locales/zh.ts | 16 + desktop/src/lib/backgroundTasks.ts | 31 + desktop/src/pages/ActiveSession.test.tsx | 710 +++++++++++++++++- desktop/src/pages/ActiveSession.tsx | 53 +- desktop/src/stores/chatStore.test.ts | 270 +++++++ desktop/src/stores/chatStore.ts | 140 +++- desktop/src/types/chat.ts | 1 + 18 files changed, 1754 insertions(+), 51 deletions(-) create mode 100644 desktop/src/components/chat/BackgroundTasksBar.tsx create mode 100644 desktop/src/lib/backgroundTasks.ts 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