# Chat

## Usage

The chat display is a set of pieces that stack into a transcript: a scrolling layout, a message log, rows for each sender, bubbles, tool calls, a reasoning disclosure, a typing line, and an unread rule. Message bodies are slots. This suite does not render Markdown; pass plain text, or bring your own renderer.

`ChatLayout` is a column. The transcript grows, and the composer stays pinned to the bottom over a frosted scrim. By default the layout itself scrolls. Pass `scrollRef` when an ancestor should scroll instead, and the dock switches from sticky to fixed.

While the reader is at the bottom, new text keeps the view pinned there. Scrolling up lets go immediately and reveals the scroll-to-bottom button. If messages arrive while the view is unlocked, the button label changes to the localized “New messages” string, a live region announces it, and `ChatUnreadDivider` can mark the boundary. `ChatLayout` wires this through `useChatStreamScroll` and `useChatNewMessages`. Set `showScrollButton` to `false` to hide the built-in button, or replace it with the `scrollButton` slot.

The field is ChatComposer, placed in the layout's composer slot. The shell owns the draft, send, and stop. The default editor accepts chips, sends on Enter, and inserts a newline on Shift+Enter. The attachment drawer, header, and footer are slots. Dictation uses the browser's speech recognition, and the microphone stays hidden when that API is missing. The landing page and the transcript with a document panel are in [Patterns](/en/guide/patterns).

```vue
<script setup lang="ts">
import { ChatComposer, ChatLayout, ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'

function onSubmit(value: string) {
  console.log(value)
}
</script>

<template>
  <ChatLayout>
    <ChatMessageList>
      <ChatMessage sender="user">
        <ChatMessageBubble>
          Check the release notes for me.
        </ChatMessageBubble>
      </ChatMessage>
      <ChatMessage sender="assistant">
        <ChatMessageBubble name="Assistant">
          The notes are in the docs.
        </ChatMessageBubble>
      </ChatMessage>
    </ChatMessageList>
    <template #composer>
      <ChatComposer placeholder="Type a message…" @submit="onSubmit" />
    </template>
  </ChatLayout>
</template>
```

`ChatMessageList` is a `role="log"`. Pass `isStreaming` while an assistant reply is still growing, so a screen reader waits and announces the finished message once. `scrollToTopAction` runs when the top sentinel enters the scroll parent, and a spinner stays up until the promise settles.

`ChatMessage` is an article. `sender` is `user`, `assistant`, or `system`. A system row is centered and ignores the avatar, name, and metadata slots. Put the visible name on `ChatMessageBubble` when the body starts with a bubble, so the name lines up with the bubble padding.

`ChatToolCalls` takes the tool list an assistant turn already produced. One call is a single row. Several calls collapse behind a header; `label` replaces the localized count while the group is open. A call with `detail`, or the `detail` slot, can expand on its own. `v-model:is-expanded` controls the group.

`ChatReasoning` is the same kind of disclosure for a thinking trace. It starts collapsed. A plain-text default slot is reused as the one-line preview. `isStreaming` shimmers the label and hides both the preview and `duration`.

`ChatTypingIndicator` draws three dots. Pass `names` and it writes a sentence for one person, two people, or a named person plus a count. The root is `role="status"`, and that region is the only announcement. `ChatUnreadDivider` marks the first unread message, and new rows go below it.

The examples start with the composer beside the transcript, then follow layout, scroll button, message, bubble, list, metadata, system message, and tool calls.

### Anatomy

- **Message area**: Scrollable region for messages. Renders the default slot (typically ChatMessageList) in a flex column that pushes content to the bottom when the list is short.
- **Frosted glass dock**: Sticky or fixed container at the bottom with a backdrop-blur layer. Houses the scroll button and composer.
- **Scroll-to-bottom button**: Appears when the user scrolls up or new messages arrive. Defaults to ChatLayoutScrollButton. Set showScrollButton to false to hide it, or replace it with the scrollButton slot.
- **Composer**: The input area for sending messages, typically ChatComposer. Docked at the bottom inside the frosted glass layer.
- **Empty state**: Centered placeholder shown when no messages exist. Use EmptyState for a consistent look.
- **Avatar**: A sender avatar rendered beside the message. Typically Avatar with size="md". Hidden for system messages.
- **Name**: Sender name above the message body. Place on the bubble when using bubbles, or on the message wrapper for raw content.
- **Content**: The message body: one or more ChatMessageBubble elements, or any free-form slot content like images or tool calls. Wrap non-bubble content in a ghost bubble to align it with the bubble text column.
- **Metadata**: Timestamp, delivery status, and footer actions below the message. Place on the last bubble or on the message wrapper.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Keep the transcript in the message list and the composer in the bottom slot, so the frosted dock and the scroll button stay aligned. |
| Do | Set `isStreaming` for the whole time an assistant body is growing, and clear it when the message is complete. |
| Do | Use `ChatUnreadDivider` at the first message the reader has not seen. The scroll button’s “New messages” label is the announcement; the divider is the visible mark. |
| Do | Put plain text, citations, or your own renderer in the bubble slot. The bubble only supplies the surface. |
| Don't | Render a second scroll parent inside the layout. One element should own the transcript scroll, either the layout or the element passed as `scrollRef`. |
| Don't | Announce every streamed token yourself. The typing indicator speaks once through its own `role="status"`, and the new-message label speaks through the layout live region. |

## Examples

### ChatLayout — Panel View

