# Message

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

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

<Message
  from="assistant"
  parts={message.parts}
  getPartKey={(part) => part.id}
  renderPart={renderPart}
  actionConfig={{ copy: { text: plainText } }}
/>;
```


## Usage [#usage]

Pass your existing typed parts directly. The renderer receives the original part, including tool IDs, metadata and custom fields.

```tsx
import { Message } from '@commenda/pathfinder/message';
import { Markdown } from '@commenda/pathfinder/markdown';

<Message
  from="assistant"
  parts={message.parts}
  getPartKey={(part, index) => part.id ?? `${message.id}:${index}`}
  renderPart={(part) => {
    switch (part.type) {
      case 'text':
        return <Markdown content={part.text} />;
      case 'request-recap':
        return <ResponseRecap {...part.recap} />;
      default:
        return renderCustomPart(part);
    }
  }}
  actionConfig={{
    copy: { text: plainText },
    feedback: { value: feedback, onChange: saveFeedback },
    retry: { isLatest: isLatestReply, onRetry: regenerate },
  }}
/>;
```

Use a stable tool/part ID where available. An index fallback is safe only for append-only SDK parts; never reorder those parts. Keep Markdown a separate subpath import. Message does not import a parser or require an AI SDK provider.

## User and people threads [#user-and-people-threads]


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

<Message
  from="user"
  parts={message.parts}
  getPartKey={getPartKey}
  renderPart={renderPart}
  authorConfig={{ identity: currentPerson, showName: true }}
/>;
```


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

<Message
  from="person"
  parts={message.parts}
  getPartKey={getPartKey}
  renderPart={renderPart}
  authorConfig={{ identity: currentPerson, showName: true }}
/>;
```


`from` is a presentation choice. A person in a shared thread does not introduce a new AI SDK role. User avatars come from `authorConfig`; set `showName` for shared conversations. People threads show the name automatically. Assistant replies never render the author avatar.

## System events [#system-events]


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

<Message
  from="system"
  parts={message.parts}
  getPartKey={getPartKey}
  renderPart={renderPart}
/>;
```


Keep system events short. They are visual timeline events, not model system instructions.

## Versions [#versions]


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

<Message
  from="assistant"
  parts={message.parts}
  getPartKey={getPartKey}
  renderPart={renderPart}
  branchConfig={{
    index: activeVersion,
    count: versions.length,
    onChange: setActiveVersion,
  }}
/>;
```


The host stores alternatives and swaps the selected message. Keep the active index within `[0, count - 1]`. Controls appear only with at least two alternatives; changing version does not call the model.

## Interleaved activity and content [#interleaved-activity-and-content]


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

<Message
  from="assistant"
  parts={message.parts}
  getPartKey={getPartKey}
  renderPart={renderPart}
  // renderPart maps reasoning, tools, text and recaps in their original order.
/>;
```


Reasoning and tool calls remain ordered parts of their originating reply. Applications map their existing tool state into ToolCall without flattening the transcript.

## Streaming and stopped replies [#streaming-and-stopped-replies]


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

<Message
  from="assistant"
  parts={message.parts}
  getPartKey={getPartKey}
  renderPart={renderPart}
  state={{ status: isStreaming ? 'streaming' : 'stopped' }}
/>;
```


Hide actions while streaming. A stopped reply retains its parts and exposes its configured actions. Put activity components in the original parts array when activity is associated with that turn.

## Partial failure [#partial-failure]


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

<Message
  from="assistant"
  parts={message.parts}
  getPartKey={getPartKey}
  renderPart={renderPart}
  state={{ status: 'error', message: 'Connection lost.' }}
  actionConfig={{ retry: { isLatest: true, onRetry: regenerate } }}
/>;
```


