Skip to content

Commit 2710244

Browse files
authored
improvement(chat): keep the previous step's label while the next call's title streams (#8230)
1 parent 6377330 commit 2710244

4 files changed

Lines changed: 315 additions & 30 deletions

File tree

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/lane-activity'
2525
import { MainAgentActivity } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/main-agent-activity'
2626
import {
27+
getActivityHeaderTool,
2728
getActivityStatusTool,
2829
getCompletedActivityLabel,
2930
getInProgressActivityLabel,
@@ -119,13 +120,14 @@ export function AgentGroupView({
119120
const isMainAgent = agentName === 'mothership'
120121
const tools = isMainAgent ? [] : collectGroupTools(items)
121122
const statusTool = getActivityStatusTool(tools)
122-
const statusTitle = useToolCallTitle(
123-
statusTool && {
124-
...statusTool,
125-
toolCallId: statusTool.id,
123+
const headerTool = statusTool && getActivityHeaderTool(tools, statusTool)
124+
const headerTitle = useToolCallTitle(
125+
headerTool && {
126+
...headerTool,
127+
toolCallId: headerTool.id,
126128
displayTitle:
127-
statusTool.displayTitle ||
128-
getToolDisplayTitle(String(statusTool.toolName ?? ''), undefined),
129+
headerTool.displayTitle ||
130+
getToolDisplayTitle(String(headerTool.toolName ?? ''), undefined),
129131
}
130132
)
131133
const activityDescriptor =
@@ -283,8 +285,13 @@ export function AgentGroupView({
283285
const headerText = error
284286
? agentLabel
285287
: inProgress
286-
? statusTool && statusTitle
287-
? getInProgressActivityLabel(statusTitle.activeLabel, statusTool, tools)
288+
? headerTool && headerTitle
289+
? getInProgressActivityLabel(
290+
headerTitle.activeLabel,
291+
headerTool,
292+
tools,
293+
activityDescriptor?.title
294+
)
288295
: 'Thinking'
289296
: tools.length > 0
290297
? getCompletedActivityLabel(tools, activityDescriptor)
@@ -307,7 +314,7 @@ export function AgentGroupView({
307314
) : (
308315
<ActivityStream
309316
activity={{ label: headerText, isActive: headerActive, icon: agentIcon }}
310-
activityKey={statusTool?.id}
317+
activityKey={headerTool?.id}
311318
expandedLabel={pendingInteraction ? undefined : activityDescriptor?.title}
312319
attentionKey={`${getActivityAttentionKey(tools)}:${activeBrowserTakeover?.id ?? ''}`}
313320
collapsible={collapsible}

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

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ describe('AgentGroup inline main activity', () => {
310310
})
311311

312312
it.each(['sim_cli', 'run_code'])(
313-
'shows %s argument preparation before its concrete call',
313+
'names a first %s call by its activity intent, else its own title, while its arguments stream',
314314
(toolName) => {
315315
vi.useFakeTimers()
316316
const render = (
@@ -342,16 +342,107 @@ describe('AgentGroup inline main activity', () => {
342342
)
343343
)
344344
act(() => vi.advanceTimersByTime(1000))
345+
expect(container.textContent).not.toContain('Working')
345346
return container.textContent
346347
}
347-
expect(render({}, 'executing')).toBe('Working…')
348-
const params = { code: '1', activity: { id: 'check', completedTitle: 'Checked inputs' } }
348+
expect(render({}, 'executing')).toBe('Running checks')
349+
expect(render({ activity: { id: 'check', title: 'Checking inputs' } }, 'executing')).toBe(
350+
'Checking inputs'
351+
)
352+
const params = { code: '1', activity: { id: 'check', title: 'Checking inputs' } }
349353
expect(render(params, 'executing')).toBe('Running checks')
350354
expect(render(params, 'success')).toBe('Running checks')
351355
expect(render(params, 'success', false)).toBe('Ran checks')
352356
}
353357
)
354358

359+
it.each([
360+
[
361+
'sim_cli',
362+
{ toolName: 'sim_cli', displayTitle: 'Running CLI command', params: {} },
363+
{
364+
toolName: 'cli_workflows_list',
365+
displayTitle: 'Listing workflows',
366+
params: { args: ['workflows', 'list'] },
367+
},
368+
],
369+
[
370+
'run_code',
371+
{ toolName: 'run_code', displayTitle: 'Running code', params: { activity: { id: 'a' } } },
372+
{
373+
toolName: 'run_code',
374+
displayTitle: 'Summing invoices',
375+
params: { activity: { id: 'a' }, title: 'Summing invoices', code: '1' },
376+
},
377+
],
378+
[
379+
'call_integration_tool',
380+
{ toolName: 'call_integration_tool', displayTitle: 'Calling integration', params: {} },
381+
{
382+
toolName: 'call_integration_tool',
383+
displayTitle: 'Sending the report',
384+
params: {},
385+
streamingArgs: '{"description":"Sending the report",',
386+
},
387+
],
388+
])(
389+
'keeps the previous call in the live header while a %s call has no title',
390+
(_name, pending, titled) => {
391+
vi.useFakeTimers()
392+
const read = {
393+
id: 'read',
394+
toolName: 'read',
395+
displayTitle: 'Reading notes.md',
396+
status: 'success' as const,
397+
}
398+
const render = (next: Partial<ToolCallData>) => {
399+
act(() =>
400+
root.render(
401+
createElement(AgentGroup, {
402+
agentName: 'mothership',
403+
agentLabel: 'Sim',
404+
activity: { id: 'a', title: 'Reconciling accounts' },
405+
isStreaming: true,
406+
isLaneOpen: true,
407+
items: [
408+
{ type: 'tool', data: read },
409+
{
410+
type: 'tool',
411+
data: { id: 'next', status: 'executing', ...next } as ToolCallData,
412+
},
413+
],
414+
})
415+
)
416+
)
417+
act(() => vi.advanceTimersByTime(1000))
418+
const header = container.querySelector('[role="status"]')!
419+
expect(header.querySelector('[class*="shimmer"]')).not.toBeNull()
420+
expect(container.textContent).not.toContain('Working')
421+
return { text: header.textContent, icon: header.querySelector('svg')?.outerHTML }
422+
}
423+
const iconOf = (data: Partial<ToolCallData>) => {
424+
act(() =>
425+
root.render(
426+
createElement(AgentGroup, {
427+
agentName: 'mothership',
428+
agentLabel: 'Sim',
429+
items: [
430+
{ type: 'tool', data: { id: 'solo', status: 'success', ...data } as ToolCallData },
431+
],
432+
})
433+
)
434+
)
435+
return container.querySelector('svg')?.outerHTML
436+
}
437+
const readIcon = iconOf(read)
438+
const titledIcon = iconOf(titled)
439+
expect(readIcon).not.toBe(titledIcon)
440+
441+
expect(render(pending)).toEqual({ text: 'Reading notes.md', icon: readIcon })
442+
expect(render(titled)).toEqual({ text: titled.displayTitle, icon: titledIcon })
443+
}
444+
)
445+
355446
it('keeps an activity unfinished while a standalone approval is pending', () => {
356447
act(() =>
357448
root.render(
@@ -1175,6 +1266,48 @@ describe('AgentGroup nested status line', () => {
11751266
expect(header).toContain('Deploying Invoice Sync as API')
11761267
})
11771268

1269+
it('keeps the previous call in a subagent header while the next call has no title', () => {
1270+
vi.useFakeTimers()
1271+
const header = (items: AgentGroupItem[]) => {
1272+
render(items)
1273+
act(() => vi.advanceTimersByTime(1000))
1274+
const status = container.querySelector('[role="status"]')!
1275+
expect(status.querySelector('[class*="shimmer"]')).not.toBeNull()
1276+
expect(container.textContent).not.toContain('Working')
1277+
return status.textContent
1278+
}
1279+
const cli = (data: Partial<ToolCallData>): AgentGroupItem => ({
1280+
type: 'tool',
1281+
data: { id: 'cli', status: 'executing', startedAt: 2, ...data } as ToolCallData,
1282+
})
1283+
const pending = cli({ toolName: 'sim_cli', displayTitle: 'Running CLI command', params: {} })
1284+
const titled = cli({
1285+
toolName: 'cli_workflows_list',
1286+
displayTitle: 'Listing workflows',
1287+
params: { args: ['workflows', 'list'] },
1288+
})
1289+
try {
1290+
expect(header([namedTool('Reading workflow', 'success' as ToolCallStatus, 1), pending])).toBe(
1291+
'Reading workflow'
1292+
)
1293+
expect(header([namedTool('Reading workflow', 'success' as ToolCallStatus, 1), titled])).toBe(
1294+
'Listing workflows'
1295+
)
1296+
expect(header([pending])).toBe('Running CLI command')
1297+
expect(
1298+
header([
1299+
cli({
1300+
toolName: 'sim_cli',
1301+
displayTitle: 'Running CLI command',
1302+
params: { activity: { id: 'a', title: 'Checking inputs' } },
1303+
}),
1304+
])
1305+
).toBe('Checking inputs')
1306+
} finally {
1307+
vi.useRealTimers()
1308+
}
1309+
})
1310+
11781311
it('falls back to the last tool at any depth when nothing is running', () => {
11791312
const header = render([
11801313
namedTool('Reading workflow', 'success' as ToolCallStatus, 1),

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

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/** @vitest-environment node */
22
import { describe, expect, it } from 'vitest'
3-
import { getCompletedActivityLabel } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group'
3+
import {
4+
getActivityHeaderTool,
5+
getCompletedActivityLabel,
6+
getInProgressActivityLabel,
7+
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group'
48
import type { ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
59

610
/** A finished activity without a model title is labelled by its action summary. */
@@ -134,3 +138,85 @@ describe('interrupted activity summaries', () => {
134138
).toBe('Ran invoice workflow')
135139
})
136140
})
141+
142+
describe('in-progress activity header', () => {
143+
const read = tool('read', 'Reading notes.md')
144+
const pendingCli = tool('sim_cli', 'Running CLI command', 'executing', {
145+
activity: { id: 'a' },
146+
})
147+
const pendingGateway = tool('call_integration_tool', 'Calling integration', 'executing')
148+
149+
it.each([
150+
['a CLI call before its command parses', pendingCli],
151+
['a code call before its arguments resolve', tool('run_code', 'Running code', 'executing')],
152+
['a gateway call before its description streams', pendingGateway],
153+
])('holds the previous call for %s', (_case, pending) => {
154+
const tools = [read, pending]
155+
expect(getActivityHeaderTool(tools, pending)).toBe(read)
156+
expect(getInProgressActivityLabel('Reading notes.md', read, tools, 'Reconciling')).toBe(
157+
'Reading notes.md'
158+
)
159+
})
160+
161+
it('holds the call the header described before, skipping untitled and failed calls', () => {
162+
const failed = tool('grep', 'Searching', 'error')
163+
const tools = [read, failed, pendingGateway, pendingCli]
164+
expect(getActivityHeaderTool(tools, pendingCli)).toBe(read)
165+
})
166+
167+
it('never holds a failed call, falling back to the new call itself', () => {
168+
const failed = tool('grep', 'Searching', 'error')
169+
expect(getActivityHeaderTool([failed, pendingCli], pendingCli)).toBe(pendingCli)
170+
expect(
171+
getInProgressActivityLabel(
172+
'Running CLI command',
173+
pendingCli,
174+
[failed, pendingCli],
175+
'Reconciling'
176+
)
177+
).toBe('Reconciling')
178+
})
179+
180+
it('keeps a call waiting on approval as the header, never holding past or onto it', () => {
181+
const gatedGateway = {
182+
...pendingGateway,
183+
id: 'gateway-gated',
184+
status: 'awaiting_approval' as const,
185+
}
186+
expect(getActivityHeaderTool([read, gatedGateway], gatedGateway)).toBe(gatedGateway)
187+
const gatedRead = { ...read, id: 'read-gated', status: 'awaiting_approval' as const }
188+
expect(getActivityHeaderTool([read, gatedRead, pendingCli], pendingCli)).toBe(read)
189+
})
190+
191+
it.each([
192+
['a parsed CLI command', tool('cli_workflows_list', 'Listing workflows', 'executing')],
193+
[
194+
'resolved code arguments',
195+
tool('run_code', 'Summing', 'executing', { activity: { id: 'a' }, code: '1' }),
196+
],
197+
[
198+
'a streamed gateway description',
199+
{
200+
...pendingGateway,
201+
id: 'gateway-described',
202+
streamingArgs: '{"description":"Sending the report",',
203+
},
204+
],
205+
[
206+
'a model activity description',
207+
{ ...pendingCli, id: 'cli-described', activityDescription: 'Listing the workflows' },
208+
],
209+
])('describes the new call once it has %s', (_case, titled) => {
210+
expect(getActivityHeaderTool([read, titled], titled)).toBe(titled)
211+
})
212+
213+
it('reads a first untitled call as the activity intent, else its own title', () => {
214+
expect(getActivityHeaderTool([pendingCli], pendingCli)).toBe(pendingCli)
215+
expect(
216+
getInProgressActivityLabel('Running CLI command', pendingCli, [pendingCli], 'Reconciling')
217+
).toBe('Reconciling')
218+
expect(
219+
getInProgressActivityLabel('Running CLI command', pendingCli, [pendingCli], undefined)
220+
).toBe('Running CLI command')
221+
})
222+
})

0 commit comments

Comments
 (0)