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 b659eae8b45..f32b4385062 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 @@ -11,6 +11,7 @@ export function HeroToolCallItem({ renderStatus, toolName, displayTitle, + activityDescription, status, }: ToolCallItemProps) { const Icon = @@ -21,7 +22,7 @@ export function HeroToolCallItem({ : getToolIcon(toolName) const 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 c9733459807..32183aec931 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 @@ -4,6 +4,7 @@ import { type ComponentType, type ReactNode, useMemo, useState } from 'react' import { ActivityStatus } from '@/components/ui/activity-status' import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport' import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools' +import { getToolStatusDisplayTitle } from '@/lib/copilot/tools/tool-display' import { ActivityDisclosure } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-disclosure' import { BrowserAgentIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon' import { renderInlineMarkdown } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/inline-markdown' @@ -52,7 +53,12 @@ export interface AgentGroupProps { } function toolStatusTitle(tool: ToolCallData): string { - return tool.displayTitle || String(tool.toolName ?? '') + return getToolStatusDisplayTitle( + tool.displayTitle || String(tool.toolName ?? ''), + tool.status, + tool.toolName, + tool.activityDescription + ) } /** @@ -213,6 +219,7 @@ export function AgentGroupView({ toolCallId={item.data.id} toolName={item.data.toolName} displayTitle={item.data.displayTitle} + activityDescription={item.data.activityDescription} status={item.data.status} params={item.data.params} result={item.data.result} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts index 8dd4c725517..1e0b1923cdc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts @@ -120,6 +120,43 @@ describe('AgentGroup inline main activity', () => { container.remove() }) + it.each(['mothership', 'workflow', 'browser'])( + 'uses the same model description in the %s live header and expanded row', + (agentName) => { + act(() => + root.render( + createElement(AgentGroup, { + agentName, + agentLabel: agentName, + defaultExpanded: true, + isLaneOpen: true, + isStreaming: true, + items: [ + { + type: 'tool', + data: { + id: 'described-read', + toolName: 'read', + displayTitle: 'Reading files', + activityDescription: 'Checking the project timeline', + status: 'executing', + }, + }, + ], + }) + ) + ) + + const statuses = [...container.querySelectorAll('[role="status"]')] + expect(statuses).toHaveLength(agentName === 'mothership' ? 1 : 2) + for (const status of statuses) { + expect(status.textContent).toContain('Checking the project timeline') + } + expect(container.textContent).not.toContain('Reading files') + expect(container.querySelector('[class*="shimmer"]')).not.toBeNull() + } + ) + it.each([ ['executing', 'Reading notes'], ['success', 'Read notes'], @@ -733,6 +770,6 @@ describe('AgentGroup nested status line', () => { namedTool('Reading workflow', 'success' as ToolCallStatus, 1), group([namedTool('Deploying Invoice Sync as API', 'success' as ToolCallStatus, 2)]), ]) - expect(header).toContain('Workflow Agent โ€” Deploying Invoice Sync as API') + expect(header).toContain('Workflow Agent โ€” Deployed Invoice Sync as API') }) }) 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 d426fe587da..d86c2fbd3af 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 @@ -15,7 +15,12 @@ const MAX_SUMMARY_ACTIONS = 2 export function getToolActivitySummary(tools: ToolCallData[]): string { if (tools.length === 1) { const tool = tools[0] - return getToolStatusDisplayTitle(tool.displayTitle, tool.status, tool.toolName) + return getToolStatusDisplayTitle( + tool.displayTitle, + tool.status, + tool.toolName, + tool.activityDescription + ) } const labels = new Set() let failed = 0 diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx index b52d7b1b0e6..52e9489811d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx @@ -48,6 +48,90 @@ describe('ToolCallItem', () => { expect(markup).not.toContain('Writing brief.md') }) + it.each([ + ['executing', 'Checking the invoice totals'], + ['success', 'Checked the invoice totals'], + ['error', 'Failed checking the invoice totals'], + ['cancelled', 'Stopped checking the invoice totals'], + ['rejected', 'Failed checking the invoice totals'], + ['skipped', 'Skipped checking the invoice totals'], + ] as const)( + 'projects %s from the actual tool status onto the model description', + (status, title) => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain(title) + expect(markup).not.toContain('report.md') + } + ) + + it.each([' ', 'a'.repeat(161)])( + 'uses the existing title for an invalid description', + (activityDescription) => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('Searching files') + } + ) + + it('keeps an executing wait countdown in place of the model phrase', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('10s') + expect(markup).not.toContain('Waiting for the export') + }) + + it('renders model descriptions as text, without interpreting markup', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('<script>') + expect(markup).not.toContain('