Skip to content

Topics and naming

A chat is a set of topics — separate conversations, most recently active first. This page covers the topic lifecycle: what a topic is to the components, how one is started, and how a new topic gets its name from the host.

interface ChatTopic {
id: ChatIdentifier;
name?: string; // unset or blank while not named yet
startedBy?: ChatIdentifier; // resolved through authorOf at render time
started?: Date;
lastActivity?: Date; // orders the list; falls back to started
metadata?: Record<string, unknown>;
}

The list orders topics by lastActivity (falling back to started) itself — hand it topics in any order.

onStartTopic raises the intent; creating the topic is the application’s business. Answer with the new topic’s id — directly or through a promise — and the sidebar opens it, ready for the first message:

<ChatSidebar
...
onStartTopic={async () => {
const topicId = Guid.create();
await createTopic(topicId); // your command, your storage
return topicId;
}}
/>

Omit onStartTopic to leave the new-topic affordance off entirely.

A topic is not named by the components — it is named by the host, typically by asking an LLM for a good title once there is a first message to derive it from. The contract:

  1. A topic without a usable name counts as unnamed. It renders a placeholder (New topic, overridable through labels) in a pending style — in the topics list and as the conversation’s title.
  2. When the first message is sent in an unnamed topic, the sidebar invokes onRequestTopicName(topic, firstMessageBody) — once. It is not invoked again for later messages, and never for a topic that already has a name.
  3. The host produces a name however it likes and supplies it through the topic data. When the updated topic arrives (the next query push, the next render), the placeholder simply stops being needed.
<ChatSidebar
...
onRequestTopicName={async (topic, firstMessage) => {
const name = await askLlmForTopicName(firstMessage); // your model, your prompt
await renameTopic(topic.id, name); // your command — flows back in via the data
}}
/>

The default rule is “no name, or only whitespace”. A backend that stores a literal placeholder for unnamed topics overrides the rule — and the placeholder label — without touching anything else:

<ChatSidebar
...
isTopicUnnamed={topic => topic.name === 'New topic'}
labels={{ unnamedTopic: 'Naming…' }}
/>

Left alone, the sidebar keeps the open topic internally and reports changes through onTopicSelected(topicId, topic)undefined when the person goes back to the list. To own the selection (deep links, restoring state), pass selectedTopicId: an identifier opens that topic, null shows the list.