# Artifact workspace

Versioned React source written by the model through the createArtifact and updateArtifact tools, inspected beside the chat: a card in the transcript, a nonmodal panel that can be resized, version navigation with a diff, and a preview that runs only when somebody presses Run, in an isolated iframe. Nothing is stored beyond the conversation and nothing executes in the host page.

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

Public parts: 27

## 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/chat-artifacts@^0.1.2 @slices.sh/ai-chat@^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

## Anatomy

### Composition

```text
Chat.Root
  ChatArtifacts.Root
    Chat.Messages
      Chat.Message
        Chat.Parts
          ChatArtifacts.PartCard
            ChatArtifacts.Open artifactId="landing-page"
          ChatArtifacts.OtherTool
    ChatArtifacts.Resize
    ChatArtifacts.Panel
      ChatArtifacts.Title
      ChatArtifacts.Description
      ChatArtifacts.Close
      ChatArtifacts.Selected
        ChatArtifacts.Previous
        ChatArtifacts.Next
        ChatArtifacts.Diff
        ChatArtifacts.Runtime
          ChatArtifacts.Tabs
            ChatArtifacts.TabList
              ChatArtifacts.Tab value="preview"
              ChatArtifacts.Tab value="preview"
              ChatArtifacts.Tab value="preview"
            ChatArtifacts.TabPanel value="preview"
              ChatArtifacts.PreviewIdle
                ChatArtifacts.Run
              ChatArtifacts.PreviewLoading
              ChatArtifacts.PreviewError
              ChatArtifacts.PreviewFrame
              ChatArtifacts.StopPreview
            ChatArtifacts.TabPanel value="preview"
            ChatArtifacts.TabPanel value="preview"
              ChatArtifacts.Console
                ChatArtifacts.ConsoleEmpty
              ChatArtifacts.ClearConsole
```

## Artifact cards

```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 { ChatArtifacts } from "@slices.sh/chat-artifacts/headless"

export function Example() {
  return (
    <Chat.Root>
      <ChatArtifacts.Root>
        <Chat.Messages>
          <Chat.Message>
            <Chat.Parts>
              <ChatArtifacts.PartCard>
                {({ version }) => (
                  <ChatArtifacts.Open artifactId={version.artifactId}>
                    {version.title}
                  </ChatArtifacts.Open>
                )}
              </ChatArtifacts.PartCard>
            </Chat.Parts>
          </Chat.Message>
        </Chat.Messages>
        <ChatArtifacts.Panel>
          <ChatArtifacts.Close>Close</ChatArtifacts.Close>
        </ChatArtifacts.Panel>
      </ChatArtifacts.Root>
    </Chat.Root>
  )
}

```

Requires a chat whose tools produce artifacts. The echo provider does not produce them.

## Your own element

```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 { ChatArtifacts } from "@slices.sh/chat-artifacts/headless"

export function Example() {
  return (
    <Chat.Root>
      <ChatArtifacts.Root>
        <Chat.Messages>
          <Chat.Message>
            <Chat.Parts>
              <ChatArtifacts.PartCard>
                {({ version }) => (
                  <ChatArtifacts.Open
                    render={
                      <button className="rounded-lg border p-4 text-left" />
                    }
                    artifactId={version.artifactId}
                  >
                    {version.title}
                  </ChatArtifacts.Open>
                )}
              </ChatArtifacts.PartCard>
            </Chat.Parts>
          </Chat.Message>
        </Chat.Messages>
        <ChatArtifacts.Panel>
          <ChatArtifacts.Close>Close</ChatArtifacts.Close>
        </ChatArtifacts.Panel>
      </ChatArtifacts.Root>
    </Chat.Root>
  )
}

```

The same behavior, with your element and your styles. All other parts stay unchanged.

## Complete API reference

### ChatArtifacts.Runtime

scope

Runtime scopes the compilation, frame and console of the selected version (artifact-runtime). Run compiles in a worker and shows the preview; StopPreview cancels and removes the frame. PreviewFrame locks its srcdoc, sandbox and permissions. PreviewIdle, PreviewLoading and PreviewError gate its states. Changing version or closing the panel discards the previous run.

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

