Skip to content

Commit 6377330

Browse files
authored
improvement(search): title search activity by intent, collapse finished searches, and tighten results (#8229)
1 parent 75fbc72 commit 6377330

4 files changed

Lines changed: 271 additions & 59 deletions

File tree

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ interface SearchActivityResultsProps {
2525
query: string
2626
}
2727

28-
/** Bounded results keep every match available without growing the activity transcript. */
28+
/**
29+
* Bounded results keep every match available without growing the activity
30+
* transcript. The list shows four and a half 32px rows, so a clipped row signals
31+
* that it scrolls.
32+
*/
2933
export function SearchActivityResults({ sources, query }: SearchActivityResultsProps) {
3034
const scrollRef = useRef<HTMLDivElement>(null)
3135
const edges = useScrollEdges(scrollRef)
@@ -36,7 +40,7 @@ export function SearchActivityResults({ sources, query }: SearchActivityResultsP
3640
ref={scrollRef}
3741
role='region'
3842
aria-label={`Results for ${query}`}
39-
className={cn('max-h-[184px] overflow-y-auto overscroll-contain p-1', scrollFadeClass)}
43+
className={cn('max-h-[152px] overflow-y-auto overscroll-contain p-1', scrollFadeClass)}
4044
{...scrollFadeAttributes(edges)}
4145
>
4246
<ul className='m-0 list-none p-0'>
@@ -50,7 +54,7 @@ export function SearchActivityResults({ sources, query }: SearchActivityResultsP
5054
rel='noopener noreferrer'
5155
onClick={(event) => handleExternalLinkClick(event, source.url)}
5256
className={cn(
53-
'not-prose flex h-10 min-w-0 items-center gap-2 px-2 text-[var(--text-body)] text-small no-underline transition-colors focus-visible:bg-[var(--surface-hover)] focus-visible:outline-none',
57+
'not-prose flex h-8 min-w-0 items-center gap-2 px-2 text-[var(--text-body)] text-small no-underline transition-colors focus-visible:bg-[var(--surface-hover)] focus-visible:outline-none',
5458
chipHoverSurfaceClass,
5559
chipRadiusClass
5660
)}

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

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,17 @@ describe('inline search activity', () => {
7070
tools={[{ ...completedSearch('search-1', 'Launch review'), params: tool.params }]}
7171
/>
7272
)
73-
expect(header().getAttribute('aria-expanded')).toBe('true')
74-
act(() => header().click())
7573
expect(header().getAttribute('aria-expanded')).toBe('false')
74+
act(() => header().click())
75+
expect(header().getAttribute('aria-expanded')).toBe('true')
7676
})
7777

7878
it('renders a completed query field from partial streamed arguments and retains stopped status', () => {
7979
const streamed = { ...tool, params: undefined, streamingArgs: '{"query":"launch review"' }
8080
render(<SearchActivity tools={[streamed]} />)
8181
expect(container.textContent).toContain('launch review')
8282
render(<SearchActivity tools={[{ ...streamed, status: 'cancelled' }]} />)
83-
expect(container.textContent).toContain('Search stopped')
83+
expect(container.textContent).toBe('launch review · stopped')
8484
expect(container.textContent).not.toContain('Searched sources')
8585
})
8686

@@ -99,6 +99,16 @@ describe('inline search activity', () => {
9999
{ ...tool, toolName: 'search_sources', status: 'success', params: { action: 'list' } },
100100
'Checked connected sources',
101101
],
102+
[
103+
'failed sources check',
104+
{ ...tool, toolName: 'search_sources', status: 'error', params: { action: 'list' } },
105+
'Checked connected sources',
106+
],
107+
[
108+
'stopped sources check',
109+
{ ...tool, toolName: 'search_sources', status: 'cancelled', params: { action: 'list' } },
110+
'Connected sources',
111+
],
102112
] as const)(
103113
'labels a %s search and leaves it static unless its lane names it live',
104114
(_state, call, label) => {
@@ -205,26 +215,27 @@ function completedSearch(id: string, title: string): ToolCallData {
205215
it('keeps all ranked matches in each query and preserves disclosure state when later queries arrive', () => {
206216
const first = completedSearch('one', 'First query')
207217
render(<SearchActivity tools={[first]} />)
218+
expect(header().getAttribute('aria-expanded')).toBe('false')
219+
act(() => header().click())
220+
expect(header().getAttribute('aria-expanded')).toBe('true')
208221
expect(container.querySelectorAll('a')).toHaveLength(3)
209222
expect(container.textContent).toContain('First query first')
210223
expect(container.textContent).not.toContain('First query duplicate')
211224
expect(container.textContent).not.toContain('Unsafe result')
212225
expect(container.textContent).toContain('example.com')
213-
expect(header().getAttribute('aria-expanded')).toBe('true')
214-
act(() => header().click())
215-
expect(header().getAttribute('aria-expanded')).toBe('false')
216226
render(<SearchActivity tools={[first, completedSearch('two', 'Second query')]} />)
217-
expect(header().getAttribute('aria-expanded')).toBe('false')
227+
expect(header().getAttribute('aria-expanded')).toBe('true')
218228
const headers = container.querySelectorAll<HTMLElement>('[role="button"]')
219-
expect(headers[1].getAttribute('aria-expanded')).toBe('true')
229+
expect(headers[1].getAttribute('aria-expanded')).toBe('false')
220230
expect(container.querySelectorAll('a')).toHaveLength(3)
221-
act(() => header().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
231+
act(() => headers[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
222232
expect(container.querySelectorAll('a')).toHaveLength(6)
223233
expect(container.querySelectorAll('[role="region"]')).toHaveLength(2)
224234
})
225235

226236
it('falls back to a document icon when a source favicon is unavailable', () => {
227237
render(<SearchActivity tools={[completedSearch('one', 'First query')]} />)
238+
act(() => header().click())
228239
const firstLink = container.querySelector('a')!
229240
expect(firstLink.querySelector('img')).not.toBeNull()
230241
act(() => firstLink.querySelector('img')!.dispatchEvent(new Event('error')))
@@ -235,8 +246,8 @@ it('falls back to a document icon when a source favicon is unavailable', () => {
235246
it('distinguishes empty search results from interrupted calls and never previews failed output', () => {
236247
render(<SearchActivity tools={[{ ...completedSearch('one', 'First query'), status: 'error' }]} />)
237248
expect(container.querySelectorAll('a')).toHaveLength(0)
238-
expect(container.textContent).toContain('Search failed')
239-
expect(container.textContent).not.toContain('No results found')
249+
expect(container.textContent).toBe('First query')
250+
expect(container.querySelector('[role="button"]')).toBeNull()
240251
render(
241252
<SearchActivity
242253
tools={[
@@ -248,5 +259,147 @@ it('distinguishes empty search results from interrupted calls and never previews
248259
]}
249260
/>
250261
)
251-
expect(container.textContent).toContain('No results found')
262+
expect(container.textContent).toBe('Launch review · in:launch review · no results')
263+
})
264+
265+
describe('described search rows', () => {
266+
const described: ToolCallData = { ...tool, activityDescription: 'Searching launch review notes' }
267+
const headerText = () => container.querySelector('[role="status"]')!.textContent
268+
const outcomeSuffix = () => container.querySelector('[role="status"] + span')
269+
270+
it.each([
271+
['executing', 'Searching launch review notes', undefined],
272+
['success', 'Searched launch review notes', ' · 3 results'],
273+
['error', 'Searching launch review notes', undefined],
274+
['rejected', 'Searching launch review notes', ' · declined'],
275+
['cancelled', 'Stopped searching launch review notes', undefined],
276+
['interrupted', 'Stopped searching launch review notes', undefined],
277+
['skipped', 'Skipped searching launch review notes', undefined],
278+
] as const)('titles a %s call with the shared status wording', (status, title, suffix) => {
279+
render(
280+
<SearchActivity
281+
tools={[
282+
{
283+
...completedSearch('one', 'Launch review'),
284+
params: tool.params,
285+
activityDescription: described.activityDescription,
286+
status,
287+
},
288+
]}
289+
/>
290+
)
291+
expect(headerText()).toBe(title)
292+
expect(outcomeSuffix()?.textContent).toBe(suffix)
293+
})
294+
295+
it('reads in progress only while running, and never on a finished call', () => {
296+
render(<SearchActivity tools={[described]} liveToolId={described.id} />)
297+
expect(headerText()).toBe('Searching launch review notes')
298+
expect(container.querySelector('[class*="shimmer"]')).not.toBeNull()
299+
render(<SearchActivity tools={[{ ...described, status: 'success' }]} />)
300+
expect(headerText()).toBe('Searched launch review notes')
301+
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
302+
})
303+
304+
it('moves the raw query into one truncated line at the top of the body', () => {
305+
render(<SearchActivity tools={[described]} />)
306+
expect(header().getAttribute('aria-expanded')).toBe('true')
307+
const body = container.querySelector(`#${CSS.escape(header().getAttribute('aria-controls')!)}`)!
308+
const queryLine = body.querySelector('.text-caption')!
309+
expect(queryLine.textContent).toBe('Launch review · in:launch review')
310+
expect(queryLine.className).toContain('whitespace-nowrap')
311+
expect(queryLine.className).toContain('overflow-hidden')
312+
expect(headerText()).not.toContain('Launch review')
313+
})
314+
315+
it('shows the query only once when there is no description', () => {
316+
render(<SearchActivity tools={[completedSearch('one', 'Launch review')]} />)
317+
act(() => header().click())
318+
expect(container.textContent?.match(/Launch review(?! first| second| third)/g)).toHaveLength(1)
319+
})
320+
321+
it('opens while running, closes on completion, and keeps a manual choice either way', () => {
322+
render(<SearchActivity tools={[described]} />)
323+
expect(header().getAttribute('aria-expanded')).toBe('true')
324+
render(<SearchActivity tools={[{ ...described, status: 'success' }]} />)
325+
expect(header().getAttribute('aria-expanded')).toBe('false')
326+
327+
act(() => root.unmount())
328+
root = createRoot(container)
329+
render(<SearchActivity tools={[described]} />)
330+
act(() => header().click())
331+
expect(header().getAttribute('aria-expanded')).toBe('false')
332+
render(<SearchActivity tools={[{ ...described, status: 'success' }]} />)
333+
expect(header().getAttribute('aria-expanded')).toBe('false')
334+
act(() => header().click())
335+
expect(header().getAttribute('aria-expanded')).toBe('true')
336+
337+
act(() => root.unmount())
338+
root = createRoot(container)
339+
render(<SearchActivity tools={[described]} />)
340+
act(() => header().click())
341+
act(() => header().click())
342+
render(<SearchActivity tools={[{ ...described, status: 'success' }]} />)
343+
expect(header().getAttribute('aria-expanded')).toBe('true')
344+
})
345+
346+
it('renders a reloaded message with every finished search collapsed', () => {
347+
render(
348+
<SearchActivity
349+
tools={[
350+
{ ...completedSearch('one', 'First query'), activityDescription: 'Searching specs' },
351+
completedSearch('two', 'Second query'),
352+
{ ...described, id: 'three', status: 'error' },
353+
]}
354+
/>
355+
)
356+
const headers = [...container.querySelectorAll<HTMLElement>('[role="button"]')]
357+
expect(headers.map((row) => row.getAttribute('aria-expanded'))).toEqual([
358+
'false',
359+
'false',
360+
'false',
361+
])
362+
expect(container.querySelectorAll('a')).toHaveLength(0)
363+
})
364+
365+
it('keeps a failed search silent: static neutral title, no failure text or error styling', () => {
366+
render(<SearchActivity tools={[{ ...tool, status: 'error' }]} />)
367+
expect(container.querySelector('[role="button"]')).toBeNull()
368+
expect(container.textContent).toBe('Launch review · in:launch review')
369+
render(
370+
<SearchActivity tools={[{ ...completedSearch('one', 'x'), ...described, status: 'error' }]} />
371+
)
372+
expect(header().getAttribute('aria-expanded')).toBe('false')
373+
expect(headerText()).toBe('Searching launch review notes')
374+
expect(outcomeSuffix()).toBeNull()
375+
expect(container.querySelector('[class*="shimmer"]')).toBeNull()
376+
act(() => header().click())
377+
expect(container.querySelectorAll('a')).toHaveLength(0)
378+
expect(container.textContent).toBe(
379+
'Searching launch review notesLaunch review · in:launch review'
380+
)
381+
expect(container.textContent).not.toMatch(/fail/i)
382+
expect(container.innerHTML).not.toContain('--text-error')
383+
})
384+
385+
it('keeps user-initiated and empty outcomes as muted suffixes', () => {
386+
render(<SearchActivity tools={[{ ...described, status: 'rejected' }]} />)
387+
expect(outcomeSuffix()?.textContent).toBe(' · declined')
388+
expect(outcomeSuffix()?.className).toContain('text-[var(--text-tertiary)]')
389+
render(<SearchActivity tools={[{ ...tool, status: 'skipped' }]} />)
390+
expect(outcomeSuffix()?.textContent).toBe(' · skipped')
391+
render(<SearchActivity tools={[completedSearch('one', 'First query')]} />)
392+
expect(outcomeSuffix()?.textContent).toBe(' · 3 results')
393+
expect(container.innerHTML).not.toContain('--text-error')
394+
})
395+
396+
it('lays results out in dense rows with a matching bounded list', () => {
397+
render(<SearchActivity tools={[completedSearch('one', 'First query')]} />)
398+
act(() => header().click())
399+
expect(container.querySelector('[role="region"]')!.className).toContain('max-h-[152px]')
400+
for (const link of container.querySelectorAll('a')) {
401+
expect(link.className).toContain('h-8')
402+
expect(link.className).not.toContain('h-10')
403+
}
404+
})
252405
})

0 commit comments

Comments
 (0)