Scrim UI

Agent Plan UI

The checklist an agent writes for itself and then edits mid-run — steps added, skipped and re-ordered, without the list jumping under the reader.

agentplantaskschecklist
npx shadcn@latest add https://scrimui.dev/r/agent-plan.json
Agent promptClaude Code · Cursor · any agent
Add the Agent Plan UI component from the Scrim UI registry to this project and use it in the configuration described below.

Agent Plan UI — The checklist an agent writes for itself and then edits mid-run — steps added, skipped and re-ordered, without the list jumping under the reader.

## 1. Install

```bash
npx shadcn@latest add https://scrimui.dev/r/agent-plan.json
```

This writes a single file to `components/ui/agent-plan.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/agent-plan to the same path by hand; nothing in the file depends on shadcn.

## 2. Use it

```tsx
import { AgentPlan } from "@/components/ui/agent-plan";

// A step the agent dropped becomes state: "skipped" with a reason.
// It never disappears — a plan that loses items cannot be audited.
const STEPS = plan.steps;

<AgentPlan
  steps={STEPS}
  planning
/>
```

## 3. The configuration that matters

This is the component's "Writing the plan" state — Two steps so far and more coming. The list is short because it is unfinished, not because the task is small — the header says which.

These props were set deliberately and should be preserved as written:

- `phase` = "planning" — Where the run is
- `revision` = 0 — Times revised

Every other prop is at its default; leave those off the call site rather than writing the default value out.

Reference: https://scrimui.dev/components/agent-plan

Follows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.

Plan0/2 donestill planning
  1. Search open issues for reports of dropped citations
  2. Read lib/chunk.ts and lib/retrieve.ts

Presets

Props

0

Component source

Single-file React + Tailwind component. No dependencies — drop it into any project with Tailwind configured.

agent-plan.tsx
"use client";

import * as React from "react";

/**
 * The checklist an agent writes for itself, and then edits while you watch.
 *
 * A static list of steps is a five-minute component. What makes this one
 * worth its own file is that **the plan changes mid-run**, and every naive
 * rendering of that change is worse than not showing the plan at all:
 *
 * **Completed steps must never move.** The model re-emits its plan and the
 * new list is not the old list — a step was inserted at position two, another
 * was dropped. Rendering the new array wholesale slides everything the reader
 * has already read to a new position, and they lose their place in a list
 * they were using precisely to keep it. So finished steps hold their order,
 * and new work arrives underneath.
 *
 * **A dropped step is not a deleted step.** Something the agent said it would
 * do and then decided not to is the most interesting line in the plan. It
 * becomes `skipped`, with the reason, rather than vanishing — a plan that
 * silently loses items is a plan you cannot audit afterwards.
 *
 * **A newly added step should announce itself.** `added` marks work that was
 * not in the original plan, because "the agent decided to do three more
 * things" is a fact the reader wants before it finishes doing them.
 *
 * **A revision counter, not a diff.** Full before/after diffing of a plan is
 * a lot of machinery for a reader who mostly wants to know *that* it changed.
 * `revision` says how many times, and the per-step marks say where.
 */

export type PlanStepState = "pending" | "active" | "done" | "skipped" | "failed";

export type PlanStep = {
  id: string;
  text: string;
  state: PlanStepState;
  /** Why it was skipped, or what it produced. One line. */
  note?: string;
  /** True for a step the agent added after the first plan. */
  added?: boolean;
};

export type AgentPlanProps = {
  steps: PlanStep[];
  /** 0 = the original plan. Anything higher is shown next to the title. */
  revision?: number;
  /** The plan itself is still being written. */
  planning?: boolean;
  title?: string;
  className?: string;
};

/* ------------------------------------------------------------------ */
/* Step marks                                                          */
/* ------------------------------------------------------------------ */

function Mark({ state }: { state: PlanStepState }) {
  const base = "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border";
  switch (state) {
    case "done":
      return (
        <span className={`${base} border-emerald-600 bg-emerald-600 text-white dark:border-emerald-500 dark:bg-emerald-500`}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" width="9" height="9">
            <path d="M20 6 9 17l-5-5" />
          </svg>
        </span>
      );
    case "active":
      return (
        <span className={`${base} border-blue-500 text-blue-500`}>
          <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
        </span>
      );
    case "failed":
      return (
        <span className={`${base} border-red-500 bg-red-500 text-white`}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" width="9" height="9">
            <path d="M18 6 6 18M6 6l12 12" />
          </svg>
        </span>
      );
    case "skipped":
      return (
        <span className={`${base} border-zinc-300 text-zinc-400 dark:border-zinc-600 dark:text-zinc-500`}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" width="9" height="9">
            <path d="M5 12h14" />
          </svg>
        </span>
      );
    default:
      return <span className={`${base} border-zinc-300 dark:border-zinc-600`} />;
  }
}

/* ------------------------------------------------------------------ */
/* AgentPlan                                                           */
/* ------------------------------------------------------------------ */

export function AgentPlan({
  steps,
  revision = 0,
  planning = false,
  title = "Plan",
  className = "",
}: AgentPlanProps) {
  const done = steps.filter((s) => s.state === "done").length;
  const settled = steps.filter((s) => s.state !== "pending" && s.state !== "active").length;

  return (
    <div className={`rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900 ${className}`}>
      <div className="flex flex-wrap items-baseline gap-x-2 gap-y-1 border-b border-zinc-100 px-3.5 py-2.5 dark:border-zinc-800">
        <span className="text-[13px] font-medium text-zinc-900 dark:text-zinc-100">{title}</span>
        <span className="tabular-nums text-[11px] text-zinc-500 dark:text-zinc-400">
          {done}/{steps.length} done
        </span>
        {revision > 0 && (
          /* The reader's mental model of "the plan" is now out of date, and
             saying so costs one word. */
          <span className="rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium text-amber-800 dark:bg-amber-900/40 dark:text-amber-300">
            revised {revision}×
          </span>
        )}
        {planning && (
          <span className="ml-auto inline-flex items-center gap-1.5 text-[11px] text-zinc-500 dark:text-zinc-400">
            <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
            still planning
          </span>
        )}
      </div>

      <ol className="px-3.5 py-3">
        {steps.map((step, i) => (
          <li key={step.id} className="relative flex gap-2.5 pb-3 last:pb-0">
            {/* Rail, drawn between marks rather than behind them, so a
                skipped step reads as part of the sequence it was cut from. */}
            {i < steps.length - 1 && (
              <span className="absolute left-2 top-5 h-[calc(100%-1.25rem)] w-px bg-zinc-200 dark:bg-zinc-700" aria-hidden />
            )}
            <Mark state={step.state} />
            <div className="min-w-0 flex-1">
              <div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
                <span
                  className={`text-[13px] leading-5 ${
                    step.state === "skipped"
                      ? "text-zinc-400 line-through decoration-zinc-300 dark:text-zinc-500 dark:decoration-zinc-600"
                      : step.state === "done"
                        ? "text-zinc-500 dark:text-zinc-400"
                        : "text-zinc-800 dark:text-zinc-100"
                  }`}
                >
                  {step.text}
                </span>
                {step.added && (
                  <span className="shrink-0 rounded-md bg-blue-100 px-1.5 py-px text-[10px] font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
                    added
                  </span>
                )}
              </div>
              {step.note && (
                <p className="mt-0.5 text-[11px] leading-4 text-zinc-500 dark:text-zinc-400">
                  {step.note}
                </p>
              )}
            </div>
          </li>
        ))}
      </ol>

      {settled > 0 && settled === steps.length && (
        <p className="border-t border-zinc-100 px-3.5 py-2 text-[11px] text-zinc-500 dark:border-zinc-800 dark:text-zinc-400">
          Plan complete — {done} done, {steps.length - done} skipped or failed.
        </p>
      )}
    </div>
  );
}

When to use it

  • Give every step a stable id from the run, not an array index. The plan gets re-emitted with items inserted, and index keys mean React re-uses the wrong row.
  • Keep completed steps in the order they completed. A step inserted at position two must not slide the four the reader has already read.
  • Turn a dropped step into `skipped` with the reason. It is the most informative line in the plan, and deleting it leaves a plan that ran cleanly next to a run that did not.
  • Mark steps the agent added after the first plan. 'It decided to do three more things' is a fact worth having before it finishes doing them.
  • Show the revision count. Most readers only need to know the plan changed and roughly how much, which is a counter rather than a diff.
  • Say when the plan is still being written. A short list mid-planning is indistinguishable from a short plan.

What breaks in production

  • Re-rendering the new plan array wholesale. Everything moves, and the reader loses their place in the list they were using to keep it.
  • Deleting steps the agent abandoned. The run becomes unauditable at exactly the point somebody asks why it did not do the thing it said it would.
  • Using the array index as the key. Insert one step and React re-uses rows across different steps — states and animations land on the wrong lines.
  • Rendering the plan as prose the model wrote. It reads fine and cannot be diffed, checked off, or pointed at when something goes wrong.
  • Showing a progress percentage over a list whose length keeps changing. It goes backwards, which reads as a bug rather than as a revision.

Related Components