Skip to content

Task list

Plan rows that tick through queued, running, done and failed.

Status
Beta
Category
AI
Adoption
Not used yet
import { TaskList } from "@oration/canon/components/ai/task-list";
packages/canon/src/components/ai/task-list.tsx

Update the prompt and publish version 14

Applies both edits and starts a 20% rollout.

What happens when you approve

0 of 4
  1. Accept partial invoice numbers with the supplier nameNot started
  2. Read the scheduled run date instead of “processing”Not started
  3. Re-run the payment status regression suiteNot started24 cases, about 3 minutes
  4. Publish version 14 to 20% of callsNot started
0 of 4 steps done
import { type TaskItem, TaskList } from "@oration/canon/components/ai/task-list";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function Hero() {    const plan = [        {            id: "p1",            label: "Accept partial invoice numbers with the supplier name",        },        {            id: "p2",            label: "Read the scheduled run date instead of “processing”",        },        {            id: "p3",            label: "Re-run the payment status regression suite",            detail: "24 cases, about 3 minutes",        },        { id: "p4", label: "Publish version 14 to 20% of calls" },    ];    const [running, setRunning] = React.useState<number | null>(null);    const finished = running !== null && running >= plan.length;    React.useEffect(() => {        if (running === null || running >= plan.length) return;        const id = window.setTimeout(            () => setRunning((n) => (n === null ? n : n + 1)),            750,        );        return () => window.clearTimeout(id);    }, [running, plan.length]);    React.useEffect(() => {        if (!finished) return;        toast.add({            type: "success",            title: "Version 14 is live",            description: "Running on 20% of calls to the Payments desk agent.",        });    }, [finished]);    const tasks: TaskItem[] = plan.map((task, index) => ({        ...task,        status:            running === null || index > running                ? "pending"                : index < running                  ? "done"                  : "running",    }));    return (        <div className="flex w-full max-w-md flex-col gap-3 rounded-xl bg-card p-4 text-left shadow-border">            <div className="flex flex-col gap-0.5">                <h3 className="text-sm font-semibold text-foreground">                    Update the prompt and publish version 14                </h3>                <p className="text-13 text-muted-foreground">                    Applies both edits and starts a 20% rollout.                </p>            </div>            <TaskList                title="What happens when you approve"                tasks={tasks}                className="rounded-[10px] bg-muted/70 p-3"            />            <div className="flex justify-end gap-2">                {finished ? (                    <Button                        type="button"                        variant="outline"                        size="sm"                        onClick={() => setRunning(null)}                    >                        Reset                    </Button>                ) : (                    <>                        <Button                            type="button"                            variant="ghost"                            size="sm"                            disabled={running !== null}                            onClick={() =>                                toast.add({                                    title: "Skipped",                                    description: "Version 13 stays live.",                                })                            }                        >                            Skip                        </Button>                        <Button                            type="button"                            size="sm"                            disabled={running !== null}                            onClick={() => setRunning(0)}                        >                            Publish version 14                        </Button>                    </>                )}            </div>        </div>    );}

Usage#

Task list shows a plan as rows that tick from not started to in progress to done, with skipped and failed for plans that don't go to script. Each row has a status icon on a thin rail, a label and an optional detail, and a titled list counts 2 of 4 as it goes. It is how an agent shows what it is about to do and then does it, in front of the person who approved it. The common mistake is using it for reasoning or for tool calls: a task list is a plan with outcomes, so every row should be something the person would recognize as a step of the job.

When to use

  • Under an approval, to say what will happen when someone approves: What happens when you approve.
  • While an approved plan runs, advancing one row at a time until everything is done.
  • While a generated artifact is built in a few visible stages, such as a report layout.
  • For a campaign or batch with a few named phases that can fail on their own, such as an overdue W-9 collection.

When not to use

  • For the model's reasoning before it answers. Use Thinking
  • For individual tool calls with arguments and results. Use Tool chip
  • For a single task with a known percentage done. Use Progress
  • For the stage a record is in, such as a pipeline or onboarding stage. Use Stepper
  • For a checklist a person ticks by hand. Use Checkbox

