Skip to content

Commit c6eb7e8

Browse files
committed
improvement(chat): restore stable inline tool activity headers and action summaries
Mothership v1.0.0 reintroduced the "Searching -> Searched -> Searching" flicker that staging had fixed: between two calls of an open main-agent activity, the header dropped to the past tense, lost its shimmer, and the turn-level Thinking row appeared until the next call started. It also removed the header icon and summarized headers as "<latest title> + N". Stable in-progress header - An open main-agent activity group (the streaming turn's tail, with nothing after its last call) stays in progress through gaps: present-tense label of its latest call, shimmer while that call runs or has succeeded. Only that tail group is open; a pending approval or terminal handoff later in the lane no longer re-opens earlier finished groups. - Exactly one live indicator at a time: a main-agent search label now shimmers while its call streams or runs, and hides Thinking like any executing call. Once a trailing search finishes it shows static results, so the gap after it belongs to Thinking. The open-run check follows the same search/non-search split the transcript renders. - assistantMessageHasVisibleActivity again lets that open group own the wait, so Thinking no longer appears between its calls. Thinking still shows at turn start, after prose closes the group, and after a failed or stopped call. - Succeeded calls arrive with past-tense titles, so the open header maps them back through the exact inverse of the completed-verb rewrite (getToolActiveTitle). Subagent lanes use the same mapping; their open/active rules are unchanged. Header icons - Group headers and single inline rows render the running or latest call's icon again; a finished multi-call header shows the first successful call's icon, matching the summary's first phrase. Summaries without "+N" - Running headers show only the active call. Finished groups use the model's completedTitle when every call succeeded, and otherwise a fallback naming up to three distinct successful actions ("Navigated, read pages, clicked elements"), picked by first occurrence in transcript order. Failed calls are left out, and the " · N stopped/skipped" outcome suffix stays. - A rebuilt action catalog covers the current tool surface: every visible catalog tool and operation, plus sim_cli commands. Each command's verb comes from its own display title, and the object comes from its resource path, so commands on one resource share a word ("Listed, read tables"). `mkdir` commands read as creating folders and `tables upsert` as writing table rows. Tools outside the catalog use their own completed title. Separate finished activity groups - groupByActivity still splits main-agent calls by activity id, but no longer merges finished groups into one header, so the completedGroupCount plumbing is removed, along with the unused getAgentIcon fallback parameter.
1 parent 2d0353a commit c6eb7e8

20 files changed

Lines changed: 1447 additions & 235 deletions

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

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
146146
advance(100)
147147
render([tool('first', 'success'), tool('second')])
148148
render([tool('first', 'success'), tool('second', status)])
149-
const label = agentName === 'mothership' ? 'Read first' : 'Reading first'
149+
const label = 'Reading first'
150150
expect(header()?.textContent).toBe(label)
151151
advance(1500)
152152
expect(header()?.textContent).toBe(label)
@@ -164,7 +164,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
164164
advance(100)
165165
render([tool('first', 'success'), tool('second')])
166166
render([tool('first', 'success'), tool('second', 'success')], false)
167-
const completed = 'Read second + 1'
167+
const completed = 'Read files'
168168
expect(header()?.textContent).toBe(completed)
169169
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
170170
advance(2000)
@@ -241,7 +241,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
241241
}
242242
)
243243