A narrow column. Press Enter and the draft is appended to the transcript. A tight container uses compact density, which fits a side panel or a drawer.

```vue
<script setup lang="ts">
import { ChatComposer, ChatLayout, ChatMessage, ChatMessageBubble, ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
import { ref } from 'vue'

interface Row {
  id: number
  sender: 'user' | 'assistant'
  text: string
}

let nextId = 3
const rows = ref<Row[]>([
  { id: 1, sender: 'user', text: 'Can you review the Button component and fix the focus ring?' },
  { id: 2, sender: 'assistant', text: 'Found the issue — the border radius was hardcoded. Replaced with the theme token.' },
])

function onSubmit(value: string): void {
  rows.value = [...rows.value, { id: nextId, sender: 'user', text: value }]
  nextId += 1
}
</script>

<template>
  <ChatLayout class="chat-preview__panel">
    <ChatMessageList>
      <ChatSystemMessage variant="divider">
        Today
      </ChatSystemMessage>
      <ChatMessage v-for="row in rows" :key="row.id" :sender="row.sender">
        <ChatMessageBubble v-if="row.sender === 'user'">
          {{ row.text }}
        </ChatMessageBubble>
        <div v-else class="chat-preview__lines">
          <p>{{ row.text }}</p>
        </div>
      </ChatMessage>
    </ChatMessageList>
    <template #composer>
      <ChatComposer placeholder="Ask something..." @submit="onSubmit" />
    </template>
  </ChatLayout>
</template>
```

### ChatComposer — Shell

A field with an attachment drawer, a header button, and dictation. With no text the send button is muted and cannot be clicked; it turns into the solid button once there is something to send. Attachments alone do not enable it. The drawer handle collapses the attachments. The microphone starts listening in browsers that support speech recognition.

```vue
<script setup lang="ts">
import { Badge } from '@astryx-vue/core/Badge'
import { ChatComposer, ChatComposerDrawer, ChatDictationButton } from '@astryx-vue/core/Chat'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { reactive, ref } from 'vue'

const listening = ref(false)
const dictation = reactive({
  isSupported: true,
  bands: [0.12, 0.4, 0.72, 0.36, 0.16],
  volume: 0.05,
  get isListening() {
    return listening.value
  },
  toggle() {
    listening.value = !listening.value
  },
})
const sent = ref('')

function onSubmit(value: string): void {
  sent.value = value
}
</script>

<template>
  <div class="chat-preview-composer">
    <ChatComposer placeholder="Ask about the notes..." @submit="onSubmit">
      <template #drawer>
        <ChatComposerDrawer :count="2" label="Attachments">
          <Badge label="notes.md" variant="neutral" />
          <Badge label="focus.png" variant="neutral" />
        </ChatComposerDrawer>
      </template>
      <template #headerActions>
        <IconButton label="Add attachment" size="sm" variant="ghost">
          <Icon name="carbon:attachment" />
        </IconButton>
      </template>
      <template #headerContext>
        Balanced
      </template>
      <template #footerActions>
        <ChatDictationButton :dictation="dictation" />
      </template>
    </ChatComposer>
    <p v-if="sent" class="chat-preview__caption">
      {{ sent }}
    </p>
  </div>
</template>
```

### ChatComposer — States

The top field is disabled while a reply is in progress, and Stop stays clickable. The middle one is compact and flat, with a warning. The bottom one puts an error above the field.

```vue
<script setup lang="ts">
import { ChatComposer } from '@astryx-vue/core/Chat'
import { ref } from 'vue'

const streaming = ref(true)
</script>

<template>
  <div class="chat-preview-composer">
    <ChatComposer is-disabled is-stop-shown model-value="Still writing the answer." @stop="streaming = false" />
    <ChatComposer
      density="compact"
      elevation="none"
      placeholder="Flat and compact"
      :status="{ type: 'warning', message: 'The connection is slow.' }"
    />
    <ChatComposer
      :status="{ type: 'error', message: streaming ? 'Could not send the last message.' : 'Cleared.' }"
      status-position="top"
    />
  </div>
</template>
```

### ChatComposerInput — Mentions

Type @ at the start of a word and keep going through spaces, as in Ada Lov, and the list narrows to Ada Lovelace. Arrow keys move the highlight, and Enter inserts that row rather than the first person in the list. A phrase with no match hides the list and leaves the typed text alone. Type / for a one-word command; a space closes that list. A person becomes a chip. A command is inserted as text.

```vue
<script setup lang="ts">
import type { ChatComposerTrigger } from '@astryx-vue/core/Chat'
import { createStaticSource } from '@astryx-vue/core'
import { ChatComposer, ChatComposerInput } from '@astryx-vue/core/Chat'
import { ref } from 'vue'

const people = createStaticSource([
  { id: 'ada', label: 'Ada Lovelace' },
  { id: 'maria', label: 'María José García' },
  { id: 'lin', label: 'Lin', auxiliaryData: { group: 'Reviewers' } },
  { id: 'grace', label: 'Grace Hopper', auxiliaryData: { group: 'Reviewers' } },
])

const commands = createStaticSource([
  { id: 'summarize', label: 'summarize' },
  { id: 'translate', label: 'translate' },
])

const triggers: ChatComposerTrigger[] = [
  {
    character: '@',
    hasMultiWordQuery: true,
    menuLabel: 'People',
    searchSource: people,
    onSelect: item => ({ value: `@${item.id}`, label: `@${item.label}`, variant: 'blue' }),
  },
  {
    character: '/',
    menuLabel: 'Commands',
    searchSource: commands,
    onSelect: item => `/${item.label} `,
  },
]

const sent = ref('')

function onSubmit(value: string): void {
  sent.value = value
}
</script>

<template>
  <div class="chat-preview-composer">
    <ChatComposer placeholder="Mention with @, or run /summarize" @submit="onSubmit">
      <template #input>
        <ChatComposerInput :triggers="triggers" />
      </template>
    </ChatComposer>
    <p v-if="sent" class="chat-preview__caption">
      {{ sent }}
    </p>
  </div>
</template>
```

