Elements

32 / 122 · Tool use

Parallel tools

Calls that went out together, collapsed to one row until you want the detail.

read_filepackages/core/src/convertMessages.ts42ms
read_filepackages/ui/src/composer.tsx38ms
grepuseDraft61ms
read_filepackages/core/src/queue/message-queue.ts

Installation

npx shadcn@latest add "@assistant-ui/elements-tool-group"

Usage

import { ToolGroup } from "@/components/elements/tool-group";

<ToolGroup
  label="Read 4 files in parallel"
  tools={tools}
  open={open}
  onOpenChange={setOpen}
/>

Props

ToolGroup

label*stringWhat the group did, as one line.
tools*GroupedTool[]Calls issued together. The header summarizes them: progress while any run, failures if any failed, otherwise the count.
open*booleanWhether the per-call list is expanded.
onOpenChange(open: boolean) => voidCalled when the group is expanded or collapsed.
classNamestringExtra classes merged onto the root.

GroupedTool

id*stringStable identity for the row key.
name*stringTool name, rendered in mono.
target*stringWhat the call acted on: a path, a query, an id.
state*"running" | "done" | "failed"Per-call state, rolled up into the header summary.
durationMsnumberPrinted once the call settles. Omit while running.

Source

tool-group.tsx
"use client";

import { CheckIcon, ChevronRightIcon, Loader2Icon, XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";

export type GroupedToolState = "running" | "done" | "failed";

export interface GroupedTool {
  id: string;
  name: string;
  target: string;
  state: GroupedToolState;
  durationMs?: number;
}

export function ToolGroup({
  label,
  tools,
  open,
  onOpenChange,
  className,
}: {
  label: string;
  tools: readonly GroupedTool[];
  open: boolean;
  onOpenChange?: (open: boolean) => void;
  className?: string;
}) {
  const running = tools.filter((tool) => tool.state === "running").length;
  const failed = tools.filter((tool) => tool.state === "failed").length;

  return (
    <div
      className={cn(
        paper,
        "flex w-full max-w-sm flex-col overflow-hidden rounded-2xl",
        className,
      )}
    >
      <button
        type="button"
        aria-expanded={open}
        onClick={() => onOpenChange?.(!open)}
        className="hover:bg-foreground/[0.03] flex items-center gap-2.5 px-3.5 py-2.5 text-start transition-colors"
      >
        <ChevronRightIcon
          className={cn(
            "text-foreground/25 size-3 shrink-0 transition-transform duration-200 motion-reduce:transition-none",
            open && "rotate-90",
          )}
        />
        <span className="min-w-0 flex-1 truncate text-[13.5px]">{label}</span>
        <span className={cn(mono, "text-foreground/30 shrink-0 tabular-nums")}>
          {running > 0
            ? `${tools.length - running}/${tools.length}`
            : failed > 0
              ? `${failed} failed`
              : `${tools.length} done`}
        </span>
        {running > 0 ? (
          <Loader2Icon className="text-foreground/35 size-3.5 shrink-0 animate-spin motion-reduce:animate-none" />
        ) : failed > 0 ? (
          <XIcon className="size-3.5 shrink-0 text-red-500" />
        ) : (
          <CheckIcon className="size-3.5 shrink-0 text-emerald-500" />
        )}
      </button>

      {open && (
        <div className="border-foreground/[0.06] fade-in slide-in-from-top-1 animate-in flex flex-col border-t duration-200">
          {tools.map((tool) => (
            <div
              key={tool.id}
              className="flex items-center gap-2.5 px-3.5 py-2"
            >
              <span className="flex size-3.5 shrink-0 items-center justify-center">
                {tool.state === "running" ? (
                  <Loader2Icon className="text-foreground/35 size-3 animate-spin motion-reduce:animate-none" />
                ) : tool.state === "failed" ? (
                  <XIcon className="size-3 text-red-500" />
                ) : (
                  <CheckIcon className="size-3 text-emerald-500" />
                )}
              </span>
              <span className={cn(mono, "text-foreground/55 shrink-0")}>
                {tool.name}
              </span>
              <span className="text-foreground/80 min-w-0 flex-1 truncate text-[13px]">
                {tool.target}
              </span>
              {tool.durationMs !== undefined && (
                <span
                  className={cn(
                    mono,
                    "text-foreground/25 shrink-0 tabular-nums",
                  )}
                >
                  {tool.durationMs}ms
                </span>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}