🔍 fix: Render Web Search Favicons on Raw SERP Results During Streaming (#13741)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run

The streaming favicon stack was gated on `source.processed === true`, but the
agents scrape pipeline marks sources processed only after a `Promise.all`
barrier (the slowest page fetch). Raw SERP results — with everything needed to
render favicons — arrive in the first attachment well before that, so the UI sat
on "Searching the web" with no favicons for the entire scrape window.

Render favicons from the raw sources as soon as they land instead of waiting for
`processed`, filling the dead window and moving the label to "Processing
results" immediately. Completed-state, turn scoping, and finalizing behavior are
unchanged.
This commit is contained in:
Danny Avila 2026-06-14 13:57:23 -04:00 committed by GitHub
parent 7cf2877b45
commit 1ae54b39ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 21 deletions

View file

@ -141,26 +141,21 @@ export default function WebSearch({
return [];
}, [searchResults, attachments, ownTurn]);
const processedSources = useMemo(() => {
// Show favicons from the raw SERP results immediately rather than waiting for
// each source to flip to `processed`; the agents scrape barrier would otherwise
// freeze the stack on "Searching the web" for the slowest scrape's duration.
const streamingSources = useMemo(() => {
if (complete && !finalizing) {
return [];
}
if (!searchResults) {
return [];
}
const result = searchResults[ownTurn];
const result = searchResults?.[ownTurn];
if (!result) {
return [];
}
if (finalizing) {
return [...(result.organic || []), ...(result.topStories || [])];
}
return [...(result.organic || []), ...(result.topStories || [])].filter(
(source) => source.processed === true,
);
return [...(result.organic || []), ...(result.topStories || [])];
}, [searchResults, complete, finalizing, ownTurn]);
const showSources = processedSources.length > 0;
const showSources = streamingSources.length > 0;
const progressText = useMemo(() => {
let text: ProgressKeys =
ownTurn !== '0' ? 'com_ui_web_searching_again' : 'com_ui_web_searching';
@ -278,7 +273,7 @@ export default function WebSearch({
<span className="sr-only" aria-live="polite" aria-atomic="true">
{progressText}
</span>
{showSources && <StackedFavicons sources={processedSources} start={-5} />}
{showSources && <StackedFavicons sources={streamingSources} start={-5} />}
<Globe className="size-4 shrink-0 text-text-secondary" aria-hidden="true" />
<span className="tool-status-text shimmer font-medium text-text-secondary">
{progressText}

View file

@ -205,13 +205,13 @@ describe('WebSearch', () => {
});
});
describe('processedSources scoping', () => {
it('shows processed sources only from ownTurn during streaming', () => {
describe('streaming favicons', () => {
it('renders favicons for all ownTurn sources during streaming, before they are processed', () => {
const searchResults = makeSearchResults({
0: {
organic: [
{ link: 'https://a.com', title: 'A', processed: true } as ValidSource,
{ link: 'https://b.com', title: 'B', processed: false } as ValidSource,
{ link: 'https://b.com', title: 'B' } as ValidSource,
],
},
1: {
@ -229,11 +229,27 @@ describe('WebSearch', () => {
initialProgress: 0.5,
});
const favicons = screen.queryAllByTestId('stacked-favicons');
if (favicons.length > 0) {
const count = Number(favicons[0].getAttribute('data-count'));
expect(count).toBeLessThanOrEqual(2);
}
const favicons = screen.getByTestId('stacked-favicons');
// Both turn-0 sources show immediately — including the unprocessed one —
// while the turn-1 source stays scoped out.
expect(Number(favicons.getAttribute('data-count'))).toBe(2);
expect(screen.getAllByText('Processing results').length).toBeGreaterThanOrEqual(1);
});
it('stays on "Searching the web" until any source for the turn arrives', () => {
const searchResults = makeSearchResults({ 0: { organic: [] } });
const attachments = [makeAttachment(0, searchResults['0'])];
renderWebSearch({
searchResults,
attachments,
isSubmitting: true,
isLast: true,
initialProgress: 0.5,
});
expect(screen.queryByTestId('stacked-favicons')).not.toBeInTheDocument();
expect(screen.getAllByText('Searching the web').length).toBeGreaterThanOrEqual(1);
});
});