PixelDosa UI

AI Approval Gate

A checkpoint before a consequential agent action — what will happen, how much it touches, whether it can be undone, and three ways to answer.

High riskWaiting for your approval

Send the Q3 reconciliation summary to 243 contacts

Recipients
243
Attachments
2
Sends from
finance@acme.com

This cannot be undone.

Medium confidencevia the 14 August import batch

Three invoices share a billing contact, which accounts for the duplicate-payment warning. The summary explains the grouping and the corrected totals.

  1. 1.invoices/2026-q3.csv5 flagged rows, 3 sharing a contact.
  2. 2.ledger/august-import.json

Installation

bash
npx shadcn@latest add @pixeldosa/ai-approval-gate

Requires `cn` at `@/lib/utils`. Installing this pulls in AI Action Toolbar, AI Context Surface and Confidence Meter, which it composes. Phrase `action` as the consequence rather than the tool call — the whole component is worthless if the user cannot evaluate what they are approving.

Usage

"use client";

import { AIApprovalGate } from "@/components/ui/ai-approval-gate";

<AIApprovalGate
  action="Send the Q3 reconciliation summary to 243 contacts"
  risk="high"
  reversible={false}
  impact={[
    { label: "Recipients", value: "243" },
    { label: "Attachments", value: "2" },
  ]}
  confidence="medium"
  explanation="Three invoices share a billing contact, which accounts for the warning."
  onApprove={() => resume("approve")}
  onReject={() => resume("reject")}
  onEdit={() => openDraft()}
/>

Phrase action as the consequence, not the tool call. "Send the Q3 summary to 243 contacts" is reviewable; "Execute send_email" is not, and the whole component is worthless if the user can't evaluate what they're approving.

Features

  • reversible is required, not optional. Whether something can be taken back is the single most decision-relevant fact on the surface, and almost nothing surfaces it. It renders as a plain sentence — an icon is the wrong weight for the most consequential thing present.
  • The impact list is the scope. An approval without one is a yes/no question with the information removed. "243 recipients" is what makes it reviewable, in tabular figures so numbers align.
  • High risk changes the Approve button, not just a badge. A dangerous action must never look identical to a harmless confirmation.
  • Composes the trust primitives rather than re-deriving them — Confidence Meter, AI Context Surface, and AI Action Toolbar. The decision strip inherits real ARIA toolbar semantics, roving tabindex, and busy handling for free.
  • It holds perfectly still. Your turn. Pair with AgentPresence state="awaitingApproval".

Variants

High risk, irreversible

Approve carries the destructive treatment. The border shifts. The consequence is stated without hedging.

High riskWaiting for your approval

Delete 1,284 archived records from the production ledger

Records
1,284
Table
ledger_archive

This cannot be undone.

High confidence

Needs review, reversible

The default weight. Undo exists, so the surface doesn't shout.

Needs reviewWaiting for your approval

Apply the suggested category to 18 uncategorised transactions

Transactions
18

This can be undone afterwards.

Medium confidencevia 12 similar records

Low risk

Low riskWaiting for your approval

Add a note to the reconciliation thread

This can be undone afterwards.

With reasoning

Composed AI Context Surface — collapsed by default, because provenance should be available rather than forced into view.

High riskWaiting for your approval

Send the Q3 reconciliation summary to 243 contacts

Recipients
243
Attachments
2

This cannot be undone.

Medium confidencevia the 14 August import batch

Three invoices share a billing contact, which accounts for the duplicate-payment warning.

  1. 1.invoices/2026-q3.csv5 flagged rows, 3 sharing a contact.

Examples

Wiring to a LangGraph interrupt — the graph pauses on a checkpoint, you resume with the decision:

{interrupt ? (
  <AIApprovalGate
    action={interrupt.value.description}
    risk={interrupt.value.risk}
    reversible={interrupt.value.reversible}
    impact={interrupt.value.impact}
    onApprove={() => resume({ type: "accept" })}
    onReject={() => resume({ type: "reject" })}
    onEdit={() => resume({ type: "edit" })}
  />
) : null}

