Search Results
A collection the agent retrieved. Filter, sort, page through and pick from it.
≈ 2,469 tokens per use (tool 950 · guidance 291 · typical payload 1,228)How token costs are estimated
Example: Search Results
Find apartments under €1,500 near Alfama
Apartments near Alfama
Showing 8 of 46
When to use
- The agent retrieved a collection the user will browse and narrow: listings, products, articles, tickets, people.
- There are enough results (roughly 5 or more) that filtering, sorting or paging helps.
When not to use
- There are 1–3 results. Describe them, or use entity cards.
- The user needs to compare records across many columns or act on them in bulk. Use a Data Table.
- The user asked a question with one answer. Answer it.
How the agent should use it
- Every result, total and facet value comes from a tool result. Never invent results or counts.
- Send the query as you ran it. Set
totalwhen the source reports more matches than you sent, andhasMorewhen it can return another page. - Only define facets that have values in the results, and put each result's raw values in its
facets(numbers for ranges,true/falsefor toggles, strings or string arrays for checkboxes). Sort keys read from the same place, so add an ISO date there for "Recently updated". - Highlight matched terms in snippets with
**double asterisks**, nothing else. - On
search, run the query again with the filters. Onload-more, fetch the page atoffsetand send all results so far. - Filters are local. Never say the source was filtered unless you searched again.
- Lifecycle: set
fetchedAtandstaleAfterMson the call, since listings, prices and tickets change; the Renderer then offers a refresh. When asearchis answered with a new call, supersede the old one.expiresAtisn't needed.
Interaction
- The search box starts with
query. Editing it and pressing Enter (or Search) sendssearchwith the current filters; nothing is filtered locally by the text. - The header shows "Showing 8 of 46" only when
totalis larger than what was sent. Once filters are on, "5 match your filters" is announced to screen readers; the list itself shows the matches. - Checkbox facets are dropdown multi-selects with a count per option. Counts respect the other active filters, like real faceted search. Range facets are two-thumb sliders with bounds taken from the results; an edge left at its bound stays open, so later pages are still covered. Toggles are switches.
- Each active filter is a removable chip, with Clear all. If nothing matches, the list says "No matches" with Clear filters.
- Sort and the list/grid layout are local too. The first
sortoption is the starting order;relevancekeeps the order the agent sent. - Titles link to
urlin a new tab. Without aurl, clicking the title sendsopen. - Load more sends
load-moreand shows "Loading…" until the component receives more results. New results animate in; filters and sort apply to them straight away. - With
selectable, the footer sendsselect(which locks the component into a record of the picks) ordismiss("None of these"). Nothing else locks.
Installation
npx shadcn@latest add https://your-site.com/r/search-results.json
Usage
import { tool } from "ai"
import { searchResultsTool } from "@/components/genui/search-results.schema"
const tools = {
// No execute(): the call is completed by the user's first action.
[searchResultsTool.name]: tool({
description: searchResultsTool.description,
inputSchema: searchResultsTool.inputSchema,
}),
}import { SearchResults } from "@/components/genui/search-results"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-show_search_results") return null
return (
<SearchResults
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "show_search_results",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{ action: "search", query: "apartments near Graça", filters: { price: { max: 1300 }, bedrooms: ["2"] } }
{ action: "load-more", query: "apartments under €1,500 near Alfama", filters: {}, offset: 8 }
{ action: "select", ids: ["PAY-412", "PAY-359"] }
{ action: "open", id: "PAY-405" }
{ action: "dismiss" }filters holds only active filters: checkbox values as strings, ranges as { min?, max? } with only the narrowed edges, toggles as true.
For load-more, the agent answers with all results so far. To keep one component per search, pass the newer call's props to the same component: "Loading…" clears when the longer results arrive. Otherwise render the new call and mark the old one superseded with the Renderer's markSuperseded.
With LangGraph
Push it from a graph node with ui.push({ name: "show_search_results", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer. For the next page, push again with the same UI message id and the longer results, so the same component updates in place.
Restoring from history
When a conversation is reloaded, pass the stored result back as result. A select renders the read-only record of the picked results. The other actions show what was asked ("Asked for more results") with filtering still available locally. It never calls onResult again. With the AI SDK, that's the tool call's output:
<SearchResults
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
query | string | The query as run. Pre-fills the search box |
title | string? | Defaults to "Results for “query”" |
total | number? | Data-bound. Matches at the source; may exceed results |
results | Result[] | Data-bound. Up to 200 |
facets | Facet[]? | Up to 6 filter definitions. Options and counts come from the results |
sort | { id, label, key, direction }[]? | Up to 6. key is a facets key or "relevance"; the first is the starting order |
view | "list" | "grid" | Default "list". Grid for listings and products |
hasMore | boolean | Default false. Adds Load more |
selectable | "none" | "single" | "multiple" | Default "none" |
submitLabel | string? | Selection button, e.g. "Add to sprint". Default "Select" |
Result
| Field | Type | Notes |
|---|---|---|
id | string | Data-bound. The record's real ID |
title | string | Data-bound |
subtitle | string? | One line of context |
snippet | string? | Excerpt; **term** highlights matches |
url | string? | http(s) only. Without it, the title sends open |
image | { src, alt }? | http(s) only |
meta | { label, value }[]? | Up to 4, e.g. Price, Area, Updated |
facets | Record<string, string | number | boolean | string[]>? | Data-bound. Raw values for filters and sorting |
Facet
| Field | Type | Notes |
|---|---|---|
id | string | Key into each result's facets |
label | string | |
type | "checkbox" | "range" | "toggle" | |
unit | string? | After range numbers, e.g. "m²" |
format | "currency" | "number"? | Range number format |
currency | string? | ISO 4217 code for "currency" |
Host props: status (streaming, ready, selected), onResult, result and className.
Source
Comparison Matrix
Two to five options side by side across shared criteria, with the differences and the best value per row marked, and optionally one to pick.
Relationship Explorer
How things connect, as an interactive graph you can pan, zoom and select from. Hierarchies, dependencies, cause and effect, networks.




