Tool Approval
The consent prompt before a tool runs. The arguments are shown in full above the buttons, because “Allow issue_refund?” asks you to approve a function name — what matters is that it wants to move £3,400.
src/components/ui/tool-approval.tsxAbout
From the component's own source, where it was written.
The consent prompt before a tool runs.
The most consequential component in an agent UI, and the one most often built wrong. Three rules it follows:
**The arguments are shown, in full, before the buttons.** A prompt that says "Allow issue_refund?" is asking you to approve a function name. What matters is that it wants to refund £3,400 to an account added this morning, and that is in the arguments.
**"Always allow" is scoped and labelled as a standing grant.** It is the option people click by reflex, so it says what it will do next time rather than reading as a slightly faster "yes".
**Deny is not styled as the dangerous choice.** Declining a tool call is always safe; running one may not be. A destructive tool puts the emphasis on approve, not on cancel, so the visual weight matches the actual risk.
There is no default focus on approve, and no timeout that auto-approves.
Install
npx astralyx-ui add tool-approval
Usage
import { ToolApproval } from '@/components/ui/tool-approval'
<ToolApproval
tool="issue_refund"
origin="billing-mcp"
destructive
args={call.arguments}
onDecide={(decision) => resolve(decision)}
/>Composer
Change the props and the source below follows.
issue_refundDestructivebilling-mcpMoves money out of the merchant account.
Arguments
{
"customer_id": "cus_8812",
"amount": {
"value": 340000,
"currency": "GBP"
},
"reason": "requested_by_customer"
}<ToolApproval
tool="issue_refund"
origin="billing-mcp"
destructive
args={call.arguments}
onDecide={resolve}
/>Examples
A destructive call
<ToolApproval tool="issue_refund" destructive args={args} onDecide={resolve} />An ordinary one
<ToolApproval tool="search_docs" args={args} onDecide={resolve} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
ToolApproval
Extends Omit<ComponentProps<'div'>, 'onSelect'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| tool | string | required | The tool the agent wants to call. |
| origin | string | — | Which server or agent is asking. |
| description | ReactNode | — | — |
| args | unknown | — | Rendered in full, above the buttons. This is the thing actually being approved; the tool name is the label on the envelope. |
| destructive | boolean | false | Puts the weight on the approve action rather than on cancel, so the emphasis matches the actual risk. |
| onDecide | (decision: ToolApprovalDecision) => void | required | Three outcomes, not two. “Always” is a standing grant and says what it will do next time rather than reading as a faster yes. |
| busy | boolean | false | Disables the buttons while the decision is being applied. |
| onceLabel | string | 'Allow once' | — |
| alwaysLabel | string | 'Always allow' | — |
| denyLabel | string | 'Deny' | — |
| alwaysHint | (tool: string) => ReactNode | (name) => `${name} will run without asking again in this project.` | Explains the standing grant. Receives the tool name. |
| argsLabel | string | 'Arguments' | — |
| destructiveLabel | string | 'Destructive' | — |
Types
| Type | Definition | Description |
|---|---|---|
ToolApprovalDecision | 'once' | 'always' | 'deny' | The consent prompt before a tool runs. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| deny styling | ghost, never destructive | Declining a tool call is always safe; running one may not be. Styling deny as the dangerous choice inverts that. |
| no auto-approve | by design | There is no timeout and no default focus on approve. |