logoassistant-ui
Custom Backend

Custom Thread List

Overview

useRemoteThreadListRuntime lets you plug a custom thread database into assistant-ui. It keeps the UI and local runtime logic in sync while you provide persistence, archiving, and metadata for every conversation. The hook is exported as unstable_useRemoteThreadListRuntime; we refer to it here as Custom Thread List.

When to Use

Use a Custom Thread List when you need to:

  • Persist conversations in your own database or multitenant backend
  • Share threads across devices, teams, or long-lived sessions
  • Control thread metadata (titles, archived state, external identifiers)
  • Layer additional adapters (history, attachments) around each thread runtime

How It Works

Custom Thread List merges two pieces of state:

  1. Per-thread runtime – powered by any runtime hook (for example useLocalRuntime or useAssistantTransportRuntime).
  2. Thread list adapter – your adapter that reads and writes thread metadata in a remote store.

When the hook mounts it calls list() on your adapter, hydrates existing threads, and uses your runtime hook to spawn a runtime whenever a thread is opened. Creating a new conversation calls initialize(threadId) so you can create a record server-side and return the canonical remoteId.

The built-in Assistant Cloud runtime is implemented with the same API. Inspect useCloudThreadListAdapter for a production-ready reference adapter.

Build a Custom Thread List

Provide a runtime per thread

Use any runtime hook that returns an AssistantRuntime. In most custom setups this is useLocalRuntime(modelAdapter) or useAssistantTransportRuntime(...).

Implement the adapter contract

Your adapter decides how threads are stored. Implement the methods in the table below to connect to your database or API.

Compose the provider

Wrap AssistantRuntimeProvider with the runtime returned from the Custom Thread List hook.

app/CustomThreadListProvider.tsx
"use client";

import type {  } from "react";
import {
  ,
  ,
   as ,
  type  as ,
} from "@assistant-ui/react";
import {  } from "assistant-stream";
import {  } from "./model-adapter"; // your chat model adapter

const :  = {
  async () {
    const  = await ("/api/threads");
    const  = await .();
    return {
      : .map((: any) => ({
        : .id,
        : .external_id ?? ,
        : .is_archived ? "archived" : "regular",
        : .title ?? ,
      })),
    };
  },
  async () {
    const  = await ("/api/threads", {
      : "POST",
      : { "Content-Type": "application/json" },
      : .({  }),
    });
    const  = await .();
    return { : .id, : .external_id };
  },
  async (, ) {
    await (`/api/threads/${}`, {
      : "PATCH",
      : { "Content-Type": "application/json" },
      : .({  }),
    });
  },
  async () {
    await (`/api/threads/${}/archive`, { : "POST" });
  },
  async () {
    await (`/api/threads/${}/unarchive`, { : "POST" });
  },
  async () {
    await (`/api/threads/${}`, { : "DELETE" });
  },
  async (, ) {
    return (async () => {
      const  = await (`/api/threads/${}/title`, {
        : "POST",
        : { "Content-Type": "application/json" },
        : .({  }),
      });
      const {  } = await .();
      .();
    });
  },
};

export function ({
  ,
}: <{ :  }>) {
  const  = ({
    : () => (),
    : ,
  });

  return (
    < ={}>
      {}
    </>
  );
}

Adapter Responsibilities

RemoteThreadListAdapter

list:

() => Promise<{ threads: RemoteThreadMetadata[] }>

Return the current threads. Each thread must include status, remoteId, and any metadata you want to show immediately.

initialize:

(localId: string) => Promise<{ remoteId: string; externalId?: string }>

Create a new remote record when the user starts a conversation. Return the canonical ids so later operations target the right thread.

rename:

(remoteId: string, title: string) => Promise<void>

Persist title changes triggered from the UI.

archive:

(remoteId: string) => Promise<void>

Mark the thread as archived in your system.

unarchive:

(remoteId: string) => Promise<void>

Restore an archived thread to the active list.

delete:

(remoteId: string) => Promise<void>

Permanently remove the thread and stop rendering it.

generateTitle:

(remoteId: string, unstable_messages: readonly ThreadMessage[]) => Promise<AssistantStream>

Return a streaming title generator. You can reuse your model endpoint or queue a background job.

unstable_Provider?:

ComponentType<PropsWithChildren>

Optional wrapper rendered around all thread runtimes. Use it to inject adapters such as history or attachments (see the Cloud adapter).

Thread Lifecycle Cheatsheet

  • list() hydrates threads on mount and during refreshes.
  • Creating a new conversation calls initialize() once the user sends the first message.
  • archive, unarchive, and delete are called optimistically; throw to revert the UI.
  • generateTitle() powers the automatic title button and expects an AssistantStream.
  • Provide a runtimeHook that always returns a fresh runtime instance per active thread.

Optional Adapters

If you need history or attachment support, expose them via unstable_Provider. The cloud implementation wraps each thread runtime with RuntimeAdapterProvider to inject:

  • history – e.g. useAssistantCloudThreadHistoryAdapter
  • attachments – e.g. CloudFileAttachmentAdapter

Reuse that pattern to register any capability your runtime requires.