Meowland docs
Workbench

Component reference

ChatToolCall

One tool call in an assistant's work: what ran, on what, and how it ended.

Preview

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 { ChatToolCall } from "meowtail";

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

Examples

The other stories that show this component.

Actions and expansion

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.

packages/meowtail/stories/chat-message.stories.tsxline 138tsx
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>
  );
}

Four states and expansion on demand

Static states and real native expansion behavior are separate: each of the four states shows both a plain and a collapsible carrier.

packages/meowtail/stories/chat.stories.tsxline 266tsx
render: () => (
  <div className="workshop-composition">
    <ChatProcess label="Expanded check evidence" open>
      <p>
        This content shows only component states; it does not mean a real project was checked.
      </p>
    </ChatProcess>
    <ChatProcess label="Supporting process you expand when needed">
      <p>Tab to the heading, then use Enter or Space to expand and the same keys to collapse.</p>
    </ChatProcess>
    {(["running", "complete", "waiting", "error"] as const).map((state) => (
      <section className="workshop-section" aria-label={`${state} execution state`}>
        <ChatToolCall
          icon={<Icon name="list-checks" />}
          label={`Component check · ${state}`}
          state={state}
          detail="The plain carrier shows the evidence directly."
        />
        <ChatToolCall
          icon={<Icon name="list-checks" />}
          label={`Expand the full check details and follow-up notes for ${state}`}
          state={state}
          collapsible
          detail="The summary and details belong to the same execution."
        >
          <p>
            Continue checking how the long path
            packages/meowtail/src/theme/components-and-conversation-state-review.ts is displayed.
          </p>
        </ChatToolCall>
      </section>
    ))}
    <ChatSources
      label="Design evidence"
      open
      items={[
        {
          title: "Apple's typography roles on desktop and the boundary of local Web adaptation",
          url: "https://developer.apple.com/design/human-interface-guidelines/typography",
          description:
            "Source titles and descriptions can wrap; opening the link leaves the local sample.",
        },
        {
          title: "Source without a supporting description",
          url: "https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html",
        },
      ]}
    />
    <ChatSources items={[]} />
  </div>
)

Task, progress, and actual results

A fixed engineering example: identify what was done while collapsed, then check the command and result when expanded. Running, waiting, and failure each describe the current progress; no command is executed and no model is requested.

packages/meowtail/stories/chat.stories.tsxline 335tsx
render: () => (
  <ChatFrame>
    <ChatMessageList ariaLabel="Order summary fix sample">
      <ChatMessage role="user" author="You">
        Fix how the order summary handles special SKUs, add regression tests, and update the
        changelog.
      </ChatMessage>
      <ChatMessage role="assistant" author="Assistant">
        <p>
          I'll check the summary logic first, then review the test results for special key names
          and empty orders.
        </p>
        <ChatProcess label="Checks and changes · 3 items" open>
          <ChatToolCall
            icon={<Icon name="file-text" />}
            label="Read the order summary and tests"
            state="complete"
            collapsible
          >
            <p>
              Read <code>orders.ts</code> and <code>orders.test.ts</code>. The summary object uses
              a plain object, so <code>__proto__</code> and <code>constructor</code> hit prototype
              properties.
            </p>
          </ChatToolCall>
          <ChatToolCall
            icon={<Icon name="file-text" />}
            label="Fix the summary for special SKUs"
            state="complete"
            collapsible
          >
            <p>
              Store SKU counts with <code>Object.create(null)</code> while keeping the existing
              return fields. Added regression tests for special key names and updated{" "}
              <code>CHANGELOG.md</code>.
            </p>
          </ChatToolCall>
          <ChatToolCall
            icon={<Icon name="terminal" />}
            label="Run the order summary tests"
            state="complete"
            collapsible
            open
          >
            <pre>
              <code>{`$ bun test orders.test.ts

✓ empty orders return zero counts
✓ counts only paid orders
✓ merges quantities for duplicate SKUs
✓ supports __proto__, constructor, toString

4 pass · 0 fail · 10 expect calls
Exit code 0`}</code>
            </pre>
          </ChatToolCall>
        </ChatProcess>
        <p>
          The fix and regression tests are complete. Expand the process to check the change
          evidence and test output.
        </p>
      </ChatMessage>
      <ChatMessage role="assistant" author="Assistant" continuation>
        <ChatToolCall
          icon={<Icon name="file-text" />}
          label="Check component library types"
          state="running"
          collapsible
          open
          detail="Started checking MeowUI; no final result yet."
        >
          <pre>
            <code>{`$ bun run --filter meowtail typecheck
Checking packages/meowtail/src/chat/tool-call.tsx`}</code>
          </pre>
        </ChatToolCall>
        <ChatToolCall
          icon={<Icon name="terminal" />}
          label="Install project dependencies"
          state="waiting"
          collapsible
          detail="Waiting for permission to access the dependency repository; nothing installed yet."
        >
          <p>
            <code>bun install</code> needs to download the missing dependencies. During the wait,
            file changes that are already done are kept.
          </p>
        </ChatToolCall>
        <ChatToolCall
          icon={<Icon name="terminal" />}
          label="Check formatting across the repository"
          state="error"
          collapsible
          open
        >
          <pre>
            <code>{`$ bun run format:check
[warn] packages/meowtail/stories/conversation/long-running-execution-and-recovery.stories.tsx
[warn] Code style issues found in the above file.
Exit code 1`}</code>
          </pre>
          <p>
            The format check did not pass; this result is not a typecheck or test failure. Recheck
            after fixing the formatting.
          </p>
        </ChatToolCall>
      </ChatMessage>
    </ChatMessageList>
  </ChatFrame>
),
});