Omit onEdit when the payload genuinely can't be changed before it runs — offering an edit that does nothing is worse than not offering one:

<AIApprovalGate action="Rotate the API signing key" risk="high" reversible={false}
  onApprove={rotate} onReject={cancel} />

Reflecting a request in flight — busy is caller-owned, as everywhere else here:

<AIApprovalGate {...props} busy={pending} onApprove={approve} onReject={reject} />

Props

PropTypeDefaultDescription
actionstring—Required. The consequence, not the tool call.
reversibleboolean—Required. Rendered as a plain statement.
risk"low" | "medium" | "high""medium"high makes Approve destructive.
impact{ label, value }[]—The blast radius as countable facts.
confidence"low" | "medium" | "high"—Composed Confidence Meter.
provenancestring—Source caption beside the confidence.
explanation, sources——Composed AI Context Surface.
onApprove / onReject() => void—Required.
onEdit() => void—Only when the payload is genuinely editable.
busy"approve" | "reject" | null—Caller-owned in-flight state.
headingLevel2 | 3 | 4 | 5 | 63Heading level, so the component fits your page's outline.

Accessibility

  • A labelled <section>, with the action as its accessible name.
  • The decision strip is a real ARIA toolbar — one tab stop, arrow-key navigation, disabled actions skipped — inherited from AI Action Toolbar.
  • Impact facts are a <dl>, so the label/value relationship survives without visual layout.
  • Risk is carried by a text label as well as colour; irreversibility is a sentence, not an icon alone.
  • "Waiting for your approval" is announced through an aria-live region.

When not to use

Disambiguating between options the agent can't choose between is Agent Ask — that's "which did you mean?", where this is "may I do this?". Reviewing a text rewrite hunk by hunk is Diff Accept. And don't gate low-risk reversible actions; friction where it isn't warranted trains people to approve without reading.

why it's built this way

Engineering Notes

This is the payoff component for the system's trust thesis, and it is deliberately a composition rather than a new implementation: Confidence Meter, AI Context Surface and AI Action Toolbar are all consumed as-is, which is the 'compose existing trust primitives' rule (DESIGN.md §3.6) applied at the scale it was written for. Composing the toolbar rather than hand-rolling three buttons also means the decision strip inherits the real ARIA toolbar pattern — roving tabindex, arrow navigation, disabled-action skipping, busy handling — none of which a bespoke approval widget would have got right. Building it surfaced a genuine gap in that toolbar: it had no `destructive` intent, because nothing had needed one until an approval could be dangerous. Adding it there rather than special-casing here is the correct direction, since the house rule that a destructive action must never look identical to a harmless primary one applies system-wide. Two content decisions carry most of the value. `reversible` is a required prop rather than an optional flag: whether something can be undone determines how carefully the rest of the surface deserves to be read, and it renders as a plain sentence ('This cannot be undone') rather than an icon, because an icon is exactly the wrong weight for the most consequential fact present. The `impact` list exists because an approval without a scope is a yes/no question with the information removed — '243 recipients' is what makes the decision reviewable, and it uses tabular figures so the numbers align down the column.

how it moves, and when it doesn't

Motion Notes

Deliberately still. This is a user-turn surface under the turn-taking grammar (DESIGN.md §3a): motion means the machine is busy, and animating an approval gate would put it in the same visual register as the agent's own working states — precisely backwards, since nothing here proceeds until the user acts. The only motion is inherited: the composed AI Action Toolbar's colour transitions on hover and focus at `--pd-duration-instant`, and its busy spinner while an approve or reject is in flight, which maps to a real request rather than decorating a wait. AI Context Surface brings its own grid-template-rows disclosure transition when expanded. Nothing animates on entrance — that belongs to whatever surface reveals this gate, the same rule Confidence Meter and AI Action Toolbar hold. Under prefers-reduced-motion every inherited transition drops to instant and the component loses nothing, because it never carried information in movement to begin with.