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 @@ -23,7 +23,7 @@ export function HeroToolCallItem({
<ActivityStatus
label={getToolStatusDisplayTitle(displayTitle, status, toolName)}
isActive={status === 'executing'}
icon={Icon && <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' />}
icon={<Icon className='size-full' />}
/>
)
return renderStatus ? renderStatus(activity) : activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export function AgentGroupView({
agentName === 'browser' ? (
<BrowserAgentIcon items={items} />
) : (
<AgentIcon className='size-[16px] text-[var(--text-icon)]' />
<AgentIcon className='size-full' />
)
const isMainAgent = agentName === 'mothership'
/** Open lanes surface their latest work, including work delegated to nested agents. */
Expand Down Expand Up @@ -256,15 +256,7 @@ export function AgentGroupView({
) : (
<div className='flex min-w-0 flex-col gap-1.5 py-0.5 pl-6'>{items.map(renderItem)}</div>
)
const header = (
<ActivityStatus
label={headerText}
isActive={isWorking}
icon={
<span className='flex size-[16px] shrink-0 items-center justify-center'>{agentIcon}</span>
}
/>
)
const header = <ActivityStatus label={headerText} isActive={isWorking} icon={agentIcon} />

return (
<div className='flex min-w-0 flex-col gap-1.5'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,22 @@ function BrowserAgentFavicon({ url, canLoad }: BrowserAgentFaviconProps) {
const [status, setStatus] = useState<'loading' | 'loaded' | 'failed'>('loading')

return (
<span className='relative flex size-[16px] items-center justify-center' aria-hidden='true'>
<span className='relative flex size-full items-center justify-center' aria-hidden='true'>
{/** History alone must not contact a site; keep an already loaded image after the tab closes. */}
{url && status !== 'failed' && (canLoad || status === 'loaded') && (
<img
src={url}
referrerPolicy='no-referrer'
alt=''
className={cn(
'size-[16px] rounded-[3px]',
'size-full rounded-[3px]',
status !== 'loaded' && 'pointer-events-none absolute opacity-0'
)}
onLoad={() => setStatus('loaded')}
onError={() => setStatus('failed')}
/>
)}
{(!url || status !== 'loaded') && <Globe className='size-[16px] text-[var(--text-icon)]' />}
{(!url || status !== 'loaded') && <Globe className='size-full' />}
</span>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('getToolActivitySummary', () => {
tool('terminal_run', 'cancelled'),
tool('browser_type', 'rejected'),
])
).toBe('Read files · 1 failed · 1 stopped · 1 skipped')
).toBe('Read files · 2 failed · 1 stopped')
})

it('does not invent actions when all calls failed or were stopped', () => {
Expand All @@ -54,6 +54,7 @@ describe('getToolActivitySummary', () => {
})

it.each([
['rejected', 'Failed running checks'],
['skipped', 'Skipped running checks'],
['interrupted', 'Stopped running checks'],
] as const)('labels a single %s tool as finished', (status, expected) => {
Expand Down Expand Up @@ -96,6 +97,25 @@ describe('getToolActivitySummary', () => {
).toBe('Read files, searched files +2 more · 1 failed · 1 stopped · 1 skipped')
})

it('uses the same outcome wording for rejected individual and grouped calls', () => {
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'
)
})

it('deduplicates related tools and preserves opposite operations in the summary', () => {
expect(
getToolActivitySummary([
{ ...tool('deploy_as_api'), params: { action: 'deploy' } },
{ ...tool('deploy_as_chat'), params: { action: 'deploy' } },
{ ...tool('deploy_as_mcp'), params: { action: 'undeploy' } },
tool('read'),
])
).toBe('Deployed workflows, undeployed workflows +1 more')
})

it('describes terminal runs from their operation', () => {
expect(
getToolActivitySummary([{ ...tool('terminal'), params: { operation: 'run' } }, tool('read')])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { type ComponentType, Fragment, useState } from 'react'
import { ActivityStatus } from '@/components/ui/activity-status'
import { getToolActivityLabel } from '@/lib/copilot/tools/tool-activity'
import { getToolStatusDisplayTitle } from '@/lib/copilot/tools/tool-display'
import { ActivityDisclosure } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-disclosure'
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
Expand All @@ -10,54 +11,6 @@ import { type ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]

const MAX_SUMMARY_ACTIONS = 2

const ACTIVITY_LABELS: Readonly<Record<string, string>> = {
read: 'read files',
read_document: 'read documents',
glob: 'found files',
grep: 'searched files',
web_search: 'searched the web',
web_fetch: 'read web pages',
web_scrape: 'read web pages',
search_library_docs: 'read documentation',
search_knowledge_base: 'searched sources',
call_integration_tool: 'used integrations',
prepare_file_edit: 'prepared file edits',
apply_file_edit: 'edited files',
create_workflow: 'created workflows',
edit_workflow: 'edited workflows',
run_workflow: 'ran workflows',
run_workflow_until_block: 'ran workflows',
deploy_as_api: 'deployed workflows',
table_rows: 'used tables',
terminal: 'used the terminal',
terminal_run: 'ran commands',
terminal_input: 'sent terminal input',
terminal_read: 'read terminal output',
run_function: 'ran code',
run_code: 'ran code',
browser_navigate: 'navigated pages',
browser_open_url: 'navigated pages',
browser_open_tab: 'opened tabs',
browser_switch_tab: 'switched tabs',
browser_close_tab: 'closed tabs',
browser_snapshot: 'read pages',
browser_read_text: 'read pages',
browser_extract: 'read pages',
browser_find: 'searched pages',
browser_click: 'clicked elements',
browser_click_at: 'clicked elements',
browser_drag: 'dragged elements',
browser_type: 'entered text',
browser_insert_text: 'entered text',
browser_fill_form: 'filled forms',
browser_screenshot: 'captured screenshots',
browser_scroll: 'scrolled pages',
browser_select_option: 'selected options',
browser_set_checked: 'updated selections',
open_resource: 'opened resources',
wait: 'waited',
} as const

/** Summarize completed actions without describing failed or skipped work as successful. */
export function getToolActivitySummary(tools: ToolCallData[]): string {
if (tools.length === 1) {
Expand All @@ -70,17 +23,12 @@ export function getToolActivitySummary(tools: ToolCallData[]): string {
let skipped = 0
for (const tool of tools) {
if (tool.status === ToolCallStatus.success) {
const label =
tool.toolName === 'terminal' && tool.params?.operation === 'run'
? 'ran commands'
: (ACTIVITY_LABELS[tool.toolName] ??
(tool.toolName.startsWith('browser_') ? 'used the browser' : 'used tools'))
labels.add(label)
} else if (tool.status === ToolCallStatus.error) failed++
labels.add(getToolActivityLabel(tool.toolName, tool.params))
} else if (tool.status === ToolCallStatus.error || tool.status === ToolCallStatus.rejected)
failed++
else if (tool.status === ToolCallStatus.cancelled || tool.status === ToolCallStatus.interrupted)
stopped++
else if (tool.status === ToolCallStatus.skipped || tool.status === ToolCallStatus.rejected)
skipped++
else if (tool.status === ToolCallStatus.skipped) skipped++
}
const summary = Array.from(labels).slice(0, MAX_SUMMARY_ACTIONS).join(', ')
const summaryLabel = summary ? summary[0].toUpperCase() + summary.slice(1) : 'Tool activity'
Expand Down Expand Up @@ -132,7 +80,7 @@ export function ToolActivityGroup({
<ActivityStatus
label={getToolActivitySummary(tools)}
isActive={false}
icon={<SummaryIcon className='size-[14px] shrink-0 text-[var(--text-icon)]' />}
icon={<SummaryIcon className='size-full' />}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,9 @@ export function ToolCallItem({
isActive={isExecuting}
icon={
BlockIcon ? (
<BrandIcon icon={BlockIcon} className='size-[14px] shrink-0' />
<BrandIcon icon={BlockIcon} className='size-full' />
) : (
<ToolIcon className='size-[14px] shrink-0 text-[var(--text-icon)]' />
<ToolIcon className='size-full' />
)
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/**
* @vitest-environment node
*/
import { Blimp, Wrench } from '@sim/emcn'
import { describe, expect, it } from 'vitest'
import { collectMessageSources } from '@/app/workspace/[workspaceId]/home/components/message-content/message-sources'
import { deriveMessagePhase, resolveToolDisplayState } from './utils'
import {
deriveMessagePhase,
getAgentIcon,
getToolIcon,
resolveToolDisplayState,
} from '@/app/workspace/[workspaceId]/home/components/message-content/utils'

describe('deriveMessagePhase', () => {
it('is streaming whenever the transport is live', () => {
Expand Down Expand Up @@ -59,3 +65,13 @@ describe('collectMessageSources', () => {
expect(collectMessageSources(['Plain prose.', ''])).toEqual([])
})
})

describe('unknown activity icons', () => {
it.each(['future_tool', '', 'constructor', 'toString', '__proto__'])(
'uses fallback icons for %s',
(name) => {
expect(getToolIcon(name)).toBe(Wrench)
expect(getAgentIcon(name)).toBe(Blimp)
}
)
})
Loading
Loading