export const AttachmentScaleStory = defineStory({
id: "attachment-scales",
title: "Attachments · Ratios, sizes, and missing information",
group: "Conversation components",
groupOrder: 50,
component: "ChatAttachments",
variant: "Ratios and missing information",
description:
  "Wide images, tall images, and files each use their own carrier. Downloads are built-in samples; no local file is read.",
covers: [
  "ChatAttachments image/file",
  "host download callback/default link",
  "wide/tall image",
  "zero/KB/MB/unknown size",
  "long filename",
  "empty list/custom label",
],
render: () => {
  const downloadFeedback = signal("Host download callback sample; no service is connected.");
  return (
    <div className="workshop-composition">
      <ChatAttachments
        label="Image ratio samples"
        items={[
          {
            id: "wide",
            name: "wide-structure-diagram.svg",
            mediaType: "image/svg+xml",
            url:
              "data:image/svg+xml," +
              encodeURIComponent(
                '<svg xmlns="http://www.w3.org/2000/svg" width="600" height="120" viewBox="0 0 600 120"><defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#e0e7ff"/><stop offset="1" stop-color="#fae8ff"/></linearGradient></defs><rect width="600" height="120" fill="url(#bg)"/><text x="300.0" y="60.0" text-anchor="middle" dominant-baseline="middle" font-family="system-ui, sans-serif" font-size="22" font-weight="500" fill="#4f46e5" fill-opacity="0.7">600 × 120</text></svg>',
              ),
          },
          {
            id: "tall",
            name: "tall-structure-diagram.svg",
            mediaType: "image/svg+xml",
            url:
              "data:image/svg+xml," +
              encodeURIComponent(
                '<svg xmlns="http://www.w3.org/2000/svg" width="120" height="360" viewBox="0 0 120 360"><defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#e0e7ff"/><stop offset="1" stop-color="#fae8ff"/></linearGradient></defs><rect width="120" height="360" fill="url(#bg)"/><text x="60.0" y="180.0" text-anchor="middle" dominant-baseline="middle" font-family="system-ui, sans-serif" font-size="16" font-weight="500" fill="#4f46e5" fill-opacity="0.7">120 × 360</text></svg>',
              ),
          },
        ]}
      />
      <ChatAttachments
        label="File size samples"
        items={[
          {
            id: "zero",
            name: "empty.txt",
            mediaType: "text/plain",
            sizeBytes: 0,
            url: "data:text/plain,",
          },
          {
            id: "kb",
            name: "component-design-language-and-all-interaction-state-check-records-with-follow-up-change-suggestions.md",
            mediaType: "text/markdown",
            sizeBytes: 2860,
            url: "data:text/plain,Example",
          },
          {
            id: "mb",
            name: "reference.pdf",
            mediaType: "application/pdf",
            sizeBytes: 1572864,
            url: "data:text/plain,Fixture%20metadata%20only",
          },
          {
            id: "unknown",
            name: "size-not-provided.txt",
            mediaType: "text/plain",
            url: "data:text/plain,Unknown%20size",
          },
        ]}
      />
      <Note>
        File sizes are fixed display data and do not claim to match the actual byte count of the
        downloaded content.
      </Note>
      <ChatAttachments
        label="Host download callback sample"
        items={[
          {
            id: "host-download",
            name: "protected-document.txt",
            mediaType: "text/plain",
            url: "data:text/plain,Host%20download%20example",
            onDownload: () =>
              downloadFeedback.set("The host took over the download; local sample only."),
          },
        ]}
      />
      <Note role="status" data-attachment-download-feedback>
        {downloadFeedback}
      </Note>
      <ChatAttachments label="No attachments" items={[]} />
    </div>
  );
},
});

API reference

Props

PropTypeDefault
labelRequired
MaybeSignal<string>–
summaryShort target or action summary, visible while collapsed.
MaybeSignal<string | undefined>–
state
MaybeSignal<ChatToolCallState>"complete"
statusLabelThe host's account of the state (who or what is being waited on, why it failed). The badge names the state in one word; this fact reads as a quiet line under the row.
MaybeSignal<string | undefined>–
detail
Child–
icon
Child–
inputTool payloads are inert text. Undefined omits a region; an empty string is explicit empty data.
MaybeSignal<string | undefined>–
output
MaybeSignal<string | undefined>–
outputSummary
MaybeSignal<string | undefined>–
maxLines
number–
collapsibleReveals this row's own body in place.
booleanfalse
onActivateRuns the host's own action instead of revealing a body; the row keeps the row chrome.
() => void–
onRetryIntent only. The host owns execution, pending state, permissions and the resulting status.
() => void–
onCancel
() => void–
canRetryWhether the host currently offers this intent; unlike disabled, false hides it.
MaybeSignal<boolean>true
canCancel
MaybeSignal<boolean>true
retryDisabled
MaybeSignal<boolean>false
cancelDisabled
MaybeSignal<boolean>false
labels
Partial<ChatToolCallLabels>–

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

DOM parts

rootheadericonlabelsummarystatusfactchevrondetailbodyinputoutputoutputSummaryactions

Registry

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