> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat History - Web

> Chat history lists prior conversations for navigation and resume.

> [!NOTE]
>
> For development, import `ChatHistory` from
> `@servicetitan/anvil2-ai-kit`. For design, find it in the
> [AI Kit Web Component Library file](https://www.figma.com/design/QEIsArLktLeJAaFvcpw3PO/AI-Kit-Web-Component-Library).

ai-kit-chat-history

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history?file=src/App.tsx)

```tsx lines theme={null}
import { ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";

const items: ChatHistoryItem[] = [
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
  {
    id: "invoice",
    label: "Invoice discrepancy follow-up",
    subtitle: "2 hours",
  },
  { id: "customer", label: "Customer churn analysis", subtitle: "Yesterday" },
];

function App() {
  return (
    <div
      style={{
        width: 240,
        height: 420,
        background: "var(--carto-color-background-surface, #f5f5f5)",
        borderRadius: 8,
        overflow: "hidden",
      }}
    >
      <ChatHistory items={items} defaultSelectedKeys={["q3"]} />
    </div>
  );
}

export default App;
```

## Options

ChatHistory renders a typed `items` array — each chat carries a required label and timestamp, and can flag itself as unread.

### Selected State

ai-kit-chat-history-selected-state

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-selected-state?file=src/App.tsx)

```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
  {
    id: "invoice",
    label: "Invoice discrepancy follow-up",
    subtitle: "2 hours",
  },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 220,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} defaultSelectedKeys={["q3"]} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

Selection is locked to single — at most one chat is ever active. The selected row keeps a persistent tint and bolds its title; drive it with `selectedKeys` / `onSelectionChange` (controlled) or `defaultSelectedKeys` (uncontrolled), the same way you would a [Menu](https://anvil.servicetitan.com/docs/components/menu/design).

### Unread

ai-kit-chat-history-unread

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-unread?file=src/App.tsx)

```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  {
    id: "auth",
    label: "Refactoring the auth flow",
    subtitle: "Just now",
    unread: true,
  },
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins", unread: true },
  { id: "karaoke", label: "Karaoke track separation", subtitle: "Yesterday" },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 220,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

Set `unread` on a chat to render a dot at the end of its title row (announced as "Unread" to assistive tech). Unread is independent of selection — a chat can be both unread and active.

### Time stamp

ai-kit-chat-history-timestamp

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-timestamp?file=src/App.tsx)

```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

// `subtitle` is rendered verbatim — the consumer owns any relative-time
// formatting. These four rows show the range a stamp typically takes: a
// relative duration, a clock time, a day name, and an absolute date.
const items: ChatHistoryItem[] = [
  { id: "auth", label: "Refactoring the auth flow", subtitle: "35 mins" },
  { id: "karaoke", label: "Karaoke track separation", subtitle: "10:40am" },
  {
    id: "onboarding",
    label: "Rewrite the onboarding email",
    subtitle: "Yesterday",
  },
  { id: "schema", label: "Postgres schema migration plan", subtitle: "Mar 14" },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 370,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

`subtitle` is rendered verbatim — Carto doesn't compute relative time. Pass whatever stamp fits the moment: a relative duration, a clock time, a day name, or an absolute date. The consumer owns the formatting and any policy for when it changes.

## Behavior

Rows respond to the standard interaction states and truncate or scroll rather than reflow the rail.

### Visual States

ai-kit-chat-history-visual-states

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-visual-states?file=src/App.tsx)

```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  { id: "default", label: "Default", subtitle: "Just now" },
  { id: "hover", label: "Hover target", subtitle: "35 mins" },
  { id: "focus", label: "Focus target", subtitle: "10:40am" },
  { id: "disabled", label: "Disabled", subtitle: "Yesterday" },
  { id: "selected", label: "Selected", subtitle: "Friday" },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 460,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory
          items={items}
          defaultSelectedKeys={["selected"]}
          disabledKeys={["disabled"]}
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

Unselected rows stay neutral — transparent at rest, with a subtle fill on hover and press. The selected row keeps that fill persistently. Keyboard focus shows the ring only, and a disabled chat (via `disabledKeys`) can't be focused, selected, or activated.

### Truncation

ai-kit-chat-history-truncation

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-truncation?file=src/App.tsx)

