From d7315df18b97376feda6f24536341ad9ca65230c 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: Mon, 18 May 2026 23:41:57 +0800 Subject: [PATCH] Keep agent background progress inside agent cards Desktop receives task lifecycle events for local subagents in addition to the visible Agent tool call. Rendering those events as standalone transcript cards exposed internal task types such as local_agent and duplicated the Agent surface. The store now keeps agent lifecycle state for Agent tool notifications without inserting separate transcript background cards, while non-agent background work remains visible with user-facing labels. The long workspace preview test now has a wider timeout so coverage mode does not fail a behaviorally passing case. Constraint: CLI task events use internal task_type values such as local_agent and local_bash. Rejected: Rename local_agent in the standalone card | still leaves duplicate Agent and task cards for one subagent. Confidence: high Scope-risk: moderate Directive: Do not render local_agent or remote_agent as standalone transcript cards unless the Agent tool card no longer exists. Tested: bun run verify passed=8 failed=0 skipped=2 Not-tested: Live provider-backed desktop session smoke. --- .../src/components/chat/MessageList.test.tsx | 95 ++++++++++-- desktop/src/components/chat/MessageList.tsx | 27 +++- .../workspace/WorkspacePanel.test.tsx | 2 +- desktop/src/i18n/locales/en.ts | 3 + desktop/src/i18n/locales/zh.ts | 3 + desktop/src/stores/chatStore.test.ts | 142 +++++++++++++----- desktop/src/stores/chatStore.ts | 20 ++- 7 files changed, 235 insertions(+), 57 deletions(-) diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 38ce84a0..2ff9da52 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -200,7 +200,7 @@ describe('MessageList nested tool calls', () => { expect(screen.getByText('Budget: 0 / unlimited tokens')).toBeTruthy() }) - it('renders background agent progress inline in the transcript', () => { + it('renders non-agent background progress inline in the transcript', () => { useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState({ @@ -212,14 +212,14 @@ describe('MessageList nested tool calls', () => { timestamp: 1, }, { - id: 'background-task-agent-1', + id: 'background-task-shell-1', type: 'background_task', timestamp: 2, task: { - taskId: 'agent-task-1', - toolUseId: 'agent-tool-1', + taskId: 'shell-task-1', + toolUseId: 'shell-tool-1', status: 'running', - taskType: 'local_agent', + taskType: 'local_bash', summary: 'Running Playwright checks', usage: { totalTokens: 1200, @@ -244,27 +244,27 @@ describe('MessageList nested tool calls', () => { render() const card = screen.getByTestId('background-task-event-card') - expect(card.textContent).toContain('local_agent') + expect(card.textContent).toContain('Background command') expect(card.textContent).toContain('running') expect(card.textContent).toContain('Running Playwright checks') expect(card.textContent).toContain('1,200 tokens') expect(card.textContent).toContain('45s') }) - it('renders stopped background agents as neutral transcript events', () => { + it('renders stopped non-agent background tasks as neutral transcript events', () => { useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState({ messages: [{ - id: 'background-task-agent-stopped', + id: 'background-task-shell-stopped', type: 'background_task', timestamp: 2, task: { - taskId: 'agent-task-stopped', - toolUseId: 'agent-tool-stopped', + taskId: 'shell-task-stopped', + toolUseId: 'shell-tool-stopped', status: 'stopped', - taskType: 'local_agent', - summary: 'Agent "Code review for todo app" was stopped', + taskType: 'local_bash', + summary: 'Command "bun test" was stopped', startedAt: 1, updatedAt: 2, }, @@ -281,6 +281,77 @@ describe('MessageList nested tool calls', () => { expect(card.querySelector('.text-\\[var\\(--color-error\\)\\]')).toBeNull() }) + it('uses user-facing labels for workflow and unknown background tasks', () => { + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + messages: [ + { + id: 'background-task-workflow', + type: 'background_task', + timestamp: 2, + task: { + taskId: 'workflow-task', + status: 'running', + taskType: 'local_workflow', + summary: 'Running release checklist', + startedAt: 1, + updatedAt: 2, + }, + }, + { + id: 'background-task-unknown', + type: 'background_task', + timestamp: 3, + task: { + taskId: 'unknown-task', + status: 'completed', + summary: 'Finished background work', + startedAt: 1, + updatedAt: 3, + }, + }, + ], + }), + }, + }) + + render() + + const cards = screen.getAllByTestId('background-task-event-card') + expect(cards).toHaveLength(2) + expect(cards[0]?.textContent).toContain('Background workflow') + expect(cards[1]?.textContent).toContain('Background task') + }) + + it('does not render agent background task events as separate transcript cards', () => { + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + messages: [{ + id: 'background-task-agent-hidden', + type: 'background_task', + timestamp: 2, + task: { + taskId: 'agent-task-hidden', + toolUseId: 'agent-tool-hidden', + status: 'running', + taskType: 'local_agent', + summary: 'Running Read', + startedAt: 1, + updatedAt: 2, + }, + }], + }), + }, + }) + + render() + + expect(screen.queryByTestId('background-task-event-card')).toBeNull() + expect(screen.queryByText('local_agent')).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 41108e9c..abb44c35 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -219,6 +219,7 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent }) const isStopped = task.status === 'stopped' const duration = formatBackgroundTaskDuration(task.usage?.durationMs) const detail = task.summary || task.lastToolName || task.description || task.outputFile || task.taskId + const label = getBackgroundTaskLabel(task.taskType, t) return (
@@ -242,7 +243,7 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent })