Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new

Queue

A task queue that separates work still to do from completed work.

Interactive preview

The shared preview uses React Base. The source below uses React Aria. Each example runs with local data.

Installation

Complete project setup with react/aria, then install these components:

pnpm exec frontfriend download trace collapsible message bubble button icon -f react --primitive aria

A todo moves to Completed when it finishes. Only the active todo shimmers while the stream runs. A todo with a description has its own disclosure.

Usage

Complete example

import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { Button } from "@/components/ui/button";
import * as React from "react";
const TODOS = [
  "Add a dark variant to the theme tokens",
  "Read the theme from a cookie",
  "Add a toggle to the settings page",
  "Check contrast on the charts",
];
const canvasStyle: React.CSSProperties = {
  width: "100%",
  maxWidth: 384,
  minWidth: 0,
  minHeight: 288,
  marginInline: "auto",
  display: "flex",
  flexDirection: "column",
  gap: 16,
  paddingBlock: 24,
  fontSize: "var(--text-sm)",
  lineHeight: 1.5,
};
export default function AiQueueExample() {
  const [current, setCurrent] = React.useState(TODOS.length);
  const running = current < TODOS.length;
  React.useEffect(() => {
    if (!running) return;
    const timer = setTimeout(() => setCurrent((value) => value + 1), 1400);
    return () => clearTimeout(timer);
  }, [current, running]);
  const parts = [
    {
      type: "tool-writeTodos",
      state: "input-available",
      input: {
        todos: TODOS.map((content, index) => ({
          content,
          status:
            index < current
              ? "completed"
              : index === current
                ? "in_progress"
                : "pending",
        })),
      },
    },
  ];
  const todos =
    parts
      .filter(
        (part) =>
          part.type === "tool-writeTodos" && part.state !== "input-streaming",
      )
      .slice(-1)[0]?.input.todos ?? [];
  const sections = [
    {
      id: "pending",
      icon: "ListTodo",
      todos: todos.filter((todo) => todo.status !== "completed"),
    },
    {
      id: "completed",
      icon: "ListChecks",
      todos: todos.filter((todo) => todo.status === "completed"),
    },
  ].filter((section) => section.todos.length > 0);
  return (
    <div style={canvasStyle}>
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Add dark mode to the settings page.</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent style={{ gap: 16 }}>
          <Trace>
            {sections.map((section) => (
              <TraceCollapsible key={section.id} defaultExpanded>
                <TraceCollapsibleTrigger>
                  <TraceLine>
                    <TraceIcon>
                      <Icon name={section.icon} />
                    </TraceIcon>
                    <TraceTitle>
                      {section.todos.length}{" "}
                      {section.id === "pending" ? "to do" : "completed"}
                    </TraceTitle>
                  </TraceLine>
                </TraceCollapsibleTrigger>
                <TraceCollapsibleContent>
                  <Trace>
                    {section.todos.map((todo) => (
                      <TraceLine
                        key={todo.content}
                        status={
                          running && todo.status === "in_progress"
                            ? "running"
                            : undefined
                        }
                      >
                        <TraceIcon>
                          <Icon
                            name={
                              todo.status === "completed"
                                ? "CircleCheck"
                                : todo.status === "in_progress"
                                  ? "CircleDot"
                                  : "Circle"
                            }
                          />
                        </TraceIcon>
                        <TraceTitle>{todo.content}</TraceTitle>
                      </TraceLine>
                    ))}
                  </Trace>
                </TraceCollapsibleContent>
              </TraceCollapsible>
            ))}
          </Trace>
          {!running && (
            <p>Dark mode is in. The toggle is under Settings, Appearance.</p>
          )}
        </MessageContent>
      </Message>
      <Button
        variant="outline"
        size="sm"
        style={{ marginTop: "auto", alignSelf: "flex-start" }}
        isDisabled={running}
        onPress={() => setCurrent(0)}
      >
        Replay
      </Button>
    </div>
  );
}

Descriptions

