Tool

A collapsible component for displaying tool invocation details in AI chatbot interfaces.

The Tool component displays a collapsible interface for showing/hiding tool details. It is designed to take the ToolUIPart type from the AI SDK and display it in a collapsible interface.

Install using CLI

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add tool

Install Manually

Copy and paste the following code in the same folder.

Tool.vue
ToolStatusBadge.vue
ToolHeader.vue
ToolContent.vue
ToolInput.vue
ToolOutput.vue
index.ts
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Collapsible } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()
</script>

<template>
  <Collapsible
    :class="cn('not-prose mb-4 w-full rounded-md border', props.class)"
    v-bind="$attrs"
  >
    <slot />
  </Collapsible>
</template>

Usage with AI SDK

Build a simple stateful weather app that renders the last message in a tool using useChat.

Add the following component to your frontend:

pages/index.vue
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'
import { computed, h } from 'vue'
import { MessageResponse } from '@/components/ai-elements/message'
import {
  Tool,
  ToolContent,
  ToolHeader,
  ToolInput,
  ToolOutput,
} from '@/components/ai-elements/tool'
import { Button } from '@/components/ui/button'

interface WeatherToolInput {
  location: string
  units: 'celsius' | 'fahrenheit'
}

interface WeatherToolOutput {
  location: string
  temperature: string
  conditions: string
  humidity: string
  windSpeed: string
  lastUpdated: string
}

type WeatherToolUIPart = ToolUIPart<{
  fetch_weather_data: {
    input: WeatherToolInput
    output: WeatherToolOutput
  }
}>

const { messages, sendMessage, status } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/weather',
  }),
})

function formatWeatherResult(result: WeatherToolOutput): string {
  if (!result)
    return ''
  return `**Weather for ${result.location}**

**Temperature:** ${result.temperature}
**Conditions:** ${result.conditions}
**Humidity:** ${result.humidity}
**Wind Speed:** ${result.windSpeed}

*Last updated: ${result.lastUpdated}*`
}

function handleWeatherClick() {
  sendMessage({ text: 'Get weather data for San Francisco in fahrenheit' })
}

const latestMessage = computed(() => {
  if (!messages.value || messages.value.length === 0) {
    return undefined
  }
  return messages.value[messages.value.length - 1]
})

const weatherTool = computed(() => {
  return latestMessage.value?.parts?.find(
    part => part.type === 'tool-fetch_weather_data'
  ) as WeatherToolUIPart | undefined
})

const weatherOutputVNode = computed(() => {
  if (!weatherTool.value?.output) {
    return null
  }
  const markdown = formatWeatherResult(weatherTool.value.output)
  return h(MessageResponse, { content: markdown })
})
</script>

<template>
  <div
    class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"
  >
    <div class="flex flex-col h-full">
      <div class="space-y-4">
        <Button
          :disabled="status !== 'ready'"
          @click="handleWeatherClick"
        >
          Get Weather for San Francisco
        </Button>

        <Tool v-if="weatherTool" :default-open="true">
          <ToolHeader
            type="tool-fetch_weather_data"
            :state="weatherTool.state"
          />
          <ToolContent>
            <ToolInput :input="weatherTool.input" />
            <ToolOutput
              :output="weatherOutputVNode"
              :error-text="weatherTool.errorText"
            />
          </ToolContent>
        </Tool>
      </div>
    </div>
  </div>
</template>

Add the following route to your backend:

server/api/agent.ts
import { convertToModelMessages, streamText, UIMessage } from 'ai'
import { defineEventHandler, readBody } from 'h3'
import { z } from 'zod'

// Allow streaming responses up to 30 seconds
export const maxDuration = 30

export default defineEventHandler(async (event) => {
  const body = await readBody(event) as { messages: UIMessage[] }
  const { messages } = body

  const result = streamText({
    model: 'openai/gpt-4o',
    messages: convertToModelMessages(messages),
    tools: {
      fetch_weather_data: {
        description: 'Fetch weather information for a specific location',
        parameters: z.object({
          location: z.string().describe('The city or location to get weather for'),
          units: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('Temperature units'),
        }),
        inputSchema: z.object({
          location: z.string(),
          units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
        }),
        execute: async ({ location, units }) => {
          await new Promise(resolve => setTimeout(resolve, 1500))
          const temp = units === 'celsius'
            ? Math.floor(Math.random() * 35) + 5
            : Math.floor(Math.random() * 63) + 41

          return {
            location,
            temperature: `${temp}°${units === 'celsius' ? 'C' : 'F'}`,
            conditions: 'Sunny',
            humidity: '12%',
            windSpeed: `35 ${units === 'celsius' ? 'km/h' : 'mph'}`,
            lastUpdated: new Date().toLocaleString(),
          }
        },
      },
    },
  })

  return result.toUIMessageStreamResponse()
})

Features

  • Collapsible interface for showing/hiding tool details
  • Visual status indicators with icons and badges
  • Support for multiple tool execution states (pending, running, completed, error)
  • Formatted parameter display with JSON syntax highlighting
  • Result and error handling with appropriate styling
  • Composable structure for flexible layouts
  • Accessible keyboard navigation and screen reader support
  • Consistent styling that matches your design system
  • Auto-opens completed tools by default for better UX

Examples

Input Streaming (Pending)

Shows a tool in its initial state while parameters are being processed.

Input Available (Running)

Shows a tool that's actively executing with its parameters.

Input Streaming (Completed)

Shows a completed tool with successful results. Opens by default to show the results. In this instance, the output is a JSON object, so we can use the CodeBlock component to display it.

Output Error

Shows a tool that encountered an error during execution. Opens by default to display the error.

Props

<Tool/>

classstring
Additional CSS classes to apply to the component.

<ToolHeader/>

typeToolUIPart['type']
The type/name of the tool.
stateToolUIPart['state']
The current state of the tool (input-streaming, input-available, output-available, or output-error).
titlestring
The title of the task.
classstring
Additional CSS classes to apply to the component.

<ToolContent/>

classstring
Additional CSS classes to apply to the component.

<ToolInput/>

inputToolUIPart['input']
The input parameters passed to the tool, displayed as formatted JSON.
classstring
Additional CSS classes to apply to the component.

<ToolOutput/>

outputToolUIPart['output']
The output/result of the tool execution.
errorTextToolUIPart['errorText']
An error message if the tool execution failed.
classstring
Additional CSS classes to apply to the component.