Approval
ProA 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.
Installation#
Usage#
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.
pnpm test --filter auth -- --coverageFile 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.
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.
- 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.
- 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.
- 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.
Edit 6 files in src/
The refactor from the plan.
Post to #launches
Visible to 48 people in Slack.
Renew the domain
$14.00 on the Visa ending 4242.
Drop the staging database
Cannot be undone.
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.
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.
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.
- 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.
drafts/q3-pricing-v1.md
drafts/q3-pricing-v2.md
drafts/q3-pricing-old.mdReceipts#
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).
Edit pricing.tsx
Raise the Team plan to $24 per seat, as agreed in the planning doc.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| state | "pending" | "editing" | "approved" | "denied" | "expired" | - | Controlled state. The last three are decided: the body folds away and ApprovalDecision unfolds. |
| defaultState | ApprovalState | "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. |
| defaultScope | ApprovalScopeValue | "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. |
| decidedAt | Date | - | Time shown on the receipt. Stamped automatically on a decision made in the card. |
| reason | string | - | Denial reason for a receipt rendered from stored data. |
| announcement | string | "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). |
| autoFocus | boolean | - | Focuses the card on mount, so Enter and Esc answer it straight away. |
| Data attribute | Description |
|---|---|
| data-state | The current state. |
| data-risk | The risk level, when set. |
| data-decided | Present once approved, denied or expired. |
| data-denying | Present 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| level | ApprovalRiskLevel | - | Overrides the root's risk for this chip only. |
| variant | BadgeProps["variant"] | - | Overrides the tone. |
| size | "sm" | "md" | "lg" | "sm" | Badge size. |
| Data attribute | Description |
|---|---|
| data-level | The resolved risk level. |
ApprovalExpiry
A role="timer" countdown in tabular-nums. Calls the root's expire when it reaches zero, and unmounts once decided.
| Prop | Type | Default | Description |
|---|---|---|---|
| seconds | number | - | Counts down from mount. Deterministic on the server. |
| deadline | Date | number | - | An absolute deadline instead, for a request created elsewhere. |
| urgentAt | number | 10 | Seconds left when the read-out turns to the warning ink. |
| children | (remaining: number) => React.ReactNode | - | Your own read-out instead of m:ss. |
| Data attribute | Description |
|---|---|
| data-urgent | Present at or under urgentAt. |
ApprovalPreview
The sheet for what will happen, set into the frame like Tool's input sheet. Folds away once decided.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | React.ReactNode | - | The sheet's label. |
ApprovalCommand
A preview for a shell command, highlighted by CodeSnippet. Becomes an inline editor in editing.
| Prop | Type | Default | Description |
|---|---|---|---|
| command | string | - | The command. |
| lang | string | "bash" | CodeSnippet language. |
| label | React.ReactNode | "Command" | The sheet's label. |
ApprovalDiff
A preview for a file change: CodeSnippet with line numbers, diff marks and Show more.
| Prop | Type | Default | Description |
|---|---|---|---|
| code | string | - | The excerpt. |
| added | number[] | - | Absolute line numbers to mark as added. |
| removed | number[] | - | Absolute line numbers to mark as removed. |
| startLine | number | 1 | First line number. |
| lang | string | "tsx" | CodeSnippet language. |
| collapsedHeight | number | 220 | The clamp, in px. |
| label | React.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.
| Prop | Type | Default | Description |
|---|---|---|---|
| to | React.ReactNode | - | Recipients. |
| cc | React.ReactNode | - | Copied recipients. |
| subject | React.ReactNode | - | Subject line. |
| body | string | - | The message. |
ApprovalPayment
A preview for a charge: the amount large and in tabular-nums, then the details.
| Prop | Type | Default | Description |
|---|---|---|---|
| amount | number | - | In major units. |
| currency | string | "USD" | ISO 4217 code. |
| locale | string | "en-US" | Fixed so server and client format alike. |
| fields | Record<string, ReactNode> | [ReactNode, ReactNode][] | - | Merchant, card and so on. |
ApprovalParams
A preview for raw tool arguments as key and value rows.
| Prop | Type | Default | Description |
|---|---|---|---|
| fields | Record<string, ReactNode> | [ReactNode, ReactNode][] | - | The arguments. |
| mono | boolean | true | Values in the mono face. |
ApprovalScope
How long the yes holds, as a segmented ToggleGroup. Reads and writes the root's scope.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | React.ReactNode | "Allow" | The row's label. |
| options | { value, label }[] | Once, This session, Always | The 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| reasonPlaceholder | string | "Tell the agent what to do instead (optional)" | Placeholder of the reason field. |
| reasonLabel | string | "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.
| Prop | Type | Default | Description |
|---|---|---|---|
| withReason | boolean | false | The first press opens the reason field in place; the second, or Enter in the field, denies. |
| cancelLabel | React.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.
| Prop | Type | Default | Description |
|---|---|---|---|
| time | React.ReactNode | - | Overrides the HH:MM read from decidedAt. |
| children | React.ReactNode | - | Overrides the wording. |