Meowland docs
Workbench

Component reference

ChatFrame

The outer surface of a conversation that holds its header, messages and composer.

Preview

ChatFrame has no story of its own; this one draws it as part of ChatToolCall.

Tool call sequence

Tool actions and their target stay on one row; results expand on demand, and non-complete states always show clearly. The static example runs no tools.

packages/meowtail/stories/chat-message.stories.tsxline 37tsx
render: () => (
  <Measure>
    <ChatFrame>
      <ChatMessageList ariaLabel="Tool call sequence">
        <ChatMessage role="user" author="You" surface="none">
          <ChatBubble>
            Check whether an old response still writes to the list after a request is canceled.
          </ChatBubble>
        </ChatMessage>
        <ChatMessage role="assistant" author="Assistant" surface="none">
          <ChatBubble>
            <p>I'll first look at where requests and results are submitted.</p>
          </ChatBubble>
          <ChatProcess label="Read, search · 3 calls" icon={<Icon name="search" />}>
            <ChatToolCall
              label="Read"
              summary="src/search/request.ts"
              icon={<Icon name="file-text" />}
              collapsible
            >
              <pre>
                <code>const request = await fetch(url, options);</code>
              </pre>
            </ChatToolCall>
            <ChatToolCall
              label="Search"
              summary="currentRequest · src/search"
              icon={<Icon name="search" />}
              collapsible
            >
              <p>Found 2 request identity checks.</p>
            </ChatToolCall>
            <ChatToolCall
              label="Read"
              summary="src/search/results.ts"
              icon={<Icon name="file-text" />}
              collapsible
            >
              <p>The request identity is not checked before submitting results.</p>
            </ChatToolCall>
          </ChatProcess>
          <ChatBubble>
            <p>
              The result write path is missing an identity check; the verification states follow.
            </p>
          </ChatBubble>
          <ChatProcess label="Run verification · 4 items" open>
            <ChatToolCall
              icon={<Icon name="terminal" />}
              label="Run"
              summary="bun test search/request.test.ts"
              state="running"
              collapsible
            >
              <p>Waiting for process output.</p>
            </ChatToolCall>
            <ChatToolCall
              icon={<Icon name="file-text" />}
              label="Read"
              summary="Restricted workspace configuration"
              state="waiting"
              collapsible
            >
              <p>Waiting for permission confirmation.</p>
            </ChatToolCall>
            <ChatToolCall
              icon={<Icon name="terminal" />}
              label="Run"
              summary="Remote connection check"
              state="error"
              collapsible
            >
              <p>The connection failed; nothing on the remote was changed.</p>
            </ChatToolCall>
            <ChatToolCall
              icon={<Icon name="terminal" />}
              label="Run"
              summary="Previous interrupted verification"
              state="unknown"
              collapsible
            >
              <p>There is no final receipt, so the execution result cannot be confirmed.</p>
            </ChatToolCall>
          </ChatProcess>
        </ChatMessage>
      </ChatMessageList>
    </ChatFrame>
  </Measure>
),
});

export const ChatToolCallActionStory = defineStory({
id: "chat-tool-call-action",
title: "Tool row · Actions and expansion",
group: "Conversation components",
groupOrder: 60,
component: "ChatToolCall",
variant: "Host actions",
description:
  "A row has two interactions: expand its own body, or let the host run an action. Both are the same focusable control, with consistent row metrics, state and hover glyph. The example only echoes local state and runs no command.",
covers: ["onActivate", "button header", "collapsible body", "row geometry"],
render: () => {
  const feedback = signal("Not activated yet");
  return (
    <Measure>
      <Stack>
        <ChatFrame>
          <ChatMessageList ariaLabel="The two interactions of a tool row">
            <ChatToolCall
              label="Run command"
              summary="bun test search/request.test.ts"
              icon={<Icon name="terminal" />}
              onActivate={() => {
                feedback.value = "Activated: the host opens the record for this command.";
              }}
              data-tool-action="demo"
            />
            <ChatToolCall
              label="Read"
              summary="src/search/request.ts"
              icon={<Icon name="file-text" />}
              collapsible
            >
              <p>The expanded row leaves the result in place.</p>
            </ChatToolCall>
            <ChatToolCall
              label="Run"
              summary="Remote connection check"
              icon={<Icon name="terminal" />}
              state="error"
            />
          </ChatMessageList>
        </ChatFrame>
        <Note role="status" data-tool-action-result>
          {feedback}
        </Note>
      </Stack>
    </Measure>
  );
},
});

