# Tool

A tool call the agent makes, from input to output, including errors, approvals and output in a sheet.

Source: https://suiss.app/ui/docs/ai/tool

**React**

**Example**

```tsx
import { TerminalIcon } from "@phosphor-icons/react"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Message, MessageContent } from "@/components/ui/message"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const calls = [
  { command: "node --version", output: "v24.18.0" },
  { command: "npm --version", output: "11.6.2" },
  { command: "pnpm --version", error: "command not found: pnpm" },
]

export default function ToolDemo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Which package managers are installed?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-4">
          <Trace>
            {calls.map((call) => (
              <TraceCollapsible
                key={call.command}
                defaultOpen={Boolean(call.error)}
              >
                <TraceCollapsibleTrigger>
                  <TraceLine status={call.error ? "error" : undefined}>
                    <TraceIcon>
                      <TerminalIcon />
                    </TraceIcon>
                    <TraceTitle>
                      {call.error ? "Failed to run" : "Ran"} {call.command}
                    </TraceTitle>
                  </TraceLine>
                </TraceCollapsibleTrigger>
                <TraceCollapsibleContent>
                  <CodeBlock size="sm">
                    <CodeBlockContent wrap className="max-h-64">
                      {call.error ?? call.output}
                    </CodeBlockContent>
                  </CodeBlock>
                </TraceCollapsibleContent>
              </TraceCollapsible>
            ))}
          </Trace>
          <p>Node 24 and npm 11 are installed. pnpm is not.</p>
        </MessageContent>
      </Message>
    </div>
  )
}
```

Tool is a tool call the agent makes, such as running a command or reading a file. The line follows the call from input to output, including errors and calls that wait for approval.

It is built with [Trace](https://suiss.app/ui/docs/components/trace): a plain `TraceLine` while the call runs, and a `TraceCollapsible` with a [Code Block](https://suiss.app/ui/docs/components/code-block) once its output arrives. For several calls under one summary, see [Chain of Thought](https://suiss.app/ui/docs/ai/chain-of-thought).

## Installation

Tool is built with [Trace](https://suiss.app/ui/docs/components/trace) and [Code Block](https://suiss.app/ui/docs/components/code-block). The approval prompt uses [Item](https://suiss.app/ui/docs/components/item) and [Button](https://suiss.app/ui/docs/components/button), and long output can open in a [Sheet](https://suiss.app/ui/docs/components/sheet).

```bash title="Terminal"
npx shadcn@latest add @esuiss/trace @esuiss/code-block @esuiss/item @esuiss/button @esuiss/sheet
```

Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block item button sheet
```

## Usage

```tsx
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"
```

```tsx
<Trace>
  <TraceCollapsible>
    <TraceCollapsibleTrigger>
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Ran git status --short</TraceTitle>
      </TraceLine>
    </TraceCollapsibleTrigger>
    <TraceCollapsibleContent>
      <CodeBlock size="sm">
        <CodeBlockContent wrap className="max-h-64">
          {output}
        </CodeBlockContent>
      </CodeBlock>
    </TraceCollapsibleContent>
  </TraceCollapsible>
</Trace>
```

## Examples

### States

A finished call, a failed call, a denied call and a running call.

**Example**

```tsx
import { TerminalIcon } from "@phosphor-icons/react"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

export default function ToolStates() {
  return (
    <Trace className="max-w-sm">
      <TraceCollapsible>
        <TraceCollapsibleTrigger>
          <TraceLine>
            <TraceIcon>
              <TerminalIcon />
            </TraceIcon>
            <TraceTitle>Ran git status --short</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent wrap className="max-h-64">
              {" M app/api/chat/route.ts\n M components/chat.tsx"}
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
      <TraceCollapsible>
        <TraceCollapsibleTrigger>
          <TraceLine status="error">
            <TraceIcon>
              <TerminalIcon />
            </TraceIcon>
            <TraceTitle>Failed to run pnpm dev</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent wrap className="max-h-64">
              Command timed out after 120 seconds.
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Denied rm -rf dist</TraceTitle>
      </TraceLine>
      <TraceLine status="running">
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Running pnpm test</TraceTitle>
      </TraceLine>
    </Trace>
  )
}
```

Map each tool call's state to a `status` and a title:

| Tool call | `status` | Title |
| --- | --- | --- |
| Input streaming or ready, while the chat streams | `"running"` | Running … |
| Input streaming or ready, after the chat stops | `"error"` | Failed to run … |
| Waiting for approval | — | Not a line. An `Item` outside `Trace` with Approve and Deny buttons. |
| Approved and running | `"running"` | Running … |
| Output available | none | Ran … |
| Output error | `"error"` | Failed to run … |
| Denied | none | Denied … |

### Output in a sheet

For long output, wrap the `TraceLine` in a `SheetTrigger` and show the output in the sheet.

**Example**

```tsx
import { CaretRightIcon, FileTextIcon } from "@phosphor-icons/react"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Message, MessageContent } from "@/components/ui/message"
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

const PACKAGE_JSON = `{
  "name": "acme-chat",
  "private": true,
  "dependencies": {
    "@ai-sdk/react": "^4.0.0",
    "ai": "^7.0.0",
    "next": "^16.3.0"
  }
}`

export default function ToolSheet() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>
              Which AI SDK version does this app use?
            </BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-4">
          <Trace>
            <Sheet>
              <SheetTrigger>
                <TraceLine>
                  <TraceIcon>
                    <FileTextIcon />
                  </TraceIcon>
                  <TraceTitle>Read package.json</TraceTitle>
                  <TraceIcon>
                    <CaretRightIcon />
                  </TraceIcon>
                </TraceLine>
              </SheetTrigger>
              <SheetContent>
                <SheetHeader>
                  <SheetTitle className="font-mono">package.json</SheetTitle>
                  <SheetDescription>
                    {PACKAGE_JSON.split("\n").length} lines
                  </SheetDescription>
                </SheetHeader>
                <CodeBlock className="mx-6 mb-6 w-auto">
                  <CodeBlockContent lineNumbers>
                    {PACKAGE_JSON}
                  </CodeBlockContent>
                </CodeBlock>
              </SheetContent>
            </Sheet>
          </Trace>
          <p>It uses ai 7 with @ai-sdk/react 4.</p>
        </MessageContent>
      </Message>
    </div>
  )
}
```

### Approval

A pending approval is a question for the person, not a step the agent took, so it sits outside `Trace` as an `Item`. Once the person answers, the call goes back to being a line. See [Confirmation](https://suiss.app/ui/docs/ai/confirmation).

**Example**

```tsx
"use client"

