# Conversation

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/conversation
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=543-2

```tsx
import { Conversation } from '@commenda/pathfinder/conversation';

<Conversation
  messages={messages}
  getMessageKey={(message) => message.id}
  renderMessage={renderMessage}
  footer={<Composer {...composerProps} />}
  announcement="Response complete."
  className="conversation-layout"
/>;
```


## Usage [#usage]

```tsx
import { Conversation } from '@commenda/pathfinder/conversation';
import { Message } from '@commenda/pathfinder/message';

<Conversation
  messages={messages}
  getMessageKey={(message) => message.id}
  renderMessage={(message, { isLast }) => (
    <Message
      from={message.role}
      parts={message.parts}
      getPartKey={getPartKey}
      renderPart={renderPart}
      actionConfig={{
        retry: { isLatest: isLast, onRetry: regenerate },
      }}
    />
  )}
  announcement={turnAnnouncement}
  footer={<Composer {...composerProps} />}
  className="conversation-layout"
/>;
```

Give the container a bounded height or `flex: 1; min-height: 0` inside a bounded flex layout. Conversation owns its scrolling viewport, not the page scroll. Change its React `key` when switching conversation IDs.

## Docked request and recap [#docked-request-and-recap]


```tsx
import { Conversation } from '@commenda/pathfinder/conversation';

<Conversation
  messages={messages}
  getMessageKey={(message) => message.id}
  renderMessage={renderMessage}
  footer={
    <div className="composer-dock">
      {pendingRequest && (
        <QuestionCard {...pendingRequest} onSubmit={saveAnswerAndAppendRecap} />
      )}
      <Composer {...composerProps} />
    </div>
  }
  className="conversation-layout"
/>;
```


The live question sits 12px above Composer. Acceptance appends a stable recap part to the originating reply and removes the docked card. Temporary dismissal keeps the question pending and provides a way to reopen it. Escape belongs to the focused nested overlay first; the request handles it only after the overlay closes. Typing in Composer never activates a question's numeric shortcuts.

## Empty conversation [#empty-conversation]


```tsx
import { Conversation } from '@commenda/pathfinder/conversation';

<Conversation
  messages={[]}
  getMessageKey={getMessageKey}
  renderMessage={renderMessage}
  emptyState={<SuggestedPromptGroup {...suggestions} />}
  className="conversation-layout"
/>;
```


Supply `emptyState` with suggested prompts or introductory content. Message fetching, permissions and transport remain in the host.

## History and stream following [#history-and-stream-following]

Use **Load earlier messages**, scroll into the history, and choose **Append reply** or **Stream chunk**. The viewport follows only within 32px of the bottom. Earlier rows retain their keys and the first visible row retains its offset. **Jump to latest** resumes following. ResizeObserver also handles delayed image/font/content growth. Scroll positioning is immediate and respects reduced-motion users without animated jumps.

`historyConfig` reports loading and an optional inline error. The host guards overlapping loads, keeps previously loaded items, and retries via `onLoadMore`. This component does not virtualize an unbounded history: paginate on the server and profile realistic histories before adding a virtualization layer.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `messages` | `readonly Item[]` | required | Original typed application/SDK objects |
| `getMessageKey` | `((message: Item, index: number) => string)` | required | Stable unique identity across prepend/append |
| `renderMessage` | `((message: Item, context: { index: number; isLast: boolean; }) => ReactNode)` | required | Host maps roles, states and parts |
| `historyConfig` | `{ hasMore: boolean; isLoading: boolean; onLoadMore: () => void; error?: string; }` | optional | App-owned earlier-history loading |
| `emptyState` | `ReactNode` | optional | Empty transcript content |
| `footer` | `ReactNode` | optional | Composer, docked requests and related controls |
| `announcement` | `string` | optional | Concise turn-level live status |
| `label` | `string` | optional | Conversation and transcript accessible names |
| `className` | `string` | optional | Bounded layout; no default fixed height |

### historyConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `hasMore` | `boolean` | Yes |  |
| `isLoading` | `boolean` | Yes |  |
| `onLoadMore` | `() => void` | Yes |  |
| `error` | `string` | No |  |

## Design rules and integration [#design-rules-and-integration]

- **CONVERSATION-01:** Preserve original objects and keyed part identity. Do not flatten text around tool calls or discard unknown custom parts.
- **CONVERSATION-02:** Do not pull a reader to the bottom while they inspect history. Preserve the visible row during prepend and delayed layout changes.
- **CONVERSATION-03:** The transcript is a keyboard-focusable region, not a per-token live log. Use `announcement` for “Response complete”, “Stopped” or an error once per transition.
- **CONVERSATION-04:** Transport, providers, execution, feedback persistence and message storage remain in the application. Compose existing Pathfinder components through the footer and typed render callbacks.

The API follows the separation in [Vercel AI Elements Conversation](https://elements.ai-sdk.dev/components/conversation) and [Message](https://elements.ai-sdk.dev/components/message), using Pathfinder's direct props instead of requiring compound children. Existing `useChat` messages can pass through unchanged; adapt rendering at the boundary. Migration of the existing application is out of scope. Download/export, virtualization and automatic message trimming are not performed here.