Capability: artifact-runtime

ChatArtifacts.Runtime requires ChatArtifacts.Root.

```tsx
RootProps
```

Source: @slices.sh/chat-artifacts/src/headless/artifact-runtime.tsx

### ChatArtifacts.Run

action · Element: button

Runtime scopes the compilation, frame and console of the selected version (artifact-runtime). Run compiles in a worker and shows the preview; StopPreview cancels and removes the frame. PreviewFrame locks its srcdoc, sandbox and permissions. PreviewIdle, PreviewLoading and PreviewError gate its states. Changing version or closing the panel discards the previous run.

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

Capability: artifact-runtime

ChatArtifacts.Run requires ChatArtifacts.Root.

ChatArtifacts.Run requires ChatArtifacts.Runtime.

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

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

### ChatArtifacts.StopPreview

action · Element: button

Runtime scopes the compilation, frame and console of the selected version (artifact-runtime). Run compiles in a worker and shows the preview; StopPreview cancels and removes the frame. PreviewFrame locks its srcdoc, sandbox and permissions. PreviewIdle, PreviewLoading and PreviewError gate its states. Changing version or closing the panel discards the previous run.

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

Capability: artifact-runtime

ChatArtifacts.StopPreview requires ChatArtifacts.Runtime.

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

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

### ChatArtifacts.PreviewFrame

content · Element: iframe

Runtime scopes the compilation, frame and console of the selected version (artifact-runtime). Run compiles in a worker and shows the preview; StopPreview cancels and removes the frame. PreviewFrame locks its srcdoc, sandbox and permissions. PreviewIdle, PreviewLoading and PreviewError gate its states. Changing version or closing the panel discards the previous run.

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

Capability: artifact-runtime

ChatArtifacts.PreviewFrame requires ChatArtifacts.Runtime.

```tsx
PartProps<"iframe">
```

Source: @slices.sh/chat-artifacts/src/headless/preview-frame.tsx

### ChatArtifacts.PreviewIdle

gate

Runtime scopes the compilation, frame and console of the selected version (artifact-runtime). Run compiles in a worker and shows the preview; StopPreview cancels and removes the frame. PreviewFrame locks its srcdoc, sandbox and permissions. PreviewIdle, PreviewLoading and PreviewError gate its states. Changing version or closing the panel discards the previous run.

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

Capability: artifact-runtime

ChatArtifacts.PreviewIdle requires ChatArtifacts.Runtime.

```tsx
GateProps
```

Source: @slices.sh/chat-artifacts/src/headless/preview-idle.tsx

### ChatArtifacts.PreviewLoading

gate

Runtime scopes the compilation, frame and console of the selected version (artifact-runtime). Run compiles in a worker and shows the preview; StopPreview cancels and removes the frame. PreviewFrame locks its srcdoc, sandbox and permissions. PreviewIdle, PreviewLoading and PreviewError gate its states. Changing version or closing the panel discards the previous run.

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

Capability: artifact-runtime

ChatArtifacts.PreviewLoading requires ChatArtifacts.Runtime.

```tsx
GateProps
```

Source: @slices.sh/chat-artifacts/src/headless/preview-loading.tsx

### ChatArtifacts.PreviewError

gate

Runtime scopes the compilation, frame and console of the selected version (artifact-runtime). Run compiles in a worker and shows the preview; StopPreview cancels and removes the frame. PreviewFrame locks its srcdoc, sandbox and permissions. PreviewIdle, PreviewLoading and PreviewError gate its states. Changing version or closing the panel discards the previous run.

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

Capability: artifact-runtime

ChatArtifacts.PreviewError requires ChatArtifacts.Runtime.

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

Source: @slices.sh/chat-artifacts/src/headless/preview-error.tsx

### ChatArtifacts.Console

content · Element: ol

Console gives { entries }, bounded text-only output from the frame. ConsoleEmpty renders before anything is logged, and ClearConsole empties it.

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

ChatArtifacts.Console requires ChatArtifacts.Runtime.

```tsx
PartProps<"ol", { entries: ArtifactLog[] }>
```

Source: @slices.sh/chat-artifacts/src/headless/console.tsx

