Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>('[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<HTMLElement>('[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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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'],
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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'),
Expand All @@ -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'
)
})

Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand Down
Loading