Mischief

118 / Agent UI

Tool Approval

Asks before an agent runs a tool. Shows what the call will do, offers Allow once, Always allow and Deny, and leaves a one-line record of the answer.

The agent is cleaning up after the 0.8 release.

Allow read_file to run?

path
src/app/settings/page.tsx
lines
1-120

Allow delete_branch to run?

This can make changes that are hard to undo.

Deletes release/0.7 on origin. Anyone who has not pulled it loses the branch.

Press and hold to allow. With a keyboard, hold Enter or Space.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/tool-approval
import { ToolApproval } from "mischief-ui/tool-approval"
Also installs
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/tool-approval/tool-approval.tsx
"use client" import * as React from "react"import { Ban, Check, ChevronRight, Loader, ShieldCheck } from "lucide-react" import { cn } from "@/lib/utils" export type ToolApprovalDecision = "allow-once" | "allow-always" | "deny" export type ToolApprovalProps = Omit<  React.HTMLAttributes<HTMLDivElement>,  "children" | "input"> & {  /** The tool the agent wants to run, such as `delete_branch`. */

Usage

export function Permission({ call }: { call: PendingCall }) {
  return (
    <ToolApproval
      name={call.tool}
      input={call.arguments}
      destructive={call.tool === "delete_branch"}
      onDecision={(decision) => agent.respond(call.id, decision)}
    />
  )
}

Three answers, then a record

onDecision receives "allow-once", "allow-always" or "deny". Remembering an Always allow is your job; the card only reports it. When the callback returns a promise the card shows it is sending, disables every choice, and waits. A rejection puts the choices back with a short error, so a dropped request never reads as an answer.

Once the answer lands the card does not vanish. It shrinks to one line, such as Allowed once read_file, so the transcript still says who let what run. Focus moves to that line rather than falling to the page. Pass decision to draw the same line for a card restored from history.

Saying what the call will do

A plain object input is listed key by key, and the raw input sits behind a disclosure for anyone who wants the exact payload. When keys are not enough, pass summary with a sentence of your own.

<ToolApproval
  name="delete_branch"
  input={{ branch: "release/0.7", remote: "origin" }}
  summary={<p>Deletes release/0.7 on origin.</p>}
  destructive
  onDecision={respond}
/>

Destructive calls

destructive turns both allow buttons into press and hold. Letting go early cancels. From the keyboard, holding Enter or Space does the same, and a quick press does nothing, so a stray Enter cannot delete a branch. Assistive technology that activates a button directly, with no key or pointer behind it, gets a two-step confirmation instead: the first activation asks, the second allows. Deny always takes one press.

API

namestringThe tool the agent wants to run.
inputunknownThe raw arguments. A plain object is also listed key by key.
summaryReactNodeSays what the call will do, in place of the key list.
onDecision(decision: ToolApprovalDecision) => void | Promise<void>Receives the answer. A rejection puts the choices back.
destructivebooleanAllowing takes a press and hold. Defaults to false.
decisionToolApprovalDecision | nullAn earlier answer, drawn as the resolved line.
allowAlwaysbooleanOffer Always allow. Defaults to true.
holdDurationnumberMilliseconds a destructive allow is held. Defaults to 1200.
decision (ToolApprovalDecision)"allow-once" | "allow-always" | "deny"The three answers.
classNamestringClasses for the card and the resolved line.
...divPropsHTMLAttributesNative div attributes.

Accessibility

The card is a group named by its question, Allow read_file to run?, and described by the argument list or summary. Every choice is a native button. Destructive allows are described by the hold instruction, announce when holding has started, and offer a two-step confirmation to assistive technology that activates without a key press. The raw input is a disclosure with aria-expanded. The resolved line is a status that takes focus after a decision, so keyboard users are not dropped to the top of the page. Reduced motion stops the sending spinner and the press scale; the hold fill still tracks the press because it is the only measure of how long is left.