Skip to content

Commit ad66fdf

Browse files
authored
fix(search): use shared inline tool activity (#8233)
* fix(search): use shared inline tool activity * fix(search): clarify activity result states and labels
1 parent b7a8087 commit ad66fdf

15 files changed

Lines changed: 319 additions & 543 deletions

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ describe('flat expanded activity layout', () => {
110110
expect(reservedSlot(column)?.classList).toContain(ICON_SLOT)
111111
})
112112

113-
it('stacks main-lane blocks one gap-3 apart and search queries one gap-1.5 apart', () => {
113+
it('keeps search and ordinary tool rows in the same history with shared spacing', () => {
114114
render('mothership', [
115115
tool('a'),
116116
{
@@ -138,9 +138,13 @@ describe('flat expanded activity layout', () => {
138138
expect(blocks.contains(statuses()[0])).toBe(true)
139139
expect(blocks.classList).toContain('gap-3')
140140
expect(blocks.classList).not.toContain('gap-1.5')
141-
const queries = statuses().filter((status) => status.textContent === 'first')
142-
const searchList = queries[0].closest('.flex-col.gap-1\\.5')!
143-
expect(searchList).not.toBeNull()
144-
expect(searchList.parentElement?.closest('.flex-col.gap-3')).toBe(blocks)
141+
expect(statuses()).toHaveLength(1)
142+
expand()
143+
const rows = statuses().slice(1)
144+
expect(rows).toHaveLength(3)
145+
for (const row of rows) {
146+
expect(row.closest('.flex-col')!.classList).toContain('gap-1.5')
147+
expect(iconSlot(row).classList).toContain(ICON_SLOT)
148+
}
145149
})
146150
})

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { cn, scrollFadeAttributes, scrollFadeClass, useScrollEdges } from '@sim/
66
interface ActivityViewportProps {
77
children: ReactNode
88
isStreaming: boolean
9-
/** A nested blocking interaction must not be clipped by this ancestor's log viewport. */
9+
/** Keeps nested interactions or independently scrolling detail lists from being clipped. */
1010
unbounded?: boolean
1111
}
1212

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function call(id: string, toolName: string, extra: Partial<ToolCallData> = {}):
1414
const layout = (items: AgentGroupItem[]) =>
1515
splitMainLane(items).map((entry) =>
1616
entry.type === 'run'
17-
? `${entry.run.isSearch ? 'search' : 'tools'}:${entry.run.tools.map((tool) => tool.id).join(',')}`
17+
? `tools:${entry.run.tools.map((tool) => tool.id).join(',')}`
1818
: entry.item.type
1919
)
2020

@@ -24,7 +24,7 @@ const liveId = (items: AgentGroupItem[], isOpen = true) => {
2424
}
2525

2626
describe('splitMainLane', () => {
27-
it('splits runs at search boundaries and interactions, in transcript order', () => {
27+
it('groups search with other tools and splits only at interactions, in transcript order', () => {
2828
expect(
2929
layout([
3030
call('a', 'read'),
@@ -35,7 +35,7 @@ describe('splitMainLane', () => {
3535
call('approval', 'edit_workflow', { status: 'awaiting_approval' }),
3636
call('c', 'read'),
3737
])
38-
).toEqual(['tools:a', 'search:s1,s2', 'tools:setup,b', 'tool', 'tools:c'])
38+
).toEqual(['tools:a,s1,s2,setup,b', 'tool', 'tools:c'])
3939
})
4040
})
4141

@@ -53,9 +53,9 @@ describe('getLaneLiveIndicator', () => {
5353
expect(indicator?.type === 'call' && indicator.tool.id).toBe('a')
5454
})
5555

56-
it('gives the gap to a succeeded trailing call, never a finished search or a failure', () => {
56+
it('gives the gap to a succeeded trailing call, including search, never a failure', () => {
5757
expect(liveId([call('s', 'search_workspace'), call('a', 'read')])).toBe('a')
58-
expect(liveId([call('a', 'read'), call('s', 'search_workspace')])).toBeUndefined()
58+
expect(liveId([call('a', 'read'), call('s', 'search_workspace')])).toBe('s')
5959
expect(liveId([call('a', 'read'), call('b', 'read', { status: 'error' })])).toBeUndefined()
6060
expect(liveId([call('a', 'read')], false)).toBeUndefined()
6161
})

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

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {
99
AgentGroupItem,
1010
NestedAgentGroup,
1111
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
12-
import { isSearchActivityTool } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity'
1312
import { needsToolInput } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-interactions'
1413
import { type ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
1514

@@ -27,29 +26,27 @@ function canHoldIndicator(tool: ToolCallData): boolean {
2726
return !needsToolInput(tool) && tool.toolName !== RETIRED_BROWSER_REQUEST_TAKEOVER_ID
2827
}
2928

30-
/** A run of consecutive calls the main lane renders together, as one group or one search list. */
29+
/** A run of consecutive calls the main lane renders together, as one activity group. */
3130
export interface ActivityRun {
3231
tools: ToolCallData[]
33-
isSearch: boolean
3432
}
3533

3634
export type MainLaneEntry =
3735
| { type: 'run'; run: ActivityRun }
3836
| { type: 'item'; item: AgentGroupItem; index: number }
3937

4038
/**
41-
* How the main lane lays out its items: consecutive calls form runs, a search
42-
* call never shares a run with another kind of call, and an interaction stands
39+
* How the main lane lays out its items: consecutive calls form runs,
40+
* including search and document reads, and an interaction stands
4341
* on its own and closes the run before it.
4442
*/
4543
export function splitMainLane(items: AgentGroupItem[]): MainLaneEntry[] {
4644
const entries: MainLaneEntry[] = []
4745
let run: ActivityRun | undefined
4846
for (const [index, item] of items.entries()) {
4947
if (item.type === 'tool' && !isStandaloneItem(item)) {
50-
const isSearch = isSearchActivityTool(item.data)
51-
if (!run || run.isSearch !== isSearch) {
52-
run = { tools: [], isSearch }
48+
if (!run) {
49+
run = { tools: [] }
5350
entries.push({ type: 'run', run })
5451
}
5552
run.tools.push(item.data)
@@ -142,13 +139,10 @@ export interface LaneActivityInput {
142139
isOpen: boolean
143140
}
144141

145-
/**
146-
* The latest call of the main lane's last run, which owns the trailing gap. A
147-
* finished search shows static results, so its gap is never a call's.
148-
*/
142+
/** The latest call of the main lane's last run, which owns the trailing gap. */
149143
function getMainTrailingCall(items: AgentGroupItem[]): ToolCallData | undefined {
150144
const last = splitMainLane(items).at(-1)
151-
return last?.type === 'run' && !last.run.isSearch ? last.run.tools.at(-1) : undefined
145+
return last?.type === 'run' ? last.run.tools.at(-1) : undefined
152146
}
153147

154148
/**
@@ -204,12 +198,11 @@ export interface TurnLiveIndicators {
204198
* call anywhere in the lane, across all of its runs and the lanes nested in
205199
* it. With none running and the lane open, the latest call of its trailing
206200
* run owns the gap, and is live only if it succeeded; an error, rejection,
207-
* stop, skip, or interruption hands the wait to the thinking row. A finished
208-
* main-lane search shows static results, so its gap is never live. A subagent
201+
* stop, skip, or interruption hands the wait to the thinking row. A subagent
209202
* lane's trailing call is its latest call, and an open subagent lane with
210203
* narration but no calls shows its own "Thinking" header.
211-
* - Only the run holding the live call shimmers: its tool group header, or its
212-
* one search row. A parent lane whose live call sits in a nested lane defers
204+
* - Only the run holding the live call shimmers through its tool group header.
205+
* A parent lane whose live call sits in a nested lane defers
213206
* to that lane while the nested lane is still working and visible; a nested
214207
* lane that has ended hands its last call back to the parent.
215208
* - A header reads in the present tense exactly while it is live or its call

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { type ComponentType, Fragment, type ReactNode } from 'react'
22
import type { ToolActivity } from '@/lib/mothership/generated/protocol'
33
import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
44
import { splitMainLane } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/lane-activity'
5-
import { SearchActivity } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity'
65
import { ToolActivityGroup } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group'
76
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
87

@@ -37,10 +36,8 @@ export function MainAgentActivity({
3736
</Fragment>
3837
)
3938
}
40-
const { tools, isSearch } = entry.run
41-
return isSearch ? (
42-
<SearchActivity key={tools[0].id} tools={tools} liveToolId={liveToolId} />
43-
) : (
39+
const { tools } = entry.run
40+
return (
4441
<ToolActivityGroup
4542
key={tools[0].id}
4643
tools={tools}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
collectRetrievalCitationEvidence,
3+
parseCitationRecord,
4+
} from '@/lib/mothership/chat/citation-evidence'
5+
import { SearchActivityResults } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-results'
6+
import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
7+
import { indexSourcesByUrl } from '@/app/workspace/[workspaceId]/home/components/message-content/sources-by-url'
8+
import { type ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
9+
10+
/** Safe sources, an explicit empty result, or no displayable search details. */
11+
export function getSearchActivitySources(tool: ToolCallData): SourceTagData[] | undefined {
12+
if (tool.toolName !== 'search_workspace') return undefined
13+
const evidence = collectRetrievalCitationEvidence([
14+
{ toolCall: { name: tool.toolName, status: tool.status, result: tool.result } },
15+
])
16+
const sources = [...indexSourcesByUrl(evidence.values()).values()]
17+
const output = parseCitationRecord(tool.result?.output)
18+
const data = parseCitationRecord(output?.data) ?? output
19+
const noResults = Boolean(
20+
tool.status === ToolCallStatus.success &&
21+
tool.result?.success &&
22+
output?.success !== false &&
23+
parseCitationRecord(data?.retrieval)?.status !== 'partial' &&
24+
Array.isArray(data?.results) &&
25+
data.results.length === 0
26+
)
27+
28+
return sources.length > 0 || noResults ? sources : undefined
29+
}
30+
31+
interface SearchActivityDetailsProps {
32+
sources: SourceTagData[]
33+
label: string
34+
}
35+
36+
/** Per-call evidence stays in the shared activity history, never in the live header. */
37+
export function SearchActivityDetails({ sources, label }: SearchActivityDetailsProps) {
38+
return sources.length > 0 ? (
39+
<SearchActivityResults sources={sources} label={label} />
40+
) : (
41+
<p className='text-[var(--text-muted)] text-caption'>No results</p>
42+
)
43+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/component
2222

2323
interface SearchActivityResultsProps {
2424
sources: SourceTagData[]
25-
query: string
25+
label: string
2626
}
2727

2828
/**
2929
* Bounded results keep every match available without growing the activity
3030
* transcript. The list shows four and a half 32px rows, so a clipped row signals
3131
* that it scrolls.
3232
*/
33-
export function SearchActivityResults({ sources, query }: SearchActivityResultsProps) {
33+
export function SearchActivityResults({ sources, label }: SearchActivityResultsProps) {
3434
const scrollRef = useRef<HTMLDivElement>(null)
3535
const edges = useScrollEdges(scrollRef)
3636

@@ -39,7 +39,7 @@ export function SearchActivityResults({ sources, query }: SearchActivityResultsP
3939
<div
4040
ref={scrollRef}
4141
role='region'
42-
aria-label={`Results for ${query}`}
42+
aria-label={label}
4343
className={cn('max-h-[152px] overflow-y-auto overscroll-contain p-1', scrollFadeClass)}
4444
{...scrollFadeAttributes(edges)}
4545
>

0 commit comments

Comments
 (0)