```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  {
    id: "long",
    label: "A conversation title long enough to overflow the rail width",
    subtitle: "An unread stamp long enough to overflow the rail width too",
  },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 140,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

A title or subtitle longer than the rail's width truncates to a single line with a trailing ellipsis — the row never wraps or grows taller.

### Overflow

ai-kit-chat-history-overflow

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-overflow?file=src/App.tsx)

```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  { id: "auth", label: "Refactoring the auth flow", subtitle: "Just now" },
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
  { id: "karaoke", label: "Karaoke track separation", subtitle: "10:40am" },
  {
    id: "onboarding",
    label: "Rewrite the onboarding email",
    subtitle: "Yesterday",
  },
  {
    id: "schema",
    label: "Postgres schema migration plan",
    subtitle: "Yesterday",
  },
  { id: "pricing", label: "Pricing page copy review", subtitle: "Friday" },
  { id: "incident", label: "Incident postmortem draft", subtitle: "Mar 14" },
  { id: "roadmap", label: "2027 roadmap brainstorm", subtitle: "Mar 12" },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 280,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} defaultSelectedKeys={["karaoke"]} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

A history longer than the rail's height scrolls within it; the "New Chat" button stays pinned above the list.

## Usage Guidelines

### When to Use

Use the Chat History when:

* **Resuming earlier conversations** – Users need to see prior chats and return to one.
* **Filling the shell's left rail** – The App Shell or Window needs its history rail, with New Chat pinned above the list.
* **Showing the active and unread chats** – One chat is marked as active while others flag unread activity.

### When not to use

Avoid using the Chat History for:

* **Selecting several items at once** – Selection is locked to a single active chat.
* **Presenting actions or options from a trigger** – Use the [Menu](https://anvil.servicetitan.com/docs/components/menu/ai-kit.md) instead.
* **Switching between related views in the supporting workspace** – Use the [Tabbed Layout](https://anvil.servicetitan.com/docs/components/tabbed-layout/web.md) instead.

## Keyboard Interaction

Users can navigate and select chats using standard keyboard controls.

| Key | Description |
| - | - |
| Tab | Moves focus into or out of the list |
| Arrow Up / Arrow Down | Moves focus to the previous/next chat |
| Enter / Space | Selects the focused chat |
| Type-ahead | Jumps focus to the next chat matching the typed characters |

## Patterns that use this component

* [Agentic Experience Shell](https://anvil.servicetitan.com/docs/patterns/agentic-experience-shell/web.md) — Provides access to previous conversations

## Code

**Implementation**

ai-kit-chat-history

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history?file=src/App.tsx)

```tsx lines expandable theme={null}
import { ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";

const items: ChatHistoryItem[] = [
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
  {
    id: "invoice",
    label: "Invoice discrepancy follow-up",
    subtitle: "2 hours",
  },
  { id: "customer", label: "Customer churn analysis", subtitle: "Yesterday" },
];

function App() {
  return (
    <div
      style={{
        width: 240,
        height: 420,
        background: "var(--carto-color-background-surface, #f5f5f5)",
        borderRadius: 8,
        overflow: "hidden",
      }}
    >
      <ChatHistory items={items} defaultSelectedKeys={["q3"]} />
    </div>
  );
}

export default App;
```

## Common Examples

To use the `ChatHistory`, pass configured conversations through `items`, handle selection through React Aria's selection APIs, and use `onNewChat` to start a conversation.

```tsx theme={null}
import { ChatHistory } from "@servicetitan/anvil2-ai-kit";

function ExampleComponent() {
  return (
    <ChatHistory
      items={chats}
      selectedKeys={[activeId]}
      onSelectionChange={(keys) => openChat([...keys][0])}
      onNewChat={startChat}
    />
  );
}
```

Selection is fixed to single. Omit `items` or pass an empty array to render only the New Chat button.

### Selected and unread

`selectedKeys` controls the active conversation. Set `unread` on an item to add a status dot and localized screen-reader text.

ai-kit-chat-history-selected-state

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-selected-state?file=src/App.tsx)

```tsx lines expandable theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
  {
    id: "invoice",
    label: "Invoice discrepancy follow-up",
    subtitle: "2 hours",
  },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 220,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} defaultSelectedKeys={["q3"]} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

ai-kit-chat-history-unread

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-unread?file=src/App.tsx)

```tsx lines expandable theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  {
    id: "auth",
    label: "Refactoring the auth flow",
    subtitle: "Just now",
    unread: true,
  },
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins", unread: true },
  { id: "karaoke", label: "Karaoke track separation", subtitle: "Yesterday" },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 220,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Subtitle

Every item requires a `subtitle`, typically a relative timestamp or last-message stamp. The component renders it verbatim; your application owns formatting and relative-time policy.

ai-kit-chat-history-timestamp

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-timestamp?file=src/App.tsx)