### ChatArtifacts.ConsoleEmpty

gate

Console gives { entries }, bounded text-only output from the frame. ConsoleEmpty renders before anything is logged, and ClearConsole empties it.

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

ChatArtifacts.ConsoleEmpty requires ChatArtifacts.Runtime.

```tsx
GateProps
```

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

### ChatArtifacts.ClearConsole

action · Element: button

Console gives { entries }, bounded text-only output from the frame. ConsoleEmpty renders before anything is logged, and ClearConsole empties it.

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

ChatArtifacts.ClearConsole requires ChatArtifacts.Runtime.

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

Source: @slices.sh/chat-artifacts/src/headless/clear-console.tsx

### ChatArtifacts.Root

scope · Element: div

Root reads artifacts from the Chat inside Chat.Root and owns the selection and split. PartCard gates a valid streamed or completed artifact call inside Chat.Parts and hands { version }. OtherTool renders its children for every tool call that is not an artifact, so the chat's own tool UI handles approvals, refusals and malformed calls. Open artifact={id} version={n} opens the panel on that version.

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

ChatArtifacts.Root requires Chat.Root.

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

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

### ChatArtifacts.PartCard

gate

Root reads artifacts from the Chat inside Chat.Root and owns the selection and split. PartCard gates a valid streamed or completed artifact call inside Chat.Parts and hands { version }. OtherTool renders its children for every tool call that is not an artifact, so the chat's own tool UI handles approvals, refusals and malformed calls. Open artifact={id} version={n} opens the panel on that version.

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

ChatArtifacts.PartCard requires Chat.Parts.

ChatArtifacts.PartCard requires ChatArtifacts.Root.

```tsx
GateProps<{ version: ArtifactVersion }>
```

Source: @slices.sh/chat-artifacts/src/headless/part-card.tsx

### ChatArtifacts.OtherTool

gate

Root reads artifacts from the Chat inside Chat.Root and owns the selection and split. PartCard gates a valid streamed or completed artifact call inside Chat.Parts and hands { version }. OtherTool renders its children for every tool call that is not an artifact, so the chat's own tool UI handles approvals, refusals and malformed calls. Open artifact={id} version={n} opens the panel on that version.

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

ChatArtifacts.OtherTool requires Chat.Parts.

ChatArtifacts.OtherTool requires ChatArtifacts.Root.

```tsx
GateProps
```

Source: @slices.sh/chat-artifacts/src/headless/other-tool.tsx

### ChatArtifacts.Open

action · Element: button

Root reads artifacts from the Chat inside Chat.Root and owns the selection and split. PartCard gates a valid streamed or completed artifact call inside Chat.Parts and hands { version }. OtherTool renders its children for every tool call that is not an artifact, so the chat's own tool UI handles approvals, refusals and malformed calls. Open artifact={id} version={n} opens the panel on that version.

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

ChatArtifacts.Open requires ChatArtifacts.Root.

```tsx
PartProps<"button"> & { artifactId: string; callId?: string }
```

Source: @slices.sh/chat-artifacts/src/headless/open.tsx

### ChatArtifacts.Panel

content · Element: div

Panel is an inline nonmodal dialog on wide screens and a full-viewport modal on narrow ones; the chat stays usable beside it and Escape returns focus. Title, Description and Close label and dismiss it. Resize is a pointer and keyboard separator between the chat and the panel, 58% to the artifact at first, within 30% to 75%.

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

ChatArtifacts.Panel requires ChatArtifacts.Root.

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

Source: @slices.sh/chat-artifacts/src/headless/panel.tsx

### ChatArtifacts.Close

action · Element: button

Panel is an inline nonmodal dialog on wide screens and a full-viewport modal on narrow ones; the chat stays usable beside it and Escape returns focus. Title, Description and Close label and dismiss it. Resize is a pointer and keyboard separator between the chat and the panel, 58% to the artifact at first, within 30% to 75%.

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

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

Source: @slices.sh/chat-artifacts/src/headless/close.tsx

### ChatArtifacts.Selected

gate

Selected hands the selected artifact, its version, the preceding version and its index. Previous and Next move between versions in the transcript without asking the model anything. Diff gives a bounded source diff against the preceding version, with a coarse fallback when it is too large.

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