The Ink Fill Rule

A finished row is a filled Ink 80 circle with a check, not a green one. Progress isn't a verdict, so only a failure turns a row's icon semantic (a 12% Signal Red tint).

The Label-Beside-Color Rule

Every status has its own shape: dashed circle, spinner, filled check, dash, exclamation. Each row also carries a hidden status word for screen readers.

The Tabular Figures Rule

The 2 of 4 count in the title row is set in tabular figures so it doesn't shift as it changes.

Anatomy#

Overdue W-9 collection

1 of 3
  1. Email the W-9 requestDone
  2. Call suppliers who haven't repliedIn progress7 of 12 called
  3. Attach signed formsNot started
1 of 3 steps done
  1. Title. Optional 13px medium heading, rendered as an h3.
  2. Count. 12px Slate Meta done of total at the right, when both title and tasks are set. Skipped rows count as done.
  3. Status icon. 16px: a dashed ring for not started, a spinning arc for in progress, a filled ink check for done, a dashed-through ring for skipped, a red exclamation for failed.
  4. Rail. A 1px hairline from each icon down to the next row, hidden after the last row.
  5. Label. 13px. Medium weight with the shimmer while in progress, Slate Meta once done, struck through when skipped.
  6. Detail. Optional 12px Slate Meta line under the label. Signal Red on a failed row.

Examples#

Statuses

Not started, in progress, done, skipped and failed. Each has its own icon shape; only failed turns red.

  1. Email the W-9 request to 48 suppliersDone
  2. Call the 12 who haven't opened itIn progress7 of 12 called
  3. Attach signed forms to supplier recordsNot started
  4. Text suppliers without an emailSkippedEvery supplier has an email on file.
  5. Hold Friday's payments for missing W-9sFailedThe ERP rejected the hold. Nothing was changed.
2 of 5 steps done
import { TaskList } from "@oration/canon/components/ai/task-list";export function Statuses() {    return (        <div className="w-full max-w-sm">            <TaskList                tasks={[                    {                        id: "s1",                        label: "Email the W-9 request to 48 suppliers",                        status: "done",                    },                    {                        id: "s2",                        label: "Call the 12 who haven't opened it",                        status: "running",                        detail: "7 of 12 called",                    },                    {                        id: "s3",                        label: "Attach signed forms to supplier records",                        status: "pending",                    },                    {                        id: "s4",                        label: "Text suppliers without an email",                        status: "skipped",                        detail: "Every supplier has an email on file.",                    },                    {                        id: "s5",                        label: "Hold Friday's payments for missing W-9s",                        status: "error",                        detail: "The ERP rejected the hold. Nothing was changed.",                    },                ]}            />        </div>    );}

Untitled, while something is built

Without a title there is no count, just the rows. Here a report draft is laid out in three visible stages, then a toast confirms it.

Describe the report you want

A Monday review of transfers by agent and why suppliers get handed off.

  1. Reading your requestIn progress
  2. Choosing metrics and breakdownsNot started
  3. Laying out insightsNot started
0 of 3 steps done
import { type TaskItem, TaskList } from "@oration/canon/components/ai/task-list";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function InACard() {    const steps = [        "Reading your request",        "Choosing metrics and breakdowns",        "Laying out insights",    ];    const [step, setStep] = React.useState(0);    React.useEffect(() => {        if (step > steps.length) return;        const id = window.setTimeout(() => {            if (step === steps.length) {                toast.add({                    type: "success",                    title: "Draft layout added",                    description:                        "Drag to reorder, or open any insight to change it.",                });            }            setStep((n) => n + 1);        }, 700);        return () => window.clearTimeout(id);    }, [step, steps.length]);    const tasks: TaskItem[] = steps.map((label, index) => ({        id: label,        label,        status: step > index ? "done" : step === index ? "running" : "pending",    }));    return (        <section className="w-full max-w-md rounded-xl bg-card p-4 text-left shadow-border">            <h3 className="text-sm font-semibold text-foreground">                Describe the report you want            </h3>            <p className="mt-0.5 text-13 text-muted-foreground">                A Monday review of transfers by agent and why suppliers get                handed off.            </p>            <div className="mt-3">                <TaskList tasks={tasks} />            </div>        </section>    );}

