Search Results

A collection the agent retrieved. Filter, sort, page through and pick from it.

Pattern

≈ 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

Filter and sort
Price
€950 – €1,490
Area
38 m² – 95 m²
View

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 total when the source reports more matches than you sent, and hasMore when 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/false for 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. On load-more, fetch the page at offset and send all results so far.
  • Filters are local. Never say the source was filtered unless you searched again.
  • Lifecycle: set fetchedAt and staleAfterMs on the call, since listings, prices and tickets change; the Renderer then offers a refresh. When a search is answered with a new call, supersede the old one. expiresAt isn't needed.

Interaction

  • The search box starts with query. Editing it and pressing Enter (or Search) sends search with the current filters; nothing is filtered locally by the text.
  • The header shows "Showing 8 of 46" only when total is 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 sort option is the starting order; relevance keeps the order the agent sent.
  • Titles link to url in a new tab. Without a url, clicking the title sends open.
  • Load more sends load-more and shows "Loading…" until the component receives more results. New results animate in; filters and sort apply to them straight away.
  • With selectable, the footer sends select (which locks the component into a record of the picks) or dismiss ("None of these"). Nothing else locks.

Installation

npx shadcn@latest add https://your-site.com/r/search-results.json

Usage

app/api/chat/route.ts
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,
  }),
}
components/message.tsx
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

PropTypeNotes
querystringThe query as run. Pre-fills the search box
titlestring?Defaults to "Results for “query”"
totalnumber?Data-bound. Matches at the source; may exceed results
resultsResult[]Data-bound. Up to 200
facetsFacet[]?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
hasMorebooleanDefault false. Adds Load more
selectable"none" | "single" | "multiple"Default "none"
submitLabelstring?Selection button, e.g. "Add to sprint". Default "Select"

Result

FieldTypeNotes
idstringData-bound. The record's real ID
titlestringData-bound
subtitlestring?One line of context
snippetstring?Excerpt; **term** highlights matches
urlstring?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
facetsRecord<string, string | number | boolean | string[]>?Data-bound. Raw values for filters and sorting

Facet

FieldTypeNotes
idstringKey into each result's facets
labelstring
type"checkbox" | "range" | "toggle"
unitstring?After range numbers, e.g. "m²"
format"currency" | "number"?Range number format
currencystring?ISO 4217 code for "currency"

Host props: status (streaming, ready, selected), onResult, result and className.

Source

On this page