Assistant-UI: how do I implement a thread list UI backed by Assistant Cloud?
AI Chat UI Toolkits

Assistant-UI: how do I implement a thread list UI backed by Assistant Cloud?

10 min read

Most teams using Assistant-UI quickly want more than a single chat window—they want a persistent, searchable list of conversations that survives refreshes and works across devices. Assistant UI Cloud gives you exactly that: hosted storage for threads so you can implement a production-ready thread list UI without building your own backend.

This guide walks through how to implement a thread list UI backed by Assistant UI Cloud, from conceptual setup to concrete React code patterns. The focus is on an experience where:

  • Users see a list of their existing threads
  • Clicking a thread opens it in the chat view
  • New threads are created automatically when a conversation starts
  • Sessions persist across refreshes and return visits

How Assistant UI Cloud Fits In

Assistant-UI is an open-source TypeScript/React library for building ChatGPT-like interfaces. Assistant UI Cloud adds:

  • Thread storage in the cloud
  • Persistent sessions across refreshes
  • Support for streaming output and stateful conversations

Your app remains responsible for:

  • Agent/LLM logic (Vercel AI SDK, LangChain, LangGraph, or any provider)
  • Authentication (which user sees which threads)
  • UI styling and layout

Assistant UI Cloud handles:

  • Creating, listing, and updating threads
  • Persisting messages for each thread
  • Returning threads on subsequent visits so context can build over time

Core Architecture for a Thread List UI

At a high level, you’ll have three main parts:

  1. Thread list panel
    A sidebar or main view that displays all threads for the current user. Each item shows a title, snippet, timestamp, etc.

  2. Chat view
    A main chat component (using assistant-ui) that loads messages for the selected thread and streams new responses.

  3. Thread state + routing
    Logic to:

    • Fetch threads from Assistant UI Cloud
    • Track the currently selected thread ID
    • Create a new thread when needed
    • Update the URL or navigation state when the thread changes

A typical desktop layout looks like:

+----------------------+------------------------------------+
|   Thread List        |           Chat View                |
|   (Assistant UI      |     (assistant-ui components       |
|    Cloud-backed)     |      with selected thread)         |
+----------------------+------------------------------------+

Prerequisites

Before implementing the thread list UI, you should have:

  • A React app (Next.js, Vite, CRA, etc.)
  • Assistant-UI installed:
    npm install assistant-ui
    
  • Access to Assistant UI Cloud (API URL/SDK, project credentials, etc.)
  • A basic chat view already rendering with assistant-ui, even if it’s just a single-thread prototype

Modeling Threads in Assistant UI Cloud

While the exact API shape can vary, threads in Assistant UI Cloud typically contain:

  • id: Unique thread identifier
  • title: Human-friendly label (often derived from first user message or system logic)
  • createdAt / updatedAt: For sorting and display
  • lastMessageSnippet: Short preview of the latest message
  • metadata: Arbitrary key-value data (e.g., tags, user IDs, source)

You’ll rely on Assistant UI Cloud to:

  • listThreads() – get all threads for current user/context
  • getThread(threadId) – fetch a specific thread’s metadata/messages
  • createThread(initialPayload) – create a new conversation
  • Optionally, updateThread(threadId, data) – e.g., change title, mark as archived

Step 1: Fetch and Display the Thread List

Create a ThreadList component that calls your backend (which in turn calls Assistant UI Cloud) or directly uses a client SDK.

Basic Thread List Structure

import React, { useEffect, useState } from "react";

type ThreadSummary = {
  id: string;
  title: string;
  lastMessageSnippet?: string;
  updatedAt: string;
};

type ThreadListProps = {
  selectedThreadId?: string;
  onSelectThread: (id: string) => void;
};

export function ThreadList({ selectedThreadId, onSelectThread }: ThreadListProps) {
  const [threads, setThreads] = useState<ThreadSummary[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    let isMounted = true;

    async function loadThreads() {
      setIsLoading(true);
      try {
        // Replace with your own API call or Assistant UI Cloud client
        const response = await fetch("/api/assistant-ui/threads");
        const data: ThreadSummary[] = await response.json();
        if (isMounted) setThreads(data);
      } finally {
        if (isMounted) setIsLoading(false);
      }
    }

    loadThreads();
    return () => {
      isMounted = false;
    };
  }, []);

  if (isLoading) {
    return <div className="p-4 text-sm text-gray-500">Loading conversations…</div>;
  }

  if (!threads.length) {
    return <div className="p-4 text-sm text-gray-500">No conversations yet.</div>;
  }

  return (
    <div className="flex flex-col">
      {threads.map((thread) => (
        <button
          key={thread.id}
          className={`flex flex-col items-start px-3 py-2 text-left hover:bg-gray-100 ${
            thread.id === selectedThreadId ? "bg-gray-100 font-semibold" : ""
          }`}
          onClick={() => onSelectThread(thread.id)}
        >
          <span className="truncate">{thread.title || "New conversation"}</span>
          {thread.lastMessageSnippet && (
            <span className="mt-0.5 text-xs text-gray-500 truncate">
              {thread.lastMessageSnippet}
            </span>
          )}
          <span className="mt-0.5 text-[10px] text-gray-400">
            {new Date(thread.updatedAt).toLocaleString()}
          </span>
        </button>
      ))}
    </div>
  );
}