Errors preserve streamed content and hide the regular action toolbar. Retry appears inline only for an eligible latest assistant reply. The host handles retry failures and supplies the next state; Message never starts network requests.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `from` | `"user" \| "assistant" \| "system" \| "person"` | required | Display kind, separate from the app's model role |
| `parts` | `readonly Part[]` | required | Original ordered typed parts |
| `getPartKey` | `((part: Part, index: number) => string)` | required | Stable unique key within the message |
| `renderPart` | `((part: Part, index: number) => ReactNode)` | required | Typed content renderer; preserve unsupported parts intentionally |
| `state` | `MessageState` | optional | Done by default |
| `authorConfig` | `{ identity: PersonIdentity; showName?: boolean; }` | optional | User/person Avatar, no assistant avatar |
| `timestamp` | `{ dateTime: string; label: string; }` | optional | Host-formatted accessible time |
| `attachmentConfig` | `AttachmentGroupProps` | optional | Attachments above the bubble, aligned by author |
| `actionConfig` | `MessageActionConfig` | optional | Copy, controlled feedback, latest-only retry, reply and controlled pin |
| `branchConfig` | `MessageBranchConfig` | optional | Controlled zero-based alternative selection |
| `label` | `string` | optional | Accessible article name and layout extension |
| `className` | `string` | optional | Accessible article name and layout extension |

### state fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `"streaming" \| "done" \| "stopped"` | Yes |  |

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `"error"` | Yes |  |
| `message` | `string` | Yes |  |

### authorConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `identity` | `PersonIdentity` | Yes |  |
| `showName` | `boolean` | No |  |

### authorConfig.identity fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `"person"` | Yes |  |
| `userId` | `string` | No |  |
| `name` | `string` | No |  |
| `email` | `string` | No |  |
| `src` | `string` | No |  |

### timestamp fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `dateTime` | `string` | Yes |  |
| `label` | `string` | Yes |  |

### attachmentConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | `readonly AttachmentItem[]` | Yes |  |
| `align` | `"start" \| "end"` | No |  |
| `maxRows` | `1 \| 2 \| 3` | No |  |
| `getPreview` | `((item: AttachmentItem) => AttachmentProps["previewConfig"])` | No |  |
| `className` | `string` | No |  |
| `label` | `string` | No |  |
| `onPreview` | `((item: AttachmentItem) => void)` | No |  |
| `onRetry` | `((item: AttachmentItem) => void)` | No |  |
| `onRemove` | `((item: AttachmentItem) => void)` | No |  |

### actionConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `copy` | `{ text: string; }` | No |  |
| `feedback` | `{ value: "good" \| "bad" \| null; onChange: (value: "good" \| "bad" \| null) => void; }` | No |  |
| `retry` | `{ isLatest: boolean; onRetry: () => void; disabled?: boolean; }` | No |  |
| `onReply` | `(() => void)` | No |  |
| `pin` | `{ isPinned: boolean; onChange: (value: boolean) => void; }` | No |  |

### actionConfig.copy fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | `string` | Yes |  |

### actionConfig.feedback fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `value` | `"good" \| "bad" \| null` | Yes |  |
| `onChange` | `(value: "good" \| "bad" \| null) => void` | Yes |  |

### actionConfig.retry fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isLatest` | `boolean` | Yes |  |
| `onRetry` | `() => void` | Yes |  |
| `disabled` | `boolean` | No |  |

### actionConfig.pin fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isPinned` | `boolean` | Yes |  |
| `onChange` | `(value: boolean) => void` | Yes |  |

### branchConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `index` | `number` | Yes | Zero-based active alternative; keep the message/part keys stable within each alternative. |
| `count` | `number` | Yes |  |
| `onChange` | `(index: number) => void` | Yes |  |

`actionConfig.copy` accepts `{ text }`. Feedback accepts `{ value: 'good' | 'bad' | null, onChange }`; clicking a chosen thumb clears it. Retry accepts `{ isLatest, onRetry, disabled? }`. People-thread actions use `onReply` and `pin: { isPinned, onChange }`. Callbacks own persistence and error reporting; only copy writes to the clipboard locally.

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

- **MESSAGE-01:** User bubble pearl 300, person bubble pearl 200, radius 12, padding 12 vertically/16 horizontally, content at most 520px. Rendered frames use Copy/15, superseding the stale Copy/13 description.
- **MESSAGE-02:** Assistant parts stack 16px apart without an avatar. Generated files, requests and recaps stay in their originating reply.
- **MESSAGE-03:** Actions sit 8px below per the component description. No actions while streaming; branches require alternatives; retry requires latest-reply eligibility. Copy changes to a check for two seconds, selected feedback changes fill rather than weight.
- **MESSAGE-04:** Keep partial output and show the error inline. Do not announce each token or replace the reply with a toast.

Long messages wrap; turn long pasted documents into attachments at the application boundary. Screen readers navigate named articles and labeled action buttons. The Storybook worst-case switch retains Unicode, unbroken text and long author fixtures.
