AI Elements

Reasoning

Displays the agent's thinking as it streams.

Why is the checkout deploy so slow?
The deploy log has three phases. Install takes 38s and next build takes 52s, both normal for this repo. Image optimization takes 3m 50s. It reruns on every deploy because its cache key includes the commit hash, so the cache never hits.

Image optimization is the bottleneck. Key its cache on the lockfile and the image directory instead of the commit hash, and the deploy should take under two minutes.

Reasoning is a line for the agent's thinking. It reads "Thinking" and opens while the model thinks, then reads "Thought" and closes when it is done. The thinking itself sits inside, and the line can show how long it took.

Reasoning is built with UiTrace with a UiTraceCollapsible for the thinking.

Installation

Reasoning is built with the Trace component.

Usage

<template>
  <UiTrace>
    <UiTraceCollapsible>
      <UiTraceCollapsibleTrigger>
        <UiTraceLine>
          <UiTraceIcon>
            <UiIcon name="lucide:circle-check" />
          </UiTraceIcon>
          <UiTraceTitle>Thought</UiTraceTitle>
        </UiTraceLine>
      </UiTraceCollapsibleTrigger>
      <UiTraceCollapsibleContent class="whitespace-pre-wrap">
        {{ text }}
      </UiTraceCollapsibleContent>
    </UiTraceCollapsible>
  </UiTrace>
</template>

Duration

The AI SDK does not time reasoning. Measure it on the server, send it as message metadata, and show it in a UiTraceDescription after the title.

<template>
  <UiTraceLine>
    <UiTraceIcon>
      <CircleCheckIcon />
    </UiTraceIcon>
    <UiTraceTitle>Thought</UiTraceTitle>
    <UiTraceDescription>{{ seconds }}s</UiTraceDescription>
  </UiTraceLine>
</template>

AI SDK

Render each reasoning part as a line. It starts as a plain UiTraceLine and becomes a UiTraceCollapsible once text arrives, so there is nothing to open before the model has written anything. It opens while the model thinks and closes when it finishes. If the person opens or closes it, their choice wins. See Open State.

<template>
  <UiTraceLine v-if="text.length === 0" :status="running ? 'running' : undefined">
    <UiTraceIcon>
      <UiIcon name="lucide:loader-circle" v-if="running" class="animate-spin" />
      <UiIcon name="lucide:circle-check" v-else />
    </UiTraceIcon>
    <UiTraceTitle>{{ running ? "Thinking" : "Thought" }}</UiTraceTitle>
  </UiTraceLine>
  <UiTraceCollapsible v-else :open="open ?? running" @update:open="open = $event">
    <UiTraceCollapsibleTrigger>
      <UiTraceLine :status="running ? 'running' : undefined">
        <UiTraceIcon>
          <UiIcon name="lucide:loader-circle" v-if="running" class="animate-spin" />
          <UiIcon name="lucide:circle-check" v-else />
        </UiTraceIcon>
        <UiTraceTitle>{{ running ? "Thinking" : "Thought" }}</UiTraceTitle>
      </UiTraceLine>
    </UiTraceCollapsibleTrigger>
    <UiTraceCollapsibleContent class="whitespace-pre-wrap">
      {{ text }}
    </UiTraceCollapsibleContent>
  </UiTraceCollapsible>
</template>

<script lang="ts" setup>
  defineProps<{
    text: string;
    running: boolean;
  }>();

  const open = ref<boolean>();
</script>
<template>
  <UiTrace>
    <ReasoningLine
      v-for="(part, index) in message.parts.filter((part) => part.type === 'reasoning')"
      :key="index"
      :text="part.text"
      :running="streaming && part.state === 'streaming'"
    />
  </UiTrace>
</template>

Some models send several reasoning parts in one message. Each one is a line in the same UiTrace.

API Reference

See Trace.