From d2d27b3845d22889abb99884a44a2e1610ac53c6 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: Sat, 16 May 2026 23:04:02 +0800 Subject: [PATCH] Avoid noisy stopped states for background agents Goal sessions were showing stopped background-agent cards because the parent model used TaskStop after reading enough partial review output. That writes a killed task notification, which the desktop then rendered like a failure. TaskStop and async agent launch guidance now make cancellation an exceptional action rather than a normal cleanup step. Stopped background-agent transcript cards also render as neutral interrupted events instead of error-styled failures. Constraint: Background agent task notifications must remain truthful; killed tasks still surface as stopped instead of being hidden. Rejected: Hide stopped notifications in /goal sessions | this would mask real user cancellations and runaway-task stops. Confidence: medium Scope-risk: narrow Directive: Do not encourage parent turns to kill background agents only because partial output was read. Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx -t "renders stopped background agents as neutral transcript events" Tested: cd desktop && bun run lint Tested: bun run check:server --- .../src/components/chat/MessageList.test.tsx | 30 +++++++++++++++++++ desktop/src/components/chat/MessageList.tsx | 8 +++-- src/tools/AgentTool/AgentTool.tsx | 3 +- src/tools/TaskStopTool/prompt.ts | 6 +++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index e6b16210..3db66ebd 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -251,6 +251,36 @@ describe('MessageList nested tool calls', () => { expect(card.textContent).toContain('45s') }) + it('renders stopped background agents as neutral transcript events', () => { + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + messages: [{ + id: 'background-task-agent-stopped', + type: 'background_task', + timestamp: 2, + task: { + taskId: 'agent-task-stopped', + toolUseId: 'agent-tool-stopped', + status: 'stopped', + taskType: 'local_agent', + summary: 'Agent "Code review for todo app" was stopped', + startedAt: 1, + updatedAt: 2, + }, + }], + }), + }, + }) + + render() + + const card = screen.getByTestId('background-task-event-card') + expect(card.getAttribute('data-status')).toBe('stopped') + expect(card.textContent).toContain('stopped') + expect(card.querySelector('.text-\\[var\\(--color-error\\)\\]')).toBeNull() + }) + it('restores the full transcript when scrolling away from latest', async () => { useChatStore.setState({ sessions: { diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 35d3335c..20b7e1b1 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1,5 +1,5 @@ import { useRef, useEffect, useMemo, memo, useState, useCallback, useLayoutEffect, type ReactNode } from 'react' -import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, LoaderCircle, Settings, Target, XCircle } from 'lucide-react' +import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, CircleStop, LoaderCircle, Settings, Target, XCircle } from 'lucide-react' import { ApiError } from '../../api/client' import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions' import { useChatStore } from '../../stores/chatStore' @@ -233,7 +233,8 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent }) const t = useTranslation() const { task } = message const isRunning = task.status === 'running' - const isFailed = task.status === 'failed' || task.status === 'stopped' + const isFailed = task.status === 'failed' + const isStopped = task.status === 'stopped' const duration = formatBackgroundTaskDuration(task.usage?.durationMs) const detail = task.summary || task.lastToolName || task.description || task.outputFile || task.taskId @@ -241,6 +242,7 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent })
@@ -248,6 +250,8 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent })