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.
The topic interface
Section titled “The topic interface”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.
Starting a topic
Section titled “Starting a topic”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.
Host-side auto-naming
Section titled “Host-side auto-naming”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:
- A topic without a usable
namecounts 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. - 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. - 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 }}/>When “unnamed” means something else
Section titled “When “unnamed” means something else”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…' }}/>Owning the selection
Section titled “Owning the selection”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.