# Approval Prompt

> Human-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.

- **Slug:** `approval-prompt` · **Category:** agent · **Status:** beta
- **Import:** `import { ApprovalDecision, ApprovalPrompt } from "@kura-ui/react/approval-prompt";`
- **Docs:** https://kuro.abstergo.fyi/components/approval-prompt
- **Registry item:** https://kuro.abstergo.fyi/r/approval-prompt.json
- **Tags:** approval, permission, human-in-the-loop, confirm, agent, security, shortcuts, consent

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/approval-prompt.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add approval-prompt
# npm package
npm install @kura-ui/react
```

## When to use

- 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.

## When not to use

- 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 | Required | Description |
| --- | --- | --- | --- | --- |
| `title` | `React.ReactNode` | — | yes | What the agent wants, e.g. "Kura wants to run a command". Labels the region. |
| `command` | `string` | — | no | The exact command/action, shown verbatim in mono after a $ prompt (wraps, never truncated). |
| `description` | `React.ReactNode` | — | no | Why and what it touches; becomes the region's accessible description. |
| `risk` | `"low" \| "medium" \| "high"` | `"medium"` | no | Badge (green/amber/red dot). high also turns Approve into a red “Run anyway” button. |
| `details` | `React.ReactNode` | — | no | Content behind a “Details” expander: a diff (CodeBlock), arguments, affected files. |
| `decision` | `"pending" \| "approved" \| "denied" \| "always"` | — | no | Decision (controlled). |
| `defaultDecision` | `"pending" \| "approved" \| "denied" \| "always"` | `"pending"` | no | Initial decision when uncontrolled (use a resolved value to render history). |
| `onDecisionChange` | `(decision: ApprovalDecision) => void` | — | no | Called with every decision. |
| `onApprove` | `() => void` | — | no | Called on Approve (button or ⌘/Ctrl+Enter). |
| `onDeny` | `() => void` | — | no | Called on Deny (button or Esc). |
| `onAlwaysAllow` | `() => void` | — | no | Shows an “Always allow” ghost button; called when chosen. |
| `shortcuts` | `boolean` | `true` | no | 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` | — | no | Approve label. Default “Approve”, or “Run anyway” for high risk. |
| `denyLabel` | `string` | `"Deny"` | no | Deny label. |
| `alwaysAllowLabel` | `string` | `"Always allow"` | no | Always-allow label. |
| `icon` | `React.ReactNode` | — | no | Icon in the header tile. Default a terminal. |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **ApprovalPrompt** (`data-slot="approval-prompt"`) — <section> labelled by the title and the command (so stacked prompts have distinct names). `data-decision="pending" | "approved" | "denied" | "always"`, `data-risk`.
- **Title** (`data-slot="approval-prompt-title"`) — Heading; mutes once resolved.
- **Risk** (`data-slot="approval-prompt-risk"`) — Risk badge with a coloured dot and text.
- **Command** (`data-slot="approval-prompt-command"`) — Mono command well; the <code> is `approval-prompt-code`.
- **Details** (`data-slot="approval-prompt-details"`) — Collapsible details (grid-rows transition, inert when closed).
- **Footer** (`data-slot="approval-prompt-footer"`) — Fixed-height footer holding actions and the resolution, so resolving never shifts layout.
- **Waiting** (`data-slot="approval-prompt-waiting"`) — Jade “Needs approval” pill — the view's one accent while pending.
- **Actions** (`data-slot="approval-prompt-actions"`) — Always allow / Deny (Esc) / Approve (⌘↵); inert once resolved.
- **Resolution** (`data-slot="approval-prompt-resolution"`) — role="status" line that cross-fades in: Approved / Denied / Always allowed.

## 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.

## Examples

### Low-risk command (minimal)

Title, command and risk.

```tsx
import { ApprovalPrompt } from "@kura-ui/react/approval-prompt";

export function ApprovalPromptMinimal() {
  return (
    <ApprovalPrompt
      title="Kura wants to run a command"
      command="pnpm install"
      risk="low"
      shortcuts={false}
      className="w-full max-w-lg"
    />
  );
}
```

### High-risk, every option (full)

Description, details, controlled decision, callbacks, Always allow, custom labels and ⌘↵ / Esc.

