React UI Base
Loading...

ThreadHistory

Compose thread list browsing, search filtering, and thread switching with unstyled parts.

ThreadHistory

ThreadHistory in @tambo-ai/react-ui-base owns thread collection state, search filtering, and selection behavior while keeping styling in @tambo-ai/ui-registry or your own UI layer.

Demo

Loading...

import { ThreadHistory } from "@tambo-ai/react-ui-base/thread-history";import { Plus, Search } from "lucide-react";export function DemoThreadHistory() {  return (    <ThreadHistory.Root>      <div className="flex flex-col gap-3">        <div className="flex items-center gap-2">          <div className="relative flex-1">            <Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-neutral-400" />            <ThreadHistory.Search className="w-full rounded-lg border border-neutral-200 bg-transparent py-1.5 pl-8 pr-3 text-sm placeholder:text-neutral-400 focus:outline-none focus:ring-1 focus:ring-neutral-300 dark:border-neutral-700 dark:focus:ring-neutral-600" />          </div>          <ThreadHistory.NewThreadButton className="flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-900 text-white hover:bg-neutral-700 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-300">            <Plus className="h-4 w-4" />          </ThreadHistory.NewThreadButton>        </div>        <ThreadHistory.List          render={(props, state) => (            <div {...props} className="flex flex-col gap-1">              {state.isLoading && <p className="py-2 text-center text-sm text-neutral-500">Loading...</p>}              {state.isEmpty && !state.isLoading && <p className="py-2 text-center text-sm text-neutral-500">No threads found</p>}              {state.filteredThreads.map((thread) => (                <ThreadHistory.Item                  key={thread.id}                  thread={thread}                  className="w-full rounded-lg px-3 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 data-[active]:bg-neutral-100 data-[active]:font-medium dark:text-neutral-300 dark:hover:bg-neutral-800"                >                  {thread.id}                </ThreadHistory.Item>              ))}            </div>          )}        />      </div>    </ThreadHistory.Root>  );}

Anatomy

<ThreadHistory.Root>
  <ThreadHistory.Search />
  <ThreadHistory.NewThreadButton />
  <ThreadHistory.List>
    <ThreadHistory.Item thread={thread} />
  </ThreadHistory.List>
</ThreadHistory.Root>

Examples

Active Thread Highlighting

Use the data-active attribute on Item to style the currently selected thread:

<ThreadHistory.Item
  thread={thread}
  className="rounded px-2 py-1 data-active:bg-blue-100"
>
  {thread.id}
</ThreadHistory.Item>

Thread Change Callback

Respond to thread switches and new thread creation:

<ThreadHistory.Root onThreadChange={() => console.log("thread changed")}>
  {/* ... */}
</ThreadHistory.Root>

Accessing List State via Render Props

Use the render prop on List to access filtered threads, loading, and error state:

<ThreadHistory.List
  render={(props, state) => (
    <div {...props}>
      {state.isLoading && <p>Loading...</p>}
      {state.hasError && <p>Error: {state.error?.message}</p>}
      {state.isEmpty && <p>No threads found</p>}
      {state.filteredThreads.map((thread) => (
        <ThreadHistory.Item key={thread.id} thread={thread}>
          {thread.id}
        </ThreadHistory.Item>
      ))}
    </div>
  )}
/>

API reference

Root

PropTypeDefaultDescription
onThreadChange() => voidundefinedCallback invoked after a thread switch or new thread creation.

Render state:

FieldTypeDescription
threadCountnumberCount of filtered threads.
hasSearchQuerybooleanWhether a search query is active.
isLoadingbooleanWhether the thread list is loading.

Renders as <input type="text">. No custom props. Reads and writes the search query from Root context.

Render state:

FieldTypeDescription
searchQuerystringCurrent search query.

List

No custom props. Provides thread data and status through render state.

Render state:

FieldTypeDescription
isEmptybooleanWhether filtered threads list is empty.
isLoadingbooleanWhether the thread list is loading.
hasErrorbooleanWhether an error occurred.
filteredThreadsThreadListItem[]Array of search-filtered thread items.
searchQuerystringCurrent search query.
errorError | nullError from thread list fetch.

Item

PropTypeDefaultDescription
threadThreadListItemRequiredThe thread to display and select.

Renders as <button>. Sets data-active and aria-current="true" when the thread is the current active thread. When no children are provided, renders thread.name as the button label, falling back to thread.id if the name is empty or undefined.

Render state:

FieldTypeDescription
isActivebooleanWhether this thread is the active thread.
threadThreadListItemThe thread object.

NewThreadButton

No custom props. Renders as <button>. Calls startNewThread(), refetch(), and onThreadChange() on click.

Render state:

FieldTypeDescription
slotstringThe component slot name.

Accessibility

  • Item renders as a <button> with aria-current="true" on the active thread for screen-reader accessibility.
  • Search renders as an <input> with aria-label="Search threads" built in.
  • NewThreadButton renders as a <button>.

Styling Hooks

  • data-slot="thread-history"
  • data-slot="thread-history-search"
  • data-slot="thread-history-list"
  • data-slot="thread-history-item"
  • data-slot="thread-history-new-thread-button"
  • data-active on Item when thread is active
  • aria-current="true" on Item when thread is active