Pro

Plan approval

Pro component

The moment an agent proposes a multi-step plan and the person reviews it before anything runs. Approve or skip each step from the keyboard, then watch the steps check in as the plan runs.

Copper plans Saturday supper

Copper’s plan for Saturday

5 steps
0 of 5 decided
  1. Read Saturday’s RSVPs and note dietary needs. 12 booked, 2 on the waitlist.

    Usesacme.list_rsvps

  2. Built around the lamb shoulder, with a vegetarian main for the 3 guests who asked.

    Usesacme.draft_menu

  3. Compare the shopping list at the market, the grocer and the butcher, and pick the cheapest in stock.

    Usesprices.search

  4. “Supper is at 7:30 on Saturday, 14 Alder Lane. Reply with any allergies by Thursday.”

    Usespostbox.send_sms

  5. Two hours from 10am with Tidy & Co, $64. Holds the slot; you pay after.

    Usestidy.book_slot

0 approved
Press A to approve, S to skip.

Arrow through the steps, Enter opens one, A approves and S skips.

Installation

This is a Pro component. UI Polish Pro costs $149 one time and includes every Pro component, block and template, now and later. Add your license key to your MCP connection or components.json, then install it either way below.

With UI Polish connected, ask your agent:

Add the UI Polish plan approval to this project.

Or with the shadcn CLI:

npx shadcn@latest add @uipolish/plan-approval
Brings inBadge, Button, Icons, Kbd, Text

The card an agent shows when it has a plan of several steps and wants the person to look it over before anything runs. Each step is a row: its icon, what it will do, and a status chip. One row is open at a time, showing what exactly will happen, the tool it calls, and Approve and Skip. The footer counts what's approved and holds Approve all and Run plan. Once the plan runs, the steps check in one after another: the running one turns a small spinner and fills its chip, finished ones draw a check, and a line along the footer travels as the work moves on.