```tsx lines expandable theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

// `subtitle` is rendered verbatim — the consumer owns any relative-time
// formatting. These four rows show the range a stamp typically takes: a
// relative duration, a clock time, a day name, and an absolute date.
const items: ChatHistoryItem[] = [
  { id: "auth", label: "Refactoring the auth flow", subtitle: "35 mins" },
  { id: "karaoke", label: "Karaoke track separation", subtitle: "10:40am" },
  {
    id: "onboarding",
    label: "Rewrite the onboarding email",
    subtitle: "Yesterday",
  },
  { id: "schema", label: "Postgres schema migration plan", subtitle: "Mar 14" },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 370,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Overflow

Labels and subtitles truncate to one line. Place `ChatHistory` in a height-constrained container so long lists scroll instead of growing the rail.

ai-kit-chat-history-overflow

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-chat-history-overflow?file=src/App.tsx)

```tsx lines expandable theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/anvil2-ai-kit";
import type { ChatHistoryItem } from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

const items: ChatHistoryItem[] = [
  { id: "auth", label: "Refactoring the auth flow", subtitle: "Just now" },
  { id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
  { id: "karaoke", label: "Karaoke track separation", subtitle: "10:40am" },
  {
    id: "onboarding",
    label: "Rewrite the onboarding email",
    subtitle: "Yesterday",
  },
  {
    id: "schema",
    label: "Postgres schema migration plan",
    subtitle: "Yesterday",
  },
  { id: "pricing", label: "Pricing page copy review", subtitle: "Friday" },
  { id: "incident", label: "Incident postmortem draft", subtitle: "Mar 14" },
  { id: "roadmap", label: "2027 roadmap brainstorm", subtitle: "Mar 12" },
];

function App() {
  return (
    <CartoTheme>
      <div
        style={{
          width: 240,
          height: 280,
          background: "var(--carto-color-background-surface, #f5f5f5)",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatHistory items={items} defaultSelectedKeys={["karaoke"]} />
      </div>
    </CartoTheme>
  );
}

export default App;
```

## Internationalization

`ChatHistory` owns these localized strings:

* `ai-kit.chatHistory.newChat`
* `ai-kit.chatHistory.list`
* `ai-kit.chatHistory.unread`
* `ai-kit.chatHistory.recents`

Localize each item's `label` and `subtitle` before passing it. Mount `AiKitIntlProvider` inside `CartoTheme` to switch the built-in strings.

## React Accessibility

* React Aria supplies single-selection listbox semantics, arrow-key navigation, and typeahead.
* Each item label is its accessible name; `subtitle` is its accessible description.
* Unread items include localized hidden text. The visual status dot is decorative.
* Pass `aria-label` or `aria-labelledby` to override the localized list name.

**ChatHistory Props**

```tsx theme={null}
<ChatHistory
  items={chats}
  selectedKeys={[activeId]}
  onSelectionChange={setActive}
  onNewChat={startChat}
/>
```

## `ChatHistory` Props

The `ChatHistory` accepts the following props and forwards remaining React Aria `ListBox` props except layout, orientation, selection mode, and composed children.

**className** `string`

Class merged onto the rail container.

**items** `ChatHistoryItem[]`

Conversations to list in display order. Omit or pass an empty array for
no history.

**onNewChat** `() =\> void`

Handles the New Chat button.

**onSelectionChange** `(keys: Selection) =\> void`

Called when the selected conversation changes.

**selectedKeys** `Selection`

Controlled selected conversation key.

**ChatHistoryItem**

```tsx theme={null}
const chat: ChatHistoryItem = {
  id: "invoice-review",
  label: "Invoice review",
  subtitle: "5 minutes ago",
  unread: true,
};
```

## `ChatHistoryItem`

**id** `Key` *required*

Selection key reported through listbox selection APIs.

**label** `string` *required*

Visible conversation title and accessible name.

**subtitle** `string` *required*

Visible secondary line and accessible description. Supply formatted,
localized text.

**isDisabled** `boolean`

Prevents focus and selection.

**unread** `boolean` (default: false)

Displays an unread indicator and adds localized hidden text.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.