Skip to content

Commit ae32a4d

Browse files
authored
feat(copilot): show model-authored tool activity (#7803)
* feat(copilot): show model-authored tool activity * chore(copilot): remove unused outcome title wrappers * fix(copilot): keep model activity outcomes authoritative
1 parent f75f55a commit ae32a4d

26 files changed

Lines changed: 697 additions & 55 deletions

apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function HeroToolCallItem({
1111
renderStatus,
1212
toolName,
1313
displayTitle,
14+
activityDescription,
1415
status,
1516
}: ToolCallItemProps) {
1617
const Icon =
@@ -21,7 +22,7 @@ export function HeroToolCallItem({
2122
: getToolIcon(toolName)
2223
const activity = (
2324
<ActivityStatus
24-
label={getToolStatusDisplayTitle(displayTitle, status, toolName)}
25+
label={getToolStatusDisplayTitle(displayTitle, status, toolName, activityDescription)}
2526
isActive={status === 'executing'}
2627
icon={<Icon className='size-full' />}
2728
/>

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type ComponentType, type ReactNode, useMemo, useState } from 'react'
44
import { ActivityStatus } from '@/components/ui/activity-status'
55
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
66
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools'
7+
import { getToolStatusDisplayTitle } from '@/lib/copilot/tools/tool-display'
78
import { ActivityDisclosure } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-disclosure'
89
import { BrowserAgentIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon'
910
import { renderInlineMarkdown } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/inline-markdown'
@@ -52,7 +53,12 @@ export interface AgentGroupProps {
5253
}
5354

5455
function toolStatusTitle(tool: ToolCallData): string {
55-
return tool.displayTitle || String(tool.toolName ?? '')
56+
return getToolStatusDisplayTitle(
57+
tool.displayTitle || String(tool.toolName ?? ''),
58+
tool.status,
59+
tool.toolName,
60+
tool.activityDescription
61+
)
5662
}
5763

5864
/**
@@ -213,6 +219,7 @@ export function AgentGroupView({
213219
toolCallId={item.data.id}
214220
toolName={item.data.toolName}
215221
displayTitle={item.data.displayTitle}
222+
activityDescription={item.data.activityDescription}
216223
status={item.data.status}
217224
params={item.data.params}
218225
result={item.data.result}

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,43 @@ describe('AgentGroup inline main activity', () => {
120120
container.remove()
121121
})
122122

123+
it.each(['mothership', 'workflow', 'browser'])(
124+
'uses the same model description in the %s live header and expanded row',
125+
(agentName) => {
126+
act(() =>
127+
root.render(
128+
createElement(AgentGroup, {
129+
agentName,
130+
agentLabel: agentName,
131+
defaultExpanded: true,
132+
isLaneOpen: true,
133+
isStreaming: true,
134+
items: [
135+
{
136+
type: 'tool',
137+
data: {
138+
id: 'described-read',
139+
toolName: 'read',
140+
displayTitle: 'Reading files',
141+
activityDescription: 'Checking the project timeline',
142+
status: 'executing',
143+
},
144+
},
145+
],
146+
})
147+
)
148+
)
149+
150+
const statuses = [...container.querySelectorAll('[role="status"]')]
151+
expect(statuses).toHaveLength(agentName === 'mothership' ? 1 : 2)
152+
for (const status of statuses) {
153+
expect(status.textContent).toContain('Checking the project timeline')
154+
}
155+
expect(container.textContent).not.toContain('Reading files')
156+
expect(container.querySelector('[class*="shimmer"]')).not.toBeNull()
157+
}
158+
)
159+
123160
it.each([
124161
['executing', 'Reading notes'],
125162
['success', 'Read notes'],
@@ -733,6 +770,6 @@ describe('AgentGroup nested status line', () => {
733770
namedTool('Reading workflow', 'success' as ToolCallStatus, 1),
734771
group([namedTool('Deploying Invoice Sync as API', 'success' as ToolCallStatus, 2)]),
735772
])
736-
expect(header).toContain('Workflow Agent — Deploying Invoice Sync as API')
773+
expect(header).toContain('Workflow Agent — Deployed Invoice Sync as API')
737774
})
738775
})

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ const MAX_SUMMARY_ACTIONS = 2
1515
export function getToolActivitySummary(tools: ToolCallData[]): string {
1616
if (tools.length === 1) {
1717
const tool = tools[0]
18-
return getToolStatusDisplayTitle(tool.displayTitle, tool.status, tool.toolName)
18+
return getToolStatusDisplayTitle(
19+
tool.displayTitle,
20+
tool.status,
21+
tool.toolName,
22+
tool.activityDescription
23+
)
1924
}
2025
const labels = new Set<string>()
2126
let failed = 0

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,90 @@ describe('ToolCallItem', () => {
4848
expect(markup).not.toContain('Writing brief.md')
4949
})
5050

51+
it.each([
52+
['executing', 'Checking the invoice totals'],
53+
['success', 'Checked the invoice totals'],
54+
['error', 'Failed checking the invoice totals'],
55+
['cancelled', 'Stopped checking the invoice totals'],
56+
['rejected', 'Failed checking the invoice totals'],
57+
['skipped', 'Skipped checking the invoice totals'],
58+
] as const)(
59+
'projects %s from the actual tool status onto the model description',
60+
(status, title) => {
61+
const markup = renderToStaticMarkup(
62+
<ToolCallItem
63+
toolName='prepare_file_edit'
64+
displayTitle='Editing report.md'
65+
activityDescription='Checking the invoice totals'
66+
status={status}
67+
streamingArgs='{"operation":"patch","title":"report.md"}'
68+
/>
69+
)
70+
71+
expect(markup).toContain(title)
72+
expect(markup).not.toContain('report.md')
73+
}
74+
)
75+
76+
it.each([' ', 'a'.repeat(161)])(
77+
'uses the existing title for an invalid description',
78+
(activityDescription) => {
79+
const markup = renderToStaticMarkup(
80+
<ToolCallItem
81+
toolName='grep'
82+
displayTitle='Searching files'
83+
activityDescription={activityDescription}
84+
status='executing'
85+
/>
86+
)
87+
88+
expect(markup).toContain('Searching files')
89+
}
90+
)
91+
92+
it('keeps an executing wait countdown in place of the model phrase', () => {
93+
const markup = renderToStaticMarkup(
94+
<ToolCallItem
95+
toolName='wait'
96+
displayTitle='Waiting'
97+
activityDescription='Waiting for the export'
98+
status='executing'
99+
params={{ seconds: 10 }}
100+
/>
101+
)
102+
103+
expect(markup).toContain('10s')
104+
expect(markup).not.toContain('Waiting for the export')
105+
})
106+
107+
it('renders model descriptions as text, without interpreting markup', () => {
108+
const markup = renderToStaticMarkup(
109+
<ToolCallItem
110+
toolName='read'
111+
displayTitle='Reading a page'
112+
activityDescription='Reading <script>alert(1)</script>'
113+
status='executing'
114+
/>
115+
)
116+
117+
expect(markup).toContain('&lt;script&gt;')
118+
expect(markup).not.toContain('<script>')
119+
})
120+
121+
it('does not let model-authored outcome wording override a failure', () => {
122+
const markup = renderToStaticMarkup(
123+
<ToolCallItem
124+
toolName='read'
125+
displayTitle='Reading a page'
126+
activityDescription='Stopped checking invoices'
127+
status='error'
128+
/>
129+
)
130+
131+
expect(markup).toContain('Failed checking invoices')
132+
expect(markup).not.toContain('Stopped checking invoices')
133+
})
134+
51135
it('defensively applies the completed verb for every successful tool row', () => {
52136
const markup = renderToStaticMarkup(
53137
<ToolCallItem toolName='diff_workflows' displayTitle='Comparing workflows' status='success' />

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export function CircleStop({ className }: { className?: string }) {
4545
export interface ToolCallItemProps {
4646
toolName: string
4747
displayTitle: string
48+
activityDescription?: string
4849
status: ToolCallStatus
4950
params?: Record<string, unknown>
5051
result?: ToolCallData['result']
@@ -125,6 +126,7 @@ function useElapsedMs(
125126
export function ToolCallItem({
126127
toolName,
127128
displayTitle,
129+
activityDescription,
128130
status,
129131
params,
130132
result,
@@ -188,7 +190,12 @@ export function ToolCallItem({
188190
const liveTitle = isCountingDown
189191
? getWaitCountdownTitle(params, elapsedMs)
190192
: liveWorkspaceFileTitle || displayTitle
191-
const title = getToolStatusDisplayTitle(liveTitle, status, toolName)
193+
const title = getToolStatusDisplayTitle(
194+
liveTitle,
195+
status,
196+
toolName,
197+
isCountingDown ? undefined : activityDescription
198+
)
192199

193200
// A waiting terminal handoff swaps its row for the hand-back chip, the same
194201
// way a browser takeover does: the row would otherwise spin with nothing
@@ -209,7 +216,7 @@ export function ToolCallItem({
209216
<ToolPermissionCard
210217
toolCallId={toolCallId}
211218
toolName={toolName}
212-
displayTitle={liveTitle}
219+
displayTitle={title}
213220
params={params}
214221
/>
215222
)

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,33 @@ describe('getOrchestratorMessageText', () => {
162162
})
163163

164164
describe('parseBlocks span-identity tree', () => {
165+
it.each(['read', 'respond', 'prepare_file_edit'])(
166+
'prefers invocation intent over %s fallback titles',
167+
(name) => {
168+
const segments = parseBlocks([
169+
{
170+
type: 'tool_call',
171+
toolCall: {
172+
id: 'described-tool',
173+
name,
174+
status: 'success',
175+
displayTitle: 'Fallback title',
176+
activityDescription: ' Checking\nlaunch updates ',
177+
params: { path: 'workspace/files/brief.md' },
178+
},
179+
timestamp: 1,
180+
},
181+
])
182+
const group = segments[0]
183+
if (group.type !== 'agent_group') throw new Error('expected mothership group')
184+
const tool = group.items[0]
185+
if (tool?.type !== 'tool') throw new Error('expected tool activity')
186+
expect(tool.data.displayTitle).toBe('Checked launch updates')
187+
expect(tool.data.activityDescription).toBe('Checking launch updates')
188+
expect(tool.data.params).toEqual({ path: 'workspace/files/brief.md' })
189+
}
190+
)
191+
165192
it('refines a completed credential rename with its previous and new names', () => {
166193
const segments = parseBlocks([
167194
{

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
getToolDisplayTitle,
2020
getToolStatusDisplayTitle,
2121
humanizeToolName,
22+
normalizeToolActivityDescription,
2223
} from '@/lib/copilot/tools/tool-display'
2324
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
2425
import type { CredentialSubmissionPayload } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
@@ -200,15 +201,22 @@ function getOverrideDisplayTitle(tc: NonNullable<ContentBlock['toolCall']>): str
200201
}
201202

202203
function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
204+
const activityDescription = normalizeToolActivityDescription(tc.activityDescription)
203205
const overrideDisplayTitle = getOverrideDisplayTitle(tc)
204206
const resolvedTitle =
205207
overrideDisplayTitle || tc.displayTitle || getToolDisplayTitle(tc.name, tc.params)
206-
const displayTitle = getToolStatusDisplayTitle(resolvedTitle, tc.status, tc.name)
208+
const displayTitle = getToolStatusDisplayTitle(
209+
resolvedTitle,
210+
tc.status,
211+
tc.name,
212+
activityDescription
213+
)
207214

208215
return {
209216
id: tc.id,
210217
toolName: tc.name,
211218
displayTitle,
219+
activityDescription,
212220
status: tc.status,
213221
params: tc.params,
214222
result: tc.result,

0 commit comments

Comments
 (0)