244-
it('shows the latest concrete action and keeps the complete history available', () => {
244+
it('names the first distinct actions without a count and keeps the complete history', () => {
245245
render(
246246
[
247247
tool('first', 'success'),
@@ -251,7 +251,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
251251
],
252252
false
253253
)
254-
expect(header()?.textContent).toBe('Read fourth + 3')
254+
expect(header()?.textContent).toBe('Read files, searched files, ran commands')
255255
const trigger = container.querySelector<HTMLElement>('[role="button"]')!
256256
act(() => trigger.click())
257257
expect(container.querySelector('[data-state="open"]')?.textContent).toBe(
@@ -281,26 +281,21 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
281281
expect(rows[0].querySelector('[class*="shimmer"]')).toBeNull()
282282
}
283283
const liveRow = rows[rows.length - 1]
284-
expect(liveRow.textContent).toBe(agentName === 'mothership' ? 'Read second' : 'Reading second')
285-
if (agentName === 'mothership') {
286-
expect(liveRow.querySelector('[class*="shimmer"]')).toBeNull()
287-
} else {
288-
expect(liveRow.querySelector('[class*="shimmer"]')).not.toBeNull()
289-
}
284+
expect(liveRow.textContent).toBe('Reading second')
285+
expect(liveRow.querySelector('[class*="shimmer"]')).not.toBeNull()
290286
})
291287

292-
it('distinguishes a finished tool from an open agent lane', () => {
288+
it('keeps a finished tool in progress while its lane stays open', () => {
293289
render([tool('first')])
294290
advance(100)
295291
render([tool('first', 'success')])
296-
expect(header()?.textContent).toBe(agentName === 'mothership' ? 'Read first' : 'Reading first')
297-
if (agentName === 'mothership') {
298-
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
299-
} else {
300-
expect(container.querySelector('[class*="shimmer"]')).not.toBeNull()
301-
}
292+
expect(header()?.textContent).toBe('Reading first')
293+
expect(container.querySelector('[class*="shimmer"]')).not.toBeNull()
302294
advance(1500)
303-
expect(header()?.textContent).toBe(agentName === 'mothership' ? 'Read first' : 'Reading first')
295+
expect(header()?.textContent).toBe('Reading first')
296+
render([tool('first', 'success')], false)
297+
expect(header()?.textContent).toBe('Read first')
298+
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
304299
})
305300

306301
it('keeps the present-tense intent when expanded while completed collapse uses past tense', () => {
@@ -326,7 +321,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
326321
)
327322
)
328323
renderActivity('executing', true)
329-
expect(header()?.textContent).toBe('Reading second + 1')
324+
expect(header()?.textContent).toBe('Reading second')
330325
act(() => container.querySelector<HTMLElement>('[role="button"]')!.click())
331326
expect(header()?.textContent).toBe(activity.title)
332327
renderActivity('success', false)
@@ -363,7 +358,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
363358
}
364359
)
365360

366-
it('does not claim a merged activity completed when its unlabelled validation failed', () => {
361+
it('does not claim an activity completed when its unlabelled validation failed', () => {
367362
const activity = {
368363
id: 'review',
369364
title: 'Reviewing invoices',
@@ -375,7 +370,6 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
375370
agentName='mothership'
376371
agentLabel='Sim'
377372
activity={activity}
378-
completedGroupCount={2}
379373
items={items([
380374
{ ...tool('read', 'success'), params: { activity: { id: 'first' } } },
381375
{ ...tool('configure', 'success'), params: { activity } },
@@ -384,7 +378,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
384378
/>
385379
)
386380
)
387-
expect(header()?.textContent).toBe('Read configure + 1')
381+
expect(header()?.textContent).toBe('Read files')
388382
expect(header()?.textContent).not.toContain(activity.completedTitle)
389383
act(() => container.querySelector<HTMLElement>('[role="button"]')!.click())
390384
expect(header()?.textContent).toBe(activity.title)
@@ -393,7 +387,7 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
393387
)
394388
})
395389

396-
it('shows active tool names and count, reserving the grouped completed title for lane closure', () => {
390+
it('shows the active call without a count, reserving the completed title for lane closure', () => {
397391
const activity: ToolActivity = {
398392
id: 'research',
399393
title: 'Comparing files',
@@ -412,11 +406,11 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
412406
)
413407
)
414408
renderActivity([tool('first'), tool('second')], true)
415-
expect(header()?.textContent).toBe('Reading second + 1')
409+
expect(header()?.textContent).toBe('Reading second')
416410
expect(container.textContent).not.toContain(activity.completedTitle)
417411
renderActivity([tool('first', 'success'), tool('second', 'success')], true)
418-
expect(header()?.textContent).toBe('Read second')
419-
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
412+
expect(header()?.textContent).toBe('Reading second')
413+
expect(container.querySelector('[class*="shimmer"]')).not.toBeNull()
420414
renderActivity([tool('first', 'success'), tool('second', 'success')], false)
421415
expect(header()?.textContent).toBe(activity.completedTitle)
422416
advance(2000)
@@ -448,8 +442,8 @@ describe.each(['mothership', 'workflow', 'browser', 'deploy'])('%s activity', (a
448442
renderActivity(true)
449443
expect(
450444
[...container.querySelectorAll('[role="status"]')].map((row) => row.textContent)
451-
).toEqual(['Read first', 'Read second'])
452-
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
445+
).toEqual(['Read first', 'Reading second'])
446+
expect(container.querySelectorAll('[class*="shimmer"]')).toHaveLength(1)
453447
renderActivity(false)
454448
expect(
455449
[...container.querySelectorAll('[role="status"]')].map((row) => row.textContent)

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

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ 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'
88
import { readToolActivity } from '@/lib/mothership/tools/tool-activity'
9-
import { getToolDisplayTitle, getToolStatusDisplayTitle } from '@/lib/mothership/tools/tool-display'
9+
import {
10+
getToolActiveTitle,
11+
getToolDisplayTitle,
12+
getToolStatusDisplayTitle,
13+
} from '@/lib/mothership/tools/tool-display'
1014
import { ActivityStream } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream'
1115
import {
1216
collectGroupTools,
@@ -19,6 +23,7 @@ import {
1923
getActiveToolActivityTitle,
2024
getActivityStatusTool,
2125
getToolActivitySummary,
26+
isActivityStatusShimmering,
2227
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group'
2328
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
2429
import {
@@ -54,7 +59,6 @@ export type AgentGroupItem =
5459

5560
export interface AgentGroupProps {
5661
activity?: ToolActivity
57-
completedGroupCount?: number
5862
error?: string
5963
agentName: string
6064
agentLabel: string
@@ -69,13 +73,15 @@ export interface AgentGroupProps {
6973
autoScrollActivity?: boolean
7074
}
7175

76+
/** A working lane describes its latest call in progress, even after that call succeeded. */
7277
function activeToolTitle(tool: ToolCallData): string {
73-
return getToolStatusDisplayTitle(
78+
const title = getToolStatusDisplayTitle(
7479
tool.displayTitle || getToolDisplayTitle(String(tool.toolName ?? ''), undefined),
7580
tool.status === ToolCallStatus.success ? ToolCallStatus.executing : tool.status,
7681
tool.toolName,
7782
tool.activityDescription
7883
)
84+
return tool.status === ToolCallStatus.success ? getToolActiveTitle(title) : title
7985
}
8086

8187
/** Reveal blocking interactions even when a parent group was manually collapsed. */
@@ -148,7 +154,6 @@ export function AgentGroupView({
148154
agentName,
149155
agentLabel,
150156
activity: groupActivity,
151-
completedGroupCount,
152157
error,
153158
items,
154159
isDelegating = false,
@@ -169,7 +174,6 @@ export function AgentGroupView({
169174
.map((tool) => readToolActivity(tool.params, tool.streamingArgs))
170175
.reverse()
171176
.find((entry) => entry?.title || entry?.completedTitle)
172-
const runningCount = tools.filter((tool) => !isToolDone(tool.status)).length
173177
const resolved = isAgentGroupResolved(items)
174178
const browserAgentAvailable = isBrowserAgentAvailable()
175179
const activeBrowserTakeover =
@@ -253,7 +257,6 @@ export function AgentGroupView({
253257
const activity = isMainAgent ? (
254258
<MainAgentActivity
255259
activity={groupActivity}
256-
completedGroupCount={completedGroupCount}
257260
items={items}
258261
ToolCallComponent={ToolCallComponent}
259262
renderItem={renderItem}
@@ -267,11 +270,7 @@ export function AgentGroupView({
267270
? agentLabel
268271
: isWorking
269272
? statusTool
270-
? getActiveToolActivityTitle(
271-
`${activeToolTitle(statusTool)}${runningCount > 1 ? ` + ${runningCount - 1}` : ''}`,
272-
statusTool,
273-
tools
274-
)
273+
? getActiveToolActivityTitle(activeToolTitle(statusTool), statusTool, tools)
275274
: 'Thinking'
276275
: activityDescriptor?.completedTitle &&
277276
tools.every((tool) => tool.status === ToolCallStatus.success)
@@ -280,11 +279,7 @@ export function AgentGroupView({
280279
? getToolActivitySummary(tools)
281280
: agentLabel
282281
const headerActive =
283-
!error &&
284-
isWorking &&
285-
(!statusTool ||
286-
statusTool.status === ToolCallStatus.executing ||
287-
statusTool.status === ToolCallStatus.success)
282+
!error && isWorking && (!statusTool || isActivityStatusShimmering(statusTool))
288283
const collapsible =
289284
meaningfulItems.length > 1 ||
290285
meaningfulItems.some(

0 commit comments

Comments
 (0)