TSX
<PlanApproval  title="Copper’s plan for Saturday"  agent="Copper"  steps={[    {      id: "guests",      title: "Check the guest list",      status: "planned",      icon: <Icon name="search" />,      detail: "Read Saturday’s RSVPs and note dietary needs.",      tool: "acme.list_rsvps",    },    // …  ]}  running={running}  onDecide={(id, decision) => setStatus(id, decision === "approve" ? "approved" : "skipped")}  onApproveAll={approveRemaining}  onRun={run}  onStop={stop}  onRetry={retry}/>

Everything is controlled: your agent owns the plan, and the card shows it. Pair it with message-thread, where it sits as the agent's turn, and tool-steps for what the agent does after the plan is over.

When to use it

  • An agent that proposes several actions at once and should get the go-ahead before starting: planning an event, a migration, a batch of emails.
  • Plans where the person may want some steps and not others. Skipping one is as easy as approving it.

When not to

  • A single tool call that needs an OK: use tool-approval.
  • Steps the agent has already decided to take and is reporting: use tool-steps.
  • A checklist the person works through themselves. These steps are the agent's.

Steps

FieldWhat to put there
idStable across updates; decisions and focus follow it.
titleThe action as a verb phrase, with the count: "Text the 12 guests".
statusplanned, approved, skipped, running, done or failed.
detailOptional. What exactly will happen, in a sentence or two. Quote what will be sent. Shown when the step is open.
toolOptional. The tool the step calls, in mono under the detail.
iconOptional. A 16px icon (<Icon name="send" />). Without one, the step's number shows.
errorOptional. Shown on a failed step: what happened and what to do.

Props

PropWhat it does
titleThe card's title. Defaults to "Plan".
agentWho made the plan, used in announcements and the empty state.
runningThe plan is running. Defaults to whether a step is running; set it yourself to cover the gap between steps.
onDecide(id, decision)decision is "approve" or "skip". Without it, the card is read-only.
onApproveAll()Approve every step that's still planned.
onRun()Start the plan. Steps still planned when it's pressed are yours to decide; skipping them is the safe call.
onStop()Shows Stop in the footer while the plan runs.
onRetry(id)Shows Retry on a failed step.

States

The card's phase follows from the steps:

PhaseWhenHeaderFooter
ReviewNothing has run yet"2 of 5 decided""2 approved", Approve all, Run plan (disabled at 0 approved)
Runningrunning, or a step is runningA breathing dot and "Running""1 of 4 done · 1 skipped", Stop, and Run plan busy as "Running…"
PausedSomething ran, approved steps remain, nothing running (a failure, or Stop)"Stopped" after a failure, "Paused" otherwiseThe done count, and Resume
FinishedNothing approved or planned is leftA check and "Plan done", or "Stopped" if a step failedThe done count; the buttons step away

Per step:

StatusGlyphChipTitle
plannedThe step's icon, muted"Planned", neutralText color
approvedThe icon in the accent"Approved", accent tintText color
skippedThe icon, faded"Skipped", outline onlyMuted, regular weight: back, but readable
runningA small spinner in the accent"Running", solid accent; the row takes a trace of accentText color
doneA check draws in on a success disc"Done", success tintText color
failedA mark on a danger disc; the step opens with its error"Failed", danger tintText color

Which step is open. The first planned step opens on mount. Deciding a step folds it and opens the next one still planned, so a plan can be read and decided from the top without reaching for the pointer. Clicking a row, or Enter on it, opens or closes it. Starting the run folds them all; a failure opens the failed step, with Retry and Skip. Skipping a failed step is a decision like any other: carry on with the rest of the plan.

Changing your mind. A decided step still opens to Approve and Skip while the plan is in review or paused, so a person can skip what they approved. The button for the current choice is pressed.

Nothing shifts. Every chip label sits in one cell, so the chip keeps the width of "Approved" whatever it says, and only its color and label change. The header's and footer's labels for every phase share a cell the same way. Counts hold the width of their widest digits, since the system face has no tabular figures, and a new value rolls up into place.

Empty. With no steps the card says "No steps yet. Copper will list them here before anything runs." Render it while the agent is still planning, or wait for the plan.

Keyboard

The steps are one tab stop. From a step:

KeyDoes
↑ / ↓, Home / EndMove between steps
Enter or SpaceOpen or close the step
AApprove the step, then move to the next one still planned
SSkip the step (a failed step too), then move on
TabInto the open step's buttons, then the footer

A and S work from anywhere in a step, its buttons included, and do nothing while the plan runs. The open step's buttons show their key; touch screens don't.

Motion

WhatDurationEasing
A step opens: height, then its content clears from faint, 2px blur and 98.5% scale, the clip's bottom edge a fade--ui-dur-panel (280ms)--ui-ease-expand; content --ui-ease-enter, 60ms later
One step closes as another opensThe same 280ms, so the heights trade off and the steps below hold still--ui-ease-expand
The open step closes--ui-dur-base (200ms) height, --ui-dur-instant fade--ui-ease-collapse, --ui-ease-exit
Chip color changes--ui-dur-base--ui-ease-color
Chip label swaps: old fades, new rises 2px--ui-dur-instant out, --ui-dur-fast in, half a beat later--ui-ease-color, --ui-ease-enter
Glyph swaps (icon, spinner, check, mark)--ui-dur-fast fade, scale from 97%--ui-ease-enter, --ui-ease-snap
The check draws in--ui-dur-base, after the disc--ui-ease-draw
Spinner turnsThree --ui-dur-panel per turnlinear
A count rolls up--ui-dur-glide (320ms)--ui-ease-glide
The footer line fills to the share done--ui-dur-glide--ui-ease-glide: it never passes its end

Under prefers-reduced-motion: reduce, steps open and close at once, the content and labels fade without blur, scale or rise, the check appears whole, counts change in place, and the spinner stops turning and breathes instead.

Accessibility

  • The card is a section named by its title. The steps are an ordered list.
  • Each row is a button with aria-expanded and aria-controls, named by the step and its status ("Check the guest list, Approved"). Closed steps are inert.
  • Rows carry aria-keyshortcuts="A S" and a description, "Press A to approve, S to skip", while there's something to decide.
  • Status is in words in every row; color only repeats it.
  • A polite role="status" region says what changed: "Text the 12 guests: approved", "Copper is running 4 steps", "Text the 12 guests failed: Postbox couldn’t reach…", "Plan done".
  • After a decision the open step's buttons fold away, so focus moves to the next step it opened (or stays on the step).
  • Rows are 40px tall, and so are the buttons. In a plan narrower than 360px, Approve and Skip drop their key hints to stay on one line. In one narrower than 400px the header's status moves under the title, and the footer folds: the tally and Approve all (or Stop) share the top line and Run plan spans the card under them.
  • Long titles and details wrap; nothing is cut.

Before / after

Before: a wall of text with the plan as a numbered list, and "Reply yes to go ahead". The person can't take one step out, and once it runs there's no telling where it is.

TSX
<Message>  Here’s my plan: 1. Check the guest list 2. Draft a menu 3. Text the guests…  Shall I go ahead?</Message>

After: every step can be read, approved or skipped from the keyboard, and the same card shows the plan running, with a failure where it happened.

TSX
<PlanApproval  title="Copper’s plan for Saturday"  agent="Copper"  steps={plan.steps}  running={plan.running}  onDecide={(id, d) => agent.decide(id, d)}  onApproveAll={() => agent.approveAll()}  onRun={() => agent.run()}  onStop={() => agent.stop()}  onRetry={(id) => agent.retry(id)}/>