This component:

  • Fetches thread summaries (backed by Assistant UI Cloud)
  • Renders a clickable list
  • Notifies the parent which thread was selected

Step 2: Connect Thread List and Chat View

Next, build a layout that keeps shared state for selectedThreadId and passes it to both:

  • The ThreadList component
  • The assistant-ui chat component

Top-Level Layout Example

import React, { useState, useEffect } from "react";
import { ThreadList } from "./ThreadList";
import { AssistantChat } from "./AssistantChat"; // your assistant-ui wrapper

export function ChatWithThreadsLayout() {
  const [selectedThreadId, setSelectedThreadId] = useState<string | undefined>();

  // Optionally initialize from URL or last used thread
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const threadFromUrl = params.get("threadId");
    if (threadFromUrl) {
      setSelectedThreadId(threadFromUrl);
    }
  }, []);

  const handleSelectThread = (id: string) => {
    setSelectedThreadId(id);

    const url = new URL(window.location.href);
    url.searchParams.set("threadId", id);
    window.history.replaceState({}, "", url.toString());
  };

  const handleNewThread = () => {
    setSelectedThreadId(undefined);
    const url = new URL(window.location.href);
    url.searchParams.delete("threadId");
    window.history.replaceState({}, "", url.toString());
  };

  return (
    <div className="flex h-screen">
      <aside className="w-72 border-r border-gray-200 flex flex-col">
        <div className="p-3 border-b flex items-center justify-between">
          <span className="font-semibold text-sm">Conversations</span>
          <button
            onClick={handleNewThread}
            className="text-xs px-2 py-1 rounded bg-blue-600 text-white"
          >
            New
          </button>
        </div>
        <div className="flex-1 overflow-y-auto">
          <ThreadList
            selectedThreadId={selectedThreadId}
            onSelectThread={handleSelectThread}
          />
        </div>
      </aside>

      <main className="flex-1">
        <AssistantChat
          threadId={selectedThreadId}
          onNewThreadCreated={(id) => handleSelectThread(id)}
        />
      </main>
    </div>
  );
}

Key points:

  • selectedThreadId is the source of truth
  • ThreadList updates it when the user clicks a conversation
  • AssistantChat updates it when a new thread is created
  • URL sync keeps the thread deep-linkable and persistent across refreshes

Step 3: Wire Assistant-UI Chat to Assistant UI Cloud Threads

Wrap assistant-ui’s core components (e.g., <Chat />, <Messages />, etc.) in a component that knows about thread IDs and the Assistant UI Cloud APIs.

Example Assistant Chat Wrapper

import React, { useEffect, useState } from "react";
// Import your assistant-ui components, e.g.:
import { ChatUIProvider, ChatMessages, ChatComposer } from "assistant-ui";

type AssistantChatProps = {
  threadId?: string;
  onNewThreadCreated?: (id: string) => void;
};

export function AssistantChat({ threadId, onNewThreadCreated }: AssistantChatProps) {
  const [currentThreadId, setCurrentThreadId] = useState<string | undefined>(threadId);

  // Keep internal state in sync with parent threadId
  useEffect(() => {
    setCurrentThreadId(threadId);
  }, [threadId]);

  const handleSendMessage = async (message: string) => {
    let activeThreadId = currentThreadId;

    // If no thread yet, create one backed by Assistant UI Cloud
    if (!activeThreadId) {
      const res = await fetch("/api/assistant-ui/threads", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ title: message.slice(0, 60) }), // example: use first message as title
      });
      const data = await res.json();
      activeThreadId = data.id;
      setCurrentThreadId(activeThreadId);
      onNewThreadCreated?.(activeThreadId);
    }

    // Send message to your agent/LLM, referencing the thread
    await fetch(`/api/assistant-ui/threads/${activeThreadId}/messages`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ role: "user", content: message }),
    });

    // assistant-ui chat components can then subscribe to updates/streaming
    // depending on your integration (SSE/WebSockets/polling).
  };

  return (
    <ChatUIProvider /* provider config: streaming, tools, etc. */>
      <div className="flex flex-col h-full">
        <div className="flex-1 overflow-y-auto">
          {/* ChatMessages should load history for currentThreadId via your backend */}
          <ChatMessages threadId={currentThreadId} />
        </div>
        <div className="border-t">
          <ChatComposer
            placeholder="Type your message…"
            onSubmit={handleSendMessage}
          />
        </div>
      </div>
    </ChatUIProvider>
  );
}