Composed rows

Pass TaskRow children when labels need markup. Composed lists don't get the count or the status announcement, so the title stands alone.

Overdue W-9 collection

  1. Email the request to 48 suppliersDoneSent from payments@cedarline.example on Monday, Sep 28.
  2. Call the 12 who haven't opened itIn progress7 of 12 called
  3. Attach forms as w9-signed.pdfNot started
import { TaskList, TaskRow } from "@oration/canon/components/ai/task-list";export function Composed() {    return (        <div className="w-full max-w-sm">            <TaskList title="Overdue W-9 collection">                <TaskRow                    status="done"                    label={                        <>                            Email the request to{" "}                            <span className="font-medium">48 suppliers</span>                        </>                    }                    detail="Sent from payments@cedarline.example on Monday, Sep 28."                />                <TaskRow                    status="running"                    label="Call the 12 who haven't opened it"                    detail={                        <span className="tabular-nums">7 of 12 called</span>                    }                />                <TaskRow                    status="pending"                    label={                        <>                            Attach forms as{" "}                            <span className="font-mono text-xs">                                w9-signed.pdf                            </span>                        </>                    }                />            </TaskList>        </div>    );}

A step that fails

The failed row says why in its detail and the step that depended on it is skipped. Retrying moves both forward.

Overdue W-9 collection

2 of 3
  1. Email the W-9 request to 48 suppliersDone
  2. Call the 12 who haven't opened itFailed3 suppliers have no phone number on file. The other 9 were called.
  3. Attach signed forms to supplier recordsSkipped
2 of 3 steps done
import { type TaskItem, TaskList } from "@oration/canon/components/ai/task-list";import { Button } from "@oration/canon/components/button";import * as React from "react";export function Failure() {    const [retried, setRetried] = React.useState<"failed" | "running" | "done">(        "failed",    );    React.useEffect(() => {        if (retried !== "running") return;        const id = window.setTimeout(() => setRetried("done"), 1400);        return () => window.clearTimeout(id);    }, [retried]);    const tasks: TaskItem[] = [        {            id: "f1",            label: "Email the W-9 request to 48 suppliers",            status: "done",        },        {            id: "f2",            label: "Call the 12 who haven't opened it",            status: retried === "failed" ? "error" : retried,            detail:                retried === "failed"                    ? "3 suppliers have no phone number on file. The other 9 were called."                    : retried === "running"                      ? "Calling the 3 with numbers added"                      : "All 12 reached",        },        {            id: "f3",            label: "Attach signed forms to supplier records",            status:                retried === "done"                    ? "done"                    : retried === "failed"                      ? "skipped"                      : "pending",        },    ];    return (        <div className="flex w-full max-w-sm flex-col gap-3">            <TaskList title="Overdue W-9 collection" tasks={tasks} />            {retried === "failed" ? (                <Button                    type="button"                    variant="outline"                    size="sm"                    className="self-start"                    onClick={() => setRetried("running")}                >                    Retry calls                </Button>            ) : null}        </div>    );}

States#

States
StateTreatment
Not startedA dashed Firm Hairline ring. status="pending".
In progressA spinning arc and a shimmering medium-weight label. status="running".
DoneA filled Ink 80 check and a Slate Meta label. The circle scales in and the check draws itself, but only when the row turns done on screen; rows that load done render still.
SkippedA ring with a dash and a struck-through Slate Meta label. Counts toward done.
FailedA 12% red disc with a red exclamation; the detail turns red. Doesn't count toward done. status="error".
Reduced motionThe spinner and shimmer stop. The check still appears, without the scale.

Behavior#

  • TaskList is presentational. Status comes from you: advance rows on a timer for a simulation, or from real job events.
  • Pass tasks to render rows plus the count and a status announcement, or compose TaskRow children for labels with markup. Children get neither the count nor the announcement.
  • A row remembers its status at mount. If it mounts done, the check renders still; if it turns done later, the circle scales in on spring.moderate and the check path draws on spring.slow.
  • The rail is drawn per row, so rows can be added or removed without redrawing a single long line.
  • The spinner uses animate-spin with motion-reduce:animate-none.