export const ChatMessageCompositionStory = defineStory({
id: "chat-message-composition",
title: "Message · Text, files and evidence",
group: "Conversation components",
groupOrder: 60,
component: "ChatMessage",
variant: "Composition and surface",
description:
  "The message owns identity and arrangement; the Bubble owns the reading surface. Attachments and process can sit outside the bubble, and actions only echo local state.",
covers: [
  "ChatMessage header/footer/surface",
  "ChatBubble auto/plain/filled",
  "ChatMessageActions",
  "mixed content",
],
render: () => (
  <Measure>
    <Stack>
      <ChatFrame>
        <ChatMessageList ariaLabel="Message composition example">
          <ChatMessage
            role="user"
            author="You"
            timestamp="14:20"
            surface="none"
            footer={<span>Delivered</span>}
          >
            <ChatAttachments
              items={[
                {
                  id: "log",
                  name: "search-request.log",
                  url: "data:text/plain,Search%20request%20example",
                  mediaType: "text/plain",
                  sizeBytes: 22,
                },
              ]}
            />
            <ChatBubble>
              <p>
                This is the log from continuous searches. Help me understand why an old result
                overwrote a new one; give me a suggestion first.
              </p>
            </ChatBubble>
          </ChatMessage>
          <ChatMessage
            role="assistant"
            author="Assistant"
            surface="none"
            header={
              <>
                <strong>Assistant</strong>
                <span>Investigation record · Static example</span>
              </>
            }
            footer={<span>No real request ran; the suggestion waits for your confirmation.</span>}
            actions={
              <ChatMessageActions>
                <Button
                  variant="outline"
                  size="small"
                  data-message-choice="Kept the fix suggestion"
                >
                  Keep suggestion
                </Button>
                <Button
                  variant="quiet"
                  size="small"
                  data-message-choice="Marked for further checking"
                >
                  Keep checking
                </Button>
              </ChatMessageActions>
            }
          >
            <ChatBubble>
              <p>
                <strong>
                  The old request returned later, and the write did not confirm that it was still
                  the current request.
                </strong>
                Keep the interface and check the request identity before submitting results.
              </p>
            </ChatBubble>
            <ChatProcess label="View investigation evidence · 2 items">
              <ChatToolCall
                icon={<Icon name="file-text" />}
                label="Read request log"
                state="complete"
                collapsible
                detail="In this example the second request finished first."
              >
                <p>
                  14:20:01 searched for “components”; 14:20:02 searched for “component library”.
                </p>
              </ChatToolCall>
              <ChatToolCall
                icon={<Icon name="file-text" />}
                label="Check the result write path"
                state="complete"
                collapsible
                detail="The callback replaces the list directly, without an identity check."
              >
                <p>Fix location: the result submission callback.</p>
              </ChatToolCall>
            </ChatProcess>
            <ChatBubble variant="plain">
              <p>
                Canceling the old request can reduce work, but a final check before writing is
                still needed:
              </p>
              <pre tabIndex={0} aria-label="Suggested code">
                <code>{`if (currentRequest === request) {
renderResults(results);
}`}</code>
              </pre>
            </ChatBubble>
            <ChatSources items={[reference]} />
          </ChatMessage>
          <ChatMessage
            role="user"
            author="You"
            status="error"
            surface="none"
            footer={<span>Send failed; the draft is still local.</span>}
            actions={
              <ChatMessageActions>
                <Button
                  variant="quiet"
                  size="small"
                  data-message-choice="Kept the retry content (local example)"
                >
                  Retry send
                </Button>
              </ChatMessageActions>
            }
          >
            <ChatBubble variant="filled">
              <p>Apply this fix, and add a test for out-of-order responses while you're at it.</p>
            </ChatBubble>
          </ChatMessage>
        </ChatMessageList>
      </ChatFrame>
      <Note role="status" data-message-result>
        Actions only report back to the local example.
      </Note>
    </Stack>
  </Measure>
)

Installation

bun add meowtail

Usage

Taken from the examples on this page.

usage.tsxtsx
import { ChatFrame } from "meowtail";

<ChatFrame>
  <ChatMessageList ariaLabel="Message list with direct children">
    <ChatMessageListContent>{rows(false)}</ChatMessageListContent>
  </ChatMessageList>
</ChatFrame>

Examples

Other stories that draw ChatFrame.

Row spacing and continuation

The list column declares the row pitch and each row reads it, so layout-neutral wrappers inserted when mounting row by row do not change the result. A user message starts a new round, a continuation answer sits close to the previous one, and a tool row sits close to the work it reports.

packages/meowtail/stories/chat-message.stories.tsxline 463tsx
render: () => (
  <Measure>
    <Stack>
      <ChatFrame>
        <ChatMessageList ariaLabel="Message list with direct children">
          <ChatMessageListContent>{rows(false)}</ChatMessageListContent>
        </ChatMessageList>
      </ChatFrame>
      <ChatFrame>
        <ChatMessageList ariaLabel="Message list mounted row by row">
          <ChatMessageListContent>{rows(true)}</ChatMessageListContent>
        </ChatMessageList>
      </ChatFrame>
    </Stack>
  </Measure>
)

Avatar, long title, and actions

The avatar, author, time, body, and actions of one conversation; long content must not crowd out actions or break narrow screens.

packages/meowtail/stories/chat.stories.tsxline 38tsx
render: () => (
  <div className="workshop-composition">
    <ChatFrame>
      <ChatHeader
        eyebrow="Current project"
        title="Review the component's text, icons, dark surfaces, and narrow-screen interactions"
        description="The title and description wrap when space runs out without hiding the work context."
        status={<ChatStatus label="Waiting for review" tone="waiting" />}
        actions={
          <IconButton label="View conversation information" data-message-info>
            <Icon name="eye" />
          </IconButton>
        }
      />
      <ChatMessageList ariaLabel="Component review sample messages">
        <ChatMessage
          role="assistant"
          author="Assistant handling the component review"
          timestamp="09:41"
          avatar={<AssistantMark />}
          actions={
            <ChatMessageActions>
              <Button variant="quiet" size="small" data-message-action="Marked this reply.">
                Mark reply
              </Button>
              <IconButton label="Reply details" data-message-info>
                <Icon name="eye" />
              </IconButton>
              <Button variant="quiet" size="small" disabled>
                Waiting for check results
              </Button>
            </ChatMessageActions>
          }
        >
          <p>
            The title, author, and actions must keep the reading order together. Longer paths
            should also stay inside the current container:
          </p>
          <p>
            packages/meowtail/stories/review-of-typography-and-component-interaction-states.tsx
          </p>
        </ChatMessage>
        <ChatMessage
          role="assistant"
          author="Assistant handling the component review"
          continuation
          timestamp="09:42"
          avatar={<AssistantMark />}
        >
          Continues the previous reply in the same column: the avatar slot stays but goes blank,
          and the author line keeps the turn attributed.
        </ChatMessage>
        <ChatMessage
          role="user"
          author="You"
          timestamp="09:43"
          avatar={<Icon name="user-round" />}
        >
          Keep the relationship between input, messages, and attachments, and make sure long
          content stays fully readable.
        </ChatMessage>
        <ChatMessage
          role="assistant"
          author="Assistant"
          timestamp="09:44"
          actions={
            <ChatMessageActions>
              <Button data-message-action="Opened the local suggestion sample.">
                Review suggestion
              </Button>
              <Button variant="outline" data-message-action="Opened the local diff sample.">
                View diff
              </Button>
              <button
                type="button"
                data-message-action="Kept the check record; you can come back later"
              >
                Handle later
              </button>
            </ChatMessageActions>
          }
        >
          <p>
            The check record is ready. You can review the suggestion first and then decide whether
            to adopt it, or keep the record and handle it later.
          </p>
        </ChatMessage>
      </ChatMessageList>
    </ChatFrame>
    <ChatHeader title="Compact conversation with a title only" />
    <Note role="status" data-message-feedback>
      Actions only report back to the local sample.
    </Note>
  </div>
)

API reference

Props

Also takes className, children and the attributes and events of its root element.

DOM parts

root

Registry

The component is registered as chat-frame in the meowtail component registry.