import { CheckCircleIcon, TerminalIcon } from "@phosphor-icons/react"
import * as React from "react"

import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"
import { Message, MessageContent } from "@/components/ui/message"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

type Decision = "approved" | "denied" | null

export default function ToolApproval() {
  const [decision, setDecision] = React.useState<Decision>(null)

  return (
    <div className="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Clean up the build folder.</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-4">
          <Trace>
            <TraceCollapsible>
              <TraceCollapsibleTrigger>
                <TraceLine>
                  <TraceIcon>
                    <CheckCircleIcon />
                  </TraceIcon>
                  <TraceTitle>Thought</TraceTitle>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent>
                The build output lives in dist. Deleting it cannot be undone, so
                I should ask before I run the command.
              </TraceCollapsibleContent>
            </TraceCollapsible>
          </Trace>
          {decision === null ? (
            <Item variant="outline" size="sm">
              <ItemMedia variant="icon">
                <TerminalIcon />
              </ItemMedia>
              <ItemContent>
                <ItemTitle>Run rm -rf dist?</ItemTitle>
                <ItemDescription>
                  The command runs in this project.
                </ItemDescription>
              </ItemContent>
              <ItemActions>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => setDecision("denied")}
                >
                  Deny
                </Button>
                <Button size="sm" onClick={() => setDecision("approved")}>
                  Approve
                </Button>
              </ItemActions>
            </Item>
          ) : (
            <>
              <Trace>
                <TraceLine>
                  <TraceIcon>
                    <TerminalIcon />
                  </TraceIcon>
                  <TraceTitle>
                    {decision === "approved" ? "Ran" : "Denied"} rm -rf dist
                  </TraceTitle>
                </TraceLine>
              </Trace>
              <p>
                {decision === "approved"
                  ? "Removed the dist folder."
                  : "Okay, I left the dist folder in place."}
              </p>
            </>
          )}
        </MessageContent>
      </Message>
      {decision !== null && (
        <Button
          variant="outline"
          size="sm"
          className="self-start"
          onClick={() => setDecision(null)}
        >
          Reset
        </Button>
      )}
    </div>
  )
}
```

## AI SDK

Render each tool part as a line. A call is a plain `TraceLine` while it runs, and a `TraceCollapsible` once its output arrives.

```tsx
function ToolLine({ part, streaming }: { part: ToolPart; streaming: boolean }) {
  const command = part.input?.command

  if (
    part.state === "output-denied" ||
    (part.state === "approval-responded" && !part.approval.approved)
  ) {
    return (
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Denied {command}</TraceTitle>
      </TraceLine>
    )
  }

  if (part.state === "output-available" || part.state === "output-error") {
    const failed = part.state === "output-error"

    return (
      <TraceCollapsible>
        <TraceCollapsibleTrigger>
          <TraceLine status={failed ? "error" : undefined}>
            <TraceIcon>
              <TerminalIcon />
            </TraceIcon>
            <TraceTitle>
              {failed ? "Failed to run" : "Ran"} {command}
            </TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent wrap className="max-h-64">
              {failed ? part.errorText : part.output.stdout}
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    )
  }

  return (
    <TraceLine status={streaming ? "running" : "error"}>
      <TraceIcon>
        <TerminalIcon />
      </TraceIcon>
      <TraceTitle>
        {streaming ? "Running" : "Failed to run"} {command}
      </TraceTitle>
    </TraceLine>
  )
}
```

For approvals, render the `approval-requested` state as an `Item` and answer it with `addToolApprovalResponse`:

```tsx
if (part.state === "approval-requested") {
  return (
    <Item variant="outline" size="sm">
      <ItemMedia variant="icon">
        <TerminalIcon />
      </ItemMedia>
      <ItemContent>
        <ItemTitle>Run {part.input.command}?</ItemTitle>
      </ItemContent>
      <ItemActions>
        <Button
          size="sm"
          variant="outline"
          onClick={() =>
            addToolApprovalResponse({ id: part.approval.id, approved: false })
          }
        >
          Deny
        </Button>
        <Button
          size="sm"
          onClick={() =>
            addToolApprovalResponse({ id: part.approval.id, approved: true })
          }
        >
          Approve
        </Button>
      </ItemActions>
    </Item>
  )
}
```

Pass `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses` to `useChat` so the chat continues once every approval has an answer. A tool that runs on the client stays `input-available` after the chat stops until you call `addToolOutput`; treat it as waiting, not failed.

## API Reference

See [Trace](https://suiss.app/ui/docs/components/trace#api-reference), [Code Block](https://suiss.app/ui/docs/components/code-block#api-reference), [Item](https://suiss.app/ui/docs/components/item#api-reference) and [Sheet](https://suiss.app/ui/docs/components/sheet#api-reference).

## Accessibility

- A call with output is a button with `aria-expanded`; a call in a sheet takes the sheet trigger's focus and Escape behavior.
- Write the state into the title, such as "Failed to run pnpm dev", so it does not rely on color.
- The approval buttons are regular buttons with visible labels, and they are not inside the `Trace` row.

**React Native, SwiftUI, Jetpack Compose**

```tsx
import { CheckCircleIcon, WarningCircleIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { Message, MessageContent } from "@/components/ui/message"
import { Text } from "@/components/ui/text"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function AiToolExample() {
  return (
    <View className="w-full gap-4">
      <Message align="end">
        <MessageContent>
          <Bubble variant="muted">
            <BubbleContent>Which package managers are installed?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Trace>
        <TraceCollapsible>
          <TraceCollapsibleTrigger>
            <TraceLine>
              <TraceIcon>
                <Icon as={CheckCircleIcon} />
              </TraceIcon>
              <TraceTitle>Ran node --version</TraceTitle>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <CodeBlock size="sm">
              <CodeBlockContent>v24.18.0</CodeBlockContent>
            </CodeBlock>
          </TraceCollapsibleContent>
        </TraceCollapsible>
        <TraceCollapsible>
          <TraceCollapsibleTrigger>
            <TraceLine>
              <TraceIcon>
                <Icon as={CheckCircleIcon} />
              </TraceIcon>
              <TraceTitle>Ran npm --version</TraceTitle>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <CodeBlock size="sm">
              <CodeBlockContent>11.6.2</CodeBlockContent>
            </CodeBlock>
          </TraceCollapsibleContent>
        </TraceCollapsible>
        <TraceCollapsible defaultOpen>
          <TraceCollapsibleTrigger>
            <TraceLine status="error">
              <TraceIcon>
                <Icon as={WarningCircleIcon} />
              </TraceIcon>
              <TraceTitle>Failed to run pnpm --version</TraceTitle>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <CodeBlock size="sm" variant="destructive">
              <CodeBlockContent wrap>
                zsh: command not found: pnpm
              </CodeBlockContent>
            </CodeBlock>
          </TraceCollapsibleContent>
        </TraceCollapsible>
      </Trace>
      <Text>Node 24 and npm 11 are installed. pnpm is not.</Text>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ToolExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      Message(align: .end) {
        MessageContent {
          Bubble(variant: .muted, align: .end) { BubbleContent("Which package managers are installed?") }
        }
      }
      Trace {
        TraceCollapsible {
          TraceCollapsibleTrigger {
            TraceLine {
              TraceIcon { Ph.checkCircle.regular }
              TraceTitle(verbatim: "Ran node --version")
            }
          }
          TraceCollapsibleContent {
            CodeBlock(size: .sm) { CodeBlockContent("v24.18.0") }
          }
        }
        TraceCollapsible {
          TraceCollapsibleTrigger {
            TraceLine {
              TraceIcon { Ph.checkCircle.regular }
              TraceTitle(verbatim: "Ran npm --version")
            }
          }
          TraceCollapsibleContent {
            CodeBlock(size: .sm) { CodeBlockContent("11.6.2") }
          }
        }
        TraceCollapsible(initiallyExpanded: true) {
          TraceCollapsibleTrigger {
            TraceLine(status: .error) {
              TraceIcon { Ph.warningCircle.regular }
              TraceTitle(verbatim: "Failed to run pnpm --version")
            }
          }
          TraceCollapsibleContent {
            CodeBlock(.destructive, size: .sm) { CodeBlockContent("zsh: command not found: pnpm", wrap: true) }
          }
        }
      }
      Text("Node 24 and npm 11 are installed. pnpm is not.").textStyle(.base).foregroundStyle(EsuissColor.foreground)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CheckCircle
import com.adamglin.phosphoricons.regular.WarningCircle
import com.esuiss.ui.components.Bubble
import com.esuiss.ui.components.BubbleContent
import com.esuiss.ui.components.BubbleVariant
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.CodeBlockVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Message
import com.esuiss.ui.components.MessageAlign
import com.esuiss.ui.components.MessageContent
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun ToolAIDefault() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Message(Modifier.fillMaxWidth(), align = MessageAlign.End) {
            MessageContent {
                Bubble(variant = BubbleVariant.Muted) { BubbleContent("Which package managers are installed?") }
            }
        }
        Trace {
            TraceCollapsible {
                TraceCollapsibleTrigger {
                    TraceLine {
                        TraceIcon { Icon(PhosphorIcons.Regular.CheckCircle, contentDescription = null) }
                        TraceTitle("Ran node --version")
                    }
                }
                TraceCollapsibleContent {
                    CodeBlock(size = CodeBlockSize.Sm) { CodeBlockContent("v24.18.0") }
                }
            }
            TraceCollapsible {
                TraceCollapsibleTrigger {
                    TraceLine {
                        TraceIcon { Icon(PhosphorIcons.Regular.CheckCircle, contentDescription = null) }
                        TraceTitle("Ran npm --version")
                    }
                }
                TraceCollapsibleContent {
                    CodeBlock(size = CodeBlockSize.Sm) { CodeBlockContent("11.6.2") }
                }
            }
            TraceCollapsible(initiallyExpanded = true) {
                TraceCollapsibleTrigger {
                    TraceLine(status = TraceStatus.Error) {
                        TraceIcon { Icon(PhosphorIcons.Regular.WarningCircle, contentDescription = null) }
                        TraceTitle("Failed to run pnpm --version")
                    }
                }
                TraceCollapsibleContent {
                    CodeBlock(variant = CodeBlockVariant.Destructive, size = CodeBlockSize.Sm) { CodeBlockContent("zsh: command not found: pnpm", wrap = true) }
                }
            }
        }
        Text("Node 24 and npm 11 are installed. pnpm is not.")
    }
}
```

Tool on mobile is built from the same parts as on the web: [Trace](https://suiss.app/ui/docs/components/trace), [Code Block](https://suiss.app/ui/docs/components/code-block), [Message](https://suiss.app/ui/docs/components/message), [Bubble](https://suiss.app/ui/docs/components/bubble). The example above is a static demo; connect it to your agent by rendering the same parts from your messages.

## Installation

**React Native**

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block message bubble
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block message bubble --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block message bubble --platform kotlin
```

## API Reference

See [Trace](https://suiss.app/ui/docs/components/trace), [Code Block](https://suiss.app/ui/docs/components/code-block), [Message](https://suiss.app/ui/docs/components/message), [Bubble](https://suiss.app/ui/docs/components/bubble).

