Skip to content

Commit a03e580

Browse files
committed
Show active tools before completed activity summaries
1 parent 9db700f commit a03e580

15 files changed

Lines changed: 248 additions & 63 deletions

File tree

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

Lines changed: 168 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,11 @@ describe('AgentGroup inline main activity', () => {
223223

224224
it.each([
225225
['success', 'Checked search requirements'],
226-
['error', 'Failed checking search requirements'],
227-
['cancelled', 'Stopped checking search requirements'],
226+
['error', 'Failed searching'],
227+
['cancelled', 'Stopped searching'],
228+
['skipped', 'Skipped searching'],
229+
['interrupted', 'Stopped searching'],
230+
['rejected', 'Failed searching'],
228231
] as const)('uses an honest grouped activity label after %s', (status, expected) => {
229232
const item = tool(status)
230233
act(() =>
@@ -234,12 +237,11 @@ describe('AgentGroup inline main activity', () => {
234237
agentLabel: 'Sim',
235238
activity: {
236239
id: 'search',
237-
title: 'Checking search requirements',
238240
completedTitle: 'Checked search requirements',
239241
},
240242
items: [item],
241243
isStreaming: true,
242-
isLaneOpen: true,
244+
isLaneOpen: false,
243245
})
244246
)
245247
)
@@ -255,7 +257,6 @@ describe('AgentGroup inline main activity', () => {
255257
agentLabel: 'Sim',
256258
activity: {
257259
id: 'search',
258-
title: 'Checking requirements',
259260
completedTitle: 'Checked requirements',
260261
},
261262
items: [tool('executing')],
@@ -268,6 +269,162 @@ describe('AgentGroup inline main activity', () => {
268269
expect(container.textContent).not.toContain('Checking requirements')
269270
})
270271

272+
it('counts unresolved calls and waits for a lane boundary before showing completed activity', () => {
273+
vi.useFakeTimers()
274+
const render = (statuses: ToolCallStatus[], isLaneOpen = true) => {
275+
act(() =>
276+
root.render(
277+
createElement(AgentGroup, {
278+
agentName: 'mothership',
279+
agentLabel: 'Sim',
280+
activity: { id: 'build', completedTitle: 'Built Search API' },
281+
isStreaming: true,
282+
isLaneOpen,
283+
items: statuses.map((status, index) => ({
284+
type: 'tool' as const,
285+
data: {
286+
id: `call-${index}`,
287+
toolName: 'read',
288+
displayTitle: `Reading document ${index}`,
289+
status,
290+
},
291+
})),
292+
})
293+
)
294+
)
295+
act(() => vi.advanceTimersByTime(1000))
296+
return container.querySelector('[role="status"]')!
297+
}
298+
expect(render(['executing', 'executing', 'success']).textContent).toBe('Reading document 1 + 1')
299+
expect(render(['executing', 'success', 'success']).textContent).toBe('Reading document 0')
300+
expect(render(['success', 'success', 'success']).textContent).toBe('Read document 2')
301+
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
302+
const completed = render(['success', 'success', 'success'], false)
303+
expect(completed.textContent).toBe('Built Search API')
304+
expect(completed.querySelector('svg')).toBeNull()
305+
act(() => container.querySelector<HTMLElement>('[role="button"]')?.click())
306+
expect(container.querySelector('[data-state="open"] svg')).not.toBeNull()
307+
})
308+
309+
it.each(['sim_cli', 'run_code'])(
310+
'shows %s argument preparation before its concrete call',
311+
(toolName) => {
312+
vi.useFakeTimers()
313+
const render = (
314+
params: Record<string, unknown>,
315+
status: ToolCallStatus,
316+
isLaneOpen = true
317+
) => {
318+
act(() =>
319+
root.render(
320+
createElement(AgentGroup, {
321+
agentName: 'mothership',
322+
agentLabel: 'Sim',
323+
isStreaming: true,
324+
isLaneOpen,
325+
items: [
326+
{
327+
type: 'tool',
328+
data: {
329+
id: 'call',
330+
toolName,
331+
displayTitle: 'Running checks',
332+
status,
333+
params,
334+
streamingArgs: '{"activity":{"id":"check","completedTitle":"Checked inputs"},',
335+
},
336+
},
337+
],
338+
})
339+
)
340+
)
341+
act(() => vi.advanceTimersByTime(1000))
342+
return container.textContent
343+
}
344+
expect(render({}, 'executing')).toBe('Preparing tool call…')
345+
const params = { code: '1', activity: { id: 'check', completedTitle: 'Checked inputs' } }
346+
expect(render(params, 'executing')).toBe('Running checks')
347+
expect(render(params, 'success')).toBe('Ran checks')
348+
expect(render(params, 'success', false)).toBe('Checked inputs')
349+
}
350+
)
351+
352+
it('keeps an activity unfinished while a standalone approval is pending', () => {
353+
act(() =>
354+
root.render(
355+
createElement(AgentGroupView, {
356+
agentName: 'mothership',
357+
agentLabel: 'Sim',
358+
activity: { id: 'build', completedTitle: 'Built API' },
359+
items: [
360+
{
361+
type: 'tool',
362+
data: {
363+
id: 'read',
364+
toolName: 'read',
365+
displayTitle: 'Read configuration',
366+
status: 'success',
367+
},
368+
},
369+
{
370+
type: 'tool',
371+
data: {
372+
id: 'approval',
373+
toolName: 'create',
374+
displayTitle: 'Waiting for approval',
375+
status: 'awaiting_approval',
376+
},
377+
},
378+
],
379+
ToolCallComponent: ({ displayTitle, renderStatus }: ToolCallItemProps) =>
380+
renderStatus
381+
? renderStatus({ label: displayTitle, activeLabel: displayTitle, isActive: false })
382+
: createElement('div', null, displayTitle),
383+
})
384+
)
385+
)
386+
expect(container.textContent).toBe('Read configurationWaiting for approval')
387+
expect(container.textContent).not.toContain('Built API')
388+
})
389+
390+
it('retains earlier failed outcomes when completed activities collapse into one summary', () => {
391+
act(() =>
392+
root.render(
393+
createElement(AgentGroup, {
394+
agentName: 'mothership',
395+
agentLabel: 'Sim',
396+
activity: { id: 'second', completedTitle: 'Checked inputs' },
397+
completedGroupCount: 2,
398+
items: [
399+
{
400+
type: 'tool',
401+
data: {
402+
id: 'first',
403+
toolName: 'read',
404+
displayTitle: 'Reading first document',
405+
status: 'error',
406+
params: { activity: { id: 'first' } },
407+
},
408+
},
409+
{
410+
type: 'tool',
411+
data: {
412+
id: 'second',
413+
toolName: 'read',
414+
displayTitle: 'Reading second document',
415+
status: 'success',
416+
params: { activity: { id: 'second' } },
417+
},
418+
},
419+
],
420+
})
421+
)
422+
)
423+
expect(container.querySelector('[role="status"]')?.textContent).toBe(
424+
'Checked inputs + 1 · 1 failed'
425+
)
426+
})
427+
271428
it('paces the active status in place and expands the full completed history', () => {
272429
vi.useFakeTimers()
273430
const first: AgentGroupItem = {
@@ -300,7 +457,7 @@ describe('AgentGroup inline main activity', () => {
300457
expect(container.firstElementChild).toBe(activity)
301458
expect(container.textContent).toBe('Searching files')
302459
act(() => vi.advanceTimersByTime(1000))
303-
expect(container.textContent).toBe('Reading notes')
460+
expect(container.textContent).toBe('Reading notes + 1')
304461
expect(container.querySelector('[class*="shimmer"]')).not.toBeNull()
305462
expect(
306463
container.querySelector<HTMLElement>('[role="button"]')?.getAttribute('aria-expanded')
@@ -315,7 +472,7 @@ describe('AgentGroup inline main activity', () => {
315472
],
316473
false
317474
)
318-
expect(container.textContent).toBe('Searched files, read files')
475+
expect(container.textContent).toBe('Read notes + 1')
319476
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
320477
const header = container.querySelector<HTMLElement>('[role="button"]')
321478
act(() => header?.click())
@@ -325,7 +482,7 @@ describe('AgentGroup inline main activity', () => {
325482
)
326483
act(() => header?.click())
327484
expect(header?.getAttribute('aria-expanded')).toBe('false')
328-
expect(container.textContent).toBe('Searched files, read files')
485+
expect(container.textContent).toBe('Read notes + 1')
329486
})
330487

331488
it('keeps history expanded as new tools arrive', () => {
@@ -445,7 +602,7 @@ describe('AgentGroup inline main activity', () => {
445602
read,
446603
{ ...wait, data: { ...wait.data, id: 'wait-second', status: 'success' } },
447604
])
448-
expect(header?.textContent).toBe('Waited, read files')
605+
expect(header?.textContent).toBe('Waited + 2')
449606
expect(container.querySelector('.overflow-y-auto')).toBe(viewport)
450607
expect(clearIntervalSpy).toHaveBeenCalledTimes(2)
451608
} finally {
@@ -655,7 +812,8 @@ describe('AgentGroup inline main activity', () => {
655812
Array.from(container.querySelectorAll('[data-tool-call-id]'), (row) =>
656813
row.getAttribute('data-tool-call-id')
657814
)
658-
).toEqual(['permission', 'handoff', 'latest'])
815+
).toEqual(['permission', 'handoff'])
816+
expect(container.querySelector('[role="status"]')?.textContent).toBe('Reading notes')
659817
expect(
660818
container.querySelector('[data-tool-call-id="permission"]')?.closest('[data-state]')
661819
).toBeNull()

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/componen
55
import { ToolActivityGroup } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group'
66
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
77
import { needsToolInput } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-interactions'
8+
import { isToolDone } from '@/app/workspace/[workspaceId]/home/components/message-content/utils'
89
import type { ToolCallData } from '@/app/workspace/[workspaceId]/home/types'
910

1011
interface MainAgentActivityProps {
@@ -36,6 +37,7 @@ export function MainAgentActivity({
3637
isActive,
3738
}: MainAgentActivityProps) {
3839
const activity: ReactNode[] = []
40+
const unresolved = items.some((item) => item.type === 'tool' && !isToolDone(item.data.status))
3941
let tools: ToolCallData[] = []
4042
const flushTools = (active = false) => {
4143
if (tools.length === 0) return
@@ -45,7 +47,7 @@ export function MainAgentActivity({
4547
tools={tools}
4648
activity={groupActivity}
4749
completedGroupCount={completedGroupCount}
48-
isActive={active}
50+
isActive={active || unresolved}
4951
ToolCallComponent={ToolCallComponent}
5052
autoScrollActivity={autoScrollActivity}
5153
/>

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

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getToolStatusDisplayTitle } from '@/lib/mothership/tools/tool-display'
1111
import { ActivityStream } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream'
1212
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
1313
import { getActivityAttentionKey } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-interactions'
14-
import { getToolIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/utils'
14+
import { isToolDone } from '@/app/workspace/[workspaceId]/home/components/message-content/utils'
1515
import { type ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
1616

1717
const MAX_SUMMARY_ACTIONS = 2
@@ -72,8 +72,7 @@ export function getActivityStatusTool(tools: ToolCallData[]): ToolCallData | und
7272
return (
7373
tools.reduce<ToolCallData | undefined>(
7474
(newest, tool) =>
75-
tool.status === ToolCallStatus.executing &&
76-
(!newest || (tool.startedAt ?? 0) >= (newest.startedAt ?? 0))
75+
!isToolDone(tool.status) && (!newest || (tool.startedAt ?? 0) >= (newest.startedAt ?? 0))
7776
? tool
7877
: newest,
7978
undefined
@@ -105,40 +104,41 @@ export function ToolActivityGroup({
105104
activity ??
106105
tools
107106
.map((tool) => readToolActivity(tool.params, tool.streamingArgs))
108-
.findLast((entry) => entry?.title && entry.completedTitle)
109-
const working =
110-
(!groupedActivity && isActive) || tools.some((tool) => tool.status === ToolCallStatus.executing)
111-
const headerActive =
112-
working &&
113-
(statusTool.status === ToolCallStatus.executing || statusTool.status === ToolCallStatus.success)
107+
.findLast((entry) => entry?.completedTitle)
108+
const running = tools.filter((tool) => !isToolDone(tool.status))
109+
const working = running.length > 0
110+
const complete = !isActive && !working
111+
const headerActive = working && statusTool.status !== ToolCallStatus.awaiting_approval
114112
const attentionKey = getActivityAttentionKey(tools)
115-
const SummaryIcon = getToolIcon(tools[0].toolName)
116113
const activityTools =
117114
completedGroupCount > 1 && groupedActivity
118115
? tools.filter(
119116
(tool) => readToolActivity(tool.params, tool.streamingArgs)?.id === groupedActivity.id
120117
)
121118
: tools
122119
const failedActivityTool = activityTools.find(
123-
(tool) => tool.status === ToolCallStatus.error || tool.status === ToolCallStatus.rejected
124-
)
125-
const stoppedActivityTool = activityTools.find(
126-
(tool) =>
127-
tool.status === ToolCallStatus.cancelled ||
128-
tool.status === ToolCallStatus.interrupted ||
129-
tool.status === ToolCallStatus.skipped
120+
(tool) => isToolDone(tool.status) && tool.status !== ToolCallStatus.success
130121
)
131-
const completedActivityLabel =
132-
groupedActivity?.title && groupedActivity.completedTitle
133-
? failedActivityTool
134-
? getToolStatusDisplayTitle(groupedActivity.title, ToolCallStatus.error)
135-
: stoppedActivityTool
136-
? getToolStatusDisplayTitle(groupedActivity.title, ToolCallStatus.cancelled)
137-
: groupedActivity.completedTitle
138-
: undefined
122+
const completedActivityLabel = groupedActivity?.completedTitle
123+
? failedActivityTool
124+
? getToolStatusDisplayTitle(
125+
failedActivityTool.displayTitle,
126+
failedActivityTool.status,
127+
failedActivityTool.toolName,
128+
failedActivityTool.activityDescription
129+
)
130+
: groupedActivity.completedTitle
131+
: undefined
139132
const completedLabel = completedActivityLabel
140-
? `${completedActivityLabel}${completedGroupCount > 1 ? ` + ${completedGroupCount - 1}` : ''}`
133+
? [
134+
`${completedActivityLabel}${completedGroupCount > 1 ? ` + ${completedGroupCount - 1}` : ''}`,
135+
...getToolActivityOutcomes(tools.filter((tool) => !activityTools.includes(tool))),
136+
].join(' · ')
141137
: undefined
138+
const generatingCall =
139+
working &&
140+
(statusTool.toolName === 'sim_cli' || statusTool.toolName === 'run_code') &&
141+
Object.keys(statusTool.params ?? {}).every((key) => key === 'activity')
142142

143143
return (
144144
<ToolCallComponent
@@ -148,12 +148,20 @@ export function ToolActivityGroup({
148148
<ActivityStream
149149
activity={{
150150
label: working
151-
? getActiveToolActivityTitle(status.activeLabel, statusTool, tools)
152-
: (completedLabel ??
153-
(tools.length === 1 ? status.label : getToolActivitySummary(tools))),
151+
? getActiveToolActivityTitle(
152+
`${generatingCall ? 'Preparing tool call…' : status.label}${running.length > 1 ? ` + ${running.length - 1}` : ''}`,
153+
statusTool,
154+
tools
155+
)
156+
: complete
157+
? (completedLabel ??
158+
getActiveToolActivityTitle(
159+
`${status.label}${tools.length > 1 ? ` + ${tools.length - 1}` : ''}`,
160+
statusTool,
161+
tools
162+
))
163+
: getActiveToolActivityTitle(status.label, statusTool, tools),
154164
isActive: headerActive,
155-
icon:
156-
working || tools.length === 1 ? status.icon : <SummaryIcon className='size-full' />,
157165
}}
158166
activityKey={statusTool.id}
159167
attentionKey={attentionKey}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('top-level activity groups', () => {
6868
status: 'executing',
6969
params: title
7070
? {
71-
activity: { id: title, title, completedTitle: title.replace('Checking', 'Checked') },
71+
activity: { id: title, completedTitle: title.replace('Checking', 'Checked') },
7272
}
7373
: {},
7474
},

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,8 +565,7 @@ function groupByActivity(segments: MessageSegment[], isStreaming: boolean): Mess
565565
for (const item of segment.items) {
566566
if (item.type !== 'tool') continue
567567
const activity = readToolActivity(item.data.params, item.data.streamingArgs)
568-
if (activity?.title && activity.completedTitle && !labels.has(activity.id))
569-
labels.set(activity.id, activity)
568+
if (activity?.completedTitle && !labels.has(activity.id)) labels.set(activity.id, activity)
570569
}
571570
}
572571
return segments.flatMap((segment, index): MessageSegment[] => {

0 commit comments

Comments
 (0)