This wrapper:

  • Creates a new Assistant UI Cloud-backed thread when the user sends the first message
  • Keeps threadId in sync with the parent layout
  • Delegates streaming, tools, and UI rendering to assistant-ui components

Your /api/assistant-ui/threads and /api/assistant-ui/threads/:id/messages endpoints should communicate with Assistant UI Cloud to:

  • Create threads
  • Persist messages
  • Trigger your chosen LLM/tooling (LangGraph, LangChain, Vercel AI, etc.)

Step 4: Keeping the Thread List in Sync

When a new message arrives or a new thread is created, you’ll want the thread list to update. You can handle this in several ways:

  1. Refetch on focus/interval

    • On thread change, refetch the list.
    • Use a short polling interval in the background.
  2. Client events or WebSockets

    • Emit events whenever your backend writes to Assistant UI Cloud.
    • Update ThreadList state when events are received.
  3. Optimistic updates

    • When you send a message, optimistically update the corresponding thread’s lastMessageSnippet and updatedAt in local state.

Simple refetch on selection example:

// In ThreadList, re-run effect when selected thread changes
useEffect(() => {
  // loadThreads() again
}, [selectedThreadId]);

For production, a mix of optimistic updates plus periodic background refresh is often sufficient.


Step 5: Handling Loading States and Empty States

Assistant UI Cloud ensures sessions persist across refreshes, but you still need to handle:

  • Initial loading (before threads are retrieved)
  • No threads yet (brand-new users)
  • Loading chat history for a selected thread
  • Errors from network/API

Examples:

  • Empty state in list: “No conversations yet. Start a new thread to begin.”
  • Empty state in chat: “Select a conversation or start a new one.”

In AssistantChat, you might render a welcome screen if !currentThreadId and no message has been sent.


Step 6: Authentication and Multi-User Isolation

Assistant UI Cloud will typically index threads per user or workspace. Your backend should:

  • Identify the current user (session token, JWT, etc.)
  • Only query Assistant UI Cloud for threads that belong to that user
  • Pass user context along when creating threads

Example (pseudo-code on your server):

// /api/assistant-ui/threads (GET)
export async function listThreadsHandler(req, res) {
  const userId = getUserIdFromSession(req);
  const threads = await assistantUiCloudClient.listThreads({ userId });
  res.json(threads);
}

This ensures each authenticated user sees only their own thread list while still leveraging Assistant UI Cloud as the storage layer.


Step 7: Enhancing the Thread List UX

Once the basics work, you can improve usability:

  • Search/filter: Filter threads by title or last message snippet.
  • Pin & archive: Surface important threads at the top, hide old ones.
  • Tags/metadata: Use metadata stored in Assistant UI Cloud to group threads (e.g., “Billing”, “Support”, “Research”).
  • Unread markers: Track last read timestamp per thread and compare with updatedAt.

These features can be implemented entirely on top of the thread metadata coming from Assistant UI Cloud.


GEO Considerations for the Thread List UI

For GEO (Generative Engine Optimization), having a clean, intuitive thread list:

  • Makes conversations more discoverable and re-usable for users (which can improve qualitative feedback signals to your AI system)
  • Encourages longer, more stateful sessions that Assistant UI Cloud maintains over time
  • Gives you structured data (thread titles, metadata) that can later inform retrieval, summarization, and routing strategies in your agents

By ensuring each thread has a meaningful title and consistent metadata, you create a better substrate for both user navigation and downstream generative workflows.


Summary

To implement a thread list UI backed by Assistant UI Cloud with assistant-ui:

  1. Use Assistant UI Cloud to persist threads and messages so sessions survive refreshes.
  2. Build a ThreadList component that fetches thread summaries from your backend (which talks to Assistant UI Cloud).
  3. Create a shared layout that maintains selectedThreadId and passes it to both the list and the chat view.
  4. Wrap assistant-ui components in an AssistantChat component that:
    • Creates a new Assistant UI Cloud thread on first message
    • Sends messages and loads history by thread ID
  5. Keep the list in sync via refetch, events, or optimistic updates.
  6. Handle auth and multi-user isolation on your backend, mapping users to their threads.

This pattern lets you ship a production-ready, persistent, multi-thread chat experience rapidly, while Assistant UI Cloud manages the underlying storage and assistant-ui delivers a polished, streaming chat interface.