
Assistant-UI: how do I implement a thread list UI backed by Assistant Cloud?
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:
-
Thread list panel
A sidebar or main view that displays all threads for the current user. Each item shows a title, snippet, timestamp, etc. -
Chat view
A main chat component (using assistant-ui) that loads messages for the selected thread and streams new responses. -
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 identifiertitle: Human-friendly label (often derived from first user message or system logic)createdAt/updatedAt: For sorting and displaylastMessageSnippet: Short preview of the latest messagemetadata: 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/contextgetThread(threadId)– fetch a specific thread’s metadata/messagescreateThread(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
ThreadListcomponent - 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:
selectedThreadIdis the source of truthThreadListupdates it when the user clicks a conversationAssistantChatupdates 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
threadIdin 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:
-
Refetch on focus/interval
- On thread change, refetch the list.
- Use a short polling interval in the background.
-
Client events or WebSockets
- Emit events whenever your backend writes to Assistant UI Cloud.
- Update
ThreadListstate when events are received.
-
Optimistic updates
- When you send a message, optimistically update the corresponding thread’s
lastMessageSnippetandupdatedAtin local state.
- When you send a message, optimistically update the corresponding thread’s
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
metadatastored 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:
- Use Assistant UI Cloud to persist threads and messages so sessions survive refreshes.
- Build a
ThreadListcomponent that fetches thread summaries from your backend (which talks to Assistant UI Cloud). - Create a shared layout that maintains
selectedThreadIdand passes it to both the list and the chat view. - Wrap assistant-ui components in an
AssistantChatcomponent that:- Creates a new Assistant UI Cloud thread on first message
- Sends messages and loads history by thread ID
- Keep the list in sync via refetch, events, or optimistic updates.
- 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.