Elements

Tool call

One tool invocation with its request and result tucked behind a disclosure.

fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-tool-call"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A tool call collapses to one line while it works: a chevron, a shimmering label, and the tool's primary argument as a chip, with a checkmark once it settles. Clicking it reveals the raw request and result. With a runtime the request, result, and running state come from the matching tool-call part; standalone you supply them directly.

Getting started

A tool call renders through a toolkit entry's render field, not through a standalone primitive tree. The renderer receives the live args, argsText, result, and status for that invocation.

Render the tool call

components/assistant-ui/elements/search-docs-tool-ui.tsx
"use client";

import { useState } from "react";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { ToolCall } from "@/components/assistant-ui/elements/tool-call";

type SearchDocsResult = { count: number; bestPath: string };

export const SearchDocsToolUI: ToolCallMessagePartComponent<
  { query: string },
  SearchDocsResult
> = ({ args, argsText, result, status }) => {
  const [open, setOpen] = useState(false);
  return (
    <ToolCall
      label="Searched the docs"
      activeLabel="Searching the docs"
      query={args.query ?? ""}
      request={argsText}
      result={result ? `${result.count} matches, best hit ${result.bestPath}` : ""}
      running={status.type === "running"}
      open={open}
      onOpenChange={setOpen}
    />
  );
};

Register the tool

app/toolkit.ts
import { defineToolkit } from "@assistant-ui/react";
import { z } from "zod";
import { SearchDocsToolUI } from "@/components/assistant-ui/elements/search-docs-tool-ui";

export const toolkit = defineToolkit({
  search_docs: {
    type: "frontend",
    description: "Search the assistant-ui documentation.",
    parameters: z.object({ query: z.string() }),
    execute: async ({ query }) => {
      const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
      return res.json() as Promise<SearchDocsResult>;
    },
    render: SearchDocsToolUI,
  },
});
app/MyRuntimeProvider.tsx
import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";
import { toolkit } from "./toolkit";

export function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
  const runtime = useChatRuntime();
  const config = AuiConfig({ tools: Tools({ toolkit }) });
  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      {children}
    </AssistantRuntimeProvider>
  );
}

See Tool UI for backend-defined tools, human tools, and approval gates.

Anatomy

<div data-slot="tool-call">
  <button>
    {/* chevron, shimmering label while running, query chip, checkmark once settled */}
  </button>
  <div>{/* Request block, divider, Result block — mounted only while open */}</div>
</div>

The trigger is always visible; the checkmark only replaces the empty space once running turns false. The content panel's height animates between zero and its measured height, so opening and closing never jumps. open is entirely driven by onOpenChange: there is no internal state, no wrap-around, and no disabled state, since a tool call with an empty request or result just renders empty strings.

Examples

Restyle the disclosure

Both lanes take className on the root. The trigger, the query chip, and the request and result blocks read from the shared mono, field, and collapsePanel tokens in surfaces.tsx, so retheming those tokens restyles every disclosure-based element at once.

<ToolCall className="max-w-none" /* ... */ />

Registering more tools

A renderer factory keeps the label pair out of the toolkit definition, so the same component backs several search-shaped tools:

function makeSearchToolUI(label: string, activeLabel: string) {
  const SearchToolUI: ToolCallMessagePartComponent<
    { query: string },
    SearchDocsResult
  > = ({ args, argsText, result, status }) => {
    const [open, setOpen] = useState(false);
    return (
      <ToolCall
        label={label}
        activeLabel={activeLabel}
        query={args.query ?? ""}
        request={argsText}
        result={result ? `${result.count} matches, best hit ${result.bestPath}` : ""}
        running={status.type === "running"}
        open={open}
        onOpenChange={setOpen}
      />
    );
  };
  return SearchToolUI;
}

const toolkit = defineToolkit({
  search_docs: {
    /* ... */ render: makeSearchToolUI("Searched the docs", "Searching the docs"),
  },
  search_issues: {
    /* ... */ render: makeSearchToolUI("Searched issues", "Searching issues"),
  },
});

API reference

Tool-call render props

PropTypeDescription
argsTArgsParsed arguments. Partial while the model is still streaming them.
argsTextstringRaw JSON argument text streamed by the model; feeds request directly.
resultTResult | undefinedThe tool's return value once it completes. undefined while running.
statusToolCallMessagePartStatusstatus.type is "running", "requires-action", "complete", or "incomplete".
toolNamestringName of the tool the model called.
toolCallIdstringStable id for this invocation.
isErrorboolean | undefinedWhether result represents a tool execution error.

Register the renderer on a toolkit entry's render field and attach the toolkit with Tools({ toolkit }). See Tool UI for the full render-prop surface, including addResult, human tools, and approval gates.