From 7d06c70316904f75c2047db691b6e120dc2b5888 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 15 Sep 2026 16:16:36 -0700 Subject: [PATCH] fix(activity): simplify outcome summaries and show three actions --- .../agent-group/activity-stream.test.tsx | 44 +++++++++++++---- .../agent-group/tool-activity-group.test.ts | 48 ++++++++++++++----- .../agent-group/tool-activity-group.tsx | 20 +++----- 3 files changed, 78 insertions(+), 34 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream.test.tsx index df297e5bdcb..dedae46db01 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream.test.tsx @@ -157,24 +157,50 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a render([tool('first'), tool('second')]) advance(100) render([tool('first'), tool('second', status)]) - const outcome = + const label = status === 'error' || status === 'rejected' - ? 'failed' - : status === 'skipped' - ? 'skipped' - : 'stopped' - expect(header()?.textContent).toBe(`Reading first · 1 ${outcome}`) + ? 'Reading first' + : `Reading first · 1 ${status === 'skipped' ? 'skipped' : 'stopped'}` + expect(header()?.textContent).toBe(label) expect(container.querySelector('[class*="shimmer"]')).not.toBeNull() advance(1000) - expect(header()?.textContent).toBe(`Reading first · 1 ${outcome}`) + expect(header()?.textContent).toBe(label) } ) - it('surfaces an earlier parallel failure while the latest call keeps working', () => { + it('keeps earlier parallel failures in history without a summary badge', () => { render([tool('first'), tool('second')]) advance(100) render([tool('first', 'error'), tool('second')]) - expect(header()?.textContent).toBe('Reading second · 1 failed') + expect(header()?.textContent).toBe('Reading second') + const trigger = container.querySelector('[role="button"]')! + act(() => trigger.click()) + expect(container.querySelector('[data-state="open"]')?.textContent).toBe( + 'Failed reading firstReading second' + ) + render([tool('first', 'error'), tool('second', 'success')], false) + expect(header()?.textContent).toBe('Read files') + expect(container.querySelector('[data-state="open"]')?.textContent).toBe( + 'Failed reading firstRead second' + ) + }) + + it('shows three distinct actions and keeps the complete history available', () => { + render( + [ + tool('first', 'success'), + { ...tool('second', 'success'), toolName: 'grep' }, + { ...tool('third', 'success'), toolName: 'terminal_run' }, + { ...tool('fourth', 'success'), toolName: 'run_workflow' }, + ], + false + ) + expect(header()?.textContent).toBe('Read files, searched files, ran commands +1 more') + const trigger = container.querySelector('[role="button"]')! + act(() => trigger.click()) + expect(container.querySelector('[data-state="open"]')?.textContent).toBe( + 'Read firstRead secondRead thirdRead fourth' + ) }) it('keeps narration from prematurely completing an open lane', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.ts index e1c23b42d1b..53f44524306 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.ts @@ -12,8 +12,14 @@ function tool(toolName: string, status: ToolCallStatus = 'success'): ToolCallDat describe('getToolActivitySummary', () => { it('caps distinct actions in order and counts the remaining categories, not repeated calls', () => { expect( - getToolActivitySummary([tool('read'), tool('terminal_run'), tool('read'), tool('grep')]) - ).toBe('Read files, ran commands +1 more') + getToolActivitySummary([ + tool('read'), + tool('terminal_run'), + tool('read'), + tool('grep'), + tool('browser_navigate'), + ]) + ).toBe('Read files, ran commands, searched files +1 more') }) it('summarizes browser navigation and interactions without repeating actions', () => { @@ -24,13 +30,16 @@ describe('getToolActivitySummary', () => { tool('browser_type'), tool('browser_navigate'), ]) - ).toBe('Navigated, read pages +1 more') + ).toBe('Navigated, read pages, entered text') }) it.each([ [['browser_navigate', 'browser_read_text'], 'Navigated, read pages'], [['browser_read_text', 'browser_navigate'], 'Read, navigated pages'], - [['browser_navigate', 'browser_read_text', 'browser_scroll'], 'Navigated, read pages +1 more'], + [ + ['browser_navigate', 'browser_read_text', 'browser_scroll'], + 'Navigated, read, scrolled pages', + ], [['browser_navigate', 'browser_type'], 'Navigated pages, entered text'], [['browser_navigate', 'browser_navigate'], 'Navigated pages'], [['read', 'browser_read_text'], 'Read files, read pages'], @@ -46,7 +55,7 @@ describe('getToolActivitySummary', () => { tool('terminal_run', 'cancelled'), tool('browser_type', 'rejected'), ]) - ).toBe('Read files · 2 failed · 1 stopped') + ).toBe('Read files · 1 stopped') }) it('does not invent actions when all calls failed or were stopped', () => { @@ -55,7 +64,22 @@ describe('getToolActivitySummary', () => { tool('apply_file_edit', 'error'), tool('terminal_run', 'interrupted'), ]) - ).toBe('Tool activity · 1 failed · 1 stopped') + ).toBe('Tool activity · 1 stopped') + }) + + it('uses a neutral summary when every call failed', () => { + expect( + getToolActivitySummary([tool('run_workflow', 'error'), tool('terminal', 'rejected')]) + ).toBe('Tool activity') + }) + + it('does not infer tool failures from workflow results', () => { + expect( + getToolActivitySummary([ + tool('read'), + { ...tool('run_workflow'), result: { success: false, error: 'Workflow run failed' } }, + ]) + ).toBe('Read files, ran workflows') }) it('keeps an individual tool’s descriptive title', () => { @@ -91,10 +115,10 @@ describe('getToolActivitySummary', () => { tool('deploy_as_api'), tool('table_rows'), ]) - ).toBe('Navigated pages, filled forms +5 more') + ).toBe('Navigated pages, filled forms, entered text +4 more') }) - it('keeps failure and interruption counts visible when action categories are capped', () => { + it('keeps interruption counts without failure badges when action categories are capped', () => { expect( getToolActivitySummary([ tool('read'), @@ -105,14 +129,14 @@ describe('getToolActivitySummary', () => { tool('wait', 'interrupted'), tool('browser_type', 'skipped'), ]) - ).toBe('Read files, searched files +2 more · 1 failed · 1 stopped · 1 skipped') + ).toBe('Read files, searched files, used the terminal +1 more · 1 stopped · 1 skipped') }) - it('uses the same outcome wording for rejected individual and grouped calls', () => { + it('keeps individual failures explicit without adding aggregate failure badges', () => { const rejected = { ...tool('terminal', 'rejected'), displayTitle: 'Running checks' } expect(getToolActivitySummary([rejected])).toBe('Failed running checks') expect(getToolActivitySummary([rejected, tool('read', 'skipped')])).toBe( - 'Tool activity · 1 failed · 1 skipped' + 'Tool activity · 1 skipped' ) }) @@ -124,7 +148,7 @@ describe('getToolActivitySummary', () => { { ...tool('deploy_as_mcp'), params: { action: 'undeploy' } }, tool('read'), ]) - ).toBe('Deployed workflows, undeployed workflows +1 more') + ).toBe('Deployed workflows, undeployed workflows, read files') }) it('describes terminal runs from their operation', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx index 11b95a3833e..5535c0735c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx @@ -10,7 +10,7 @@ import { getActivityAttentionKey } from '@/app/workspace/[workspaceId]/home/comp import { getToolIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/utils' import { type ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' -const MAX_SUMMARY_ACTIONS = 2 +const MAX_SUMMARY_ACTIONS = 3 /** Summarize completed actions without describing failed or skipped work as successful. */ export function getToolActivitySummary(tools: ToolCallData[]): string { @@ -31,35 +31,29 @@ export function getToolActivitySummary(tools: ToolCallData[]): string { const summaryLabel = summary ? summary[0].toUpperCase() + summary.slice(1) : 'Tool activity' return [ additionalActions > 0 ? `${summaryLabel} +${additionalActions} more` : summaryLabel, - ...getToolActivityOutcomes(tools), + ...getToolActivityInterruptions(tools), ].join(' · ') } -function getToolActivityOutcomes(tools: ToolCallData[]): string[] { - let failed = 0 +function getToolActivityInterruptions(tools: ToolCallData[]): string[] { let stopped = 0 let skipped = 0 for (const tool of tools) { - if (tool.status === ToolCallStatus.error || tool.status === ToolCallStatus.rejected) failed++ - else if (tool.status === ToolCallStatus.cancelled || tool.status === ToolCallStatus.interrupted) + if (tool.status === ToolCallStatus.cancelled || tool.status === ToolCallStatus.interrupted) stopped++ else if (tool.status === ToolCallStatus.skipped) skipped++ } - return [ - ...(failed ? [`${failed} failed`] : []), - ...(stopped ? [`${stopped} stopped`] : []), - ...(skipped ? [`${skipped} skipped`] : []), - ] + return [...(stopped ? [`${stopped} stopped`] : []), ...(skipped ? [`${skipped} skipped`] : [])] } -/** Keep earlier parallel failures visible while the latest action continues. */ +/** Keep earlier interruptions visible while the latest action continues. */ export function getActiveToolActivityTitle( label: string, tool: ToolCallData, tools: ToolCallData[] ): string { return tool.status === ToolCallStatus.executing || tool.status === ToolCallStatus.success - ? [label, ...getToolActivityOutcomes(tools)].join(' · ') + ? [label, ...getToolActivityInterruptions(tools)].join(' · ') : label }