diff --git a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx
index e39953a37af..b659eae8b45 100644
--- a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx
+++ b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx
@@ -23,7 +23,7 @@ export function HeroToolCallItem({
}
+ icon={}
/>
)
return renderStatus ? renderStatus(activity) : activity
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx
index 584387a69f8..c9733459807 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx
@@ -155,7 +155,7 @@ export function AgentGroupView({
agentName === 'browser' ? (
) : (
-
+
)
const isMainAgent = agentName === 'mothership'
/** Open lanes surface their latest work, including work delegated to nested agents. */
@@ -256,15 +256,7 @@ export function AgentGroupView({
) : (
{items.map(renderItem)}
)
- const header = (
- {agentIcon}
- }
- />
- )
+ const header =
return (
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx
index bc698497999..a3811b418cd 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx
@@ -119,7 +119,7 @@ function BrowserAgentFavicon({ url, canLoad }: BrowserAgentFaviconProps) {
const [status, setStatus] = useState<'loading' | 'loaded' | 'failed'>('loading')
return (
-
+
{/** History alone must not contact a site; keep an already loaded image after the tab closes. */}
{url && status !== 'failed' && (canLoad || status === 'loaded') && (
setStatus('loaded')}
onError={() => setStatus('failed')}
/>
)}
- {(!url || status !== 'loaded') && }
+ {(!url || status !== 'loaded') && }
)
}
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 6a24458b245..e0b17ababbf 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
@@ -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', () => {
@@ -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) => {
@@ -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')])
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 90922371b8c..61b189ca7c1 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
@@ -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'
@@ -10,54 +11,6 @@ import { type ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]
const MAX_SUMMARY_ACTIONS = 2
-const ACTIVITY_LABELS: Readonly> = {
- 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) {
@@ -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'
@@ -132,7 +80,7 @@ export function ToolActivityGroup({
}
+ icon={}
/>
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx
index 57dd4c107cb..f7a1c4ceaae 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx
@@ -246,9 +246,9 @@ export function ToolCallItem({
isActive={isExecuting}
icon={
BlockIcon ? (
-
+
) : (
-
+
)
}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts
index d4094191237..08d4e4a85c0 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.test.ts
@@ -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', () => {
@@ -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)
+ }
+ )
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts
index b2d3d27d499..cf640924e52 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts
@@ -19,111 +19,178 @@ import {
TerminalWindow,
Wrench,
} from '@sim/emcn'
-import { Calendar, Clock, Cursor, Globe, Table as TableIcon } from '@sim/emcn/icons'
-import { AgentIcon, ImageIcon, TTSIcon, VideoIcon } from '@/components/icons'
+import {
+ Brain,
+ Calendar,
+ Clock,
+ Cursor,
+ Globe,
+ ImageUp,
+ Music,
+ Square,
+ Table as TableIcon,
+} from '@sim/emcn/icons'
import type { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
export type IconComponent = ComponentType>
-const TOOL_ICONS: Record = {
- mothership: Blimp,
- glob: FolderCode,
- grep: Search,
- read: File,
- read_document: File,
- mv: FolderCode,
- cp: Layout,
- mkdir: FolderCode,
- web_search: Search,
- web_scrape: Search,
- web_fetch: Search,
- search_library_docs: Library,
- manage_mcp_connection: Settings,
- manage_skill: Asterisk,
- user_memory: Database,
- run_function: TerminalWindow,
- run_code: TerminalWindow,
- superagent: Blimp,
- user_table: TableIcon,
- prepare_file_edit: File,
+export const TOOL_ICONS: Readonly> = {
+ agent: Brain,
apply_file_edit: File,
- create_workflow: Layout,
- edit_workflow: Pencil,
- run_workflow: PlayOutline,
- run_workflow_until_block: PlayOutline,
- deploy_as_api: Rocket,
- table_rows: TableIcon,
- workflow: Hammer,
- debug: Bug,
- run: PlayOutline,
- deploy: Rocket,
auth: Integration,
- knowledge: Database,
- knowledge_base: Database,
- search_knowledge_base: Database,
- table: TableIcon,
- query_user_table: TableIcon,
- job: Calendar,
- agent: AgentIcon,
- custom_tool: Wrench,
- research: Search,
- scout: Search,
- search: Search,
- platform: Library,
- context_compaction: Asterisk,
- open_resource: Eye,
- file: File,
- media: VideoIcon,
- generate_image: ImageIcon,
- generate_video: VideoIcon,
- generate_audio: TTSIcon,
- ffmpeg: Wrench,
browser: Globe,
- browser_navigate: Cursor,
- browser_open_url: Cursor,
- browser_go_back: Cursor,
- browser_go_forward: Cursor,
- browser_reload: Cursor,
- browser_open_tab: Cursor,
- browser_switch_tab: Cursor,
- browser_close_tab: Cursor,
- browser_list_tabs: Cursor,
- browser_list_sessions: Globe,
- browser_list_downloads: File,
- browser_wait_for: Cursor,
- browser_snapshot: Eye,
- browser_find: Search,
- browser_read_text: File,
- browser_screenshot: Eye,
- browser_extract: Search,
browser_click: Cursor,
browser_click_at: Cursor,
+ browser_close_tab: Cursor,
browser_drag: Cursor,
- browser_type: Pencil,
- browser_insert_text: Pencil,
+ browser_extract: Search,
browser_fill_form: Pencil,
+ browser_find: Search,
+ browser_go_back: Cursor,
+ browser_go_forward: Cursor,
+ browser_hover: Cursor,
+ browser_insert_text: Pencil,
+ browser_list_downloads: File,
+ browser_list_sessions: Globe,
+ browser_list_tabs: Cursor,
+ browser_navigate: Cursor,
+ browser_open_tab: Cursor,
+ browser_open_url: Cursor,
browser_press_key: Cursor,
+ browser_read_text: File,
+ browser_reload: Cursor,
+ browser_request_takeover: Cursor,
+ browser_screenshot: Eye,
browser_scroll: Cursor,
browser_select_option: Cursor,
browser_set_checked: Cursor,
- browser_hover: Cursor,
+ browser_snapshot: Eye,
+ browser_switch_tab: Cursor,
+ browser_type: Pencil,
+ browser_wait_for: Cursor,
browser_zoom: Eye,
- browser_request_takeover: Cursor,
+ call_integration_tool: Integration,
+ cancel_workflow_run: Square,
+ connect_slack_bot: Integration,
+ context_compaction: Asterisk,
+ cp: Layout,
+ create_empty_file: File,
+ create_workflow: Layout,
+ create_workspace_mcp_server: Integration,
+ custom_tool: Wrench,
+ debug: Bug,
+ delete_workspace_mcp_server: Integration,
+ deploy: Rocket,
+ deploy_as_api: Rocket,
+ deploy_as_chat: Rocket,
+ deploy_as_mcp: Rocket,
+ diff_workflows: Layout,
+ download_file: File,
+ edit_workflow: Pencil,
+ extensions: Brain,
+ extract_doc_assets: File,
+ ffmpeg: Wrench,
+ file: File,
+ generate_api_key: Settings,
+ generate_audio: Music,
+ generate_image: ImageUp,
+ generate_video: PlayOutline,
+ get_block_outputs: Layout,
+ get_block_upstream_references: Layout,
+ get_deployed_workflow_state: Rocket,
+ get_deployment_status: Rocket,
+ get_workflow_data: Layout,
+ get_workflow_run_options: PlayOutline,
+ glob: FolderCode,
+ grep: Search,
+ interrupt_agent: Brain,
+ job: Calendar,
+ knowledge: Database,
+ knowledge_base: Database,
+ list_deployment_versions: Rocket,
+ list_integration_tools: Integration,
+ list_integrations: Integration,
+ list_workspace_mcp_servers: Integration,
+ load_deployment: Rocket,
+ manage_credential: Settings,
+ manage_custom_tool: Wrench,
+ manage_knowledge_base: Database,
+ manage_mcp_connection: Settings,
+ manage_sandbox: TerminalWindow,
+ manage_skill: Asterisk,
+ media: PlayOutline,
+ mkdir: FolderCode,
+ mothership: Blimp,
+ mv: FolderCode,
+ oauth_get_auth_link: Integration,
+ oauth_request_access: Integration,
+ open_resource: Eye,
+ platform: Library,
+ prepare_file_edit: File,
+ promote_to_live: Rocket,
+ publish_custom_block: Wrench,
+ query_logs: TerminalWindow,
+ query_user_table: TableIcon,
+ read: File,
+ read_document: File,
+ redeploy: Rocket,
+ research: Search,
+ restore_resource: FolderCode,
+ rm: FolderCode,
+ run: PlayOutline,
+ run_block: PlayOutline,
+ run_code: TerminalWindow,
+ run_enrichment: TableIcon,
+ run_from_block: PlayOutline,
+ run_function: TerminalWindow,
+ run_workflow: PlayOutline,
+ run_workflow_until_block: PlayOutline,
+ save_upload: File,
+ scout: Search,
+ search: Search,
+ search_docs: Library,
+ search_knowledge_base: Database,
+ search_library_docs: Library,
+ search_workspace: Search,
+ set_block_enabled: Pencil,
+ set_environment_variables: Settings,
+ set_global_workflow_variables: Settings,
+ share_file: File,
+ steer_agent: Brain,
+ superagent: Blimp,
+ table: TableIcon,
+ table_automations: TableIcon,
+ table_columns: TableIcon,
+ table_enrichments: TableIcon,
+ table_manage: TableIcon,
+ table_rows: TableIcon,
+ table_views: TableIcon,
+ tail_agent: Brain,
terminal: TerminalWindow,
- terminal_run: TerminalWindow,
+ terminal_cwd: TerminalWindow,
terminal_input: TerminalWindow,
- terminal_read: TerminalWindow,
terminal_kill: TerminalWindow,
- terminal_cwd: TerminalWindow,
+ terminal_read: TerminalWindow,
+ terminal_run: TerminalWindow,
+ update_deployment_version: Rocket,
+ update_workspace_mcp_server: Integration,
+ user_memory: Database,
+ user_table: TableIcon,
wait: Clock,
+ wait_agents: Clock,
+ web_crawl: Search,
+ web_fetch: Search,
+ web_scrape: Search,
+ web_search: Search,
+ workflow: Hammer,
}
export function getAgentIcon(name: string): IconComponent {
- return TOOL_ICONS[name as keyof typeof TOOL_ICONS] ?? Blimp
+ return Object.hasOwn(TOOL_ICONS, name) ? TOOL_ICONS[name] : Blimp
}
export function getToolIcon(name: string): IconComponent {
- return TOOL_ICONS[name] ?? Wrench
+ return Object.hasOwn(TOOL_ICONS, name) ? TOOL_ICONS[name] : Wrench
}
export type MessagePhase = 'streaming' | 'revealing' | 'settled'
diff --git a/apps/sim/components/ui/activity-status.tsx b/apps/sim/components/ui/activity-status.tsx
index 43b66444e5b..ae24197c9ba 100644
--- a/apps/sim/components/ui/activity-status.tsx
+++ b/apps/sim/components/ui/activity-status.tsx
@@ -12,7 +12,14 @@ interface ActivityStatusProps {
export function ActivityStatus({ label, isActive, icon }: ActivityStatusProps) {
return (
- {icon}
+ {icon && (
+
+ {icon}
+
+ )}
tool.route !== 'subagent' && !tool.hidden && !isToolHiddenInUi(tool.id)
+)
+
+describe('tool activity catalog coverage', () => {
+ it('covers subagent icons under their dispatch and stream names', () => {
+ for (const tool of Object.values(TOOL_CATALOG)) {
+ if (tool.route !== 'subagent') continue
+ expect(Object.hasOwn(TOOL_ICONS, tool.id), tool.id).toBe(true)
+ if (tool.subagentId) {
+ expect(Object.hasOwn(TOOL_ICONS, tool.subagentId), tool.subagentId).toBe(true)
+ }
+ }
+ })
+
+ it.each(visibleTools)('explicitly describes $id and every declared operation', (tool) => {
+ expect(Object.hasOwn(TOOL_ACTIVITIES, tool.id), `${tool.id} summary`).toBe(true)
+ expect(Object.hasOwn(TOOL_ICONS, tool.id), `${tool.id} icon`).toBe(true)
+ const activity = TOOL_ACTIVITIES[tool.id]
+ const properties = isRecordLike(tool.parameters) ? tool.parameters.properties : undefined
+ if (!isRecordLike(properties)) return
+ for (const parameter of ['operation', 'action'] as const) {
+ const schema = properties[parameter]
+ if (!isRecordLike(schema) || !Array.isArray(schema.enum)) continue
+ expect(typeof activity, `${tool.id}.${parameter}`).toBe('object')
+ if (typeof activity === 'string') continue
+ expect(activity.parameter).toBe(parameter)
+ expect(Object.keys(activity.operations).sort()).toEqual([...schema.enum].sort())
+ if (typeof schema.default === 'string') {
+ expect(activity.defaultOperation).toBe(schema.default)
+ }
+ for (const operation of schema.enum) {
+ expect(typeof operation).toBe('string')
+ expect(getToolActivityLabel(tool.id, { [parameter]: operation })).not.toMatch(
+ /^used (tools|the browser)$/
+ )
+ }
+ }
+ })
+})
+
+describe('getToolActivityLabel', () => {
+ it.each([
+ ['browser_go_back', 'navigated pages'],
+ ['browser_go_forward', 'navigated pages'],
+ ['browser_reload', 'navigated pages'],
+ ['browser_wait_for', 'waited'],
+ ['browser_list_sessions', 'checked signed-in sites'],
+ ['browser_hover', 'hovered over elements'],
+ ['browser_press_key', 'pressed keys'],
+ ['run_block', 'ran workflows'],
+ ['run_from_block', 'ran workflows'],
+ ['redeploy', 'deployed workflows'],
+ ['generate_image', 'generated images'],
+ ['generate_audio', 'generated audio'],
+ ['generate_video', 'generated video'],
+ ['create_empty_file', 'created files'],
+ ['cp', 'copied resources'],
+ ['mv', 'moved resources'],
+ ['rm', 'deleted resources'],
+ ['terminal_run', 'ran commands'],
+ ['terminal_read', 'read terminal output'],
+ ['terminal_input', 'sent terminal input'],
+ ])('describes %s as %s', (toolName, expected) => {
+ expect(getToolActivityLabel(toolName)).toBe(expected)
+ })
+
+ it.each([
+ ['terminal', 'read', 'read terminal output'],
+ ['terminal', 'input', 'sent terminal input'],
+ ['terminal', 'kill', 'stopped commands'],
+ ['terminal', 'panes', 'listed terminal panes'],
+ ['search_knowledge_base', 'get', 'read knowledge bases'],
+ ['ffmpeg', 'probe', 'inspected media'],
+ ['ffmpeg', 'trim', 'edited media'],
+ ['query_user_table', 'query_rows', 'searched rows'],
+ ['table_rows', 'batch_insert_rows', 'added rows'],
+ ['table_rows', 'update_rows_by_filter', 'updated rows'],
+ ['table_rows', 'delete_rows_by_filter', 'deleted rows'],
+ ['table_manage', 'import_file', 'imported table data'],
+ ['table_views', 'get_view', 'read table views'],
+ ['table_views', 'update_view', 'edited table views'],
+ ['table_automations', 'list_workflow_outputs', 'read table automations'],
+ ['table_automations', 'run_column', 'ran table automations'],
+ ['table_automations', 'cancel_table_runs', 'stopped table runs'],
+ ['manage_knowledge_base', 'get', 'read knowledge bases'],
+ ['manage_knowledge_base', 'query', 'searched sources'],
+ ['manage_knowledge_base', 'update', 'updated knowledge bases'],
+ ['manage_mcp_connection', 'list', 'read connections'],
+ ['manage_mcp_connection', 'edit', 'edited connections'],
+ ['manage_sandbox', 'list', 'read sandboxes'],
+ ['manage_skill', 'list', 'read skills'],
+ ['manage_custom_tool', 'list', 'read custom tools'],
+ ['save_upload', 'import', 'imported workflows'],
+ ['save_upload', 'extract', 'extracted files'],
+ ])('describes %s %s accurately', (toolName, operation, expected) => {
+ expect(getToolActivityLabel(toolName, { operation })).toBe(expected)
+ })
+
+ it.each(['deploy_as_api', 'deploy_as_chat', 'deploy_as_mcp'])(
+ 'distinguishes deploying and undeploying with %s',
+ (toolName) => {
+ expect(getToolActivityLabel(toolName)).toBe('deployed workflows')
+ expect(getToolActivityLabel(toolName, { action: 'deploy' })).toBe('deployed workflows')
+ expect(getToolActivityLabel(toolName, { action: 'undeploy' })).toBe('undeployed workflows')
+ }
+ )
+
+ it('preserves default operations and distinguishes reversing published access', () => {
+ expect(getToolActivityLabel('save_upload')).toBe('saved files')
+ expect(getToolActivityLabel('share_file')).toBe('shared files')
+ expect(getToolActivityLabel('share_file', { action: 'unshare' })).toBe('stopped sharing files')
+ expect(getToolActivityLabel('publish_custom_block')).toBe('published custom blocks')
+ expect(getToolActivityLabel('publish_custom_block', { action: 'undeploy' })).toBe(
+ 'unpublished custom blocks'
+ )
+ })
+
+ it.each([
+ 'query_user_table',
+ 'table_manage',
+ 'table_rows',
+ 'table_columns',
+ 'table_automations',
+ 'table_enrichments',
+ ])('keeps legacy combined table operations consistent with %s', (toolName) => {
+ const activity = TOOL_ACTIVITIES[toolName]
+ expect(typeof activity).toBe('object')
+ if (typeof activity === 'string') return
+ for (const operation of Object.keys(activity.operations)) {
+ expect(getToolActivityLabel('user_table', { operation })).toBe(
+ getToolActivityLabel(toolName, { operation })
+ )
+ }
+ })
+
+ it.each(['future_operation', '', null, 4, [], {}, 'constructor', 'toString', '__proto__'])(
+ 'uses neutral fallbacks for invalid or unknown operations: %j',
+ (operation) => {
+ expect(getToolActivityLabel('terminal', { operation })).toBe('used the terminal')
+ expect(getToolActivityLabel('table_rows', { operation })).toBe('used tables')
+ expect(getToolActivityLabel('deploy_as_api', { action: operation })).toBe('used deployments')
+ }
+ )
+
+ it.each(['future_tool', '', 'constructor', 'toString', '__proto__'])(
+ 'keeps unknown/historical tool %s safe',
+ (toolName) => {
+ expect(getToolActivityLabel(toolName)).toBe('used tools')
+ }
+ )
+
+ it('keeps unknown browser calls visible', () => {
+ expect(getToolActivityLabel('browser_future_action')).toBe('used the browser')
+ })
+})
diff --git a/apps/sim/lib/copilot/tools/tool-activity.ts b/apps/sim/lib/copilot/tools/tool-activity.ts
new file mode 100644
index 00000000000..7f4b367b5ab
--- /dev/null
+++ b/apps/sim/lib/copilot/tools/tool-activity.ts
@@ -0,0 +1,394 @@
+interface OperationActivity {
+ label: string
+ parameter: 'operation' | 'action'
+ defaultOperation?: string
+ operations: Readonly>
+}
+
+const DEPLOYMENT_ACTIVITY: OperationActivity = {
+ label: 'used deployments',
+ parameter: 'action',
+ defaultOperation: 'deploy',
+ operations: { deploy: 'deployed workflows', undeploy: 'undeployed workflows' },
+}
+
+const QUERY_USER_TABLE_OPERATIONS = {
+ get: 'read tables',
+ get_schema: 'read tables',
+ get_row: 'read rows',
+ query_rows: 'searched rows',
+} as const
+
+const TABLE_MANAGE_OPERATIONS = {
+ create: 'created tables',
+ create_from_file: 'created tables',
+ import_file: 'imported table data',
+ rename: 'renamed tables',
+} as const
+
+const TABLE_ROWS_OPERATIONS = {
+ insert_row: 'added rows',
+ batch_insert_rows: 'added rows',
+ update_row: 'updated rows',
+ batch_update_rows: 'updated rows',
+ delete_row: 'deleted rows',
+ batch_delete_rows: 'deleted rows',
+ update_rows_by_filter: 'updated rows',
+ delete_rows_by_filter: 'deleted rows',
+} as const
+
+const TABLE_COLUMNS_OPERATIONS = {
+ add_column: 'edited table columns',
+ rename_column: 'edited table columns',
+ delete_column: 'edited table columns',
+ update_column: 'edited table columns',
+} as const
+
+const TABLE_AUTOMATIONS_OPERATIONS = {
+ list_workflow_outputs: 'read table automations',
+ add_workflow_group: 'edited table automations',
+ update_workflow_group: 'edited table automations',
+ delete_workflow_group: 'edited table automations',
+ add_workflow_group_output: 'edited table automations',
+ delete_workflow_group_output: 'edited table automations',
+ run_column: 'ran table automations',
+ cancel_table_runs: 'stopped table runs',
+} as const
+
+const TABLE_ENRICHMENTS_OPERATIONS = {
+ list_enrichments: 'read table enrichments',
+ add_enrichment: 'configured enrichments',
+} as const
+
+/** Client-owned summaries; the executable tool registry stays outside the UI bundle. */
+export const TOOL_ACTIVITIES: Readonly> = {
+ apply_file_edit: 'edited files',
+ browser_click: 'clicked elements',
+ browser_click_at: 'clicked elements',
+ browser_close_tab: 'closed tabs',
+ browser_drag: 'dragged elements',
+ browser_extract: 'read pages',
+ browser_fill_form: 'filled forms',
+ browser_find: 'searched pages',
+ browser_go_back: 'navigated pages',
+ browser_go_forward: 'navigated pages',
+ browser_hover: 'hovered over elements',
+ browser_insert_text: 'entered text',
+ browser_list_downloads: 'listed downloads',
+ browser_list_sessions: 'checked signed-in sites',
+ browser_list_tabs: 'listed tabs',
+ browser_navigate: 'navigated pages',
+ browser_open_tab: 'opened tabs',
+ browser_open_url: 'navigated pages',
+ browser_press_key: 'pressed keys',
+ browser_read_text: 'read pages',
+ browser_reload: 'navigated pages',
+ browser_request_takeover: 'resumed browser control',
+ browser_screenshot: 'captured screenshots',
+ browser_scroll: 'scrolled pages',
+ browser_select_option: 'selected options',
+ browser_set_checked: 'updated selections',
+ browser_snapshot: 'read pages',
+ browser_switch_tab: 'switched tabs',
+ browser_type: 'entered text',
+ browser_wait_for: 'waited',
+ browser_zoom: {
+ label: 'adjusted page zoom',
+ parameter: 'action',
+ operations: {
+ in: 'adjusted page zoom',
+ out: 'adjusted page zoom',
+ reset: 'adjusted page zoom',
+ },
+ },
+ call_integration_tool: 'used integrations',
+ cancel_workflow_run: 'stopped workflow runs',
+ connect_slack_bot: 'connected integrations',
+ cp: 'copied resources',
+ create_empty_file: 'created files',
+ create_workflow: 'created workflows',
+ create_workspace_mcp_server: 'created MCP servers',
+ delete_workspace_mcp_server: 'deleted MCP servers',
+ deploy_as_api: DEPLOYMENT_ACTIVITY,
+ deploy_as_chat: DEPLOYMENT_ACTIVITY,
+ deploy_as_mcp: DEPLOYMENT_ACTIVITY,
+ diff_workflows: 'compared workflows',
+ download_file: 'downloaded files',
+ edit_workflow: 'edited workflows',
+ extract_doc_assets: 'extracted document assets',
+ ffmpeg: {
+ label: 'used media tools',
+ parameter: 'operation',
+ operations: {
+ overlay_audio: 'edited media',
+ mix_audio: 'edited media',
+ concat: 'edited media',
+ trim: 'edited media',
+ scale_pad: 'edited media',
+ overlay_image: 'edited media',
+ add_text: 'edited media',
+ fade: 'edited media',
+ extract_audio: 'edited media',
+ convert: 'edited media',
+ thumbnail: 'edited media',
+ probe: 'inspected media',
+ },
+ },
+ generate_api_key: 'created API keys',
+ generate_audio: 'generated audio',
+ generate_image: 'generated images',
+ generate_video: 'generated video',
+ get_block_outputs: 'read workflow outputs',
+ get_block_upstream_references: 'read workflows',
+ get_deployed_workflow_state: 'read deployments',
+ get_deployment_status: 'read deployments',
+ get_workflow_data: 'read workflows',
+ get_workflow_run_options: 'read workflows',
+ glob: 'found files',
+ grep: 'searched files',
+ interrupt_agent: 'stopped agents',
+ list_deployment_versions: 'read deployments',
+ list_integration_tools: 'read integration tools',
+ list_integrations: 'read integrations',
+ list_workspace_mcp_servers: 'read MCP servers',
+ load_deployment: 'loaded workflow versions',
+ manage_credential: {
+ label: 'managed credentials',
+ parameter: 'operation',
+ operations: {
+ rename: 'renamed credentials',
+ delete: 'deleted credentials',
+ },
+ },
+ manage_custom_tool: {
+ label: 'managed custom tools',
+ parameter: 'operation',
+ operations: {
+ add: 'created custom tools',
+ edit: 'edited custom tools',
+ delete: 'deleted custom tools',
+ list: 'read custom tools',
+ },
+ },
+ manage_knowledge_base: {
+ label: 'used knowledge bases',
+ parameter: 'operation',
+ operations: {
+ create: 'created knowledge bases',
+ get: 'read knowledge bases',
+ query: 'searched sources',
+ add_file: 'added source documents',
+ update: 'updated knowledge bases',
+ delete_document: 'deleted source documents',
+ update_document: 'updated source documents',
+ list_tags: 'read source tags',
+ create_tag: 'edited source tags',
+ update_tag: 'edited source tags',
+ delete_tag: 'edited source tags',
+ get_tag_usage: 'read source tags',
+ add_connector: 'created connections',
+ update_connector: 'edited connections',
+ delete_connector: 'deleted connections',
+ sync_connector: 'synced sources',
+ },
+ },
+ manage_mcp_connection: {
+ label: 'managed connections',
+ parameter: 'operation',
+ operations: {
+ add: 'created connections',
+ edit: 'edited connections',
+ delete: 'deleted connections',
+ list: 'read connections',
+ },
+ },
+ manage_sandbox: {
+ label: 'managed sandboxes',
+ parameter: 'operation',
+ operations: {
+ add: 'created sandboxes',
+ edit: 'edited sandboxes',
+ delete: 'deleted sandboxes',
+ list: 'read sandboxes',
+ },
+ },
+ manage_skill: {
+ label: 'managed skills',
+ parameter: 'operation',
+ operations: {
+ add: 'created skills',
+ edit: 'edited skills',
+ delete: 'deleted skills',
+ list: 'read skills',
+ },
+ },
+ mkdir: 'created folders',
+ mv: 'moved resources',
+ oauth_get_auth_link: 'prepared sign-in links',
+ oauth_request_access: 'requested access',
+ open_resource: 'opened resources',
+ prepare_file_edit: {
+ label: 'prepared file edits',
+ parameter: 'operation',
+ operations: {
+ append: 'prepared file edits',
+ update: 'prepared file edits',
+ patch: 'prepared file edits',
+ },
+ },
+ promote_to_live: 'deployed workflows',
+ publish_custom_block: {
+ label: 'managed custom blocks',
+ parameter: 'action',
+ defaultOperation: 'deploy',
+ operations: {
+ deploy: 'published custom blocks',
+ undeploy: 'unpublished custom blocks',
+ },
+ },
+ query_logs: 'read logs',
+ query_user_table: {
+ label: 'read tables',
+ parameter: 'operation',
+ operations: QUERY_USER_TABLE_OPERATIONS,
+ },
+ read: 'read files',
+ read_document: 'read documents',
+ redeploy: 'deployed workflows',
+ restore_resource: 'restored resources',
+ rm: 'deleted resources',
+ run_block: 'ran workflows',
+ run_code: 'ran code',
+ run_enrichment: 'ran enrichments',
+ run_from_block: 'ran workflows',
+ run_function: 'ran code',
+ run_workflow: 'ran workflows',
+ run_workflow_until_block: 'ran workflows',
+ save_upload: {
+ label: 'used uploaded files',
+ parameter: 'operation',
+ defaultOperation: 'save',
+ operations: {
+ save: 'saved files',
+ import: 'imported workflows',
+ extract: 'extracted files',
+ },
+ },
+ search_docs: 'read documentation',
+ search_knowledge_base: {
+ label: 'searched sources',
+ parameter: 'operation',
+ operations: {
+ get: 'read knowledge bases',
+ query: 'searched sources',
+ list_tags: 'read source tags',
+ },
+ },
+ search_library_docs: 'read documentation',
+ search_workspace: 'searched the workspace',
+ set_block_enabled: 'edited workflows',
+ set_environment_variables: 'updated environment variables',
+ set_global_workflow_variables: 'updated workflow variables',
+ share_file: {
+ label: 'managed file sharing',
+ parameter: 'action',
+ defaultOperation: 'share',
+ operations: {
+ share: 'shared files',
+ unshare: 'stopped sharing files',
+ },
+ },
+ steer_agent: 'guided agents',
+ table_automations: {
+ label: 'used table automations',
+ parameter: 'operation',
+ operations: TABLE_AUTOMATIONS_OPERATIONS,
+ },
+ table_columns: {
+ label: 'used table columns',
+ parameter: 'operation',
+ operations: TABLE_COLUMNS_OPERATIONS,
+ },
+ table_enrichments: {
+ label: 'used table enrichments',
+ parameter: 'operation',
+ operations: TABLE_ENRICHMENTS_OPERATIONS,
+ },
+ table_manage: {
+ label: 'used tables',
+ parameter: 'operation',
+ operations: TABLE_MANAGE_OPERATIONS,
+ },
+ table_rows: {
+ label: 'used tables',
+ parameter: 'operation',
+ operations: TABLE_ROWS_OPERATIONS,
+ },
+ table_views: {
+ label: 'used table views',
+ parameter: 'operation',
+ operations: {
+ list_views: 'read table views',
+ get_view: 'read table views',
+ create_view: 'edited table views',
+ update_view: 'edited table views',
+ delete_view: 'edited table views',
+ set_default_view: 'edited table views',
+ },
+ },
+ tail_agent: 'read agent progress',
+ terminal: {
+ label: 'used the terminal',
+ parameter: 'operation',
+ operations: {
+ run: 'ran commands',
+ read: 'read terminal output',
+ input: 'sent terminal input',
+ kill: 'stopped commands',
+ cwd: 'checked terminal locations',
+ list: 'listed terminals',
+ new: 'opened terminals',
+ switch: 'switched terminals',
+ close: 'closed terminals',
+ panes: 'listed terminal panes',
+ handoff: 'handed over terminal control',
+ },
+ },
+ terminal_cwd: 'checked terminal locations',
+ terminal_input: 'sent terminal input',
+ terminal_kill: 'stopped commands',
+ terminal_read: 'read terminal output',
+ terminal_run: 'ran commands',
+ update_deployment_version: 'updated deployment details',
+ update_workspace_mcp_server: 'updated MCP servers',
+ user_table: {
+ label: 'used tables',
+ parameter: 'operation',
+ operations: {
+ ...QUERY_USER_TABLE_OPERATIONS,
+ ...TABLE_MANAGE_OPERATIONS,
+ ...TABLE_ROWS_OPERATIONS,
+ ...TABLE_COLUMNS_OPERATIONS,
+ ...TABLE_AUTOMATIONS_OPERATIONS,
+ ...TABLE_ENRICHMENTS_OPERATIONS,
+ },
+ },
+ wait: 'waited',
+ wait_agents: 'waited',
+ web_crawl: 'read web pages',
+ web_fetch: 'read web pages',
+ web_scrape: 'read web pages',
+ web_search: 'searched the web',
+}
+
+/** Unknown tools and operations retain a neutral summary for historical/custom calls. */
+export function getToolActivityLabel(toolName: string, params?: Record): string {
+ const activity = Object.hasOwn(TOOL_ACTIVITIES, toolName) ? TOOL_ACTIVITIES[toolName] : undefined
+ if (!activity) return toolName.startsWith('browser_') ? 'used the browser' : 'used tools'
+ if (typeof activity === 'string') return activity
+ const suppliedOperation = params?.[activity.parameter]
+ const operation = suppliedOperation === undefined ? activity.defaultOperation : suppliedOperation
+ return typeof operation === 'string' && Object.hasOwn(activity.operations, operation)
+ ? activity.operations[operation]
+ : activity.label
+}