### ChatTokenizedText — Chips in a bubble

The same chip definitions render inside a sent bubble.

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatTokenizedText } from '@astryx-vue/core/Chat'

const tokens = [
  { value: '@ada', label: '@Ada', variant: 'blue' as const },
]
</script>

<template>
  <div class="chat-preview-stack">
    <ChatMessage sender="user">
      <ChatMessageBubble>
        <ChatTokenizedText text="Ask @ada to check the focus ring." :tokens="tokens" />
      </ChatMessageBubble>
    </ChatMessage>
  </div>
</template>
```

### ChatLayoutScrollButton — Labels

Scroll button with different labels for context-specific notifications like new messages, unread replies, or a generic scroll prompt.

```vue
<script setup lang="ts">
import { ChatLayoutScrollButton } from '@astryx-vue/core/Chat'
</script>

<template>
  <div class="chat-preview-stack chat-preview-stack--center">
    <p class="chat-preview__caption">
      {{ 'Labels expand the button to give context' }}
    </p>
    <div class="chat-preview-row chat-preview-row--center">
      <ChatLayoutScrollButton is-visible />
      <ChatLayoutScrollButton is-visible label="New messages" />
      <ChatLayoutScrollButton is-visible label="3 unread replies" />
    </div>
  </div>
</template>
```

### ChatMessage — Avatar & Name

Messages with avatars and sender names. Place the name on the bubble when using bubbles, or on the message wrapper for raw content.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble :name="agent">
        {{ 'I reviewed the pull request. The changes look solid — clean code and good test coverage.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="10:15 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Thanks! Merging it now.' }}
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="10:16 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble :name="agent">
        {{ 'I can run the deployment pipeline once it lands. Just let me know.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="10:17 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessage — Ghost

Ghost variant for messages without visible bubble boundaries. Keeps padding for alignment but renders a transparent background, useful for AI-style responses.

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Here is an analysis of your production metrics from last week. Traffic peaked at 12,400 requests per second on Wednesday, with a p99 latency of 45ms. Error rate stayed below 0.1% across all endpoints.' }}
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:45 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'That looks great. Can you compare it to the week before?' }}
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Compared to the previous week, traffic is up 8% and latency improved by 3ms. The deployment on Tuesday seems to have helped.' }}
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:46 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessage — Multi-Bubble

Grouped bubbles using the group prop for corner radius reduction. Use first, middle, and last to visually connect related bubbles from the same sender.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble group="first">
        {{ 'I have a couple of questions about the new API.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="middle">
        {{ 'First, how should we handle pagination?' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'And second, what\'s the rate limit?' }}
        <template #metadata>
          <ChatMessageMetadata status="delivered" timestamp="11:00 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble group="first" :name="agent">
        {{ 'Great questions! For pagination, use cursor-based with a limit parameter. The response includes a nextCursor field.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'Rate limit is 100 requests per minute per API key. You\'ll get a 429 response with a Retry-After header if you exceed it.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="11:01 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — Custom Content

Custom in-message content aligned to the bubble text column. A ghost bubble spans the message column, and the timestamp rides the bubble metadata slot.

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Can you pull up the Q3 report?' }}
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost" width="100%" name="Navi">
        {{ 'Sure, here is the artifact from last quarter.' }}
        <div class="chat-preview__card">
          <p class="chat-preview__card-title">
            {{ 'Q3 Performance Report' }}
          </p>
          <p class="chat-preview__card-meta">
            {{ '12 pages · updated 2 days ago' }}
          </p>
        </div>
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:45 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — Density

Compact, balanced, and spacious density modes side by side. Density controls bubble padding, corner radius, and spacing between grouped bubbles.

```vue
<script setup lang="ts">
import type { ChatDensity } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
import { Divider } from '@astryx-vue/core/Divider'

const densities: { id: ChatDensity, label: string }[] = [
  { id: 'compact', label: 'Compact' },
  { id: 'balanced', label: 'Balanced' },
  { id: 'spacious', label: 'Spacious' },
]
</script>

<template>
  <div class="chat-preview-densities">
    <template v-for="(density, index) in densities" :key="density.id">
      <Divider v-if="index > 0" />
      <div>
        <p class="chat-preview__caption">
          {{ density.label }}
        </p>
        <ChatMessageList :density="density.id">
          <ChatMessage sender="assistant">
            <ChatMessageBubble>
              {{ 'The build completed in 4.2 seconds.' }}
            </ChatMessageBubble>
          </ChatMessage>
          <ChatMessage sender="user">
            <ChatMessageBubble>
              {{ 'Ship it to staging.' }}
            </ChatMessageBubble>
          </ChatMessage>
        </ChatMessageList>
      </div>
    </template>
  </div>
