AI Elements

Tool

Displays a tool call the agent makes, from input to output.

Which package managers are installed?
command not found: pnpm

Node 24 and npm 11 are installed. pnpm is not.

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.

Tool is built with UiTrace with a UiTraceCollapsible for each call that has output.

Installation

Tool is built with the Trace and Code Block components.

Usage

<template>
  <UiTrace>
    <UiTraceCollapsible>
      <UiTraceCollapsibleTrigger>
        <UiTraceLine>
          <UiTraceIcon>
            <UiIcon name="lucide:terminal" />
          </UiTraceIcon>
          <UiTraceTitle>Ran git status --short</UiTraceTitle>
        </UiTraceLine>
      </UiTraceCollapsibleTrigger>
      <UiTraceCollapsibleContent>
        <UiCodeBlock size="sm">
          <UiCodeBlockContent wrap class="max-h-64">
            {{ output }}
          </UiCodeBlockContent>
        </UiCodeBlock>
      </UiTraceCollapsibleContent>
    </UiTraceCollapsible>
  </UiTrace>
</template>

States

Trace does not read tool parts. Map each part's state to a status and a title.

Denied rm -rf distRunning pnpm test
Tool part statestatusTitle
input-streaming, input-available"running" while the chat streamsRunning …
input-streaming, input-available after the chat stops"error"Failed to run …
approval-requested-Not a line. An UiItem outside UiTrace with Approve and Deny buttons.
approval-responded"running" if approved, none if notRunning … if approved, Denied … if not
output-availablenoneRan …
output-error"error"Failed to run …
output-deniednoneDenied …

A tool is a plain UiTraceLine while it runs, and a UiTraceCollapsible once its output arrives.

<template>
  <UiTraceLine v-if="denied">
    <UiTraceIcon>
      <UiIcon name="lucide:terminal" />
    </UiTraceIcon>
    <UiTraceTitle>Denied {{ command }}</UiTraceTitle>
  </UiTraceLine>
  <UiTraceCollapsible
    v-else-if="part.state === 'output-available' || part.state === 'output-error'"
  >
    <UiTraceCollapsibleTrigger>
      <UiTraceLine :status="failed ? 'error' : undefined">
        <UiTraceIcon>
          <UiIcon name="lucide:terminal" />
        </UiTraceIcon>
        <UiTraceTitle>{{ failed ? "Failed to run" : "Ran" }} {{ command }}</UiTraceTitle>
      </UiTraceLine>
    </UiTraceCollapsibleTrigger>
    <UiTraceCollapsibleContent>
      <UiCodeBlock size="sm">
        <UiCodeBlockContent wrap class="max-h-64">
          {{ part.state === "output-error" ? part.errorText : part.output.stdout }}
        </UiCodeBlockContent>
      </UiCodeBlock>
    </UiTraceCollapsibleContent>
  </UiTraceCollapsible>
  <UiTraceLine v-else :status="streaming ? 'running' : 'error'">
    <UiTraceIcon>
      <UiIcon name="lucide:terminal" />
    </UiTraceIcon>
    <UiTraceTitle>{{ streaming ? "Running" : "Failed to run" }} {{ command }}</UiTraceTitle>
  </UiTraceLine>
</template>

<script lang="ts" setup>
  const props = defineProps<{
    part: ToolPart;
    streaming: boolean;
  }>();

  const command = computed(() => props.part.input?.command);
  const denied = computed(
    () =>
      props.part.state === "output-denied" ||
      (props.part.state === "approval-responded" && !props.part.approval.approved)
  );
  const failed = computed(() => props.part.state === "output-error");
</script>
<template>
  <UiTrace>
    <ToolLine
      v-for="part in message.parts.filter((part) => part.type === 'tool-runCommand')"
      :key="part.toolCallId"
      :part="part"
      :streaming="streaming"
    />
  </UiTrace>
</template>

A tool that runs on the client stays input-available after the chat stops until you call addToolOutput. Treat it as waiting, not failed.

Output in a Sheet

For long output, wrap the UiTraceLine in a UiSheetTrigger and show the output in the sheet.

See Trace.

Approval

A pending approval is a question for the person, not a step the agent took, so it sits outside UiTrace as an UiItem. Once the person answers, the tool goes back to being a line. See Confirmation.

<template>
  <UiItem v-if="part.state === 'approval-requested'" variant="outline" size="sm">
    <UiItemMedia variant="icon">
      <TerminalIcon />
    </UiItemMedia>
    <UiItemContent>
      <UiItemTitle>Run {{ part.input.command }}?</UiItemTitle>
    </UiItemContent>
    <UiItemActions>
      <UiButton
        size="sm"
        variant="outline"
        @click="chat.addToolApprovalResponse({ id: part.approval.id, approved: false })"
      >
        Deny
      </UiButton>
      <UiButton
        size="sm"
        @click="chat.addToolApprovalResponse({ id: part.approval.id, approved: true })"
      >
        Approve
      </UiButton>
    </UiItemActions>
  </UiItem>
</template>

Pass sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses to the Chat so the chat continues once every approval has an answer.

API Reference

See Trace.