Approval

Pro

A permission request from an agent: what it wants to do, why, how risky it is, and how long the answer holds. Allow, deny with a reason, edit first or let it expire; decided, the card folds into a one-line receipt you can keep in the thread.

The docs preview is serving stale pages. Can you rebuild it?

The build cache predates your last two commits. I need to clear it and rebuild:

Run a shell command

Deletes the build cache, then rebuilds the docs site.

Destructive
Command
rm -rf .next/cache && pnpm build --filter docs
Allow

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Activate your Pro license (once per machine)npx koalaui-cli@latest login <key># Add this component (its dependencies come along)npx koalaui-cli@latest add approval

Usage#

This example is part of Koala UI Pro

The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.

Have a license? Unlock it here

Anatomy#

A header that says what and why, a preview sheet that shows exactly what will happen, an optional scope row, and the actions. Once decided, everything below the title folds away on one timing and the decision unfolds in the description's place, so the card you answered becomes the receipt instead of being swapped for one.

import {  Approval,  ApprovalHeader,  ApprovalIcon,  ApprovalHeading,  ApprovalTitle,  ApprovalDescription,  ApprovalDecision,  ApprovalRisk,  ApprovalExpiry,  ApprovalPreview,  ApprovalScope,  ApprovalActions,  ApprovalEdit,  ApprovalDeny,  ApprovalApprove,} from "@/components/ui/approval"<Approval>  <ApprovalHeader>    <ApprovalIcon />    <ApprovalHeading>      <ApprovalTitle />      <ApprovalDescription />      <ApprovalDecision />    </ApprovalHeading>    <ApprovalRisk />    <ApprovalExpiry />  </ApprovalHeader>  <ApprovalPreview />  <ApprovalScope />  <ApprovalActions>    <ApprovalEdit />    <ApprovalDeny />    <ApprovalApprove />  </ApprovalActions></Approval>

Command#

ApprovalCommand shows the exact command on a CodeSnippet sheet, wrapped rather than scrolled, so nothing hides past the edge of the card.

Run the test suite

Checks the auth changes before I open the pull request.

Makes changes
Command
pnpm test --filter auth -- --coverage

File diff#

ApprovalDiff marks the lines that change, with real line numbers and a Show more clamp for long excerpts. Label the sheet with the file path.

Edit session.ts

Shortens idle sessions to 12 hours, per the security review.

Makes changes
lib/auth/session.ts
export const session = {  cookie: "koala_session",Removed:   maxAge: 60 * 60 * 24 * 7,Added:   maxAge: 60 * 60 * 12,Added:   rolling: true,}

Email draft#

ApprovalEmail lays out recipients and subject like a mail header, then the body as it will be sent.

Send a reminder

Invoice 2291 is five days past due.

Leaves workspace
Email
To
dana@northwind.co
Cc
ops@koalaui.com
Subject
Invoice 2291 is overdue

Hi Dana, a quick reminder that invoice 2291 ($1,240.00) was due on October 28. You can pay it from the billing page in a minute. Thanks!

Payment#

ApprovalPayment puts the amount first, large and in tabular figures, and formats it with a fixed locale so the server and the browser agree. Say the amount on the button too: a person should never approve a charge they did not read.

Upgrade to Business

The team hit the 10-seat limit this morning.

Spends money
Payment
$1,240.00
Merchant
Linear
Plan
Business, 20 seats, annual
Card
Visa ending 4242

Parameters#

For any other tool, ApprovalParams lists the arguments as key and value rows. Anything richer goes in a bare ApprovalPreview, which is just the sheet.

Query the warehouse

To chart refunds against the September release.

Read only
Parameters
table
orders
where
created_at > now() - 30d
limit
500

Risk levels#

Five levels, set once on the root. ApprovalRisk words and tones the chip from it. Destructive requests get the soft destructive fill on Approve, and destructive and financial requests ignore the Enter shortcut: those take a deliberate press.

Query the orders table

Read only, last 30 days.

Read only

Edit 6 files in src/

The refactor from the plan.

Makes changes

Post to #launches

Visible to 48 people in Slack.

Leaves workspace

Renew the domain

$14.00 on the Visa ending 4242.

Spends money

Drop the staging database

Cannot be undone.

Destructive

Scope#

ApprovalScope asks how long the yes holds: once, for this session, or always for this tool. It is a segmented ToggleGroup bound to the root's scope, and the choice comes back in onApprove.

Edit files in src/

Koala wants to apply the refactor it planned across 6 files.

Makes changes
Allow

Waiting for a decision

Expiry#

An agent waiting on a person should not wait forever. ApprovalExpiry counts down in tabular figures, turns to the warning ink for the last ten seconds and expires the request at zero, which calls onExpire. Nothing runs.

Send the weekly digest

Scheduled run. Nothing is sent unless someone allows it.

0:15

Edit before approving#

ApprovalEdit switches the card to editing. The command and email previews turn into editors in the same type, Approve becomes Allow edited, and the edited text arrives as edited in onApprove. Esc or Discard edits goes back to the original. For your own preview, read and write the draft with useApproval().

Send an email

A launch update for Dana, drafted from this thread.

Leaves workspace
Email
To
dana@northwind.co
Subject
Pricing page: first week

Hi Dana, The pricing page shipped on Tuesday. Trials are up 18% week over week, and the new annual plan already accounts for a third of upgrades. Full numbers are in the Q3 dashboard. Jordi

Deny with a reason#

A bare no leaves the agent guessing. With withReason, the first press on Deny opens a field in place, above the buttons; Enter or a second press denies and hands the text to onDeny, so the agent can change course. The reason is optional and Esc closes the field.

Delete 3 drafts

They are superseded by pricing/q3-final.md.

Destructive
Files
drafts/q3-pricing-v1.mddrafts/q3-pricing-v2.mddrafts/q3-pricing-old.md

Receipts#

Every decided state, rendered from stored data. Pass the state, the scope or reason and the time, and the card renders as a receipt from the first frame with no animation.

Run pnpm test

Allowed once

Edit files in src/

Allowed for this session

Read files

Always allowed

Drop the staging database

Denied: use a dry run first

Send the weekly digest

Expired, nothing ran

Queue#

When an agent stacks up several requests, show one at a time with a count, and keep the answered ones as receipts above it. The next card takes focus, so Enter and Esc work down the queue without the mouse (the payment still takes a press).

Waiting on you1 of 3

Edit pricing.tsx

Raise the Team plan to $24 per seat, as agreed in the planning doc.

Makes changes
app/pricing/plans.ts
  team: {Removed:     price: 19,Added:     price: 24,    seats: "per seat",

API reference#

Approval

The request, and the context every part reads. A role="group" <div> labelled by its title and described by its description, with a polite live region that announces the request and the decision. Every part accepts className, merged last, and every part is droppable.

PropTypeDefaultDescription
state"pending" | "editing" | "approved" | "denied" | "expired"-Controlled state. The last three are decided: the body folds away and ApprovalDecision unfolds.
defaultStateApprovalState"pending"Initial state when uncontrolled.
onStateChange(state: ApprovalState) => void-Called on every transition.
risk"read" | "write" | "destructive" | "financial" | "external"-Feeds ApprovalRisk. destructive turns ApprovalApprove into the soft destructive fill; destructive and financial also switch off the Enter shortcut.
scope"once" | "session" | "always" | string-Controlled scope of the grant.
defaultScopeApprovalScopeValue"once"Initial scope when uncontrolled.
onScopeChange(scope: ApprovalScopeValue) => void-Called when ApprovalScope changes.
onApprove({ scope, edited }) => void-Called once on approve. edited is the edited text when the person changed the preview, else undefined.
onDeny({ reason }) => void-Called once on deny, by button or Esc. reason is the trimmed steer text, if any.
onExpire() => void-Called when ApprovalExpiry runs out while the request is open.
decidedAtDate-Time shown on the receipt. Stamped automatically on a decision made in the card.
reasonstring-Denial reason for a receipt rendered from stored data.
announcementstring"Approval requested"What the live region says when the card appears.
variant"card" | "plain""card"card is the tinted frame Tool uses; plain drops it for a Chat turn or an inbox row.
density"comfortable" | "compact"-Frame padding and sheet insets. Inherits the nearest DensityProvider (compact with none).
autoFocusboolean-Focuses the card on mount, so Enter and Esc answer it straight away.
Data attributeDescription
data-stateThe current state.
data-riskThe risk level, when set.
data-decidedPresent once approved, denied or expired.
data-denyingPresent while the reason field is open.

ApprovalHeader

The top line: icon, heading and any trailing risk or expiry. Forwards native <div> props.

ApprovalIcon

The 16px column. Pass the tool's glyph; it morphs to a check, a prohibit sign or a timer once decided. aria-hidden. Renders a <span>.

ApprovalHeading

Stacks the title, description and decision. Forwards native <div> props.

ApprovalTitle

What the agent wants to do. Labels the card. Renders a <p>.

ApprovalDescription

Why it wants to. Describes the card, and folds away once decided. Renders a <div>.

ApprovalRisk

A Badge worded and toned from the root's risk. Forwards Badge props.

PropTypeDefaultDescription
levelApprovalRiskLevel-Overrides the root's risk for this chip only.
variantBadgeProps["variant"]-Overrides the tone.
size"sm" | "md" | "lg""sm"Badge size.
Data attributeDescription
data-levelThe resolved risk level.

ApprovalExpiry

A role="timer" countdown in tabular-nums. Calls the root's expire when it reaches zero, and unmounts once decided.

PropTypeDefaultDescription
secondsnumber-Counts down from mount. Deterministic on the server.
deadlineDate | number-An absolute deadline instead, for a request created elsewhere.
urgentAtnumber10Seconds left when the read-out turns to the warning ink.
children(remaining: number) => React.ReactNode-Your own read-out instead of m:ss.
Data attributeDescription
data-urgentPresent at or under urgentAt.

ApprovalPreview

The sheet for what will happen, set into the frame like Tool's input sheet. Folds away once decided.

PropTypeDefaultDescription
labelReact.ReactNode-The sheet's label.

ApprovalCommand

A preview for a shell command, highlighted by CodeSnippet. Becomes an inline editor in editing.

PropTypeDefaultDescription
commandstring-The command.
langstring"bash"CodeSnippet language.
labelReact.ReactNode"Command"The sheet's label.

ApprovalDiff

A preview for a file change: CodeSnippet with line numbers, diff marks and Show more.

PropTypeDefaultDescription
codestring-The excerpt.
addednumber[]-Absolute line numbers to mark as added.
removednumber[]-Absolute line numbers to mark as removed.
startLinenumber1First line number.
langstring"tsx"CodeSnippet language.
collapsedHeightnumber220The clamp, in px.
labelReact.ReactNode"Changes"Usually the file path.

ApprovalEmail

A preview for an outgoing message: recipients and subject as a DescriptionList, then the body. The body is editable in editing.

PropTypeDefaultDescription
toReact.ReactNode-Recipients.
ccReact.ReactNode-Copied recipients.
subjectReact.ReactNode-Subject line.
bodystring-The message.

ApprovalPayment

A preview for a charge: the amount large and in tabular-nums, then the details.

PropTypeDefaultDescription
amountnumber-In major units.
currencystring"USD"ISO 4217 code.
localestring"en-US"Fixed so server and client format alike.
fieldsRecord<string, ReactNode> | [ReactNode, ReactNode][]-Merchant, card and so on.

ApprovalParams

A preview for raw tool arguments as key and value rows.

PropTypeDefaultDescription
fieldsRecord<string, ReactNode> | [ReactNode, ReactNode][]-The arguments.
monobooleantrueValues in the mono face.

ApprovalScope

How long the yes holds, as a segmented ToggleGroup. Reads and writes the root's scope.

PropTypeDefaultDescription
labelReact.ReactNode"Allow"The row's label.
options{ value, label }[]Once, This session, AlwaysThe choices. Name the tool in the last one when it helps.

ApprovalActions

The decision row, plus the reason field ApprovalDeny opens above it. Folds away once decided.

PropTypeDefaultDescription
reasonPlaceholderstring"Tell the agent what to do instead (optional)"Placeholder of the reason field.
reasonLabelstring"Reason for denying"Accessible name of the reason field.

ApprovalApprove

The primary action. Allow by default, Allow edited while editing; the soft destructive fill for destructive risk. Forwards Button props.

ApprovalDeny

The ghost Deny. Forwards Button props.

PropTypeDefaultDescription
withReasonbooleanfalseThe first press opens the reason field in place; the second, or Enter in the field, denies.
cancelLabelReact.ReactNode"Cancel"The button that closes the field again.

ApprovalEdit

Toggles editing, where the preview helpers turn editable. Edit, then Discard edits. Forwards Button props.

ApprovalDecision

The receipt. Folded while open, it unfolds where the description was. Worded from the state, scope, edits and reason.

PropTypeDefaultDescription
timeReact.ReactNode-Overrides the HH:MM read from decidedAt.
childrenReact.ReactNode-Overrides the wording.

FAQ#

ToolApproval is the inline yes or no on a call the person is already reading, inside the Tool frame. Approval is the standalone object for higher-stakes requests: it carries the why, the risk, a preview, the scope of the grant, an expiry and a receipt, and it works on its own in a chat thread, an agent inbox, an Agent Monitor or a notification.