</template>
```

### ChatMessageBubble — Grouping

Multi-bubble messages using first, middle, and last group positions. Grouped bubbles tighten corner radius on the sender side.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble group="first" :name="agent">
        {{ 'I reviewed the three files you shared.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="middle">
        {{ 'The data model looks solid, but the API handler has a race condition on concurrent writes.' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'I can draft a fix if you want.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="10:45 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble group="first">
        {{ 'Yes please!' }}
      </ChatMessageBubble>
      <ChatMessageBubble group="last">
        {{ 'Also add a test for the concurrent case.' }}
        <template #metadata>
          <ChatMessageMetadata status="delivered" timestamp="10:46 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — Metadata

Bubbles with name and metadata slots aligned to bubble padding. Put name on the first bubble and metadata on the last bubble in a message.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const agent = 'Agent'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble :name="agent">
        {{ 'Your deployment finished successfully. All 14 checks passed.' }}
        <template #metadata>
          <ChatMessageMetadata footer="Claude Opus 4.6" timestamp="9:15 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Great, can you send me the production URL?' }}
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="9:16 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageBubble — Variants

Filled and ghost bubble variants for both user and assistant senders. Use filled for standard messages and ghost when content needs alignment without a visual boundary.

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <p class="chat-preview__caption">
      {{ 'Filled — sender-colored background (default)' }}
    </p>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Can you summarize the latest deployment logs?' }}
      </ChatMessageBubble>
    </ChatMessage>
    <p class="chat-preview__caption">
      {{ 'Ghost — transparent background, keeps alignment padding' }}
    </p>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'The last deploy completed at 2:41 PM with zero errors across all three regions.' }}
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageList — Density

Side-by-side comparison of compact, balanced, and spacious densities. Use compact in sidebars, balanced for most full-page chat, and spacious for long-form reading.

```vue
<script setup lang="ts">
import type { AvatarSize } from '@astryx-vue/core/Avatar'
import type { ChatDensity } from '@astryx-vue/core/Chat'
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
import { Divider } from '@astryx-vue/core/Divider'

const agent = 'Agent'

const densities: { id: ChatDensity, label: string, avatar: AvatarSize }[] = [
  { id: 'compact', label: 'Compact', avatar: 'sm' },
  { id: 'balanced', label: 'Balanced', avatar: 'md' },
  { id: 'spacious', label: 'Spacious', avatar: 'md' },
]
</script>

<template>
  <div class="chat-preview-densities">
    <template v-for="(density, index) in densities" :key="density.id">
      <Divider v-if="index > 0" />
      <div>
        <p class="chat-preview__caption">
          {{ density.label }}
        </p>
        <ChatMessageList :density="density.id">
          <ChatMessage sender="user">
            <ChatMessageBubble>
              {{ 'How does density work?' }}
            </ChatMessageBubble>
          </ChatMessage>
          <ChatMessage sender="assistant">
            <template #avatar>
              <Avatar :name="agent" :size="density.avatar" />
            </template>
            <ChatMessageBubble>
              {{ 'Density provides default spacing at every level — message gap, bubble padding, and gap between child elements. Use gap to tune row spacing independently.' }}
            </ChatMessageBubble>
          </ChatMessage>
        </ChatMessageList>
      </div>
    </template>
  </div>
</template>
```

### ChatMessageList — Full Featured

A transcript with a date divider, file names, a code sample, ghost bubbles, and metadata.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatSystemMessage } from '@astryx-vue/core/Chat'
import { CodeBlock } from '@astryx-vue/core/Code'

const agent = 'Agent'
const bubbleWidth = 'min(100%, 21.25rem)'

const snippet = `const [state, dispatch] = useReducer(
  (state, action) => ({
    ...state,
    [action.field]: action.value,
  }),
  { name: '', email: '' }
);`
</script>

<template>
  <ChatMessageList>
    <ChatSystemMessage variant="divider">
      Today
    </ChatSystemMessage>
    <ChatMessage sender="user">
      <div class="chat-preview__files">
        <span class="chat-preview__file">useReducer.ts</span>
        <span class="chat-preview__file">formState.ts</span>
      </div>
      <ChatMessageBubble>
        Can you review these files?
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble group="first" :width="bubbleWidth">
        Sure! Here's the key pattern from <strong>useReducer.ts</strong>:
      </ChatMessageBubble>
      <ChatMessageBubble group="last" :width="bubbleWidth">
        The reducer is <strong>pure and easy to test</strong> — pass in state and action, assert on the output.
      </ChatMessageBubble>
      <ChatMessageBubble variant="ghost" :width="bubbleWidth">
        <CodeBlock :code="snippet" language="tsx" width="100%" />
        <template #metadata>
          <ChatMessageMetadata timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatSystemMessage>
      Agent shared a code snippet
    </ChatSystemMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        That's clean, thanks!
        <template #metadata>
          <ChatMessageMetadata status="delivered" timestamp="2:31 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageMetadata — Footer Actions

Assistant message with footer actions: copy, retry, thumbs up/down, and model label.

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
import { IconButton } from '@astryx-vue/core/IconButton'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Summarize the Q1 revenue report.' }}
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="9:41 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Q1 revenue reached $2.4B, up 18% year-over-year. Enterprise subscriptions drove 62% of the growth, while ad revenue held steady at $890M.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="9:42 AM">
            <template #footer>
              <span class="chat-preview__actions">
                <IconButton label="Copy" variant="ghost" size="sm">
                  <span class="i-carbon-copy" />
                </IconButton>
                <IconButton label="Retry" variant="ghost" size="sm">
                  <span class="i-carbon-renew" />
                </IconButton>
                <IconButton label="Thumbs up" variant="ghost" size="sm">
                  <span class="i-carbon-thumbs-up" />
                </IconButton>
                <IconButton label="Thumbs down" variant="ghost" size="sm">
                  <span class="i-carbon-thumbs-down" />
                </IconButton>
                <span>GPT-4o</span>
              </span>
            </template>
          </ChatMessageMetadata>
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageMetadata — Status

All 5 delivery statuses (sending, sent, delivered, read, and error), each with a timestamp.

```vue
<script setup lang="ts">
import type { ChatMessageStatus } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'

const rows: { status: ChatMessageStatus, text: string }[] = [
  { status: 'sending', text: 'Deploying the update now…' },
  { status: 'sent', text: 'Config pushed to staging.' },
  { status: 'delivered', text: 'Verified on the staging cluster.' },
  { status: 'read', text: 'Looks good — promoting to prod.' },
  { status: 'error', text: 'Rollback triggered, checking logs.' },
]
</script>

<template>
  <ChatMessageList>
    <ChatMessage v-for="row in rows" :key="row.status" sender="user">
      <ChatMessageBubble>
        {{ row.text }}
        <template #metadata>
          <ChatMessageMetadata :status="row.status" timestamp="10:15 AM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatMessageMetadata — Timestamps

Timestamp-only metadata on user and assistant messages. Supports an absolute time and a relative label.

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        {{ 'Thanks — any blockers I should know about?' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        {{ 'Relative timestamps work too — helpful for older messages where the exact time matters less than recency.' }}
        <template #metadata>
          <ChatMessageMetadata timestamp="5 months ago" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatSystemMessage — Icon

System messages with a leading icon that reinforces the message type.

```vue
<script setup lang="ts">
import { ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <p class="chat-preview__caption">
      {{ 'Icons reinforce the message type' }}
    </p>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-user-follow" style="width: 14px; height: 14px" />
      </template>
      {{ 'Jordan was added to the conversation' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-locked" style="width: 14px; height: 14px" />
      </template>
      {{ 'Messages are end-to-end encrypted' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-renew" style="width: 14px; height: 14px" />
      </template>
      {{ 'Agent is generating a response…' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-carbon-checkmark" style="width: 14px; height: 14px" />
      </template>
      {{ 'Conversation verified by admin' }}
    </ChatSystemMessage>
  </ChatMessageList>
</template>
```

### ChatSystemMessage — Status Updates

Status messages in a conversation flow: membership changes, a topic change, dates, and a resolution notice.

```vue
<script setup lang="ts">
import { ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList>
    <ChatSystemMessage variant="divider">
      {{ 'March 14, 2026' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-heroicons-user-plus" style="width: 20px; height: 20px" />
      </template>
      {{ 'Sarah Chen joined the conversation' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      {{ 'Topic changed to "Q2 Launch Planning"' }}
    </ChatSystemMessage>
    <ChatSystemMessage variant="divider">
      {{ 'March 15, 2026' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-heroicons-user-minus" style="width: 20px; height: 20px" />
      </template>
      {{ 'Alex Rivera left the conversation' }}
    </ChatSystemMessage>
    <ChatSystemMessage variant="divider">
      {{ 'Today' }}
    </ChatSystemMessage>
    <ChatSystemMessage>
      <template #icon>
        <span class="i-heroicons-check-circle" style="width: 20px; height: 20px" />
      </template>
      {{ 'Conversation marked as resolved' }}
    </ChatSystemMessage>
  </ChatMessageList>
</template>
```

### ChatSystemMessage — Variants

Default and divider variants side by side. Use default for inline status updates and divider for date separators.

```vue
<script setup lang="ts">
import { ChatMessageList, ChatSystemMessage } from '@astryx-vue/core/Chat'
</script>

<template>
  <div class="chat-preview-columns" style="grid-template-columns: repeat(2, minmax(0, 1fr));">
    <div>
      <p class="chat-preview__caption">
        {{ 'Default' }}
      </p>
      <ChatMessageList class="chat-preview-pane">
        <ChatSystemMessage>
          {{ 'Alex joined the conversation' }}
        </ChatSystemMessage>
        <ChatSystemMessage>
          {{ 'Conversation marked as resolved' }}
        </ChatSystemMessage>
      </ChatMessageList>
    </div>
    <div>
      <p class="chat-preview__caption">
        {{ 'Divider' }}
      </p>
      <ChatMessageList class="chat-preview-pane">
        <ChatSystemMessage variant="divider">
          {{ 'March 15, 2026' }}
        </ChatSystemMessage>
        <ChatSystemMessage variant="divider">
          {{ 'Today' }}
        </ChatSystemMessage>
      </ChatMessageList>
    </div>
  </div>
</template>
```

### ChatToolCalls — Expandable

Tool calls with expandable result details. Click a row to reveal its result.

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const calls = computed<ChatToolCallItem[]>(() => [
  {
    name: 'edit',
    status: 'complete',
    node: 'cli:remote-server',
    target: 'src/utils/formatDate.ts',
    additions: 6,
    deletions: 3,
    duration: '85ms',
    detail: 'diff',
  },
  {
    name: 'bash',
    status: 'complete',
    node: 'cli:remote-server',
    target: 'yarn test',
    duration: '1.8s',
    detail: 'ok',
  },
  {
    name: 'web_search',
    status: 'complete',
    target: 'Intl.DateTimeFormat locale options',
    duration: '1.2s',
    detail: 'ok',
  },
])

const diff = `function formatDate(value) {
-  return value.toLocaleDateString();
+  return new Intl.DateTimeFormat(locale).format(value);
}`
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="calls" default-is-expanded>
        <template #detail="{ call }">
          <pre class="chat-preview__code">{{ call.name === 'edit' ? diff : 'passed' }}</pre>
        </template>
      </ChatToolCalls>
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatToolCalls — Simple

A single inline tool call above a collapsible multi-call group with diff stats.

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const single = computed<ChatToolCallItem[]>(() => [
  { name: 'bash', status: 'complete', target: 'git status', duration: '1.2s' },
])

const group = computed<ChatToolCallItem[]>(() => [
  { name: 'read', status: 'complete', target: 'src/components/DataGrid.tsx', duration: '30ms' },
  { name: 'edit', status: 'complete', target: 'src/components/DataGrid.tsx', additions: 24, deletions: 8, duration: '85ms' },
  { name: 'edit', status: 'complete', target: 'src/components/DataGrid.test.tsx', additions: 45, duration: '60ms' },
])
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="single" />
      <ChatToolCalls :calls="group" default-is-expanded />
    </ChatMessage>
  </ChatMessageList>
</template>
```

### ChatToolCalls — Statuses

All four status states (pending, running, complete, and error) shown together in a single group.

```vue
<script setup lang="ts">
import type { ChatToolCallItem } from '@astryx-vue/core/Chat'
import { ChatMessage, ChatMessageList, ChatToolCalls } from '@astryx-vue/core/Chat'
import { computed } from 'vue'

const calls = computed<ChatToolCallItem[]>(() => [
  { name: 'bash', status: 'pending', target: 'yarn build' },
  { name: 'bash', status: 'running', target: 'yarn test' },
  { name: 'edit', status: 'complete', target: 'src/App.tsx', additions: 8, deletions: 2, duration: '120ms' },
  {
    name: 'bash',
    status: 'error',
    target: 'yarn lint',
    errorMessage: '3 lint errors found',
  },
])
</script>

<template>
  <ChatMessageList>
    <ChatMessage sender="assistant">
      <ChatToolCalls :calls="calls" default-is-expanded />
    </ChatMessage>
  </ChatMessageList>
</template>
```

## ChatLayout

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | Spacing, bubble width, and the frosted dock. Messages can override it. |
| `scrollRef` | `HTMLElement \| null` | - | Element that scrolls instead of the layout. Leave it unset and the layout scrolls itself. The dock is sticky for self scroll and fixed for an external scroller. |
| `showScrollButton` | `boolean` | `true` | Renders the built-in scroll-to-bottom button. Set `false` when the `scrollButton` slot replaces it, or when the button should be absent. |

| Name | Description |
| --- | --- |
| `default` | The transcript. Usually one `ChatMessageList`. |
| `composer` | Pinned to the bottom, above the frosted scrim. |
| `empty` | Shown when the default slot is omitted. |
| `scrollButton` | Replaces the built-in button. Props: `isVisible`, `label` (set only while new messages are waiting), and `scrollToBottom`. |

A template ref exposes <code>element</code> and <code>scrollToBottom()</code>. <code>scrollToBottom</code> clears the new-message label and springs to the bottom.

## ChatLayoutScrollButton

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `isVisible` | `boolean` | - | Whether the pill is shown. A hidden pill stays mounted so it can fade, and is marked `inert`. |
| `label` | `string` | - | Widens the pill. Omit it for the icon-only down arrow. The accessible name falls back to “Scroll to bottom”. |

Emits <code>click</code> when the button is pressed.

## ChatMessageList

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `'top' \| 'bottom'` | `'bottom'` | Vertical alignment when the list is shorter than its container. `'bottom'` keeps a spacer above the rows; `'top'` drops it so rows start at the top. |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | Passed to messages that do not set their own. |
| `gap` | `SpacingStep` | - | Gap between top-level rows, on the spacing scale. Overrides the density gap without changing padding. |
| `scrollToTopAction` | `() => Promise<void>` | - | Loads older messages when the top sentinel intersects the scroll parent. A spinner shows while the promise is pending. A second intersection is ignored until it settles. |
| `isStreaming` | `boolean` | `false` | Sets `aria-busy` while an assistant message is still streaming. |
| `label` | `string` | localized “Messages” | Accessible name of the log. |

The <code>default</code> slot is the rows. The <code>emptyState</code> slot shows when <code>default</code> is omitted; <code>empty</code> is its alias. The root is <code>role="log"</code> with <code>aria-live="polite"</code>.

## ChatMessage

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `sender` | `'user' \| 'assistant' \| 'system'` | - | Who the row is from. `system` is centered and ignores avatar, name, and metadata. |
| `name` | `string` | - | Plain sender name. Prefer the bubble `name` when the body starts with a bubble. |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | the list, then `'balanced'` | Density for this row only. |

Slots: <code>default</code> (body), <code>avatar</code>, <code>name</code> (replaces the prop), <code>metadata</code>. The article is named from the visible name, or from “Message from {sender}”.

## ChatMessageBubble

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'filled' \| 'ghost'` | `'filled'` | `filled` paints the sender surface. `ghost` drops the background and the block padding so mixed content still lines up. |
| `name` | `string` | - | Sender name aligned to the bubble padding. |
| `group` | `'first' \| 'middle' \| 'last'` | - | Tightens a corner when several bubbles stack. Leave it unset for a standalone bubble. |

Sender and density come from the surrounding <code>ChatMessage</code>. Slots: <code>default</code>, <code>name</code>, <code>metadata</code>. Attributes fall through to the bubble element.

## ChatMessageMetadata

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `timestamp` | `string` | - | Short time text. The `timestamp` slot replaces it. |
| `footer` | `string` | - | Secondary text, such as a model name. The `footer` slot replaces it. |
| `status` | `'sending' \| 'sent' \| 'delivered' \| 'read' \| 'error'` | - | Delivery state. The row renders nothing when timestamp, footer, and status are all absent. |

The status icon’s accessible name is localized, for example “Message read”. On a user row the items run from the inline end.

## ChatSystemMessage

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'default' \| 'divider'` | `'default'` | `default` is a centered line. `divider` draws a rule on each side. The root is `role="status"`. |

Slots: <code>default</code>, and <code>icon</code> (ignored for <code>divider</code>).

## ChatToolCalls

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `calls` | `ChatToolCallItem[]` | - | Tool calls from one assistant turn. An empty array renders nothing. Each item may set `name`, `status`, `target`, `duration`, `node`, `additions`, `deletions`, `stats`, `errorMessage`, `key`, `resultDetail`, and `data`. `detail` is an alias of `resultDetail`. |
| `label` | `string` | localized “{count} tool calls” | Summary shown while a group is expanded. |
| `v-model:isExpanded` | `boolean` | - | Controlled expanded state of a multi-call group. |
| `defaultIsExpanded` | `boolean` | `false` | Initial state while `isExpanded` is unbound. |

Emits <code>expandedChange</code> with the next boolean. The <code>detail</code> slot receives <code>{ call }</code> and replaces <code>call.resultDetail</code>. A collapsed group shows the latest call; the panel is <code>inert</code> until opened. Pending and running calls use a spinner. An error is spoken from a visually hidden string.

## ChatReasoning

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | localized “Thinking” | Header label. |
| `duration` | `string` | - | Shown after the label once streaming has finished, such as `12s`. |
| `isStreaming` | `boolean` | `false` | Shimmers the label and hides the preview and the duration. |
| `v-model:isExpanded` | `boolean` | - | Controlled expanded state. |
| `defaultIsExpanded` | `boolean` | `false` | Initial state while `isExpanded` is unbound. |

The default slot is the full trace. A single text node is also the collapsed preview. Emits <code>expandedChange</code>. The panel is <code>inert</code> while collapsed.

## ChatTypingIndicator

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `names` | `string[]` | - | People currently typing. Empty names are dropped. No names leaves the dots only. One, two, and three or more each get their own sentence. |

The root is <code>role="status"</code> with <code>aria-live="polite"</code>. The sentence lives in that region and is not announced again through <code>useAnnounce</code>.

## ChatUnreadDivider

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | localized “New” | Short label at the end of the rule. The accessible name is “{label} messages below”. |

The root is <code>role="separator"</code>. It is not a <code>.astryx-chat-message</code>, so the new-message tracker does not treat it as a message.

## ChatComposer

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `string` | - | Controlled draft, bound with `v-model`. Omit it and the shell keeps the draft. |
| `placeholder` | `string` | The localized “Type a message…” | Shown while the draft is empty. |
| `isDisabled` | `boolean` | `false` | Disables editing and send. Stop stays clickable when it is shown. |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | Padding inside the body. Spacious uses the same spacing as balanced. |
| `elevation` | `'low' \| 'none'` | `'low'` | Low deepens on hover and focus. None is flat and uses a border. The focus ring appears only when the keyboard focuses the editor. |
| `isStopShown` | `boolean` | `false` | Shows stop instead of send. |
| `status` | `{ type: 'error' \| 'warning', message?: string }` | - | Error uses alert. Warning uses status. |
| `statusPosition` | `'top' \| 'bottom'` | `'bottom'` | Places the bar above or below the body, tucked behind the radius. |

| Name | Payload | Description |
| --- | --- | --- |
| `submit` | `value: string` | Fires for a trimmed, non-empty draft, then clears it. |
| `stop` | - | Stop was activated. |
| `change` | `value: string` | The draft changed, including the clear after submit. |
| `update:modelValue` | `value: string` | Emitted with change for `v-model`. |

Slots are drawer, headerActions, headerContext, input, footerActions, sendActions, and sendButton. The template ref exposes element.

## ChatComposerInput

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `string` | - | Controlled draft. Inside a shell, an omitted value follows the shell. |
| `placeholder` | `string` | The shell or the localized placeholder | Hint on the empty editor. |
| `maxRows` | `number` | `8` | The editor scrolls after this many lines. A line is 22px. |
| `triggers` | `ChatComposerTrigger[]` | - | A trigger character at a word boundary opens suggestions. `hasMultiWordQuery` on a trigger keeps the query through spaces and punctuation until a line break, 64 characters, or the same trigger character again. Without it, a space or tab ends the query. An unmatched multi-word query hides the list when `emptySearchText` is omitted. The editor becomes a combobox. |
| `debounceMs` | `number` | `150` | Delay for an async search. Sync results are immediate, and the first async search starts at once. |
| `hasHistory` | `boolean` | `true` | Arrow Up at the start recalls the previous submission. Arrow Down at the end steps forward. |
| `label` | `string` | The localized “Message input” | Accessible name of the editor. |
| `isDisabled` | `boolean` | The shell's disabled state | Disables editing. |
| `pasteAsToken` | `Return value of useChatPasteAsToken \| false` | Pastes longer than 200 characters become a chip | Pass false to insert the text as-is. |
| `pasteAction` | `(event, text) => boolean \| void` | - | First chance at a plain-text paste. Return true and the input will not insert it. |

Events are change, update:modelValue, submit, files, and keydown. files comes from a paste or a drop, and a drop does not navigate away. keydown fires after an open suggestion list declines the event. preventDefault skips Enter-to-send and history. The template ref exposes insertToken, expandToken, insertText, focus, and getValue.

## ChatComposerDrawer

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `count` | `number` | - | Enables collapse and appears in the default summary. |
| `label` | `string` | The localized “Items” | Used in the summary and in the expand and collapse names. |
| `isCollapsed` | `boolean` | - | Controlled collapsed state, bound with `v-model:is-collapsed`. |
| `defaultIsCollapsed` | `boolean` | `false` | Initial state while unbound. |

collapsedChange fires when the collapsed state changes. The default slot is the open content. collapsedSummary replaces the count and label. The button still owns the disclosure name.

## ChatSendButton

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `isStopShown` | `boolean` | The shell's stop state | Stop is secondary. Send is primary. Stop ignores disabled. |
| `isDisabled` | `boolean` | The draft is empty or the shell is disabled | Disables send only. |
| `size` | `'sm' \| 'md'` | `'md'` | Button size. The shell rounds it to the outer radius. |

Events are send, stop, and click. sendIcon and stopIcon replace the arrow and the square. Inside a shell, a click with no listener of its own submits the draft or stops the shell.

## ChatDictationButton

Pass the return value of useChatDictation or useSpeechRecognition. size defaults to md. isHiddenWhenUnsupported defaults to true, so the control is absent when speech recognition is missing. Set it to false and the button stays, disabled. label replaces the start or stop dictation name. While listening, five bars follow the volume, and a loud signal blends the accent color toward the error color.

## ChatTokenizedText

The default slot holds the message text, and the text prop does the same job when a string is easier to pass. Each non-empty value in tokens is matched literally and replaced with a badge or custom content. Empty values are ignored. The same objects a trigger returns can be reused here.

## ChatComposerTokenElement

Wraps one chip so it cannot be edited inside the field and carries the serialized value. The input inserts these itself. Use it directly when a chip should render outside the editor.

## Composables

<code>useChatStreamScroll({ scrollRef })</code> follows the bottom of <code>scrollRef</code>. The first time the content can scroll it jumps; later growth uses a spring, unless reduced motion is requested. Scrolling up unlocks at once. Settling within 10px of the bottom locks again. <code>isScrolledUp</code> feeds the button.

<code>useChatNewMessages({ isLocked, onResize })</code> watches the transcript height. When the last <code>.astryx-chat-message</code> changes while unlocked, <code>hasNewMessages</code> becomes true until <code>dismiss()</code>. <code>ChatLayout</code> announces that change.

<code>useChatStreamScroll</code>, <code>useChatNewMessages</code>, and <code>useAnnounce</code> are exported from <code>@astryx-vue/core</code>. Reduced motion is read with <code>useMediaQuery</code>.

The composer also exports useChatComposerContext, useChatComposerTokens, useChatPasteAsToken, useChatDictation, and useSpeechRecognition. Import them from the Chat entry. The package root re-exports them. Speech recognition uses the browser API and does not bind a speech service. A paste longer than 200 characters becomes a chip by default.

## Theming

`packages/themes/neutral/chat.css` maps component variables onto semantic tokens. Another theme overrides those variables.

- `--astryx-chat-bubble-background` / `--astryx-chat-foreground`: filled bubble surface and text, mapped from `--astryx-color-neutral` and `--astryx-color-text-primary`.
- `--astryx-chat-foreground-secondary`, `--astryx-chat-foreground-muted`: metadata, system lines, and secondary tool text.
- `--astryx-chat-error`, `--astryx-chat-success`, `--astryx-chat-accent`: failed status, deletions, and a running tool.
- `--astryx-chat-unread-color`: the unread rule and its label. Neutral maps it to `--astryx-color-border-red`, so both viewports use one color.
- `--astryx-chat-focus`: keyboard outline on disclosure buttons.
- `--astryx-chat-scroll-background` / `--astryx-chat-scroll-shadow`: the scroll-to-bottom pill.
- `--astryx-chat-radius`: standalone bubble radius. Compact density uses `--astryx-chat-radius-compact`. Grouped bubbles use `--astryx-chat-radius-group` on the inner corners.
- `--astryx-duration-fast-max`, `--astryx-duration-medium-max`, `--astryx-duration-slow-max`, `--astryx-ease-standard`: pill, disclosure, and typing motion.

## Native attributes

`ChatLayout`, `ChatMessageList`, `ChatMessage`, `ChatSystemMessage`, `ChatToolCalls`, `ChatReasoning`, `ChatTypingIndicator`, `ChatUnreadDivider`, `ChatComposer`, `ChatComposerDrawer`, `ChatComposerInput`, and `ChatTokenizedText` forward attributes to their root. `ChatMessageBubble` forwards them to the bubble, not the name or metadata. `ChatMessageMetadata` forwards them to the metadata row. `ChatSendButton` and `ChatDictationButton` forward them to the button.