import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import * as React from "react";
export default function AiQueueExample() {
  const todos = [
    {
      title: "Write the migration guide",
      description: "Cover the renamed props and the new data attributes.",
      done: true,
    },
    { title: "Add a changeset", done: true },
    {
      title: "Fix the focus ring on the select trigger",
      description: "It is clipped by overflow-hidden on the field.",
      done: false,
    },
    { title: "Update the registry snapshots", done: false },
    {
      title: "Add tests for the combobox",
      description: "Typing, clearing and keyboard selection.",
      done: false,
    },
  ];
  const sections = [
    {
      id: "pending",
      icon: "ListTodo",
      todos: todos.filter((todo) => !todo.done),
    },
    {
      id: "completed",
      icon: "ListChecks",
      todos: todos.filter((todo) => todo.done),
    },
  ];
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 384,
        marginInline: "auto",
        paddingBlock: 24,
      }}
    >
      <Trace>
        {sections.map((section) => (
          <TraceCollapsible
            key={section.id}
            defaultExpanded={section.id === "pending"}
          >
            <TraceCollapsibleTrigger>
              <TraceLine>
                <TraceIcon>
                  <Icon name={section.icon} />
                </TraceIcon>
                <TraceTitle>
                  {section.todos.length}{" "}
                  {section.id === "pending" ? "to do" : "completed"}
                </TraceTitle>
              </TraceLine>
            </TraceCollapsibleTrigger>
            <TraceCollapsibleContent>
              <Trace>
                {section.todos.map((todo) => {
                  const line = (
                    <TraceLine>
                      <TraceIcon>
                        <Icon name={todo.done ? "CircleCheck" : "Circle"} />
                      </TraceIcon>
                      <TraceTitle>{todo.title}</TraceTitle>
                    </TraceLine>
                  );
                  return todo.description ? (
                    <TraceCollapsible key={todo.title}>
                      <TraceCollapsibleTrigger>{line}</TraceCollapsibleTrigger>
                      <TraceCollapsibleContent>
                        {todo.description}
                      </TraceCollapsibleContent>
                    </TraceCollapsible>
                  ) : (
                    <React.Fragment key={todo.title}>{line}</React.Fragment>
                  );
                })}
              </Trace>
            </TraceCollapsibleContent>
          </TraceCollapsible>
        ))}
      </Trace>
    </div>
  );
}

AI SDK

This optional adapter accepts a UIMessage from your chat client. The complete example needs no AI SDK or provider. Install this extra package only for the adapter:

pnpm add ai

Read the last tool-writeTodos call whose input is no longer streaming. Each call contains the complete todo list. Split it into pending and completed sections. Only show a running line while the chat streams.

import type { UIMessage } from "ai";
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
type Todo = {
  content: string;
  status: "pending" | "in_progress" | "completed";
};
type QueueMessage = UIMessage<
  unknown,
  never,
  {
    writeTodos: {
      input: {
        todos: Todo[];
      };
      output: {
        saved: boolean;
      };
    };
  }
>;
function QueueAdapter({
  message,
  streaming,
}: {
  message: QueueMessage;
  streaming: boolean;
}) {
  const todos =
    message.parts
      .filter((part) => part.type === "tool-writeTodos")
      .filter((part) => part.state !== "input-streaming")
      .slice(-1)[0]?.input?.todos ?? [];
  const running = streaming;
  const sections = [
    {
      id: "pending",
      icon: "ListTodo",
      todos: todos.filter((todo) => todo.status !== "completed"),
    },
    {
      id: "completed",
      icon: "ListChecks",
      todos: todos.filter((todo) => todo.status === "completed"),
    },
  ].filter((section) => section.todos.length > 0);
  return (
    <Trace>
      {sections.map((section) => (
        <TraceCollapsible key={section.id} defaultExpanded>
          <TraceCollapsibleTrigger>
            <TraceLine>
              <TraceIcon>
                <Icon name={section.icon} />
              </TraceIcon>
              <TraceTitle>
                {section.todos.length}{" "}
                {section.id === "pending" ? "to do" : "completed"}
              </TraceTitle>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <Trace>
              {section.todos.map((todo) => (
                <TraceLine
                  key={todo.content}
                  status={
                    running && todo.status === "in_progress"
                      ? "running"
                      : undefined
                  }
                >
                  <TraceIcon>
                    <Icon
                      name={
                        todo.status === "completed"
                          ? "CircleCheck"
                          : todo.status === "in_progress"
                            ? "CircleDot"
                            : "Circle"
                      }
                    />
                  </TraceIcon>
                  <TraceTitle>{todo.content}</TraceTitle>
                </TraceLine>
              ))}
            </Trace>
          </TraceCollapsibleContent>
        </TraceCollapsible>
      ))}
    </Trace>
  );
}
const TODOS = [
  "Add a dark variant to the theme tokens",
  "Read the theme from a cookie",
  "Add a toggle to the settings page",
  "Check contrast on the charts",
];
export default function AiQueueExample() {
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 384,
        marginInline: "auto",
        paddingBlock: 24,
      }}
    >
      <QueueAdapter
        message={{
          id: "answer",
          role: "assistant",
          parts: [
            {
              type: "tool-writeTodos",
              toolCallId: "todos-1",
              state: "input-available",
              input: {
                todos: TODOS.map((content, index) => ({
                  content,
                  status: index === 0 ? "completed" : "pending",
                })),
              },
            },
          ],
        }}
        streaming={false}
      />
    </div>
  );
}

Components and API

This composition has no separate component API. Use the APIs for its installed components.

ComponentTarget reference
trace React Aria API
collapsible React Aria API
message React Aria API
bubble React Aria API
button React Aria API
icon React Aria API