Skip to content

History Sidebar ​

Manage conversation history with the built-in sidebar.

New in 0.4.0

Status badges (Working, Waiting for you, Limit reached), keyboard-operable rows, opening a conversation selects its agent, and, switched on by the host, rename, pin and select-and-delete. See Portal Experience.

Overview ​

The Chat component includes a history sidebar that shows:

  • The list of past conversations in the order the server sends them, loaded in pages of 20 with a "Load more" button
  • A "New Chat" button
  • A delete button on each row
  • Conversation titles and relative times ("Just now", "5m ago")
  • A status badge on conversations that are working, waiting for you or stopped on their usage limit
  • With enableMessageSearch, a search box that filters by title
  • With showAgentName (off by default), the agent's name beside the time

The history list with Waiting for you, Working and Limit reached badges

Enable/Disable History ​

tsx
// With history (default)
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  showHistory={true}
/>

// Without history
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  showHistory={false}
/>

// Or use ChatMinimal
import { ChatMinimal } from "@cognipeer/chat-ui";

<ChatMinimal
  baseUrl="/api/agents"
  agentId="assistant"
/>

On small screens the sidebar is an overlay opened with the header's menu button (defaultHistoryOpen opens it at first). It closes when a conversation is picked, on the backdrop and on Escape.

Multiple agents ​

When the server lists more than one agent, the history is not filtered by agent: it shows the conversations of all of them. Opening one selects its agent in the header (and calls onAgentChange), so the paperclip, the file limits and the composer mode match that conversation. With a single agent the list is filtered to that agent.

Status badges and keyboard ​

With agent-server 0.3.0 or later each conversation in the list carries runtime, and the sidebar marks the ones that need a look: Working (a turn is running, perhaps in another window), Waiting for you (an approval or a question is open) and Limit reached (the conversation's usage limit is used up). The open conversation shows what the page itself knows, and the list is fetched again when a turn ends, so a badge does not outlive the state it reports. Turn them off with showStatus={false}.

Every row is a button: Tab, Enter and Space open it, the arrow keys, Home and End move between rows, and the delete button is visible as soon as the row has keyboard focus. See ChatHistory and Running turns, Stop and reattach.

Rename, pin and delete several ​

Switch these on with historyActions (all off by default):

tsx
<Chat
  baseUrl="/api/agents"
  historyActions={{ rename: true, pin: true, bulkDelete: true }}
/>
ActionHow
RenameThe pencil button, F2 on a row, or a double click on it. Enter or leaving the field saves, Escape keeps the old title. A title is at most 200 characters
PinThe pin button. Pinned conversations are listed first and show a pin icon
Delete several"Select" turns the rows into checkboxes; "Select all", "Done", "Delete selected (n)" and a confirmation follow

Each change shows at once and is taken back when the server refuses; the reason shows under the list. What can be offered depends on the transport (renameConversation, pinConversation, deleteConversations) and on what the agent allows (metadata.experience.history).

History Callbacks ​

tsx
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  onConversationCreated={(conversation) => {
    console.log("New conversation:", conversation.id);
  }}
  onConversationSelected={(conversation) => {
    console.log("Selected:", conversation.id);
    // Update URL, analytics, etc.
  }}
  onConversationTitleGenerated={(conversationId, title) => {
    console.log("The server named it:", title);
  }}
  onAgentChange={(agentId) => console.log("Agent:", agentId)}
/>

There is no delete callback on Chat; deleting from the sidebar updates the list and clears the chat if the open conversation was deleted. For your own reaction, build the layout from the parts (below).

useChatHistory Hook ​

For custom implementations:

tsx
import { useChatHistory } from "@cognipeer/chat-ui";

function CustomHistory() {
  const {
    conversations,
    isLoading,
    hasMore,
    error,
    load,
    loadMore,
    refresh,
    deleteConversation,
  } = useChatHistory({
    baseUrl: "/api/agents",
    agentId: "assistant",       // optional: without it all conversations are listed
    authorization: "Bearer token",
    pageSize: 20,               // default 20
  });

  return (
    <div>
      {conversations.map((conv) => (
        <div key={conv.id}>
          <span>{conv.title || "Untitled"}</span>
          <button onClick={() => deleteConversation(conv.id)}>Delete</button>
        </div>
      ))}

      {hasMore && <button onClick={loadMore}>Load More</button>}
    </div>
  );
}

See useChatHistory for every option and return value, including renameConversation, pinConversation and deleteConversations.

ChatHistory Component ​

Use the standalone component:

tsx
import { ChatHistory, useChat, useChatHistory } from "@cognipeer/chat-ui";

function CustomLayout() {
  const chat = useChat({
    baseUrl: "/api/agents",
    agentId: "assistant",
  });

  const history = useChatHistory({
    baseUrl: "/api/agents",
    agentId: "assistant",
  });

  return (
    <div className="flex h-full">
      <ChatHistory
        conversations={history.conversations}
        selectedId={chat.conversation?.id}
        isLoading={history.isLoading}
        hasMore={history.hasMore}
        onLoadMore={history.loadMore}
        onSelect={(item) => chat.loadConversation(item.id)}
        onDelete={history.deleteConversation}
        onNewChat={chat.clearMessages}
      />

      <div className="flex-1">{/* Chat content */}</div>
    </div>
  );
}

Refresh the list when a turn ends (history.refresh()), so the badges and titles stay current. Chat does this for you.

tsx
<Chat
  baseUrl="/api/agents"
  renderHistoryHeader={() => <div className="px-4 py-3 font-medium">Conversations</div>}
  renderHistoryFooter={() => <div className="px-4 py-2 text-xs">Kept for 90 days</div>}
/>

renderHistoryHeader replaces the "New Chat" header, so offer your own way to start a chat (for example controllerRef.current?.newChat()). There is no per-row render prop: to draw rows yourself, build the list from useChatHistory as above.

Conversation Titles ​

The server names a conversation after the first message when it can (onConversationTitleGenerated). You can set one yourself:

tsx
// Create with a title
const conversation = await chat.createConversation("My Important Chat");

// Change it later
await client.updateConversation(conversation.id, { title: "Updated Title" });

With historyActions.rename the person can rename it from the sidebar.

Search/Filter History ​

enableMessageSearch adds a search box to the sidebar that filters the loaded conversations by title:

tsx
<Chat baseUrl="/api/agents" enableMessageSearch />

Styling ​

The sidebar uses the theme variables: --chat-bg-secondary (background), --chat-bg-hover (the open row), --chat-bg-tertiary (hover), --chat-border-primary, and the status colours for the badges. See Theming.

Troubleshooting ​

SymptomCause and fix
The list does not update after a messageChat refreshes it for you. In a custom layout, call history.refresh() after onMessageReceived / onConversationCreated.
No badgesThe server is older than agent-server 0.3.0 (no runtime on the list), or showStatus is false.
A conversation of another agent opens with the wrong paperclipMake sure the agent list (GET /agents) contains the agent of that conversation; unknown agents are ignored.
Rename and pin buttons are missinghistoryActions is off, the transport has no renameConversation / pinConversation, or the agent's experience.history turned them off.
Only 20 conversations showThe list is paged: use "Load more" (hasMore, loadMore) or raise pageSize in your own hook.

Next Steps ​

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub