# Conversation

Compose ai conversation without managing the chat transport yourself.

Import: @slices.sh/ai-chat/headless

Public parts: 138

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/ai-chat@^0.1.4 @slices.sh/composer@^0.1.4
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/ai-chat.md

### Optional: Chat media parts

```bash
npm install @slices.sh/chat-media
```

### Optional: Markdown rendering

```bash
npm install @slices.sh/markdown
```

### Optional: Migrate chat attachments

```bash
npm install @slices.sh/chat-attachments
```

## Anatomy

### Composition

```text
Chat.Root
  Chat.Unavailable
  Chat.Empty
    Chat.Suggestions
      Chat.Suggestion text="Plan a launch"
  Chat.Settings
    Chat.Models
      Chat.ModelPicker
    Chat.OptionalTools selected={false}
      Chat.ToggleTool tool="file-search"
    Chat.ContextUsage
  Chat.Transcript
    Chat.Thread
      Chat.Messages
        Chat.Message
          Chat.Speaker
          Chat.Parts
            Chat.StepBoundary
            Chat.PartText
              Markdown
            Chat.PartReasoning
              Chat.ReasoningDisclosure
                Chat.ReasoningTimer
            Chat.PartTool
              Chat.ToolRenderer
                Chat.ToolInput
                Chat.ToolOutput
                Chat.ToolError
            Chat.PartApproval
              Chat.ApprovalReason
              Chat.Approve
              Chat.Deny
              Chat.AlwaysAllow
              Chat.ApprovalError
            Chat.ApprovalReceipt
            Chat.RememberedApproval
              Chat.ForgetApproval
            Chat.PartQuestion
              Chat.Questions
                Chat.QuestionProgress
                Chat.QuestionItem name="audience"
                  Chat.Answer name="audience" value="product"
                  Chat.OtherAnswer name="audience"
                  Chat.QuestionOther name="audience"
                  Chat.ClearAnswer name="audience"
                  Chat.QuestionInvalid name="audience"
                Chat.QuestionBack
                Chat.QuestionNext
                Chat.SubmitAnswers
              Chat.SkipQuestions
              Chat.QuestionError
            Chat.QuestionReceipt
            Chat.PartSource
              Chat.SourceRoot source={source}
                Chat.SourceLink
                Chat.SourceUnavailable
            Chat.PartFile
              Chat.FileRoot file={file}
                Chat.FileKind kind="image"
                  Chat.FileImage
                Chat.FileKind kind="image"
                  Chat.FileAudio
                Chat.FileKind kind="image"
                  Chat.FileVideo
                Chat.FileLink
                Chat.FileUnavailable
            ChatMedia.Root
              ChatMedia.ImageRun
                ChatMedia.More
              ChatMedia.OtherFile
          Chat.Stopped
            Chat.Continue prompt="Continue"
          Chat.MessageEditRoot
            Chat.MessageReading
              Chat.Edit
            Chat.MessageEditing
              Chat.MessageEditor
                Chat.EditedInput
                Chat.EditedAttachments
                Chat.SaveEdit
                Chat.CancelEdit
          Chat.References
            Chat.CitationRoot
              Chat.CitationTrigger
              Chat.CitationContent
                Chat.CitationTitle
                Chat.CitationDescription
            Chat.SourceSheetRoot
              Chat.SourceSheetTrigger
              Chat.SourceSheetContent
                Chat.SourceSheetTitle
                Chat.SourceSheetDescription
                Chat.SourceSheetClose
          Chat.Sources
          Chat.BranchNavigation
            Chat.BranchPrevious
            Chat.BranchPage
            Chat.BranchNext
          Chat.MessageFooter
            Chat.Model
            Chat.Timing
            Chat.Usage
            Chat.Copy
            Chat.FeedbackRoot
              Chat.Feedback rating="up"
              Chat.FeedbackError
            Chat.ReadAloud
            Chat.Regenerate
  Chat.Waiting
  Chat.Failed
    Chat.TryAgain
  Chat.FollowUps
    Chat.Suggestion text="Plan a launch"
  Chat.JumpToLatest
  Upload.Root
    ChatAttachments.Root
      ChatAttachments.Surface
        ChatAttachments.Available
        Chat.Composer
          Chat.Input
          Chat.Submit
          Chat.Stop
      ChatAttachments.Error
        ChatAttachments.Retry
  Chat.Queued
    Chat.QueueItems
      Chat.QueueItem
        Chat.QueuePreview
          Chat.EditQueued
        Chat.QueueEditing
          Chat.QueuedInput
          Chat.SaveQueued
          Chat.CancelQueueEdit
        Chat.SendNow
        Chat.CancelQueued
  Chat.ComposerRoot name="launch"
    Chat.ComposerStandalone
  ChatDictation.Root
    ChatDictation.Voice
      ChatDictation.Confirm
    ChatDictation.Text
      ChatDictation.Input
```

## A conversation

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Chat } from "@slices.sh/ai-chat/headless"
import { Composer } from "@slices.sh/composer"

export function Example() {
  return (
    <Chat.Root>
      <Chat.Transcript>
        <Chat.Thread>
          <Chat.Messages>
            <Chat.Message>
              <Chat.Parts>
                <Chat.PartText>{({ text }) => <p>{text}</p>}</Chat.PartText>
              </Chat.Parts>
            </Chat.Message>
          </Chat.Messages>
        </Chat.Thread>
      </Chat.Transcript>
      <Composer.Root>
        <Composer.Input aria-label="Message" />
        <Composer.Submit>Send</Composer.Submit>
      </Composer.Root>
    </Chat.Root>
  )
}

```

Chat.Root receives Composer's messages. The echo provider lets you try this without a model key.

## Stop a reply

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Chat } from "@slices.sh/ai-chat/headless"
import { Composer } from "@slices.sh/composer"

export function Example() {
  return (
    <Chat.Root>
      <Composer.Root>
        <Composer.Input aria-label="Message" />
        <Composer.Submit>Send</Composer.Submit>
        <Chat.Stop>Stop replying</Chat.Stop>
      </Composer.Root>
    </Chat.Root>
  )
}

```

Add Chat.Stop to a conversation. It only appears while the assistant is replying.

## Tools & approvals

```tsx
"use client"

import "@/services"
import { Chat } from "@slices.sh/ai-chat/headless"

export function Example() {
  return (
    <Chat.Root>
      <Chat.Messages>
        <Chat.Message>
          <Chat.Parts>
            <Chat.PartTool>
              <Chat.ToolInput />
              <Chat.ToolOutput />
              <Chat.ToolError />
            </Chat.PartTool>
            <Chat.PartApproval>
              <Chat.ApprovalReason />
              <Chat.Approve>Allow once</Chat.Approve>
              <Chat.Deny>Deny</Chat.Deny>
              <Chat.ApprovalError />
            </Chat.PartApproval>
          </Chat.Parts>
        </Chat.Message>
      </Chat.Messages>
    </Chat.Root>
  )
}

```

Use the echo provider locally or configure a model provider. Controls appear only for matching message parts.

## Follow-up questions

```tsx
"use client"

import "@/services"
import { Chat } from "@slices.sh/ai-chat/headless"

export function Example() {
  return (
    <Chat.Root>
      <Chat.Messages>
        <Chat.Message>
          <Chat.Parts>
            <Chat.PartQuestion>
              <Chat.Questions>
                {({ questions }) =>
                  questions.map((question) => (
                    <Chat.QuestionItem
                      key={question.name}
                      name={question.name}
                    >
                      <legend>{question.title}</legend>
                      {question.choices.map((choice) => (
                        <label key={choice.value}>
                          <Chat.Answer
                            name={question.name}
                            value={choice.value}
                          />
                          {choice.label}
                        </label>
                      ))}
                      <Chat.OtherAnswer
                        name={question.name}
                        aria-label="Your answer"
                      />
                    </Chat.QuestionItem>
                  ))
                }
              </Chat.Questions>
              <Chat.SubmitAnswers>Send answers</Chat.SubmitAnswers>
              <Chat.SkipQuestions>Skip</Chat.SkipQuestions>
            </Chat.PartQuestion>
          </Chat.Parts>
        </Chat.Message>
      </Chat.Messages>
    </Chat.Root>
  )
}

```

Use the echo provider locally or configure a model provider. Controls appear only for matching message parts.

## Sources & citations

```tsx
"use client"

import "@/services"
import { Chat } from "@slices.sh/ai-chat/headless"

export function Example() {
  return (
    <Chat.Root>
      <Chat.Messages>
        <Chat.Message>
          <Chat.Parts>
            <Chat.PartSource>
              {({ source }) => (
                <Chat.SourceRoot source={source}>
                  <Chat.SourceLink />
                  <Chat.SourceUnavailable>
                    Source unavailable.
                  </Chat.SourceUnavailable>
                </Chat.SourceRoot>
              )}
            </Chat.PartSource>
          </Chat.Parts>
        </Chat.Message>
      </Chat.Messages>
    </Chat.Root>
  )
}

```

Use the echo provider locally or configure a model provider. Controls appear only for matching message parts.

## Complete API reference

### Chat.StepBoundary

gate

Messages repeats once per UIMessage and Message is its element: data-state user or assistant, aria-busy while it is the answer being written, data-invalid when it was cut off. Speaker says who spoke, for screen readers. Parts repeats once per SDK part in the order the model produced them, and each kind has its own gate: PartText is one run of prose, StepBoundary marks a new model step. Stopped renders only for a reply that was cut off, and Continue sends a follow-up prompt that resumes it.

Import: @slices.sh/ai-chat/headless

Chat.StepBoundary requires Chat.Parts.

Chat.StepBoundary requires Chat.Messages.

```tsx
GateProps<{ number: number }>
```

Source: @slices.sh/ai-chat/src/headless/step-boundary.tsx

### Chat.Continue

action · Element: button

Messages repeats once per UIMessage and Message is its element: data-state user or assistant, aria-busy while it is the answer being written, data-invalid when it was cut off. Speaker says who spoke, for screen readers. Parts repeats once per SDK part in the order the model produced them, and each kind has its own gate: PartText is one run of prose, StepBoundary marks a new model step. Stopped renders only for a reply that was cut off, and Continue sends a follow-up prompt that resumes it.

Import: @slices.sh/ai-chat/headless

Chat.Continue requires Chat.Root.

Chat.Continue requires Chat.Messages.

```tsx
PartProps<"button"> & { prompt: string }
```

Source: @slices.sh/ai-chat/src/headless/continue.tsx

### Chat.ReasoningTimer

content · Element: span

PartReasoning gates a reasoning part and hands its children { text, streaming }. ReasoningDisclosure is a native details element, so a find in page can reveal a match. ReasoningTimer gives { seconds, streaming } for "Thought for 4s". Without the reasoning capability the model sends no reasoning parts, and the group renders nothing.

Import: @slices.sh/ai-chat/headless

Capability: reasoning

Chat.ReasoningTimer requires Chat.PartReasoning.

```tsx
PartProps<"span", { seconds: number | null; streaming: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/reasoning-timer.tsx

### Chat.ReasoningDisclosure

content · Element: details

Native details stays searchable and lets the browser reveal a found match.

Import: @slices.sh/ai-chat/headless

Capability: reasoning

Chat.ReasoningDisclosure requires Chat.PartReasoning.

```tsx
PartProps<"details">
```

Source: @slices.sh/ai-chat/src/headless/reasoning-disclosure.tsx

### Chat.ComposerRoot

scope

Reuses the containing chat, so embedding a composer never starts a second conversation.

Import: @slices.sh/ai-chat/headless

```tsx
ComposerRootProps

type ComposerRootProps = RootProps & { name: string; chat?: AiChat; settings?: ChatSettings | null }
```

Source: @slices.sh/ai-chat/src/headless/composer-root.tsx

### Chat.ComposerStandalone

gate

Today's chat composer parts. In v2 Chat.Root is a sink (whenBusy "queue") and the generic Composer from @slices.sh/composer writes into it. Chat.Composer becomes Composer.Root. Chat.Input becomes Composer.Input, a field named text, wrapped by Dictation.Field when dictation is wanted. Chat.Submit's sending half becomes Composer.Submit. Its stopping half is chat behaviour no generic part can express, so it stays a chat part, Chat.Stop (new in v2): it calls the SDK's stop, is disabled while nothing streams and never unmounts, so focus and layout stay put when a turn ends. Chat.ComposerRoot becomes a nested Chat.Root, which shares the outer conversation for the same config. Chat.ComposerStandalone has no replacement: a pattern that draws Chat.Settings only when it stands alone takes a prop for it.

Import: @slices.sh/ai-chat/headless

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/composer-standalone.tsx

### Chat.Root

scope

Chat.Root publishes the SDK Chat itself, never a copy of its state, and provides a sink for Composer. submit(message) calls sendMessage with message.text and message.attachments as file parts. busy is true while a reply streams or a question or tool decision waits. whenBusy is "queue" unless queue={false}. accept.attachments follows the config's files capability: false for a config without files, so the attachment picker inside Composer renders nothing. A Chat.Root nested in another one with the same config and no chat shares the outer conversation.

Import: @slices.sh/ai-chat/headless

```tsx
ChatRootProps

type ChatRootProps = RootProps &
  ChatFeedbackOptions & {
    /** The key in `services/ai-chat/config.ts`: the route the chat talks to when it is handed none. Left out, the config's slug. */
    name?: string
    /** The config to talk to, `"default"` when left out. */
    config?: string
    /**
     * Left out or `true`: required, and a missing service or config throws. `"optional"`: renders
     * its children, and a Composer under it renders nothing. `false`: off. A capability name
     * requires the service and that capability.
     */
    enabled?: Enabled
    /**
     * An AI SDK `Chat` the app made — its id, the messages it opens on, a transport with your
     * headers — which every part reads with `useChat({ chat })`. Without one, a chat over the
     * installed backend's route.
     */
    chat?: AiChat
    /** From chatSession(client, name), bound to the supplied Chat transport. */
    settings?: ChatSettings | null
    /** The sink's `whenBusy`: on, a message sent mid-answer is accepted and queued; off, Composer holds it until the chat is free. */
    queue?: boolean
    /** Enable explicit per-tool consent remembered only for this Chat instance. */
    rememberApprovals?: boolean
  }
```

Source: @slices.sh/ai-chat/src/headless/root.tsx

### Chat.Transcript

content · Element: div

The scroller, and the only one.

A second scroller inside it produces a transcript that follows an end nobody can see: the
outer one is already at its bottom, and the words arrive somewhere off screen.

`role="log"` with `tabIndex` is the pair the APG asks for and the pair this was missing:
the role makes arriving messages announce themselves, and the tab stop is the only way a
keyboard reaches a scrolling region whose contents — bubbles, avatars — are not focusable.

The busy flag belongs on the message being written and not here. On the region it held
back everything the region was for: a polite log defers what arrives inside a busy node,
so the question the person just sent and the note saying an answer is coming were both
announced late or not at all — the two announcements that matter most, suppressed to
defer the one that already defers itself. → components/headless/ai-chat/message.tsx

The reader's own wheel, touch, keys and scrollbar are what take the transcript away from
where the scroller put it; the scroller's own moves never do. → hooks/ai-chat/use-follow.ts

Import: @slices.sh/ai-chat/headless

Chat.Transcript must be inside <Chat.Root>.

```tsx
PartProps<"div", { following: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/transcript.tsx

### Chat.Thread

content · Element: div

Custom renderers retain MessageScroller.Content so anchor children and resizing stay registered.

Import: @slices.sh/ai-chat/headless

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/thread.tsx

### Chat.Messages

scope

The transcript's messages, each in its own scope, exactly as the AI SDK holds them.

The repeat is a part rather than a `map` in the pattern, because a message is where the
looks change most and the iteration never does: a skin decides what a row is, and this
decides what the rows are.

Import: @slices.sh/ai-chat/headless

Chat.Messages must be inside <Chat.Root>.

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/messages.tsx

### Chat.Message

content · Element: div

Messages repeats once per UIMessage and Message is its element: data-state user or assistant, aria-busy while it is the answer being written, data-invalid when it was cut off. Speaker says who spoke, for screen readers. Parts repeats once per SDK part in the order the model produced them, and each kind has its own gate: PartText is one run of prose, StepBoundary marks a new model step. Stopped renders only for a reply that was cut off, and Continue sends a follow-up prompt that resumes it.

Import: @slices.sh/ai-chat/headless

Chat.Message must be inside <Chat.Messages>.

Chat.Message must be inside <Chat.Root>.

```tsx
PartProps<"div", MessageState> & { optimizeHistory?: boolean }

type MessageState = { role: AiChatMessage["role"]; interrupted: boolean; streaming: boolean }
```

Source: @slices.sh/ai-chat/src/headless/message.tsx

### Chat.Speaker

content · Element: span

Who said it, in words.

Colour and alignment are the only thing that told the two apart, which is nothing at all
to a screen reader and very little on a low-contrast theme. The default skin hides this
visually and leaves it in the accessibility tree, where it is the difference between a
conversation and a run of anonymous paragraphs.

Import: @slices.sh/ai-chat/headless

Chat.Speaker must be inside <Chat.Messages>.

```tsx
PartProps<"span", { role: AiChatMessage["role"] }>
```

Source: @slices.sh/ai-chat/src/headless/speaker.tsx

### Chat.Parts

scope

A message's parts, in the order the model produced them, exactly as the AI SDK holds them.

The order is the content. Reasoning before an answer, a tool call between two halves of
one, a citation after both — a transcript that grouped by kind would say the model did
things in an order it did not. Each kind gates itself inside here, and a kind no gate
draws — a step boundary, an app's own `data-*` part — draws nothing.
→ docs/rules/states-gate-themselves.md

Import: @slices.sh/ai-chat/headless

Chat.Parts must be inside <Chat.Messages>.

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/parts.tsx

### Chat.PartText

gate

One run of prose, the SDK's `text` part. A message has several when something happened
between them.

The role comes with it because whose text this is decides whether it is markup: a model
writes markdown and means it, and a person who types `2 * 3 * 4` means three numbers.

Import: @slices.sh/ai-chat/headless

Chat.PartText must be inside <Chat.Parts>.

Chat.PartText must be inside <Chat.Messages>.

Chat.PartText requires Chat.Root.

```tsx
GateProps<PartTextState>

type PartTextState = TextUIPart & { role: AiChatMessage["role"]; streaming: boolean }
```

Source: @slices.sh/ai-chat/src/headless/part-text.tsx

### Chat.PartReasoning

gate

A model thinking out loud, the SDK's `reasoning` part, and whether it has stopped.

`streaming` is what lets one block be open while it fills and closed once the answer
starts, which is the convention every product settled on: the trace is interesting while
it is the only thing happening and noise the moment it is not. A trace the SDK still
holds as `streaming` on an answer nobody is writing was cut off, and is over too.

Import: @slices.sh/ai-chat/headless

Capability: reasoning

Chat.PartReasoning must be inside <Chat.Parts>.

Chat.PartReasoning must be inside <Chat.Messages>.

Chat.PartReasoning must be inside <Chat.Root>.

```tsx
GateProps<PartReasoningState>

type PartReasoningState = ReasoningUIPart & { streaming: boolean }
```

Source: @slices.sh/ai-chat/src/headless/part-reasoning.tsx

### Chat.Usage

gate

What the turn cost, where the provider said: the SDK's usage, as the route put it on the
message's metadata when the turn finished.

Nothing at all where it did not: this field went years declared and never written, and the
website that needed it estimated from the length of the text. An estimate on screen is a
number people plan against. → docs/rules/capability-honesty.md

Import: @slices.sh/ai-chat/headless

Capability: usage

Chat.Usage must be inside <Chat.Messages>.

```tsx
GateProps<{ usage: LanguageModelUsage }>
```

Source: @slices.sh/ai-chat/src/headless/usage.tsx

### Chat.ContextUsage

gate

Model, Timing and Usage read what the route wrote into the message metadata: which model answered, how long it took, the SDK's LanguageModelUsage. ContextUsage gives { tokens, capacity, percent } for the last request while the chat is idle. Each one is null, and renders nothing, when the provider did not report the value.

Import: @slices.sh/ai-chat/headless

Capability: usage

Chat.ContextUsage must be inside <Chat.Root>.

```tsx
GateProps<UsageState>
```

Source: @slices.sh/ai-chat/src/headless/context-usage.tsx

### Chat.Model

gate

Model, Timing and Usage read what the route wrote into the message metadata: which model answered, how long it took, the SDK's LanguageModelUsage. ContextUsage gives { tokens, capacity, percent } for the last request while the chat is idle. Each one is null, and renders nothing, when the provider did not report the value.

Import: @slices.sh/ai-chat/headless

Capability: usage

Chat.Model must be inside <Chat.Messages>.

```tsx
GateProps<{ model: string }>
```

Source: @slices.sh/ai-chat/src/headless/model.tsx

### Chat.Timing

gate

Model, Timing and Usage read what the route wrote into the message metadata: which model answered, how long it took, the SDK's LanguageModelUsage. ContextUsage gives { tokens, capacity, percent } for the last request while the chat is idle. Each one is null, and renders nothing, when the provider did not report the value.

Import: @slices.sh/ai-chat/headless

Capability: usage

Chat.Timing must be inside <Chat.Messages>.

```tsx
GateProps<{ timing: NonNullable<AiChatMetadata["timing"]> }>
```

Source: @slices.sh/ai-chat/src/headless/timing.tsx

### Chat.Stopped

gate

This reply was cut off, and somebody should be told in words.

Fading the text was the whole account of it before, which is a contrast regression that
says nothing: the reader could not tell whether they had stopped it, the model had, or
the network went. → components/headless/ai-chat/writing.ts

Import: @slices.sh/ai-chat/headless

Chat.Stopped must be inside <Chat.Messages>.

Chat.Stopped must be inside <Chat.Root>.

```tsx
GateProps<{ reason: "length" | "interrupted" }>
```

Source: @slices.sh/ai-chat/src/headless/stopped.tsx

### Chat.Composer

action · Element: form

Today's chat composer parts. In v2 Chat.Root is a sink (whenBusy "queue") and the generic Composer from @slices.sh/composer writes into it. Chat.Composer becomes Composer.Root. Chat.Input becomes Composer.Input, a field named text, wrapped by Dictation.Field when dictation is wanted. Chat.Submit's sending half becomes Composer.Submit. Its stopping half is chat behaviour no generic part can express, so it stays a chat part, Chat.Stop (new in v2): it calls the SDK's stop, is disabled while nothing streams and never unmounts, so focus and layout stay put when a turn ends. Chat.ComposerRoot becomes a nested Chat.Root, which shares the outer conversation for the same config. Chat.ComposerStandalone has no replacement: a pattern that draws Chat.Settings only when it stands alone takes a prop for it.

Import: @slices.sh/ai-chat/headless

Chat.Composer must be inside <Chat.Root>.

```tsx
PartProps<"form">
```

Source: @slices.sh/ai-chat/src/headless/composer.tsx

### Chat.Input

input · Element: textarea

A textarea rather than an input, because Shift+Enter has to mean something.

No `rows`: the skin sizes it with `field-sizing: content`, so it grows with the message
and stops at a maximum. A fixed row locked every paragraph anyone ever wrote into a
thirty-six pixel slit, with the browser's own resize handle taken away.

Import: @slices.sh/ai-chat/headless

Chat.Input must be inside <Chat.Root>.

```tsx
PartProps<"textarea">
```

Source: @slices.sh/ai-chat/src/headless/input.tsx

### Chat.Submit

action · Element: button

One control that sends, and stops what it sent with the SDK's `stop`.

The composer's only submit, and the only button that ever occupies this spot. A separate
stop button had to appear between send and its neighbour mid-turn, which moved the most
used control in the component under a cursor already on its way down; it also unmounted
itself the instant the turn ended, dropping focus to the body if anyone had tabbed to it.
Morphing solves both: the element is the same element, and it is `type="submit"` only
while submitting is what it does. → docs/rules/one-submit-per-form.md

It stops an answer while there is nothing to send, and sends while there is — which is
the only reading that leaves the queue reachable by touch. Otherwise the one button
beside the box said "stop" the whole time an answer was arriving, so a follow-up typed
on a phone could be sent only from the software keyboard's own return key, and the
control right next to it did something else. Where the composer refuses a second message
outright (`queue` off) there is nothing to send by definition, so this is stop again.

Import: @slices.sh/ai-chat/headless

Chat.Submit must be inside <Chat.Root>.

```tsx
PartProps<"button", { stopping: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/submit.tsx

### Chat.Stop

action · Element: button

Stops a reply that is streaming. Stopping is the chat's own behaviour, so it is not a mode
of `Composer.Submit`: no generic part can express it.

It is disabled while nothing streams, never unmounted: a button that came and went with
every turn moved the controls beside it under a cursor already on its way, and dropped
focus to the body when the turn ended with it focused.

Import: @slices.sh/ai-chat/headless

Chat.Stop must be inside <Chat.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/stop.tsx

### Chat.Suggestions

gate

Starters, and only on an empty transcript.

Once there is a conversation they are no longer suggestions, they are a row of buttons
competing with what the person was about to type.

Import: @slices.sh/ai-chat/headless

Chat.Suggestions must be inside <Chat.Root>.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/suggestions.tsx

### Chat.FollowUps

gate

Only the last completed answer can suggest what comes next in the active branch.

Import: @slices.sh/ai-chat/headless

Chat.FollowUps requires Chat.Root.

```tsx
GateProps<{ prompts: string[] }>
```

Source: @slices.sh/ai-chat/src/headless/follow-ups.tsx

### Chat.Suggestion

action · Element: button

Sends its own text. It goes through the composer rather than the chat, so a suggestion
queued behind a running turn waits exactly as a typed message does.

Import: @slices.sh/ai-chat/headless

Chat.Suggestion must be inside <Chat.Root>.

```tsx
SuggestionProps

type SuggestionProps = PartProps<"button"> & { text: string }
```

Source: @slices.sh/ai-chat/src/headless/suggestion.tsx

### Chat.JumpToLatest

action · Element: button

Rendered only once the reader has scrolled the transcript away from its end.

Import: @slices.sh/ai-chat/headless

Chat.JumpToLatest must be inside <Chat.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/jump-to-latest.tsx

### Chat.Empty

gate

Nothing has been said yet. Where a greeting and the starters go.

Import: @slices.sh/ai-chat/headless

Chat.Empty must be inside <Chat.Root>.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/empty.tsx

### Chat.Waiting

gate

Sent, and nothing back yet: the SDK's `submitted`.

Its own gate rather than a flag on a wider one, so the condition lives where the rule
wants it and the pattern never writes `waiting ? … : null`. Once the words start arriving
they are the answer to "is anything happening", and this renders nothing.
→ docs/rules/states-gate-themselves.md

It belongs inside `Chat.Transcript`, whose `role="log"` announces what arrives in it. A
second live region around this note would have a screen reader say it twice.

Import: @slices.sh/ai-chat/headless

Chat.Waiting must be inside <Chat.Root>.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/waiting.tsx

### Chat.Failed

gate

The turn was refused — the SDK's `error` status — and the child is told which refusal it
was.

The slug reaches the screen because the common ones want different things: a quota
refusal wants an upgrade link, an outage wants the retry button, and an unknown chat wants
a developer to open `services/ai-chat/config.ts`. One banner for all three would be three
lies. → docs/rules/capability-honesty.md

Import: @slices.sh/ai-chat/headless

Chat.Failed must be inside <Chat.Root>.

```tsx
GateProps<{ error: ChatErrorSlug; retryIn: number }>
```

Source: @slices.sh/ai-chat/src/headless/failed.tsx

### Chat.TryAgain

action · Element: button

One gate per state of the conversation, so pattern code never branches on status. Empty: nothing has been said. Waiting: the SDK's submitted, sent with nothing back yet; it belongs inside Chat.Transcript, whose log announces it. Failed: the SDK's error status with the refusal slug (unauthorized, quota-exceeded, unknown-chat…); it belongs outside the scroller so a refusal never renders below the fold. TryAgain: regenerate() from inside Failed. Unavailable: the one part that renders when Chat.Root is enabled="optional" and ai-chat is not configured, for a sentence saying so.

Import: @slices.sh/ai-chat/headless

Chat.TryAgain must be inside <Chat.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/try-again.tsx

### Chat.Unavailable

gate

No chat backend is installed at all, and the screen was handed no chat.

Distinct from a refused turn, because nothing the person does will help: the app is
missing an implementation, and the only useful sentence names the command that adds one.

Import: @slices.sh/ai-chat/headless

Chat.Unavailable must be inside <Chat.Root>.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/unavailable.tsx

### Chat.ToolError

gate

PartTool gates one tool-<name> or dynamic-tool part over the SDK's seven states and hands { toolName, state }. ToolRenderer lets the app draw a tool's body from a renderers map; approval controls stay outside it. ToolInput, ToolOutput and ToolError gate the input, the output and the error text. A config without tools never produces these parts.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.ToolError requires Chat.Parts.

```tsx
GateProps<{ value: string }>
```

Source: @slices.sh/ai-chat/src/headless/tool-error.tsx

### Chat.ToolOutput

gate

PartTool gates one tool-<name> or dynamic-tool part over the SDK's seven states and hands { toolName, state }. ToolRenderer lets the app draw a tool's body from a renderers map; approval controls stay outside it. ToolInput, ToolOutput and ToolError gate the input, the output and the error text. A config without tools never produces these parts.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.ToolOutput requires Chat.Parts.

```tsx
GateProps<{ value: unknown }>
```

Source: @slices.sh/ai-chat/src/headless/tool-output.tsx

### Chat.ToolInput

gate

PartTool gates one tool-<name> or dynamic-tool part over the SDK's seven states and hands { toolName, state }. ToolRenderer lets the app draw a tool's body from a renderers map; approval controls stay outside it. ToolInput, ToolOutput and ToolError gate the input, the output and the error text. A config without tools never produces these parts.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.ToolInput requires Chat.Parts.

```tsx
GateProps<{ value: unknown }>
```

Source: @slices.sh/ai-chat/src/headless/tool-input.tsx

### Chat.ToolRenderer

scope

Render only the tool body; approval controls stay outside the app's renderer.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.ToolRenderer requires Chat.Parts.

```tsx
RootProps & { renderers?: ToolRenderers }

export type ToolRenderers = Record<string, ComponentType<{ tool: ToolView }>>

export type ToolView = (ToolUIPart | DynamicToolUIPart) & { toolName: string }
```

Source: @slices.sh/ai-chat/src/headless/tool-renderer.tsx

### Chat.ForgetApproval

action · Element: button

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.ForgetApproval requires Chat.Root.

Chat.ForgetApproval requires Chat.Parts.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/forget-approval.tsx

### Chat.RememberedApproval

content · Element: span

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.RememberedApproval requires Chat.Root.

Chat.RememberedApproval requires Chat.Parts.

```tsx
PartProps<"span">
```

Source: @slices.sh/ai-chat/src/headless/remembered-approval.tsx

### Chat.AlwaysAllow

action · Element: button

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.AlwaysAllow requires Chat.PartApproval.

Chat.AlwaysAllow requires Chat.Root.

Chat.AlwaysAllow requires Chat.Parts.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/always-allow.tsx

### Chat.ApprovalReceipt

content · Element: p

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.ApprovalReceipt requires Chat.Parts.

```tsx
PartProps<"p", { approved: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/approval-receipt.tsx

### Chat.ApprovalError

content · Element: p

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.ApprovalError requires Chat.PartApproval.

Chat.ApprovalError requires Chat.Parts.

```tsx
PartProps<"p">
```

Source: @slices.sh/ai-chat/src/headless/approval-error.tsx

### Chat.ApprovalReason

content · Element: p

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.ApprovalReason requires Chat.Parts.

```tsx
PartProps<"p">
```

Source: @slices.sh/ai-chat/src/headless/approval-reason.tsx

### Chat.Deny

action · Element: button

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.Deny requires Chat.PartApproval.

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/deny.tsx

### Chat.Approve

action · Element: button

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.Approve requires Chat.PartApproval.

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/approve.tsx

### Chat.PartApproval

content · Element: div

For a tool declared with needsApproval. PartApproval is the request, with ApprovalReason, Approve, Deny and AlwaysAllow (only with rememberApprovals). Answering calls the SDK's addToolApprovalResponse. ApprovalError says the answer did not reach the server. ApprovalReceipt replaces the controls once answered, RememberedApproval marks a call allowed by a remembered consent, and ForgetApproval takes that consent back. While a decision is pending the queue waits for it.

Import: @slices.sh/ai-chat/headless

Capability: toolApproval

Chat.PartApproval requires Chat.Parts.

Chat.PartApproval requires Chat.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/part-approval.tsx

### Chat.PartTool

gate

One tool call, the SDK's `tool-<name>` or `dynamic-tool` part, wherever it has got to.

The state reaches the screen because the seven of them want different things: an input
still arriving is a shimmer, an output is a disclosure, a denial is a sentence and an
error is a different sentence. One "called a tool" card for all seven would be six lies.
`toolName` is the SDK's own reading of the part's type, beside everything the part holds.
→ docs/rules/capability-honesty.md

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.PartTool must be inside <Chat.Parts>.

```tsx
GateProps<PartToolState>

type PartToolState = (ToolUIPart | DynamicToolUIPart) & { toolName: string }
```

Source: @slices.sh/ai-chat/src/headless/part-tool.tsx

### Chat.QuestionReceipt

gate

The askQuestions tool: a form the model asks the person to fill in. PartQuestion is the form, Questions hands the questionnaire, QuestionItem is one fieldset with Answer per choice and OtherAnswer plus QuestionOther for a free answer. ClearAnswer takes an optional pick back. QuestionProgress, QuestionBack and QuestionNext step through several questions, and QuestionInvalid says why Next is refused. SubmitAnswers sends, SkipQuestions writes a skipped output the model reads, QuestionError reports a failed write, and QuestionReceipt takes the form's place once answered.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionReceipt requires Chat.Parts.

```tsx
GateProps<Receipt>

type Receipt = { title: string; answers: { name: string; title: string; values: string[] }[] }
```

Source: @slices.sh/ai-chat/src/headless/question-receipt.tsx

### Chat.QuestionError

content · Element: p

The askQuestions tool: a form the model asks the person to fill in. PartQuestion is the form, Questions hands the questionnaire, QuestionItem is one fieldset with Answer per choice and OtherAnswer plus QuestionOther for a free answer. ClearAnswer takes an optional pick back. QuestionProgress, QuestionBack and QuestionNext step through several questions, and QuestionInvalid says why Next is refused. SubmitAnswers sends, SkipQuestions writes a skipped output the model reads, QuestionError reports a failed write, and QuestionReceipt takes the form's place once answered.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionError requires Chat.PartQuestion.

```tsx
PartProps<"p">
```

Source: @slices.sh/ai-chat/src/headless/question-error.tsx

### Chat.SubmitAnswers

action · Element: button

On the last question only: before it, Next is the way on.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.SubmitAnswers requires Chat.PartQuestion.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/submit-answers.tsx

### Chat.SkipQuestions

action · Element: button

Dismisses the open question without answering it. The queue decides what the model hears next:
the message the person typed meanwhile, as their reply, or else a nudge to carry on.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.SkipQuestions requires Chat.Root.

Chat.SkipQuestions requires Chat.PartQuestion.

Chat.SkipQuestions requires Chat.Parts.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/skip-questions.tsx

### Chat.QuestionOther

input · Element: input

The field of the "Other" row, inline with its control.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionOther requires Chat.PartQuestion.

```tsx
PartProps<"input"> & { name: string }
```

Source: @slices.sh/ai-chat/src/headless/question-other.tsx

### Chat.OtherAnswer

input · Element: input

The control of the "Other" row, last in the same group as the choices: it replaces the pick of a single question and adds to a multiple one.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.OtherAnswer requires Chat.PartQuestion.

```tsx
PartProps<"input"> & { name: string }
```

Source: @slices.sh/ai-chat/src/headless/other-answer.tsx

### Chat.ClearAnswer

action · Element: button

Takes a pick back, which a radio cannot do by itself. An optional question only: a required one is changed, not emptied.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.ClearAnswer requires Chat.PartQuestion.

```tsx
PartProps<"button"> & { name: string }
```

Source: @slices.sh/ai-chat/src/headless/clear-answer.tsx

### Chat.QuestionProgress

content · Element: p

"2 of 3": the place in a form of several questions, and nothing in a form of one.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionProgress requires Chat.PartQuestion.

```tsx
PartProps<"p", Progress>

type Progress = { position: number; count: number }
```

Source: @slices.sh/ai-chat/src/headless/question-progress.tsx

### Chat.QuestionBack

action · Element: button

The askQuestions tool: a form the model asks the person to fill in. PartQuestion is the form, Questions hands the questionnaire, QuestionItem is one fieldset with Answer per choice and OtherAnswer plus QuestionOther for a free answer. ClearAnswer takes an optional pick back. QuestionProgress, QuestionBack and QuestionNext step through several questions, and QuestionInvalid says why Next is refused. SubmitAnswers sends, SkipQuestions writes a skipped output the model reads, QuestionError reports a failed write, and QuestionReceipt takes the form's place once answered.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionBack requires Chat.PartQuestion.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/question-back.tsx

### Chat.QuestionNext

action · Element: button

Goes on once this question's answer is good; the last question has Send answers instead.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionNext requires Chat.PartQuestion.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/question-next.tsx

### Chat.QuestionInvalid

content · Element: p

What is missing from one question's answer, once the person tried to go on without it.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionInvalid requires Chat.PartQuestion.

```tsx
PartProps<"p", { problem: QuestionProblem }> & { name: string }
```

Source: @slices.sh/ai-chat/src/headless/question-invalid.tsx

### Chat.Answer

input · Element: input

One choice: a radio in a single question and a checkbox in a multiple one, so the kind is plain before anything is touched.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.Answer requires Chat.PartQuestion.

```tsx
PartProps<"input"> & { name: string; value: string }
```

Source: @slices.sh/ai-chat/src/headless/answer.tsx

### Chat.QuestionItem

content · Element: fieldset

One question: a radiogroup when the answer is one choice, a group when it is several. Only the
question of the current step is drawn. Number shortcuts stay inside it and never intercept typing.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.QuestionItem requires Chat.PartQuestion.

```tsx
PartProps<"fieldset"> & { name: string }
```

Source: @slices.sh/ai-chat/src/headless/question-item.tsx

### Chat.Questions

gate

The askQuestions tool: a form the model asks the person to fill in. PartQuestion is the form, Questions hands the questionnaire, QuestionItem is one fieldset with Answer per choice and OtherAnswer plus QuestionOther for a free answer. ClearAnswer takes an optional pick back. QuestionProgress, QuestionBack and QuestionNext step through several questions, and QuestionInvalid says why Next is refused. SubmitAnswers sends, SkipQuestions writes a skipped output the model reads, QuestionError reports a failed write, and QuestionReceipt takes the form's place once answered.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.Questions requires Chat.PartQuestion.

```tsx
GateProps<Questionnaire>
```

Source: @slices.sh/ai-chat/src/headless/questions.tsx

### Chat.PartQuestion

action · Element: form

The askQuestions tool: a form the model asks the person to fill in. PartQuestion is the form, Questions hands the questionnaire, QuestionItem is one fieldset with Answer per choice and OtherAnswer plus QuestionOther for a free answer. ClearAnswer takes an optional pick back. QuestionProgress, QuestionBack and QuestionNext step through several questions, and QuestionInvalid says why Next is refused. SubmitAnswers sends, SkipQuestions writes a skipped output the model reads, QuestionError reports a failed write, and QuestionReceipt takes the form's place once answered.

Import: @slices.sh/ai-chat/headless

Capability: tools

Chat.PartQuestion requires Chat.Parts.

Chat.PartQuestion requires Chat.Root.

```tsx
PartProps<"form">
```

Source: @slices.sh/ai-chat/src/headless/part-question.tsx

### Chat.CitationRoot

scope

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/citation-root.tsx

### Chat.CitationContent

content · Element: div

The skin's surface preserves the corresponding Motion primitive adapter.

Import: @slices.sh/ai-chat/headless

Capability: sources

Chat.CitationContent requires Chat.CitationRoot.

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/citation-content.tsx

### Chat.CitationTrigger

action · Element: button

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/citation-trigger.tsx

### Chat.CitationTitle

content · Element: h2

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
PartProps<"h2">
```

Source: @slices.sh/ai-chat/src/headless/citation-title.tsx

### Chat.CitationDescription

content · Element: p

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
PartProps<"p">
```

Source: @slices.sh/ai-chat/src/headless/citation-description.tsx

### Chat.SourceSheetRoot

scope

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/source-sheet-root.tsx

### Chat.SourceSheetContent

content · Element: div

The skin's surface preserves the corresponding Motion primitive adapter.

Import: @slices.sh/ai-chat/headless

Capability: sources

Chat.SourceSheetContent requires Chat.SourceSheetRoot.

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/source-sheet-content.tsx

### Chat.SourceSheetTrigger

action · Element: button

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/source-sheet-trigger.tsx

### Chat.SourceSheetTitle

content · Element: h2

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
PartProps<"h2">
```

Source: @slices.sh/ai-chat/src/headless/source-sheet-title.tsx

### Chat.SourceSheetDescription

content · Element: p

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
PartProps<"p">
```

Source: @slices.sh/ai-chat/src/headless/source-sheet-description.tsx

### Chat.SourceSheetClose

action · Element: button

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/source-sheet-close.tsx

### Chat.References

gate

URL and document references in native transcript order, deduplicated by source identity.

Import: @slices.sh/ai-chat/headless

Capability: sources

Chat.References requires Chat.Messages.

```tsx
GateProps<{ sources: readonly ChatSource[] }>
```

Source: @slices.sh/ai-chat/src/headless/references.tsx

### Chat.PartSource

gate

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

Chat.PartSource requires Chat.Messages.

Chat.PartSource requires Chat.Parts.

```tsx
GateProps<{ source: ChatSource }>
```

Source: @slices.sh/ai-chat/src/headless/part-source.tsx

### Chat.SourceRoot

scope

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

```tsx
RootProps & { source: ChatSource }
```

Source: @slices.sh/ai-chat/src/headless/source-root.tsx

### Chat.SourceLink

content · Element: a

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

Chat.SourceLink requires Chat.SourceRoot.

```tsx
PartProps<"a">
```

Source: @slices.sh/ai-chat/src/headless/source-link.tsx

### Chat.SourceUnavailable

gate

Sources collects a message's source-url parts. References numbers and deduplicates every source of the message, and PartSource renders the first occurrence of one in place. SourceRoot scopes one source for SourceLink (a validated http(s) link) and SourceUnavailable. CitationRoot and its Trigger, Content, Title and Description are a popover for one citation; SourceSheetRoot and its parts are a modal list of all of them. Nothing renders for an answer without sources, which is most answers from most models.

Import: @slices.sh/ai-chat/headless

Capability: sources

Chat.SourceUnavailable requires Chat.SourceRoot.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/source-unavailable.tsx

### Chat.Sources

gate

What the answer was drawn from, the SDK's `source-url` parts, collected.

A footer rather than a part in the flow: citations arrive interleaved with the prose and
read as an appendix, and a reader who wants them wants all of them at once. A document
source has no URL to open, and a citation nobody can follow is a row that looks like a
link and is not one. Nothing is rendered where the backend sent none, which is most
backends.

Import: @slices.sh/ai-chat/headless

Capability: sources

Chat.Sources must be inside <Chat.Messages>.

```tsx
GateProps<{ sources: SourceUrlUIPart[] }>
```

Source: @slices.sh/ai-chat/src/headless/sources.tsx

### Chat.FileRoot

scope

PartFile gates a file part. FileRoot validates one FileUIPart and owns its load error. FileKind gates by media kind, FileImage, FileAudio and FileVideo render the media with the source locked, FileLink is an http(s) link only, and FileUnavailable covers a bad source or a failed load. These parts display files the conversation already holds. Sending files is Composer's attachments field, and grouped previews are the chat-media parts.

Import: @slices.sh/ai-chat/headless

Capability: files

```tsx
RootProps & { file: FileUIPart }
```

Source: @slices.sh/ai-chat/src/headless/file-root.tsx

### Chat.FileKind

gate

PartFile gates a file part. FileRoot validates one FileUIPart and owns its load error. FileKind gates by media kind, FileImage, FileAudio and FileVideo render the media with the source locked, FileLink is an http(s) link only, and FileUnavailable covers a bad source or a failed load. These parts display files the conversation already holds. Sending files is Composer's attachments field, and grouped previews are the chat-media parts.

Import: @slices.sh/ai-chat/headless

Capability: files

Chat.FileKind requires Chat.FileRoot.

```tsx
GateProps & { kind: ChatMediaKind }
```

Source: @slices.sh/ai-chat/src/headless/file-kind-gate.tsx

### Chat.FileImage

content · Element: img

PartFile gates a file part. FileRoot validates one FileUIPart and owns its load error. FileKind gates by media kind, FileImage, FileAudio and FileVideo render the media with the source locked, FileLink is an http(s) link only, and FileUnavailable covers a bad source or a failed load. These parts display files the conversation already holds. Sending files is Composer's attachments field, and grouped previews are the chat-media parts.

Import: @slices.sh/ai-chat/headless

Capability: files

Chat.FileImage requires Chat.FileRoot.

File media requires Chat.FileRoot.

```tsx
PartProps<"img">
```

Source: @slices.sh/ai-chat/src/headless/file-img.tsx

### Chat.FileAudio

content · Element: audio

PartFile gates a file part. FileRoot validates one FileUIPart and owns its load error. FileKind gates by media kind, FileImage, FileAudio and FileVideo render the media with the source locked, FileLink is an http(s) link only, and FileUnavailable covers a bad source or a failed load. These parts display files the conversation already holds. Sending files is Composer's attachments field, and grouped previews are the chat-media parts.

Import: @slices.sh/ai-chat/headless

Capability: files

Chat.FileAudio requires Chat.FileRoot.

File media requires Chat.FileRoot.

```tsx
PartProps<"audio">
```

Source: @slices.sh/ai-chat/src/headless/file-audio.tsx

### Chat.FileVideo

content · Element: video

PartFile gates a file part. FileRoot validates one FileUIPart and owns its load error. FileKind gates by media kind, FileImage, FileAudio and FileVideo render the media with the source locked, FileLink is an http(s) link only, and FileUnavailable covers a bad source or a failed load. These parts display files the conversation already holds. Sending files is Composer's attachments field, and grouped previews are the chat-media parts.

Import: @slices.sh/ai-chat/headless

Capability: files

Chat.FileVideo requires Chat.FileRoot.

File media requires Chat.FileRoot.

```tsx
PartProps<"video">
```

Source: @slices.sh/ai-chat/src/headless/file-video.tsx

### Chat.FileLink

content · Element: a

Data and blob media render in their element; only web links navigate away.

Import: @slices.sh/ai-chat/headless

Capability: files

Chat.FileLink requires Chat.FileRoot.

```tsx
PartProps<"a">
```

Source: @slices.sh/ai-chat/src/headless/file-link.tsx

### Chat.FileUnavailable

gate

PartFile gates a file part. FileRoot validates one FileUIPart and owns its load error. FileKind gates by media kind, FileImage, FileAudio and FileVideo render the media with the source locked, FileLink is an http(s) link only, and FileUnavailable covers a bad source or a failed load. These parts display files the conversation already holds. Sending files is Composer's attachments field, and grouped previews are the chat-media parts.

Import: @slices.sh/ai-chat/headless

Capability: files

Chat.FileUnavailable requires Chat.FileRoot.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/file-unavailable.tsx

### Chat.PartFile

gate

A native file stays visible in history even when the rich media viewer is not installed.

Import: @slices.sh/ai-chat/headless

Capability: files

Chat.PartFile requires Chat.Parts.

```tsx
GateProps<{ file: FileUIPart }>
```

Source: @slices.sh/ai-chat/src/headless/part-file.tsx

### Chat.EditedAttachments

gate

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.EditedAttachments requires Chat.Messages.

```tsx
GateProps<{ files: readonly FileUIPart[] }>
```

Source: @slices.sh/ai-chat/src/headless/edited-attachments.tsx

### Chat.BranchNavigation

gate

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.BranchNavigation requires Chat.Root.

Chat.BranchNavigation requires Chat.Messages.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/branch-navigation.tsx

### Chat.BranchPrevious

action · Element: button

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.BranchPrevious requires Chat.Root.

Chat.BranchPrevious requires Chat.Messages.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/branch-previous.tsx

### Chat.BranchNext

action · Element: button

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.BranchNext requires Chat.Root.

Chat.BranchNext requires Chat.Messages.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/branch-next.tsx

### Chat.BranchPage

content · Element: span

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.BranchPage requires Chat.Root.

Chat.BranchPage requires Chat.Messages.

```tsx
PartProps<"span", { index: number; total: number }>
```

Source: @slices.sh/ai-chat/src/headless/branch-page.tsx

### Chat.MessageEditRoot

scope

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.MessageEditRoot requires Chat.Messages.

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/message-edit-root.tsx

### Chat.MessageEditing

gate

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.MessageEditing requires Chat.MessageEditRoot.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/message-editing.tsx

### Chat.MessageReading

gate

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.MessageReading requires Chat.MessageEditRoot.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/message-reading.tsx

### Chat.Edit

action · Element: button

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.Edit requires Chat.Root.

Chat.Edit requires Chat.Messages.

Chat.Edit requires Chat.MessageEditRoot.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/edit.tsx

### Chat.MessageEditor

action · Element: form

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.MessageEditor requires Chat.Root.

Chat.MessageEditor requires Chat.Messages.

Chat.MessageEditor requires Chat.MessageEditRoot.

```tsx
PartProps<"form">
```

Source: @slices.sh/ai-chat/src/headless/message-editor.tsx

### Chat.EditedInput

input · Element: textarea

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.EditedInput requires Chat.Root.

Chat.EditedInput requires Chat.MessageEditRoot.

```tsx
PartProps<"textarea">
```

Source: @slices.sh/ai-chat/src/headless/edited-input.tsx

### Chat.CancelEdit

action · Element: button

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.CancelEdit requires Chat.MessageEditRoot.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/cancel-edit.tsx

### Chat.SaveEdit

action · Element: button

BranchNavigation, BranchPrevious, BranchNext and BranchPage ({ index, total }) step through the alternatives Regenerate and edits create. MessageEditRoot wraps a message's content, and MessageReading and MessageEditing gate the two modes. Edit opens the editor. Editing a sent message is chat behaviour, so the editor stays a chat part: in v2 MessageEditor provides its own sink (whenBusy "hold", submit creates a branch) and renders a Composer with the message's text as defaultValue. EditedInput and SaveEdit map to Composer.Input and Composer.Submit inside it. EditedAttachments exposes the files the edited message keeps, and CancelEdit leaves history unchanged.

Import: @slices.sh/ai-chat/headless

Chat.SaveEdit requires Chat.Root.

Chat.SaveEdit requires Chat.Messages.

Chat.SaveEdit requires Chat.MessageEditRoot.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/save-edit.tsx

### Chat.MessageFooter

content · Element: div

Completed-turn actions stay available on the latest answer and to keyboard/touch readers.

Import: @slices.sh/ai-chat/headless

Chat.MessageFooter must be inside <Chat.Messages>.

Chat.MessageFooter must be inside <Chat.Root>.

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/message-footer.tsx

### Chat.Copy

action · Element: button

Puts this message's text on the clipboard, and says so.

The text and nothing else: a reader copying an answer wants the answer, not the reasoning
trace above it or the JSON of a tool call beside it. Runs of prose a tool call separated
stay separate paragraphs.

Import: @slices.sh/ai-chat/headless

Chat.Copy must be inside <Chat.Messages>.

```tsx
PartProps<"button", { copied: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/copy.tsx

### Chat.FeedbackRoot

scope

MessageFooter holds a turn's actions and is data-active on the latest answer. Copy puts the text parts on the clipboard. ReadAloud speaks the answer. Regenerate renders only on the last assistant message. FeedbackRoot, Feedback rating="up" or "down" and FeedbackError call onFeedback and render nothing without it.

Import: @slices.sh/ai-chat/headless

Chat.FeedbackRoot requires Chat.Root.

Chat.FeedbackRoot requires Chat.Messages.

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/feedback-root.tsx

### Chat.Feedback

action · Element: button

MessageFooter holds a turn's actions and is data-active on the latest answer. Copy puts the text parts on the clipboard. ReadAloud speaks the answer. Regenerate renders only on the last assistant message. FeedbackRoot, Feedback rating="up" or "down" and FeedbackError call onFeedback and render nothing without it.

Import: @slices.sh/ai-chat/headless

Chat.Feedback requires Chat.FeedbackRoot.

```tsx
PartProps<"button"> & { rating: "up" | "down" }
```

Source: @slices.sh/ai-chat/src/headless/feedback.tsx

### Chat.FeedbackError

gate

MessageFooter holds a turn's actions and is data-active on the latest answer. Copy puts the text parts on the clipboard. ReadAloud speaks the answer. Regenerate renders only on the last assistant message. FeedbackRoot, Feedback rating="up" or "down" and FeedbackError call onFeedback and render nothing without it.

Import: @slices.sh/ai-chat/headless

Chat.FeedbackError requires Chat.FeedbackRoot.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/feedback-error.tsx

### Chat.ReadAloud

action · Element: button

MessageFooter holds a turn's actions and is data-active on the latest answer. Copy puts the text parts on the clipboard. ReadAloud speaks the answer. Regenerate renders only on the last assistant message. FeedbackRoot, Feedback rating="up" or "down" and FeedbackError call onFeedback and render nothing without it.

Import: @slices.sh/ai-chat/headless

Chat.ReadAloud requires Chat.Messages.

```tsx
PartProps<"button", { playing: boolean; failed: boolean }>
```

Source: @slices.sh/ai-chat/src/headless/read-aloud.tsx

### Chat.Regenerate

action · Element: button

An older answer is safe to regenerate once its entire tail is preserved as a branch.

Import: @slices.sh/ai-chat/headless

Chat.Regenerate requires Chat.Root.

Chat.Regenerate requires Chat.Messages.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/regenerate.tsx

### Chat.ToggleTool

action · Element: button

Settings gates a toolbar that exists only when the config publishes a catalog. Models hands the models on offer and ModelPicker is a native select. OptionalTools hands the catalog, or with selected the active subset, and ToggleTool tool={id} is a pressed/unpressed button. Choices belong to this conversation and the server checks every one.

Import: @slices.sh/ai-chat/headless

Chat.ToggleTool requires Chat.Root.

```tsx
PartProps<"button"> & { tool: string }
```

Source: @slices.sh/ai-chat/src/headless/toggle-tool.tsx

### Chat.OptionalTools

gate

Settings gates a toolbar that exists only when the config publishes a catalog. Models hands the models on offer and ModelPicker is a native select. OptionalTools hands the catalog, or with selected the active subset, and ToggleTool tool={id} is a pressed/unpressed button. Choices belong to this conversation and the server checks every one.

Import: @slices.sh/ai-chat/headless

Chat.OptionalTools requires Chat.Root.

```tsx
GateProps<{ tools: NonNullable<ChatChoices["tools"]> }> & { selected?: boolean }
```

Source: @slices.sh/ai-chat/src/headless/optional-tools.tsx

### Chat.ModelPicker

input · Element: select

Settings gates a toolbar that exists only when the config publishes a catalog. Models hands the models on offer and ModelPicker is a native select. OptionalTools hands the catalog, or with selected the active subset, and ToggleTool tool={id} is a pressed/unpressed button. Choices belong to this conversation and the server checks every one.

Import: @slices.sh/ai-chat/headless

Chat.ModelPicker requires Chat.Root.

```tsx
PartProps<"select">
```

Source: @slices.sh/ai-chat/src/headless/model-picker.tsx

### Chat.Models

gate

Settings gates a toolbar that exists only when the config publishes a catalog. Models hands the models on offer and ModelPicker is a native select. OptionalTools hands the catalog, or with selected the active subset, and ToggleTool tool={id} is a pressed/unpressed button. Choices belong to this conversation and the server checks every one.

Import: @slices.sh/ai-chat/headless

Chat.Models requires Chat.Root.

```tsx
GateProps<{ models: NonNullable<ChatChoices["models"]> }>
```

Source: @slices.sh/ai-chat/src/headless/models.tsx

### Chat.Settings

gate

Settings gates a toolbar that exists only when the config publishes a catalog. Models hands the models on offer and ModelPicker is a native select. OptionalTools hands the catalog, or with selected the active subset, and ToggleTool tool={id} is a pressed/unpressed button. Choices belong to this conversation and the server checks every one.

Import: @slices.sh/ai-chat/headless

Chat.Settings requires Chat.Root.

```tsx
GateProps
```

Source: @slices.sh/ai-chat/src/headless/settings.tsx

### Chat.SendNow

action · Element: button

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.SendNow requires Chat.Root.

Chat.SendNow requires Chat.QueueItems.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/send-now.tsx

### Chat.CancelQueued

action · Element: button

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.CancelQueued requires Chat.Root.

Chat.CancelQueued requires Chat.QueueItems.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/cancel-queued.tsx

### Chat.CancelQueueEdit

action · Element: button

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.CancelQueueEdit requires Chat.Root.

Chat.CancelQueueEdit requires Chat.QueueItems.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/cancel-queue-edit.tsx

### Chat.SaveQueued

action · Element: button

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.SaveQueued requires Chat.Root.

Chat.SaveQueued requires Chat.QueueItems.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/save-queued.tsx

### Chat.EditQueued

action · Element: button

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.EditQueued requires Chat.Root.

Chat.EditQueued requires Chat.QueueItems.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/edit-queued.tsx

### Chat.QueuedInput

input · Element: textarea

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.QueuedInput requires Chat.Root.

Chat.QueuedInput requires Chat.QueueItems.

```tsx
PartProps<"textarea">
```

Source: @slices.sh/ai-chat/src/headless/queued-input.tsx

### Chat.QueueEditing

gate

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.QueueEditing requires Chat.QueueItems.

```tsx
GateProps<{ text: string }>
```

Source: @slices.sh/ai-chat/src/headless/queue-editing.tsx

### Chat.QueuePreview

gate

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.QueuePreview requires Chat.QueueItems.

```tsx
GateProps<{ text: string; files: readonly FileUIPart[] }>
```

Source: @slices.sh/ai-chat/src/headless/queue-preview.tsx

### Chat.QueueItem

content · Element: div

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.QueueItem requires Chat.QueueItems.

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/queue-item.tsx

### Chat.QueueItems

scope

The queue belongs to the sink: with whenBusy "queue", Chat.Root accepts a message while a reply streams and holds it until the chat is ready. These parts show it. Queued hands { count, waitingFor } where waitingFor is "turn", "question" or "approval", and belongs inside a region mounted from the first render. QueueItems repeats the waiting messages, QueueItem is one, QueuePreview shows its text and files. EditQueued opens QueueEditing with QueuedInput, SaveQueued and CancelQueueEdit. CancelQueued drops one. SendNow stops the running answer and sends this one first; with a question open, it skips the question and sends the message as the reply.

Import: @slices.sh/ai-chat/headless

Chat.QueueItems requires Chat.Root.

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/queue-items.tsx

### Chat.Queued

gate

How many messages are waiting, and for what: the running turn, an open question or a tool
decision. A reason is always given, because a queue that holds without saying why looks broken.

The queue was computed, returned and thrown away, so typing during an answer cleared the
box and showed nothing: three follow-ups looked like three sends that did not work, and
then arrived minutes later in an order nobody could predict.

This is a gate and not a live region, and the difference matters: a region announces what
appears *inside* it, so one that arrives already holding its sentence announces nothing.
The region is the composer's, mounted from the first render; this puts a sentence into it.

Import: @slices.sh/ai-chat/headless

Chat.Queued must be inside <Chat.Root>.

```tsx
GateProps<{ count: number; waitingFor: "turn" | "question" | "approval" }>
```

Source: @slices.sh/ai-chat/src/headless/queued.tsx

### ChatMedia.Root

scope

Root scopes one message and registers its media with Quick Look, so previous and next move through this message's files. ImageRun gates a run of consecutive images and hands { images, remaining }. More count={n} renders only when a run holds more images than are drawn, for a +N tile that opens the rest. OtherFile gates any other file and hands { file, id } so it keeps its name and media type, with a native player for audio and video.

Import: @slices.sh/chat-media/headless

ChatMedia.Root requires Chat.Messages.

```tsx
RootProps
```

Source: @slices.sh/chat-media/src/headless/root.tsx

### ChatMedia.ImageRun

gate

Root scopes one message and registers its media with Quick Look, so previous and next move through this message's files. ImageRun gates a run of consecutive images and hands { images, remaining }. More count={n} renders only when a run holds more images than are drawn, for a +N tile that opens the rest. OtherFile gates any other file and hands { file, id } so it keeps its name and media type, with a native player for audio and video.

Import: @slices.sh/chat-media/headless

ChatMedia.ImageRun requires Chat.Messages.

ChatMedia.ImageRun requires Chat.Parts.

```tsx
GateProps<ChatImageRun>
```

Source: @slices.sh/chat-media/src/headless/image-run.tsx

### ChatMedia.OtherFile

gate

Root scopes one message and registers its media with Quick Look, so previous and next move through this message's files. ImageRun gates a run of consecutive images and hands { images, remaining }. More count={n} renders only when a run holds more images than are drawn, for a +N tile that opens the rest. OtherFile gates any other file and hands { file, id } so it keeps its name and media type, with a native player for audio and video.

Import: @slices.sh/chat-media/headless

ChatMedia.OtherFile requires Chat.Messages.

ChatMedia.OtherFile requires Chat.Parts.

```tsx
GateProps<{ file: FileUIPart; id: string }>
```

Source: @slices.sh/chat-media/src/headless/other-file.tsx

### ChatMedia.More

gate

Root scopes one message and registers its media with Quick Look, so previous and next move through this message's files. ImageRun gates a run of consecutive images and hands { images, remaining }. More count={n} renders only when a run holds more images than are drawn, for a +N tile that opens the rest. OtherFile gates any other file and hands { file, id } so it keeps its name and media type, with a native player for audio and video.

Import: @slices.sh/chat-media/headless

```tsx
GateProps & { count: number }
```

Source: @slices.sh/chat-media/src/headless/more.tsx

### Markdown

content · Generic

Visual renderer — not purely headless.

Model output as markdown, safely, while it is still arriving.

Two of these are not things to write by hand, which is why there is a dependency here at
all. Completing half-streamed markdown: a fence, a bold run or a link that the next token
will finish must render as what it is becoming, or every answer flickers through its own
source. And sanitising: this text came from a model that reads the web, so a link is
untrusted input and an `<img onerror>` is a real thing to receive — the renderer hardens
the tree and this narrows what a URL may start with.

Nothing here is styled: the caller's `components` and `className` are the looks, so there
is no stylesheet to import and no class that Tailwind was never shown.

Import: @slices.sh/markdown

```tsx
MarkdownProps

type MarkdownProps = {
  extensions?: readonly ("math" | "diagrams")[]
  children: string
  labels: MarkdownLabels
  className?: string
  streaming?: boolean
  components?: MarkdownComponents
  /** Shown after the text while it is still arriving; it is decoration, so it is hidden from a screen reader. */
  cursor?: ReactElement<ComponentProps<"span">>
}

export type MarkdownComponents = {
  // An element takes the props of its tag and the tree node it came from; `never` lets each say which.
  [Tag in keyof typeof ELEMENTS]?: ComponentType<never>
} & {
  pre?: ComponentType<MarkdownCodeProps>
  diagram?: ComponentType<MarkdownDiagramProps>
}
```

Source: @slices.sh/markdown/src/markdown-view.tsx

### ChatAttachments.Root

scope

Missing storage leaves a fully functional text composer with no attachment controls.

Import: @slices.sh/chat-attachments/headless

Chat.AttachmentsRoot requires Chat.Root.

Chat.AttachmentsRoot requires Upload.Root.

```tsx
AttachmentsRootProps

type AttachmentsRootProps = RootProps & { bucket?: string; maxSize?: number; accept?: string }
```

Source: @slices.sh/chat-attachments/src/headless/attachments-root.tsx

### ChatAttachments.Available

gate

ChatAttachments.Available conditionally renders its children.

Import: @slices.sh/chat-attachments/headless

```tsx
GateProps
```

Source: @slices.sh/chat-attachments/src/headless/attachments-available.tsx

### ChatAttachments.Surface

content · Element: div

Paste and drop share the upload guard and queue.

Import: @slices.sh/chat-attachments/headless

```tsx
PartProps<"div"> & {
  /** Characters; false keeps all pasted text inline. */
  longPasteThreshold?: number | false
}
```

Source: @slices.sh/chat-attachments/src/headless/attachment-surface.tsx

### ChatAttachments.Error

gate

ChatAttachments.Error conditionally renders its children.

Import: @slices.sh/chat-attachments/headless

```tsx
GateProps
```

Source: @slices.sh/chat-attachments/src/headless/attachment-error.tsx

### ChatAttachments.Retry

action · Element: button

ChatAttachments.Retry renders a button element.

Import: @slices.sh/chat-attachments/headless

```tsx
PartProps<"button">
```

Source: @slices.sh/chat-attachments/src/headless/retry-attachments.tsx

### ChatDictation.Root

scope

The existing dictation engine writes into the same draft as the native chat composer.

Import: @slices.sh/ai-chat/headless/chat-dictation

ChatDictation.Root requires Chat.Root.

ChatDictation requires Chat.Root.

ChatDictation requires Dictation.Root.

```tsx
RootProps
```

Source: @slices.sh/ai-chat/src/headless/chat-dictation/root.tsx

### ChatDictation.Input

input · Element: textarea

Dictation.Field remains the sole owner of editing and caret restoration.

Import: @slices.sh/ai-chat/headless/chat-dictation

ChatDictation.Input requires ChatDictation.Root.

```tsx
PartProps<"textarea">
```

Source: @slices.sh/ai-chat/src/headless/chat-dictation/input.tsx

### ChatDictation.Voice

content · Element: div

Keep AnimatePresence mounted across the recording-to-text transition.

Import: @slices.sh/ai-chat/headless/chat-dictation

ChatDictation.Voice requires ChatDictation.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/chat-dictation/voice.tsx

### ChatDictation.Text

content · Element: div

Keep the actual field mounted so its caret owner survives the voice surface.

Import: @slices.sh/ai-chat/headless/chat-dictation

ChatDictation.Text requires ChatDictation.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/ai-chat/src/headless/chat-dictation/text.tsx

### ChatDictation.Confirm

action · Element: button

ChatDictation.Confirm renders a button element.

Import: @slices.sh/ai-chat/headless/chat-dictation

ChatDictation.Confirm requires ChatDictation.Root.

```tsx
PartProps<"button">
```

Source: @slices.sh/ai-chat/src/headless/chat-dictation/confirm.tsx

### Upload.Root

scope

The queue, its limits, and its field registration when it has a name inside a Composer.

Import: @slices.sh/file-storage/headless

```tsx
UploadRootProps

type UploadRootProps = RootProps & {
  /**
   * The key of this value in a Composer's message. With a name, inside a Composer, the uploader
   * is a field: its value is the finished files, and it holds the submit while one uploads.
   * Without one, or outside a Composer, it is a plain uploader.
   */
  name?: string
  /**
   * Left out or `true`: required, and a missing service throws. `"optional"`: renders its
   * children and every upload part does nothing, which is what a pattern's paperclip passes.
   * `false`: off. A Composer whose sink takes no files turns it off too.
   */
  enabled?: Enabled
  /** The file-storage config, `"default"` when left out. */
  config?: string
  bucket?: string
  /** The directory files land in. */
  path?: string
  /**
   * Bytes. A larger file is turned away before anything is sent. The server's own limit, in
   * `services/file-storage/limits.ts`, applies too: the lower of the two is the one enforced.
   */
  maxSize?: number
  maxFiles?: number
  /** An `accept` attribute: `"image/*,.pdf"`. Offered to the picker and checked on the way in. */
  accept?: string
  /**
   * Files from 5 MiB go up in resumable parts wherever the backend has them, so a large file
   * survives a dropped connection and can be paused. On unless turned off: a screen that had
   * to opt in sent a 2 GB file as one request. A backend without parts is sent whole.
   */
  multipart?: boolean
  /**
   * Hidden files inside a folder (`.DS_Store`, `.git/…`, `Thumbs.db`) are left out of what a
   * folder drop or folder pick brings in. Say `includeHidden` to send them. A hidden file
   * chosen on its own is always taken.
   */
  includeHidden?: boolean
  /** What a name that is taken does: replace, keep both, refuse, or `"ask"` and wait to be told. Left out, replace. */
  ifExists?: IfExists | "ask"
  /** Each file as it lands, with the permalink to keep for it. */
  onUploaded?(uploaded: Uploaded): void
  /** Where a message and its finished attachments go. Without it there is nowhere to send. */
  onSend?(message: { text: string; files: FileRecord[] }): void
}
```

Source: @slices.sh/file-storage/src/headless/upload-root.tsx