ChatArtifacts.Selected requires ChatArtifacts.Root.

```tsx
GateProps<NonNullable<ArtifactSelection["current"]>>
```

Source: @slices.sh/chat-artifacts/src/headless/selected.tsx

### ChatArtifacts.Previous

action · Element: button

Selected hands the selected artifact, its version, the preceding version and its index. Previous and Next move between versions in the transcript without asking the model anything. Diff gives a bounded source diff against the preceding version, with a coarse fallback when it is too large.

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

ChatArtifacts.Previous requires ChatArtifacts.Root.

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

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

### ChatArtifacts.Next

action · Element: button

Selected hands the selected artifact, its version, the preceding version and its index. Previous and Next move between versions in the transcript without asking the model anything. Diff gives a bounded source diff against the preceding version, with a coarse fallback when it is too large.

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

ChatArtifacts.Next requires ChatArtifacts.Root.

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

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

### ChatArtifacts.Diff

gate

Selected hands the selected artifact, its version, the preceding version and its index. Previous and Next move between versions in the transcript without asking the model anything. Diff gives a bounded source diff against the preceding version, with a coarse fallback when it is too large.

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

ChatArtifacts.Diff requires ChatArtifacts.Root.

```tsx
GateProps<ArtifactDiff>
```

Source: @slices.sh/chat-artifacts/src/headless/diff.tsx

### ChatArtifacts.Resize

content · Element: div

Panel is an inline nonmodal dialog on wide screens and a full-viewport modal on narrow ones; the chat stays usable beside it and Escape returns focus. Title, Description and Close label and dismiss it. Resize is a pointer and keyboard separator between the chat and the panel, 58% to the artifact at first, within 30% to 75%.

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

ChatArtifacts.Resize requires ChatArtifacts.Root.

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

Source: @slices.sh/chat-artifacts/src/headless/resize.tsx

### ChatArtifacts.Title

content

Panel is an inline nonmodal dialog on wide screens and a full-viewport modal on narrow ones; the chat stays usable beside it and Escape returns focus. Title, Description and Close label and dismiss it. Resize is a pointer and keyboard separator between the chat and the panel, 58% to the artifact at first, within 30% to 75%.

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

```tsx
React.ComponentProps<typeof Dialog.Title>
```

Source: @slices.sh/chat-artifacts/src/headless/index.ts

### ChatArtifacts.Description

content

Panel is an inline nonmodal dialog on wide screens and a full-viewport modal on narrow ones; the chat stays usable beside it and Escape returns focus. Title, Description and Close label and dismiss it. Resize is a pointer and keyboard separator between the chat and the panel, 58% to the artifact at first, within 30% to 75%.

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

```tsx
React.ComponentProps<typeof Dialog.Description>
```

Source: @slices.sh/chat-artifacts/src/headless/index.ts

### ChatArtifacts.Tabs

content · Element: div

Tabs, TabList, Tab and TabPanel switch between source, diff, preview and console with native tab keyboard behaviour.

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

ChatArtifacts.Tabs requires ChatArtifacts.Runtime.

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

Source: @slices.sh/chat-artifacts/src/headless/view-tabs.tsx

### ChatArtifacts.TabList

content

Tabs, TabList, Tab and TabPanel switch between source, diff, preview and console with native tab keyboard behaviour.

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

```tsx
React.ComponentProps<typeof Tabs.List>
```

Source: @slices.sh/chat-artifacts/src/headless/index.ts

### ChatArtifacts.Tab

content

Tabs, TabList, Tab and TabPanel switch between source, diff, preview and console with native tab keyboard behaviour.

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

```tsx
React.ComponentProps<typeof Tabs.Tab>
```

Source: @slices.sh/chat-artifacts/src/headless/index.ts

### ChatArtifacts.TabPanel

content

Tabs, TabList, Tab and TabPanel switch between source, diff, preview and console with native tab keyboard behaviour.

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

```tsx
React.ComponentProps<typeof Tabs.Panel>
```

Source: @slices.sh/chat-artifacts/src/headless/index.ts

### 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.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.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
