Approval Prompt
betaHuman-in-the-loop permission card (“Kura wants to run `rm -rf build`”) with risk level, details, Approve/Deny/Always allow, ⌘↵ and Esc, and resolved states.
Low-risk command
minimal
Kura wants to run a command
Low riskpnpm installNeeds approval
High-risk, every option
Description, details, controlled decision, callbacks, Always allow, custom labels and ⌘↵ / Esc.
full
Kura wants to delete a directory
High riskClears stale output before rebuilding. Affects 1,204 files under ./build only.
rm -rf build- Working directory: ~/code/kura-ui
- Requested by step 3 of 5, “Rebuild the docs”
- Not tracked by git — cannot be restored from history
Needs approval
Try ⌘↵ or Esc
Resolved states
Approved, denied and always-allowed, as they appear in history.
state
Kura wants to run a command
Medium riskgit push origin mainApproved
Kura wants to run a command
High riskcurl -X DELETE https://api.example.com/v1/projects/42Denied
Kura wants to read a file
Low riskcat .env.exampleAlways allowed· won't ask again this session
Approve a file edit
A diff CodeBlock in the details, driving a ToolCall's status.
composition
Edit src/config.ts+2 −1, Waiting for approval
edit_file Waiting for approvalApply this edit?
Low riskNeeds approval
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/approval-prompt.jsonCopies the source into components/kura/approval-prompt with its dependencies (kura-theme, button, kbd, use-controllable-state, use-hotkey, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- An agent needs explicit permission before a side effect: running a shell command, editing files, sending email, spending money, calling a destructive API.
- Show exactly what will run (verbatim command) and how risky it is before the user decides.
- Offer “Always allow” for a class of actions in this session (persist the rule yourself in onAlwaysAllow).
- Keep the decision visible in the transcript afterwards (Approved / Denied / Always allowed) without the card changing height.
Reach for something else when
- Showing a tool call that does not need permission — use tool-call (it also has an awaiting-approval status for compact inline approvals).
- Generic confirm dialogs unrelated to an agent (deleting a document) — use a dialog.
- Progress of a multi-step plan — use agent-timeline.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title* | React.ReactNode | — | What the agent wants, e.g. "Kura wants to run a command". Labels the region. |
| command | string | — | The exact command/action, shown verbatim in mono after a $ prompt (wraps, never truncated). |
| description | React.ReactNode | — | Why and what it touches; becomes the region's accessible description. |
| risk | "low" | "medium" | "high" | "medium" | Badge (green/amber/red dot). high also turns Approve into a red “Run anyway” button. |
| details | React.ReactNode | — | Content behind a “Details” expander: a diff (CodeBlock), arguments, affected files. |
| decision | "pending" | "approved" | "denied" | "always" | — | Decision (controlled). |
| defaultDecision | "pending" | "approved" | "denied" | "always" | "pending" | Initial decision when uncontrolled (use a resolved value to render history). |
| onDecisionChange | (decision: ApprovalDecision) => void | — | Called with every decision. |
| onApprove | () => void | — | Called on Approve (button or ⌘/Ctrl+Enter). |
| onDeny | () => void | — | Called on Deny (button or Esc). |
| onAlwaysAllow | () => void | — | Shows an “Always allow” ghost button; called when chosen. |
| shortcuts | boolean | true | Bind ⌘/Ctrl+Enter (approve) and Esc (deny) on the document while pending, and show the keys on the buttons. Only one prompt on screen should bind them. |
| approveLabel | string | — | Approve label. Default “Approve”, or “Run anyway” for high risk. |
| denyLabel | string | "Deny" | Deny label. |
| alwaysAllowLabel | string | "Always allow" | Always-allow label. |
| icon | React.ReactNode | — | Icon in the header tile. Default a terminal. |
All native attributes of the root element, className and ref pass through.
Anatomy
- ApprovalPrompt<section> labelled by the title and the command (so stacked prompts have distinct names). `data-decision="pending" | "approved" | "denied" | "always"`, `data-risk`.
data-slot="approval-prompt" - TitleHeading; mutes once resolved.
data-slot="approval-prompt-title" - RiskRisk badge with a coloured dot and text.
data-slot="approval-prompt-risk" - CommandMono command well; the <code> is `approval-prompt-code`.
data-slot="approval-prompt-command" - DetailsCollapsible details (grid-rows transition, inert when closed).
data-slot="approval-prompt-details" - FooterFixed-height footer holding actions and the resolution, so resolving never shifts layout.
data-slot="approval-prompt-footer" - WaitingJade “Needs approval” pill — the view's one accent while pending.
data-slot="approval-prompt-waiting" - ActionsAlways allow / Deny (Esc) / Approve (⌘↵); inert once resolved.
data-slot="approval-prompt-actions" - Resolutionrole="status" line that cross-fades in: Approved / Denied / Always allowed.
data-slot="approval-prompt-resolution"
Accessibility
- Role:
region - A <section> labelled by the title and described by the description, so screen-reader users land on the whole request.
- Buttons expose their shortcuts with aria-keyshortcuts; keycaps are also rendered visually.
- The resolution is a status region, so the decision is announced; the action row becomes inert and hidden.
- Risk is conveyed in text (“High risk”), not only colour. No dark patterns: Deny is as prominent as Approve.
- Reduced motion: The waiting pulse stops and resolved states cross-fade without blur; details open without height travel.
| Keys | Action |
|---|---|
| Mod+Enter | Approve (while pending, when shortcuts is on) |
| Escape | Deny (while pending, when shortcuts is on; not while typing in an input) |
| Tab | Details, Always allow, Deny, Approve |
Notes for agents
- Show the command verbatim — never paraphrase what will run.
- The decision is only UI state: actually gate the side effect on onApprove / onDecisionChange in your agent loop.
- Render past prompts with `defaultDecision` (or controlled `decision`) set to the stored outcome and `shortcuts={false}`.
- When several prompts are visible, enable `shortcuts` on the newest pending one only.