- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Code Block
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Form
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Pin Input
- Popover
- Progress
- Questionnaire
- Radio Group
- Range Calendar
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stepper
- Switch
- Table
- Tabs
- Tags Input
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Trace
- Typography
Added Open Graph defaults to app/app.vue and a title, description and image for the home page in app/pages/index.vue.
<script setup lang="ts">
import {
CircleAlertIcon,
CircleCheckIcon,
LoaderCircleIcon,
} from '@lucide/vue'
import { promiseTimeout, tryOnScopeDispose } from '@vueuse/core'
import { computed, ref } from 'vue'
import { Bubble, BubbleContent } from '@/components/ui/bubble'
import { Button } from '@/components/ui/button'
import { Message, MessageContent } from '@/components/ui/message'
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from '@/components/ui/trace'
type Status = 'running' | 'error' | undefined
interface TaskData {
title: string
steps: { title: string, status: 'done' | 'running' | 'error' }[]
}
const QUESTION = 'Add Open Graph metadata to the home page.'
const ANSWER
= 'Added Open Graph defaults to app/app.vue and a title, description and image for the home page in app/pages/index.vue.'
const TASKS = [
{
id: 'find',
title: ['Finding the project files', 'Found the project files'],
steps: [
['Searching for useSeoMeta', 'Searched for useSeoMeta'],
['Reading app/app.vue', 'Read app/app.vue'],
['Reading app/pages/index.vue', 'Read app/pages/index.vue'],
],
},
{
id: 'update',
title: ['Updating the page metadata', 'Updated the page metadata'],
steps: [
['Editing app/app.vue', 'Edited app/app.vue'],
['Editing app/pages/index.vue', 'Edited app/pages/index.vue'],
],
},
]
function taskAt(task: (typeof TASKS)[number], done: number): TaskData {
return {
title: task.title[done < task.steps.length ? 0 : 1]!,
steps: task.steps.slice(0, done + 1).map(([running, finished], index) => ({
title: (index < done ? finished : running)!,
status: index < done ? 'done' : 'running',
})),
}
}
function finalTasks() {
return TASKS.map(task => ({ id: task.id, data: taskAt(task, task.steps.length) }))
}
const tasks = ref<{ id: string, data: TaskData }[]>(finalTasks())
const text = ref(ANSWER)
const busy = ref(false)
const openState = ref<Record<string, boolean>>({})
function getStepStatus(status: TaskData['steps'][number]['status'], streaming: boolean): Status {
if (status === 'done')
return undefined
return status === 'running' && streaming ? 'running' : 'error'
}
const rows = computed(() =>
tasks.value.map((task) => {
const steps = task.data.steps.map(step => ({
title: step.title,
status: getStepStatus(step.status, busy.value),
}))
const status: Status = steps.some(step => step.status === 'error')
? 'error'
: steps.some(step => step.status === 'running')
? 'running'
: undefined
return { id: task.id, title: task.data.title, steps, status }
}),
)
// Unmounting bumps `run`, so a running script stops at its next wait.
let run = 0
tryOnScopeDispose(() => run++)
async function wait(ms: number, id: number) {
await promiseTimeout(ms)
return id === run
}
function writeTask(id: string, data: TaskData) {
const index = tasks.value.findIndex(task => task.id === id)
if (index === -1)
tasks.value.push({ id, data })
else
tasks.value[index] = { id, data }
}
async function replay() {
const id = ++run
busy.value = true
tasks.value = []
text.value = ''
openState.value = {}
for (const task of TASKS) {
for (let done = 0; done <= task.steps.length; done++) {
writeTask(task.id, taskAt(task, done))
if (!(await wait(900, id)))
return
}
}
for (const word of ANSWER.split(/(?<= )/)) {
if (!(await wait(20, id)))
return
text.value += word
}
busy.value = false
}
</script>
<template>
<div class="flex min-h-72 w-full max-w-sm flex-col gap-4 py-6 text-sm">
<Message align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>{{ QUESTION }}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message v-if="rows.length > 0 || text">
<MessageContent class="gap-4">
<Trace v-if="rows.length > 0">
<TraceCollapsible
v-for="task in rows"
:key="task.id"
:open="openState[task.id] ?? task.status === 'running'"
:unmount-on-hide="false"
@update:open="openState[task.id] = $event"
>
<TraceCollapsibleTrigger>
<TraceLine :status="task.status">
<TraceIcon>
<LoaderCircleIcon v-if="task.status === 'running'" class="animate-spin" />
<CircleAlertIcon v-else-if="task.status === 'error'" />
<CircleCheckIcon v-else />
</TraceIcon>
<TraceTitle>{{ task.title }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<Trace>
<TraceLine
v-for="step in task.steps"
:key="step.title"
:status="step.status"
>
<TraceTitle>{{ step.title }}</TraceTitle>
</TraceLine>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
<p v-if="text">
{{ text }}
</p>
</MessageContent>
</Message>
<Button
v-if="!busy"
variant="outline"
size="sm"
class="mt-auto self-start"
@click="replay"
>
Replay
</Button>
</div>
</template>Task is a piece of work the agent is doing, such as finding the project files or updating a page. Each step, like a search, a read or an edit, appears under the task when the agent takes it.
Task is built with Trace with a TraceCollapsible for each task.
Task or Plan
Both are a list of steps under a collapsible line. The difference is when the steps are known.
Use Plan when the agent decides the steps before it starts. Use Task when the steps come from the work itself, and the agent only knows the next one once it has taken the last.
| Plan | Task | |
|---|---|---|
| Steps | Every step, from the start | Each step, once the agent takes it |
| Pending steps | Yes, waiting their turn | None |
| Title | The goal, with progress: "2 of 4 done" | What the agent is doing, then what it did |
| Per reply | One plan | One or more tasks, each closing when it is done |
Installation
Task is built with the Trace component.
pnpm dlx shadcn-vue@latest add trace
Usage
<script setup lang="ts">
import { FileTextIcon, SearchIcon } from '@lucide/vue'
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from '@/components/ui/trace'
</script>
<template>
<Trace>
<TraceCollapsible default-open :unmount-on-hide="false">
<TraceCollapsibleTrigger>
<TraceLine>
<TraceTitle>Found the project files</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<Trace>
<TraceLine>
<TraceIcon>
<SearchIcon />
</TraceIcon>
<TraceTitle>Searched for app/pages/index.vue</TraceTitle>
</TraceLine>
<a href="/files/app/pages/index.vue">
<TraceLine>
<TraceIcon>
<FileTextIcon />
</TraceIcon>
<TraceTitle>Read app/pages/index.vue</TraceTitle>
</TraceLine>
</a>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</template>Status
Set the task's status from its steps. Write the title in the present tense while the task runs, and in the past tense once it is done.
| Steps | status | Title |
|---|---|---|
| A step is still running | "running" | Updating the page metadata |
| A step failed | "error" | Failed to update the page metadata |
| Every step is done | none | Updated the page metadata |
Put several tasks in one Trace. Keep the running task open, and close each task once it is done.
AI SDK
Stream each task as a data part with its own id. The AI SDK replaces a data part with the same id, so writing a task again with a new step updates it in place.
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.write({ type: 'data-task', id: 'update', data: { title, steps } })
},
})const tasks = message.parts.filter(part => part.type === 'data-task')Migrating from AI Elements Vue
If you added Task from AI Elements Vue, your project has a components/ai-elements/task/ folder. Paste this prompt into your coding agent to rebuild it on Trace. Every export keeps its name and props, so the code that uses it keeps working.
Migrate components/ai-elements/task/ from AI Elements Vue to the shadcn-vue Trace component.
Keep every export, with the same name and props, so the code that imports it keeps working. Only change how each part renders.
1. Install Trace: npx shadcn-vue@latest add trace
2. Read https://www.shadcn-vue.com/docs/ai-elements/task and build Task the same way.
3. Rebuild each part:
- Task renders a Trace with a TraceCollapsible.
- TaskTrigger renders a TraceCollapsibleTrigger with a TraceLine and a TraceTitle with title.
- TaskContent renders a TraceCollapsibleContent, with :unmount-on-hide="false" on the TraceCollapsible, and a Trace for the steps.
- TaskItem renders a TraceLine with a TraceTitle.
- Keep TaskItemFile as it is.
4. Remove the imports the files no longer use.
5. Run vue-tsc and fix any errors.API Reference
See Trace.
Sponsors
Your logo hereshadcn-vue is free and open source. Sponsorship keeps it maintained.
Become a sponsor