Do and don't#

What happens when you approve

1 of 3
  1. Accept partial invoice numbersDone
  2. Re-run the regression suiteIn progress
  3. Publish version 14 to 20% of callsNot started
1 of 3 steps done
Do. List the three to five steps a person would recognize as the job: accept partial invoice numbers, re-run the suite, publish to 20%.

What happens when you approve

4 of 8
  1. Open the ERPDone
  2. Click searchDone
  3. Type the invoice numberDone
  4. Read the resultDone
  5. Open the promptIn progress
  6. Find the paragraphNot started
  7. Edit the paragraphNot started
  8. SaveNot started
4 of 8 steps done
Don't. List every micro-action. Eight rows of Open ERP and Click search bury the one step that matters.
  1. Call the 12 who haven't opened itFailed3 suppliers have no phone number on file.
  2. Attach signed formsSkipped
1 of 2 steps done
Do. When a step fails, say why in its detail and mark the steps that depend on it as skipped.
  1. Call the 12 who haven't opened itFailed
  2. Attach signed formsIn progress
0 of 2 steps done
Don't. Leave the next row spinning after a failure, or fail with no detail. People can't tell whether anything is still happening.

Content#

  • Write each row as the action, verb first, in sentence case: Email the W-9 request to 48 suppliers, Publish version 14 to 20% of calls.
  • Keep the label the same as the row changes status. The icon says whether it is done; don't rewrite the label into the past tense.
  • Use detail for size or outcome: 24 cases, about 3 minutes, 7 of 12 called.
  • On a failure, the detail says what went wrong and what happens next: 3 suppliers have no phone number on file. They'll get a second email instead.
  • Title the list for what it is about to do: What happens when you approve, Overdue W-9 collection.

Accessibility#

  • The list is an <ol>, so screen readers announce the number of steps and each position.
  • Each row carries a visually hidden status word: Not started, In progress, Done, Skipped or Failed.
  • With tasks, a hidden role="status" announces 2 of 4 steps done as the count changes. Composed children don't get this; add your own status region if progress matters.
  • Icons are aria-hidden. The spinner and shimmer stop under reduced motion.
  • The title renders as an h3. Check it fits the heading outline where you place the list.

Design tokens#

Design tokens
TokenUsed for
--foregroundDone disc at 80%, spinner arc at 80%, labels
--backgroundThe check drawn on the done disc
--border-strongNot started and skipped rings, the spinner track
--borderThe rail between rows
--muted-foregroundDone and skipped labels, details, the count
--subtle-foregroundThe skipped dash and strikethrough
--destructiveFailed disc at 12%, its mark and detail
text-shimmerThe in-progress label
spring.moderate, spring.slowThe done circle and the check draw

API reference#

TaskList

The list, with an optional title and count.

Other props spread onto Nothing. Only the props below are read..

Props of TaskList
PropTypeDefaultDescription
tasksTaskItem[]No default{ id, label, status, detail? }[]. Renders rows, the count and the status announcement.
titlestringNo defaultHeading above the rows. With tasks, the count sits beside it.
childrenReact.ReactNodeNo defaultTaskRow elements, rendered after any tasks rows.
classNamestringNo defaultMerged onto the root.

TaskRow

One row, for composing a list by hand.

Other props spread onto Nothing.

Props of TaskRow
PropTypeDefaultDescription
labelRequiredReact.ReactNodeNo defaultThe step. Can hold markup.
statusRequired"pending" | "running" | "done" | "skipped" | "error"No defaultNot started, in progress, done, skipped or failed.
detailReact.ReactNodeNo defaultLine under the label.
classNamestringNo defaultMerged onto the <li>.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

The product only uses not started, in progress and done. Skipped and failed exist but nothing renders them, so an approved Copilot plan has no way to show a failed step.

Composed TaskRow children get no count and no status announcement.

The title is always an h3, which can skip a level inside a panel whose section heading is an h3 too.

A failed row keeps its label in ink; only the icon and detail turn red.