Skip to content

Commit 292796b

Browse files
committed
fix(mothership): show sequential activity intent and concrete tools
1 parent 1a717ef commit 292796b

16 files changed

Lines changed: 356 additions & 824 deletions

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

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
110110
act(() => trigger.dispatchEvent(event))
111111
expect(event.defaultPrevented).toBe(true)
112112
expect(trigger.getAttribute('aria-expanded')).toBe('true')
113-
expect(header()?.textContent).toBe('Tool activity')
113+
expect(header()?.textContent).toBe('Reading second')
114114
render([tool('first', 'success'), tool('second', 'success'), tool('third')])
115-
expect(header()?.textContent).toBe('Tool activity')
115+
expect(header()?.textContent).toBe('Reading third')
116116
expect(container.querySelector('[data-state="open"]')?.textContent).toBe(
117117
'Read firstRead secondReading third'
118118
)
@@ -148,9 +148,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
148148
advance(1500)
149149
expect(header()?.textContent).toBe(label)
150150
render([tool('first', 'success'), tool('second', status)], false)
151-
expect(header()?.textContent).toBe(
152-
agentName === 'mothership' ? 'Read first + 1' : 'Read files'
153-
)
151+
expect(header()?.textContent).toBe('Read first')
154152
act(() => container.querySelector<HTMLElement>('[role="button"]')?.click())
155153
expect(container.querySelector('[data-state="open"]')?.textContent).toContain(
156154
'Failed reading second'
@@ -163,7 +161,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
163161
advance(100)
164162
render([tool('first', 'success'), tool('second')])
165163
render([tool('first', 'success'), tool('second', 'success')], false)
166-
const completed = agentName === 'mothership' ? 'Read second + 1' : 'Read files'
164+
const completed = 'Read second + 1'
167165
expect(header()?.textContent).toBe(completed)
168166
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
169167
advance(2000)
@@ -241,9 +239,78 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
241239
expect(header()?.textContent).toBe(agentName === 'mothership' ? 'Read first' : 'Reading first')
242240
})
243241

