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 })
- {task.taskType || t('chat.backgroundAgents.agent')}
+ {label}
{t(`chat.backgroundAgents.status.${task.status}`)}
@@ -267,6 +268,25 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent })
)
}
+function isAgentBackgroundTaskMessage(message: UIMessage): boolean {
+ if (message.type !== 'background_task') return false
+ if (message.task.taskType === 'local_agent' || message.task.taskType === 'remote_agent') {
+ return true
+ }
+ return /^Agent (?:(?:"[^"]+" )?(completed|was stopped)|(?:"[^"]+" )?failed(?::|$))/.test(
+ message.task.summary ?? '',
+ )
+}
+
+function getBackgroundTaskLabel(
+ taskType: string | undefined,
+ t: (key: TranslationKey, params?: Record) => string,
+): string {
+ if (taskType === 'local_bash') return t('chat.backgroundTasks.command')
+ if (taskType === 'local_workflow') return t('chat.backgroundTasks.workflow')
+ return t('chat.backgroundTasks.task')
+}
+
function SelectableChatMessage({
sessionId,
messageId,
@@ -377,6 +397,9 @@ export function buildRenderModel(messages: UIMessage[]): RenderModel {
if (msg.type === 'assistant_text' && !msg.content.trim()) {
continue
}
+ if (isAgentBackgroundTaskMessage(msg)) {
+ continue
+ }
if (msg.type === 'tool_result' && toolUseIds.has(msg.toolUseId)) {
continue
@@ -412,7 +435,7 @@ function isTurnResponseMessage(message: UIMessage) {
message.type === 'assistant_text' ||
message.type === 'tool_use' ||
message.type === 'tool_result' ||
- message.type === 'background_task' ||
+ (message.type === 'background_task' && !isAgentBackgroundTaskMessage(message)) ||
message.type === 'error' ||
message.type === 'task_summary'
)
diff --git a/desktop/src/components/workspace/WorkspacePanel.test.tsx b/desktop/src/components/workspace/WorkspacePanel.test.tsx
index f5bfaf0c..b2d36688 100644
--- a/desktop/src/components/workspace/WorkspacePanel.test.tsx
+++ b/desktop/src/components/workspace/WorkspacePanel.test.tsx
@@ -970,7 +970,7 @@ describe('WorkspacePanel', () => {
expect(view.getByTestId('workspace-code').textContent).toContain('const line2300 = 2300')
})
expect(view.getByRole('button', { name: 'Collapse preview' })).toBeTruthy()
- }, 10_000)
+ }, 20_000)
it('renders image previews from workspace files', async () => {
await setWorkspaceState((state) => ({
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts
index 3fea5a09..cfb95502 100644
--- a/desktop/src/i18n/locales/en.ts
+++ b/desktop/src/i18n/locales/en.ts
@@ -966,6 +966,9 @@ export const en = {
'chat.backgroundAgents.title': 'Background agents',
'chat.backgroundAgents.count': '{count} active or recent',
'chat.backgroundAgents.agent': 'agent',
+ 'chat.backgroundTasks.command': 'Background command',
+ 'chat.backgroundTasks.workflow': 'Background workflow',
+ 'chat.backgroundTasks.task': 'Background task',
'chat.backgroundAgents.tokens': '{count} tokens',
'chat.backgroundAgents.status.running': 'running',
'chat.backgroundAgents.status.completed': 'completed',
diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts
index 5e792648..354959d4 100644
--- a/desktop/src/i18n/locales/zh.ts
+++ b/desktop/src/i18n/locales/zh.ts
@@ -968,6 +968,9 @@ export const zh: Record = {
'chat.backgroundAgents.title': '后台 Agent',
'chat.backgroundAgents.count': '{count} 个运行中或最近任务',
'chat.backgroundAgents.agent': 'agent',
+ 'chat.backgroundTasks.command': '后台命令',
+ 'chat.backgroundTasks.workflow': '后台工作流',
+ 'chat.backgroundTasks.task': '后台任务',
'chat.backgroundAgents.tokens': '{count} tokens',
'chat.backgroundAgents.status.running': '运行中',
'chat.backgroundAgents.status.completed': '已完成',
diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts
index b9c401bf..8f227742 100644
--- a/desktop/src/stores/chatStore.test.ts
+++ b/desktop/src/stores/chatStore.test.ts
@@ -436,15 +436,6 @@ describe('chatStore history mapping', () => {
const session = useChatStore.getState().sessions[TEST_SESSION_ID]
expect(session?.messages).toMatchObject([
{ id: 'visible-message', type: 'assistant_text', content: 'already rendered' },
- {
- type: 'background_task',
- task: {
- taskId: 'agent-task-1',
- toolUseId: 'agent-tool-1',
- status: 'completed',
- summary: 'Agent completed',
- },
- },
{
id: 'goal-complete',
type: 'goal_event',
@@ -1255,7 +1246,7 @@ describe('chatStore history mapping', () => {
})
})
- it('tracks background agent task lifecycle events for desktop visibility', () => {
+ it('tracks background agent task lifecycle without duplicating transcript cards', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-06T00:00:01.000Z'))
@@ -1289,18 +1280,7 @@ describe('chatStore history mapping', () => {
taskType: 'local_agent',
prompt: 'Run E2E verification',
})
- expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
- {
- type: 'background_task',
- task: {
- taskId: 'agent-task-1',
- status: 'running',
- description: 'Verify the todo app',
- },
- },
- ])
- const insertedTaskTimestamp = useChatStore.getState().sessions[TEST_SESSION_ID]?.messages[0]?.timestamp
- expect(insertedTaskTimestamp).toBe(new Date('2026-04-06T00:00:02.000Z').getTime())
+ expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toHaveLength(0)
vi.setSystemTime(new Date('2026-04-06T00:00:03.000Z'))
@@ -1331,16 +1311,7 @@ describe('chatStore history mapping', () => {
durationMs: 45000,
},
})
- expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toHaveLength(1)
- expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages[0]).toMatchObject({
- type: 'background_task',
- task: {
- taskId: 'agent-task-1',
- status: 'running',
- summary: 'Running Playwright checks',
- },
- })
- expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages[0]?.timestamp).toBe(insertedTaskTimestamp)
+ expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toHaveLength(0)
vi.setSystemTime(new Date('2026-04-06T00:00:04.000Z'))
@@ -1376,19 +1347,110 @@ describe('chatStore history mapping', () => {
summary: 'Found and fixed localStorage corruption.',
outputFile: '/tmp/agent-output.txt',
})
- expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toHaveLength(1)
- expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages[0]).toMatchObject({
- type: 'background_task',
- task: {
- taskId: 'agent-task-1',
- status: 'completed',
- summary: 'Found and fixed localStorage corruption.',
+ expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toHaveLength(0)
+ vi.useRealTimers()
+ })
+
+ it('keeps non-agent background tasks visible and updates the existing transcript card', () => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2026-04-06T00:00:01.000Z'))
+
+ useChatStore.setState({
+ sessions: {
+ [TEST_SESSION_ID]: makeSession(),
},
})
- expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages[0]?.timestamp).toBe(insertedTaskTimestamp)
+
+ useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
+ type: 'system_notification',
+ subtype: 'task_started',
+ data: {
+ task_id: 'shell-task-1',
+ tool_use_id: 'shell-tool-1',
+ description: 'Run desktop checks',
+ task_type: 'local_bash',
+ },
+ })
+
+ expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
+ {
+ type: 'background_task',
+ task: {
+ taskId: 'shell-task-1',
+ toolUseId: 'shell-tool-1',
+ status: 'running',
+ taskType: 'local_bash',
+ description: 'Run desktop checks',
+ },
+ },
+ ])
+ const insertedTaskTimestamp = useChatStore.getState().sessions[TEST_SESSION_ID]?.messages[0]?.timestamp
+
+ vi.setSystemTime(new Date('2026-04-06T00:00:02.000Z'))
+
+ useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
+ type: 'system_notification',
+ subtype: 'task_progress',
+ data: {
+ task_id: 'shell-task-1',
+ tool_use_id: 'shell-tool-1',
+ description: 'Run desktop checks',
+ summary: 'Running Vitest',
+ last_tool_name: 'Bash',
+ task_type: 'local_bash',
+ },
+ })
+
+ const session = useChatStore.getState().sessions[TEST_SESSION_ID]
+ expect(session?.messages).toHaveLength(1)
+ expect(session?.messages[0]).toMatchObject({
+ type: 'background_task',
+ task: {
+ taskId: 'shell-task-1',
+ status: 'running',
+ summary: 'Running Vitest',
+ lastToolName: 'Bash',
+ },
+ })
+ expect(session?.messages[0]?.timestamp).toBe(insertedTaskTimestamp)
vi.useRealTimers()
})
+ it('removes stale agent task transcript cards by matching tool use id', () => {
+ useChatStore.setState({
+ sessions: {
+ [TEST_SESSION_ID]: makeSession({
+ messages: [{
+ id: 'background-task-old-agent-task',
+ type: 'background_task',
+ timestamp: 1,
+ task: {
+ taskId: 'old-agent-task',
+ toolUseId: 'agent-tool-1',
+ status: 'running',
+ taskType: 'local_agent',
+ startedAt: 1,
+ updatedAt: 1,
+ },
+ }],
+ }),
+ },
+ })
+
+ useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
+ type: 'system_notification',
+ subtype: 'task_started',
+ data: {
+ task_id: 'new-agent-task',
+ tool_use_id: 'agent-tool-1',
+ task_type: 'local_agent',
+ description: 'Review app',
+ },
+ })
+
+ expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toHaveLength(0)
+ })
+
it('clears local desktop chat state when the server confirms /clear', () => {
vi.useFakeTimers()
diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts
index 600f8fed..d6007d6d 100644
--- a/desktop/src/stores/chatStore.ts
+++ b/desktop/src/stores/chatStore.ts
@@ -234,10 +234,17 @@ function upsertBackgroundTaskMessage(
task: BackgroundAgentTask,
timestamp: number,
): UIMessage[] {
- const existingIndex = messages.findIndex((message) =>
+ const isSameTaskMessage = (message: UIMessage) =>
message.type === 'background_task' &&
(message.task.taskId === task.taskId ||
- (task.toolUseId && message.task.toolUseId === task.toolUseId)))
+ (task.toolUseId && message.task.toolUseId === task.toolUseId))
+
+ if (isAgentBackgroundTask(task)) {
+ return messages.filter((message) => !isSameTaskMessage(message))
+ }
+
+ const existingIndex = messages.findIndex((message) =>
+ isSameTaskMessage(message))
if (existingIndex === -1) {
return [...messages, {
id: `background-task-${task.taskId}`,
@@ -264,6 +271,15 @@ function mergeBackgroundTaskMessages(
return [...merged].sort((a, b) => a.timestamp - b.timestamp)
}
+function isAgentBackgroundTask(task: Pick): boolean {
+ if (task.taskType === 'local_agent' || task.taskType === 'remote_agent') {
+ return true
+ }
+ return /^Agent (?:(?:"[^"]+" )?(completed|was stopped)|(?:"[^"]+" )?failed(?::|$))/.test(
+ task.summary ?? '',
+ )
+}
+
function mergeRestoredTerminalGoalEvents(
messages: UIMessage[],
restoredMessages: UIMessage[],