```tsx
"use client";

import { RotateCcw } from "lucide-react";
import * as React from "react";
import { Button } from "@kura-ui/react/button";
import { type ApprovalDecision, ApprovalPrompt } from "@kura-ui/react/approval-prompt";

export function ApprovalPromptFull() {
  const [decision, setDecision] = React.useState<ApprovalDecision>("pending");
  const [log, setLog] = React.useState<string[]>([]);
  const note = (s: string) => setLog((l) => [...l.slice(-2), s]);
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <ApprovalPrompt
        title="Kura wants to delete a directory"
        command="rm -rf build"
        description="Clears stale output before rebuilding. Affects 1,204 files under ./build only."
        risk="high"
        details={
          <ul className="flex list-disc flex-col gap-1 pl-4">
            <li>Working directory: ~/code/kura-ui</li>
            <li>Requested by step 3 of 5, “Rebuild the docs”</li>
            <li>Not tracked by git — cannot be restored from history</li>
          </ul>
        }
        decision={decision}
        onDecisionChange={setDecision}
        onApprove={() => note("approved")}
        onDeny={() => note("denied")}
        onAlwaysAllow={() => note("always allowed")}
        shortcuts
        approveLabel="Run anyway"
        denyLabel="Deny"
        alwaysAllowLabel="Always allow"
      />
      <div className="flex items-center justify-between">
        <span className="font-kura-mono text-kura-fg-subtle text-xs">
          {log.length ? log.join(" → ") : "Try ⌘↵ or Esc"}
        </span>
        <Button
          variant="ghost"
          size="sm"
          disabled={decision === "pending"}
          onClick={() => setDecision("pending")}
        >
          <RotateCcw /> Reset
        </Button>
      </div>
    </div>
  );
}
```

### Resolved states (state)

Approved, denied and always-allowed, as they appear in history.

```tsx
"use client";

import { ApprovalPrompt } from "@kura-ui/react/approval-prompt";

export function ApprovalPromptResolved() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <ApprovalPrompt
        title="Kura wants to run a command"
        command="git push origin main"
        risk="medium"
        defaultDecision="approved"
        shortcuts={false}
      />
      <ApprovalPrompt
        title="Kura wants to run a command"
        command="curl -X DELETE https://api.example.com/v1/projects/42"
        risk="high"
        defaultDecision="denied"
        shortcuts={false}
      />
      <ApprovalPrompt
        title="Kura wants to read a file"
        command="cat .env.example"
        risk="low"
        defaultDecision="always"
        onAlwaysAllow={() => {}}
        shortcuts={false}
      />
    </div>
  );
}
```

### Approve a file edit (composition)

A diff CodeBlock in the details, driving a ToolCall's status.

```tsx
"use client";

import { FilePen } from "lucide-react";
import * as React from "react";
import { CodeBlock } from "@kura-ui/react/code-block";
import { ToolCall } from "@kura-ui/react/tool-call";
import { type ApprovalDecision, ApprovalPrompt } from "@kura-ui/react/approval-prompt";

const DIFF = `@@ -1,4 +1,5 @@
 export const config = {
-  retries: 1,
+  retries: 3,
+  backoff: "exponential",
   timeout: 12_000,
 };`;

export function ApprovalPromptWithDiff() {
  const [decision, setDecision] = React.useState<ApprovalDecision>("pending");
  return (
    <div className="flex w-full max-w-lg flex-col gap-2">
      <ToolCall
        name="edit_file"
        title="Edit src/config.ts"
        icon={<FilePen />}
        status={
          decision === "pending" ? "awaiting-approval" : decision === "denied" ? "error" : "success"
        }
        error={decision === "denied" ? "Denied by user." : undefined}
        duration={decision === "approved" ? 240 : undefined}
        summary="+2 −1"
      />
      <ApprovalPrompt
        title="Apply this edit?"
        icon={<FilePen />}
        risk="low"
        decision={decision}
        onDecisionChange={setDecision}
        shortcuts={false}
        approveLabel="Apply"
        details={<CodeBlock code={DIFF} language="ts" diff header={false} />}
      />
    </div>
  );
}
```

## Dependencies

- npm: `lucide-react`
- Kura registry items: `kura-theme`, `button`, `kbd`, `use-controllable-state`, `use-hotkey`, `utils`
- Related: `tool-call`, `agent-timeline`, `code-block`, `kbd-shortcut`