242+
it('keeps the present-tense intent when expanded while completed collapse uses past tense', () => {
243+
const activity = {
244+
id: 'inputs',
245+
title: 'Checking invoice inputs',
246+
completedTitle: 'Checked invoice inputs',
247+
}
248+
const renderActivity = (status: ToolCallStatus, active: boolean) =>
249+
act(() =>
250+
root.render(
251+
<AgentGroup
252+
agentName={agentName}
253+
agentLabel='Input review'
254+
activity={activity}
255+
items={items([
256+
{ ...tool('first', status), params: { activity } },
257+
tool('second', status),
258+
])}
259+
isStreaming={active}
260+
isLaneOpen={active}
261+
/>
262+
)
263+
)
264+
renderActivity('executing', true)
265+
expect(header()?.textContent).toBe('Reading second + 1')
266+
act(() => container.querySelector<HTMLElement>('[role="button"]')!.click())
267+
expect(header()?.textContent).toBe(activity.title)
268+
renderActivity('success', false)
269+
expect(header()?.textContent).toBe(activity.title)
270+
expect(container.querySelector('[data-state="open"]')?.textContent).toBe(
271+
'Read firstRead second'
272+
)
273+
act(() => container.querySelector<HTMLElement>('[role="button"]')!.click())
274+
expect(header()?.textContent).toBe(activity.completedTitle)
275+
})
276+
244277
if (agentName === 'mothership') {
278+
it('does not claim a merged activity completed when its unlabelled validation failed', () => {
279+
const activity = {
280+
id: 'review',
281+
title: 'Reviewing invoices',
282+
completedTitle: 'Reviewed invoices',
283+
}
284+
act(() =>
285+
root.render(
286+
<AgentGroup
287+
agentName='mothership'
288+
agentLabel='Sim'
289+
activity={activity}
290+
completedGroupCount={2}
291+
items={items([
292+
{ ...tool('read', 'success'), params: { activity: { id: 'first' } } },
293+
{ ...tool('configure', 'success'), params: { activity } },
294+
tool('validation', 'error'),
295+
])}
296+
/>
297+
)
298+
)
299+
expect(header()?.textContent).toBe('Read configure + 1')
300+
expect(header()?.textContent).not.toContain(activity.completedTitle)
301+
act(() => container.querySelector<HTMLElement>('[role="button"]')!.click())
302+
expect(header()?.textContent).toBe(activity.title)
303+
expect(container.querySelector('[data-state="open"]')?.textContent).toContain(
304+
'Failed reading validation'
305+
)
306+
})
307+
245308
it('shows active tool names and count, reserving the grouped completed title for lane closure', () => {
246-
const activity: ToolActivity = { id: 'research', completedTitle: 'Compared files' }
309+
const activity: ToolActivity = {
310+
id: 'research',
311+
title: 'Comparing files',
312+
completedTitle: 'Compared files',
313+
}
247314
const renderActivity = (tools: ToolCallData[], open: boolean) =>
248315
act(() =>
249316
root.render(
@@ -269,7 +336,11 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
269336
})
270337

271338
it('closes the prior activity at normal text while keeping the trailing activity open', () => {
272-
const activity: ToolActivity = { id: 'research', completedTitle: 'Compared files' }
339+
const activity: ToolActivity = {
340+
id: 'research',
341+
title: 'Comparing files',
342+
completedTitle: 'Compared files',
343+
}
273344
const renderActivity = (open: boolean) =>
274345
act(() =>
275346
root.render(

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const ACTIVITY_UPDATE_INTERVAL_MS = 1000
1212
interface ActivityStreamProps extends Omit<ActivityDisclosureProps, 'header'> {
1313
activity: ActivityStatusProps
1414
activityKey?: string
15+
expandedLabel?: string
1516
attentionKey: string
1617
collapsible: boolean
1718
}
@@ -20,6 +21,7 @@ interface ActivityStreamProps extends Omit<ActivityDisclosureProps, 'header'> {
2021
export function ActivityStream({
2122
activity,
2223
activityKey,
24+
expandedLabel,
2325
attentionKey,
2426
expanded,
2527
collapsible,
@@ -60,7 +62,7 @@ export function ActivityStream({
6062
const header = (
6163
<ActivityStatus
6264
{...displayed}
63-
label={isExpanded && activity.isActive ? 'Tool activity' : displayed.label}
65+
label={isExpanded ? (expandedLabel ?? displayed.label) : displayed.label}
6466
isActive={activity.isActive}
6567
/>
6668
)

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ThinkingLoader } from '@/components/ui/thinking-loader'
55
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
66
import type { ToolActivity } from '@/lib/mothership/generated/protocol'
77
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/mothership/tools/retired-tools'
8+
import { readToolActivity } from '@/lib/mothership/tools/tool-activity'
89
import { getToolDisplayTitle, getToolStatusDisplayTitle } from '@/lib/mothership/tools/tool-display'
910
import { ActivityStream } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream'
1011
import {
@@ -162,6 +163,13 @@ export function AgentGroupView({
162163
const isMainAgent = agentName === 'mothership'
163164
const tools = isMainAgent ? [] : collectGroupTools(items)
164165
const statusTool = getActivityStatusTool(tools)
166+
const activityDescriptor =
167+
groupActivity ??
168+
tools
169+
.map((tool) => readToolActivity(tool.params, tool.streamingArgs))
170+
.reverse()
171+
.find((entry) => entry?.title || entry?.completedTitle)
172+
const runningCount = tools.filter((tool) => !isToolDone(tool.status)).length
165173
const resolved = isAgentGroupResolved(items)
166174
const browserAgentAvailable = isBrowserAgentAvailable()
167175
const activeBrowserTakeover =
@@ -256,14 +264,21 @@ export function AgentGroupView({
256264
<div className='flex min-w-0 flex-col gap-1.5 py-0.5 pl-6'>{items.map(renderItem)}</div>
257265
)
258266
const headerText = error
259-
? `${agentLabel} — Failed`
267+
? agentLabel
260268
: isWorking
261269
? statusTool
262-
? getActiveToolActivityTitle(activeToolTitle(statusTool), statusTool, tools)
270+
? getActiveToolActivityTitle(
271+
`${activeToolTitle(statusTool)}${runningCount > 1 ? ` + ${runningCount - 1}` : ''}`,
272+
statusTool,
273+
tools
274+
)
263275
: 'Thinking'
264-
: tools.length > 0
265-
? getToolActivitySummary(tools)
266-
: 'Tool activity'
276+
: activityDescriptor?.completedTitle &&
277+
tools.every((tool) => tool.status === ToolCallStatus.success)
278+
? activityDescriptor.completedTitle
279+
: tools.length > 0
280+
? getToolActivitySummary(tools)
281+
: agentLabel
267282
const headerActive =
268283
!error &&
269284
isWorking &&
@@ -289,6 +304,7 @@ export function AgentGroupView({
289304
<ActivityStream
290305
activity={{ label: headerText, isActive: headerActive, icon: agentIcon }}
291306
activityKey={statusTool?.id}
307+
expandedLabel={pendingInteraction ? undefined : activityDescriptor?.title}
292308
attentionKey={`${getActivityAttentionKey(tools)}:${activeBrowserTakeover?.id ?? ''}`}
293309
collapsible={collapsible}
294310
expanded={expanded}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ describe('AgentGroup inline main activity', () => {
418418
})
419419
)
420420
)
421-
expect(container.querySelector('[role="status"]')?.textContent).toBe('Checked inputs + 1')
421+
expect(container.querySelector('[role="status"]')?.textContent).toBe('Read second document')
422422
act(() => container.querySelector<HTMLElement>('[role="button"]')?.click())
423423
expect(container.textContent).toContain('Failed reading first document')
424424
})
@@ -494,7 +494,7 @@ describe('AgentGroup inline main activity', () => {
494494
)
495495
)
496496
expect(container.textContent).toBe(
497-
status === 'executing' ? 'Viewing image.png' : 'Viewed image.png + 2'
497+
status === 'executing' ? 'Viewing image.png' : 'Viewed image.png'
498498
)
499499
act(() => container.querySelector<HTMLElement>('[role="button"]')?.click())
500500
expect(container.textContent).toContain('Failed reading image.png')
@@ -650,8 +650,8 @@ describe('AgentGroup inline main activity', () => {
650650
const header = container.querySelector<HTMLElement>('[role="button"]')
651651
act(() => header?.click())
652652
expect(header?.hasAttribute('aria-label')).toBe(false)
653-
expect(header?.textContent).toBe('Tool activity')
654-
expect(header).toHaveAccessibleName('Tool activity')
653+
expect(header?.textContent).toBe('Waiting 1s')
654+
expect(header).toHaveAccessibleName('Waiting 1s')
655655
expect(container.querySelector('[data-state="open"]')?.textContent).toBe(
656656
'Waiting 1sRead notes'
657657
)
@@ -667,8 +667,8 @@ describe('AgentGroup inline main activity', () => {
667667
read,
668668
{ ...wait, data: { ...wait.data, id: 'wait-second' } },
669669
])
670-
expect(header?.textContent).toBe('Tool activity')
671-
expect(header).toHaveAccessibleName('Tool activity')
670+
expect(header?.textContent).toBe('Waiting 3s')
671+
expect(header).toHaveAccessibleName('Waiting 3s')
672672
expect(container.querySelector('.overflow-y-auto')).toBe(viewport)
673673
expect(container.querySelector('[data-state="open"]')?.textContent).toBe(
674674
'WaitedRead notesWaiting 3s'
@@ -721,8 +721,8 @@ describe('AgentGroup inline main activity', () => {
721721
)
722722
)
723723
const header = container.querySelector<HTMLElement>('[role="button"]')
724-
expect(header?.textContent).toBe('Read files, ran commands')
725-
expect(header).toHaveAccessibleName('Read files, ran commands')
724+
expect(header?.textContent).toBe('Ran checks + 1')
725+
expect(header).toHaveAccessibleName('Ran checks + 1')
726726
expect(container.querySelectorAll('[data-tool-call-id]')).toHaveLength(0)
727727
act(() => header?.click())
728728
expect(

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ describe('BrowserAgentIcon', () => {
268268
expect(container.querySelector('[role="status"]')?.textContent).toBe('Opening first page')
269269
expect(container.querySelector('[role="button"]')).not.toBeNull()
270270
act(() => vi.advanceTimersByTime(900))
271-
expect(container.querySelector('[role="status"]')?.textContent).toBe('Opening second page')
271+
expect(container.querySelector('[role="status"]')?.textContent).toBe(
272+
'Opening second page + 1'
273+
)
272274
const nextImage = container.querySelector('img')!
273275
expect(nextImage).not.toBe(img)
274276
expect(nextImage.src).toBe('https://example.org/favicon.ico')

0 commit comments

Comments
 (0)