12 / 37 · Tool use
Tool timeline
A whole working session summarized as verbs, targets, and file stats.
composer.tsx+14−3use-draft.ts+42
Installation
npx shadcn@latest add "@assistant-ui/elements-tool-timeline"
Usage
import { FileSearchIcon } from "lucide-react";
import { ToolTimeline } from "@/components/elements/tool-timeline";
<ToolTimeline
steps={[{ verb: "Read", chip: "thread.tsx", icon: FileSearchIcon }]}
visibleSteps={1}
streaming
open
onOpenChange={setOpen}
restingLabel="Worked for 12s"
activeLabel="Working for 12s"
stats={[{ file: "thread.tsx", added: 4, removed: 1 }]}
/>Props
steps*readonly TimelineStep[]Working-session verbs, chips, and icons to reveal over time.
visibleSteps*numberHow many steps from the start are currently shown.
streaming*booleanWhen true the trigger shows Working with a shimmer and elapsed timer.
open*booleanWhether the collapsible timeline body is expanded.
onOpenChange*(open: boolean) => voidCalled when the user expands or collapses the timeline.
restingLabel*stringTrigger label once streaming finishes.
activeLabel*stringFull-size live label while streaming, for example Working for 12s.
stats*TimelineStat[]Per-file addition and removal counts listed under the steps.
classNamestringExtra classes merged onto the root.
Source
tool-timeline.tsx"use client";
import { ChevronRightIcon, type LucideIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { collapsePanel, SwapLabel } from "./surfaces";
export interface TimelineStep {
verb: string;
chip: string;
icon: LucideIcon;
}
export interface TimelineStat {
file: string;
added?: number;
removed?: number;
}
export interface ToolTimelineProps {
steps: readonly TimelineStep[];
visibleSteps: number;
streaming: boolean;
open: boolean;
onOpenChange: (open: boolean) => void;
restingLabel: string;
activeLabel: string;
stats: TimelineStat[];
className?: string;
}
export function ToolTimeline({
steps,
visibleSteps,
streaming,
open,
onOpenChange,
restingLabel,
activeLabel,
stats,
className,
}: ToolTimelineProps) {
return (
<Collapsible
open={open}
onOpenChange={onOpenChange}
className={cn("w-full max-w-sm", className)}
>
<CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-1.5 rounded-md py-1 text-[13.5px] transition-colors outline-none">
<ChevronRightIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-90 group-data-panel-open/trigger:rotate-90 motion-reduce:transition-none" />
<SwapLabel
active={streaming ? 0 : 1}
className="text-start tabular-nums"
>
<span className="relative inline-block leading-none">
<span>{activeLabel}</span>
<span
aria-hidden
className="shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
>
{activeLabel}
</span>
</span>
<>{restingLabel}</>
</SwapLabel>
</CollapsibleTrigger>
<CollapsibleContent className={cn(collapsePanel, "outline-none")}>
<div className="flex flex-col gap-2.5 ps-4 pt-2.5">
{steps.slice(0, visibleSteps).map((step, index) => {
const Icon = step.icon;
const active = streaming && index === visibleSteps - 1;
return (
<div
key={step.chip}
className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-foreground/55 flex items-center gap-2 text-[13.5px] duration-300"
>
<Icon className="text-foreground/35 size-3.5 shrink-0" />
<span className="relative inline-block leading-none">
<span>{step.verb}</span>
{active && (
<span
aria-hidden
className="shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
>
{step.verb}
</span>
)}
</span>
<span className="bg-foreground/[0.06] text-foreground/70 rounded-md px-1.5 py-0.5 font-mono text-[11px]">
{step.chip}
</span>
</div>
);
})}
{stats.length > 0 && (
<div className="flex flex-wrap gap-1.5 pt-1">
{stats.map((stat) => (
<span
key={stat.file}
className="bg-foreground/[0.06] text-foreground/70 inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-[11px]"
>
<span>{stat.file}</span>
{stat.added !== undefined && (
<span className="text-emerald-600 dark:text-emerald-400">
+{stat.added}
</span>
)}
{stat.removed !== undefined && (
<span className="text-red-600 dark:text-red-400">
−{stat.removed}
</span>
)}
</span>
))}
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
);
}