From e1e9b340f397f38d5d69fa5c562f0c4ef3416921 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 30 Sep 2026 14:23:53 -0700 Subject: [PATCH] fix(chat): keep empty search activity collapsed --- .../agent-group/search-activity-details.ts | 14 +++++ .../agent-group/search-activity-details.tsx | 43 --------------- .../agent-group/tool-activity-group.test.tsx | 52 +++++++++++++++---- .../agent-group/tool-activity-group.tsx | 8 ++- 4 files changed, 59 insertions(+), 58 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.ts new file mode 100644 index 00000000000..08bf945a4bc --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.ts @@ -0,0 +1,14 @@ +import { collectRetrievalCitationEvidence } from '@/lib/mothership/chat/citation-evidence' +import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' +import { indexSourcesByUrl } from '@/app/workspace/[workspaceId]/home/components/message-content/sources-by-url' +import type { ToolCallData } from '@/app/workspace/[workspaceId]/home/types' + +/** Only searches with safe sources have displayable details. */ +export function getSearchActivitySources(tool: ToolCallData): SourceTagData[] | undefined { + if (tool.toolName !== 'search_workspace') return undefined + const evidence = collectRetrievalCitationEvidence([ + { toolCall: { name: tool.toolName, status: tool.status, result: tool.result } }, + ]) + const sources = [...indexSourcesByUrl(evidence.values()).values()] + return sources.length > 0 ? sources : undefined +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.tsx deleted file mode 100644 index d36f0297c89..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - collectRetrievalCitationEvidence, - parseCitationRecord, -} from '@/lib/mothership/chat/citation-evidence' -import { SearchActivityResults } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-results' -import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' -import { indexSourcesByUrl } from '@/app/workspace/[workspaceId]/home/components/message-content/sources-by-url' -import { type ToolCallData, ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' - -/** Safe sources, an explicit empty result, or no displayable search details. */ -export function getSearchActivitySources(tool: ToolCallData): SourceTagData[] | undefined { - if (tool.toolName !== 'search_workspace') return undefined - const evidence = collectRetrievalCitationEvidence([ - { toolCall: { name: tool.toolName, status: tool.status, result: tool.result } }, - ]) - const sources = [...indexSourcesByUrl(evidence.values()).values()] - const output = parseCitationRecord(tool.result?.output) - const data = parseCitationRecord(output?.data) ?? output - const noResults = Boolean( - tool.status === ToolCallStatus.success && - tool.result?.success && - output?.success !== false && - parseCitationRecord(data?.retrieval)?.status !== 'partial' && - Array.isArray(data?.results) && - data.results.length === 0 - ) - - return sources.length > 0 || noResults ? sources : undefined -} - -interface SearchActivityDetailsProps { - sources: SourceTagData[] - label: string -} - -/** Per-call evidence stays in the shared activity history, never in the live header. */ -export function SearchActivityDetails({ sources, label }: SearchActivityDetailsProps) { - return sources.length > 0 ? ( - - ) : ( -

No results

- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.tsx index 80fb8472a62..a56985eba48 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.test.tsx @@ -3,7 +3,7 @@ */ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ActivityStatus } from '@/components/ui/activity-status' import { ToolActivityGroup } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group' import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item' @@ -46,6 +46,8 @@ describe('ToolActivityGroup search disclosure', () => { let root: Root beforeEach(() => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + vi.useFakeTimers() container = document.createElement('div') document.body.append(container) root = createRoot(container) @@ -54,12 +56,13 @@ describe('ToolActivityGroup search disclosure', () => { afterEach(() => { act(() => root.unmount()) container.remove() + vi.useRealTimers() }) - function render(tool: ToolCallData, isLive: boolean) { + function render(tools: ToolCallData[], isLive: boolean) { act(() => root.render( - + ) ) } @@ -71,26 +74,55 @@ describe('ToolActivityGroup search disclosure', () => { } it('opens when live results arrive, closes for the answer, and respects manual choices', () => { - render(executingSearch, true) + render([executingSearch], true) expect(container.querySelector('[role="button"][aria-expanded]')).toBeNull() - render(completedSearch, true) + render([completedSearch], true) expect(disclosure().getAttribute('aria-expanded')).toBe('true') expect(container.querySelector('a[href="https://example.test/guide"]')).not.toBeNull() - render(completedSearch, false) + render([completedSearch], false) expect(disclosure().getAttribute('aria-expanded')).toBe('false') act(() => disclosure().click()) expect(disclosure().getAttribute('aria-expanded')).toBe('true') - render(completedSearch, false) + render([completedSearch], false) expect(disclosure().getAttribute('aria-expanded')).toBe('true') - render(completedSearch, true) + render([completedSearch], true) expect(disclosure().getAttribute('aria-expanded')).toBe('true') act(() => disclosure().click()) - render(completedSearch, false) - render(completedSearch, true) + render([completedSearch], false) + render([completedSearch], true) expect(disclosure().getAttribute('aria-expanded')).toBe('false') }) + + it('keeps empty searches out of the disclosure while subsequent tools run', () => { + const emptySearch: ToolCallData = { + ...completedSearch, + id: 'empty-search', + result: { success: true, output: { success: true, data: { results: [] } } }, + } + const runningRead: ToolCallData = { + id: 'read-1', + toolName: 'read_document', + displayTitle: 'Reading document', + status: 'executing', + } + + render([emptySearch], true) + expect.soft(container.querySelector('[role="button"][aria-expanded]')).toBeNull() + + render([emptySearch, runningRead], true) + expect.soft(disclosure().getAttribute('aria-expanded')).toBe('false') + + if (disclosure().getAttribute('aria-expanded') === 'false') { + act(() => disclosure().click()) + } + expect.soft(container.textContent).not.toContain('No results') + + render([emptySearch, completedSearch], true) + expect(container.querySelector('a[href="https://example.test/guide"]')).not.toBeNull() + expect(container.textContent).not.toContain('No results') + }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx index 6e5b6880956..d3ff86a946a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx @@ -16,10 +16,8 @@ import { import { getToolStatusDisplayTitle } from '@/lib/mothership/tools/tool-display' import { ActivityStream } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-stream' import { getNewestRunningTool } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-content' -import { - getSearchActivitySources, - SearchActivityDetails, -} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details' +import { getSearchActivitySources } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-details' +import { SearchActivityResults } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-results' import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item' import { getActivityAttentionKey, @@ -276,7 +274,7 @@ export function ToolActivityGroup({ )} {sources && ( -