diff --git a/web-app/src/components/ai-elements/tools/renderers/web-search-tool-renderer.tsx b/web-app/src/components/ai-elements/tools/renderers/web-search-tool-renderer.tsx
index 57f1d6f4b..e14448118 100644
--- a/web-app/src/components/ai-elements/tools/renderers/web-search-tool-renderer.tsx
+++ b/web-app/src/components/ai-elements/tools/renderers/web-search-tool-renderer.tsx
@@ -28,8 +28,9 @@ export function WebSearchToolRenderer({
)}
- {results.length > 0 &&
- results.slice(0, 4).map((result, index) => (
+ {results.length > 0 && (
+
+ {results.map((result, index) => (
- ))}
+ ))}
+
+ )}
)
}
diff --git a/web-app/src/lib/tools/presenters/__tests__/web-search-exa.test.ts b/web-app/src/lib/tools/presenters/__tests__/web-search-exa.test.ts
new file mode 100644
index 000000000..655a115fd
--- /dev/null
+++ b/web-app/src/lib/tools/presenters/__tests__/web-search-exa.test.ts
@@ -0,0 +1,71 @@
+import { describe, it, expect } from 'vitest'
+import { presentWebSearchExa } from '../web-search-exa'
+
+// Exa returns every result inside a single output text item, with individual
+// results separated by a `---` horizontal rule.
+const makeEntry = (n: number) =>
+ [
+ `Title: Result ${n}`,
+ `URL: https://example.com/${n}`,
+ `Published: 2024-01-0${n}`,
+ `Author: Author ${n}`,
+ 'Highlights:',
+ `highlight ${n}a`,
+ `highlight ${n}b`,
+ ].join('\n')
+
+const combinedBlock = (count: number) =>
+ Array.from({ length: count }, (_, i) => makeEntry(i + 1)).join('\n\n---\n\n')
+
+describe('presentWebSearchExa', () => {
+ it('renders every result when Exa packs them into one text block', () => {
+ const presentation = presentWebSearchExa({
+ input: { query: 'cafe photos' },
+ output: [{ text: combinedBlock(5) }],
+ })
+
+ expect(presentation.kind).toBe('web_search_exa')
+ expect(presentation.results).toHaveLength(5)
+ expect(presentation.subtitle).toBe('5 results')
+ expect(presentation.results.map((r) => r.url)).toEqual([
+ 'https://example.com/1',
+ 'https://example.com/2',
+ 'https://example.com/3',
+ 'https://example.com/4',
+ 'https://example.com/5',
+ ])
+ expect(presentation.results[0].title).toBe('Result 1')
+ expect(presentation.results[0].domain).toBe('example.com')
+ expect(presentation.results[0].highlights).toEqual([
+ 'highlight 1a',
+ 'highlight 1b',
+ ])
+ })
+
+ it('handles a single result with no separator', () => {
+ const presentation = presentWebSearchExa({
+ input: { query: 'q' },
+ output: [{ text: makeEntry(1) }],
+ })
+
+ expect(presentation.results).toHaveLength(1)
+ expect(presentation.subtitle).toBe('1 result')
+ expect(presentation.results[0].title).toBe('Result 1')
+ })
+
+ it('still handles one result per output item (array shape)', () => {
+ const presentation = presentWebSearchExa({
+ input: { query: 'q' },
+ output: [{ text: makeEntry(1) }, { text: makeEntry(2) }],
+ })
+
+ expect(presentation.results).toHaveLength(2)
+ expect(presentation.subtitle).toBe('2 results')
+ })
+
+ it('returns no results for empty output', () => {
+ const presentation = presentWebSearchExa({ input: { query: 'q' }, output: [] })
+ expect(presentation.results).toHaveLength(0)
+ expect(presentation.subtitle).toBeUndefined()
+ })
+})
diff --git a/web-app/src/lib/tools/presenters/web-search-exa.ts b/web-app/src/lib/tools/presenters/web-search-exa.ts
index b4074f9e3..7e3b94ecd 100644
--- a/web-app/src/lib/tools/presenters/web-search-exa.ts
+++ b/web-app/src/lib/tools/presenters/web-search-exa.ts
@@ -9,6 +9,17 @@ type SearchResultCard = {
highlights: string[]
}
+// Exa concatenates multiple results into a single text block, separated by a
+// horizontal rule (`---`). Split them so each result becomes its own card.
+// Falls back to the whole string when there is no separator (a single result,
+// or one result per output item), so both output shapes are handled.
+function splitExaResults(text: string): string[] {
+ return text
+ .split(/\n\s*-{3,}\s*\n/)
+ .map((chunk) => chunk.trim())
+ .filter((chunk) => chunk.includes('Title:'))
+}
+
function parseExaTextEntry(text: string): SearchResultCard | undefined {
const lines = text.split('\n').map((l) => l.trim())
@@ -70,7 +81,8 @@ export function presentWebSearchExa(args: {
(item): item is { text?: string } =>
!!item && typeof item === 'object' && 'text' in item
)
- .map((item) => parseExaTextEntry(item.text ?? ''))
+ .flatMap((item) => splitExaResults(item.text ?? ''))
+ .map((entry) => parseExaTextEntry(entry))
.filter(isSearchResultCard)
return {