From 59702efcc2a313cf9085167eaa8619559469caf5 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:26:25 +0200 Subject: [PATCH] fix(client): gate retrieval expansion on renderable results The row offered an expand affordance whenever raw output existed even when no sources parsed, opening an empty panel. Expansion now keys off parsed results, which also covers attachment-only sources that arrive without text output, and collapses if results disappear. --- .../Chat/Messages/Content/RetrievalCall.tsx | 24 ++++++---- .../Content/__tests__/RetrievalCall.test.tsx | 46 +++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/RetrievalCall.tsx b/client/src/components/Chat/Messages/Content/RetrievalCall.tsx index ea596b97fe..93c66f52d1 100644 --- a/client/src/components/Chat/Messages/Content/RetrievalCall.tsx +++ b/client/src/components/Chat/Messages/Content/RetrievalCall.tsx @@ -7,6 +7,7 @@ import type { TAttachment, TFile, PartMetadata } from 'librechat-data-provider'; import { useLocalize, useProgress, useExpandCollapse } from '~/hooks'; import { ToolIcon, OutputRenderer, isError } from './ToolOutput'; import { resolveToolCallPhase } from '~/utils/toolCallPhase'; +import { toolPanelSpacingClassName } from './disclosure'; import FilePreviewDialog from './FilePreviewDialog'; import { sortPagesByRelevance, cn } from '~/utils'; import { useToolCallIntent } from './Parts/intent'; @@ -380,8 +381,6 @@ export default function RetrievalCall({ }); const hasOutput = !!output && !isError(output); const autoExpand = useRecoilValue(store.autoExpandTools); - const [showOutput, setShowOutput] = useState(() => autoExpand && hasOutput); - const { style: expandStyle, ref: expandRef } = useExpandCollapse(showOutput); const fileSources = useMemo(() => extractFileSources(attachments), [attachments]); const parsedResults = useMemo( @@ -402,6 +401,9 @@ export default function RetrievalCall({ ); const hasResults = displayResults.length > 0; + const hasExpandableContent = phase !== 'failed' && hasResults; + const [showOutput, setShowOutput] = useState(() => autoExpand && hasExpandableContent); + const { style: expandStyle, ref: expandRef } = useExpandCollapse(showOutput); const [previewIndex, setPreviewIndex] = useState(null); @@ -437,10 +439,14 @@ export default function RetrievalCall({ }, [displayResults, previewIndex]); useEffect(() => { - if (autoExpand && hasOutput) { + if (!hasExpandableContent) { + setShowOutput(false); + return; + } + if (autoExpand) { setShowOutput(true); } - }, [autoExpand, hasOutput]); + }, [autoExpand, hasExpandableContent]); const handleToggleOutput = useCallback(() => { setShowOutput((prev) => { @@ -453,7 +459,7 @@ export default function RetrievalCall({ }, [onExpand]); return ( -
+
{(() => { if (phase === 'running') { @@ -474,7 +480,7 @@ export default function RetrievalCall({
} - hasInput={hasOutput} + hasInput={hasExpandableContent} isExpanded={showOutput} />
- {hasOutput && hasResults && ( -
+ {hasExpandableContent && ( +
{displayResults.map((item, i) => { return (
({ jest.mock('~/utils', () => ({ cn: (...classes: (string | boolean | undefined)[]) => classes.filter(Boolean).join(' '), + sortPagesByRelevance: (pages: number[]) => pages, logger: { error: jest.fn(), debug: jest.fn() }, })); @@ -168,6 +169,51 @@ describe('RetrievalCall - LGCY-02: Collapsible output panel', () => { expect(screen.queryByText('file results here')).not.toBeInTheDocument(); }); + it('does not offer expansion when output has no renderable results', () => { + renderRetrievalCall({ + output: 'Search completed without matches', + initialProgress: 1, + isSubmitting: false, + }); + + const progressText = screen.getByTestId('progress-text'); + expect(progressText).toHaveAttribute('data-has-input', 'false'); + + fireEvent.click(progressText); + expect(progressText).toHaveAttribute('data-expanded', 'false'); + }); + + it('offers attachment results even when the tool has no text output', () => { + renderRetrievalCall({ + initialProgress: 1, + isSubmitting: false, + attachments: [ + { + type: 'file_search', + toolCallId: 'call-1', + file_search: { + sources: [ + { + fileId: 'file-123', + fileName: 'notes.txt', + relevance: 0.8, + content: 'Attachment result content', + pages: [], + pageRelevance: {}, + }, + ], + }, + }, + ] as unknown as TAttachment[], + }); + + const progressText = screen.getByTestId('progress-text'); + expect(progressText).toHaveAttribute('data-has-input', 'true'); + + fireEvent.click(progressText); + expect(screen.getByText('Attachment result content')).toBeInTheDocument(); + }); + it('does not show panel when output contains error', () => { renderRetrievalCall({ output: 'error processing tool',