🔍 fix: Render Web Search Favicons on Raw SERP Results During Streaming (#13741)

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
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);
});
});