Skip to content

Tool Approval

Approve or deny an AI SDK tool call with a gesture that matches its risk.

// Nothing has run yet

npx shadcn@latest add @sureui/tool-approval

Mark a tool as needing approval on the server (AI SDK tool approval):

tsx
import { ToolLoopAgent } from "ai"const agent = new ToolLoopAgent({  model,  tools: { deleteProject },  toolApproval: { deleteProject: "user-approval" },})

Render ToolApproval for its tool part and pass addToolApprovalResponse from useChat:

tsx
import { useChat } from "@ai-sdk/react"import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai"import { ToolApproval } from "@/components/ui/sureui/tool-approval"const { messages, addToolApprovalResponse } = useChat({  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,}){  message.parts.map((part) =>    part.type === "tool-deleteProject" ? (      <ToolApproval        key={part.toolCallId}        part={part}        risk="critical"        phrase={part.input.name}        onRespond={addToolApprovalResponse}      />    ) : null  )}

It shows Approve and Deny while the part is approval-requested, and the outcome once it's answered. sendAutomaticallyWhen continues the conversation after you respond.

Low risk

Approve runs after an undo window, so a misclick can be taken back before the agent hears about it.

// Nothing has run yet

Medium risk

The default. Approve arms on the first click and responds on the second.

// Nothing has run yet

High risk

Approve responds once the hold completes. Letting go early cancels.

Press and hold, or activate twice, to confirm

// Nothing has run yet

Critical risk

Approve unlocks once phrase is typed, like a project or repository name.

// Nothing has run yet

Deny is one click at every level.

Arm delay

armDelay ignores Approve for that many milliseconds after it appears and after it arms, so a click meant for something that was there a moment ago doesn't approve the call.

// Nothing has run yet

Errors

If onRespond throws or rejects, the buttons unlock so you can answer again. With onConfirmError, the error goes to your handler, and errorLabel replaces the Approve label until the next attempt.

// Nothing has run yet

Batch

ToolApprovalBatch answers every pending call at once. Its Approve uses the gesture of the riskiest call, and it calls onRespond once per call. Pass risk as a function to grade each part. Parts that aren't approval-requested are skipped.

Press and hold, or activate twice, to confirm

// Nothing has run yet

tsx
import { isToolUIPart } from "ai"import { ToolApprovalBatch } from "@/components/ui/sureui/tool-approval"<ToolApprovalBatch  parts={message.parts.filter(isToolUIPart)}  risk={(part) => (part.type === "tool-deleteProject" ? "high" : "low")}  onRespond={addToolApprovalResponse}/>

Scope

Pass scopes to let people choose how long an answer holds: once, this session or always. The first entry starts selected, and onRespond receives it as scope with Approve and Deny. SureUI doesn't store permissions: keep the answer for the session or save it, and skip ToolApproval for that tool next time. addToolApprovalResponse ignores scope, so you can pass it on unchanged.

OnceThis sessionAlways

// Nothing has run yet

Note

Pass note to add a field above Approve and Deny. Whatever people type goes out as reason with either answer. addToolApprovalResponse passes it to the model, so the agent can read "only the staging table" or "use the archive tool instead". The outcome shows the note after the answer.

// Nothing has run yet

ToolApproval

onRespond takes the same argument as addToolApprovalResponse, and part takes any AI SDK tool part.

PropTypeDefault
part{ state, approval?: { id, approved?, reason? } }required
onRespond({ id, approved, reason?, scope? }) => void | PromiseLike<unknown>required
risk"low" | "medium" | "high" | "critical""medium"
phrasestringrequired with "critical"
approveLabelReactNode"Approve", or "Hold to approve" for "high"
denyLabelReactNode"Deny"
approvedLabelReactNode"Approved"
deniedLabelReactNode"Denied"
errorLabelReactNode—
onConfirmError(error: unknown) => void—
undoboolean | number | "manual"true
timeoutnumber3000
durationnumber1200
armDelaynumber0
scopes("once" | "session" | "always")[]—
scopeLabels{ group?, once?, session?, always? }{ group: "Remember", once: "Once", session: "This session", always: "Always" }
notebooleanfalse
noteLabelstring"Add a note for the agent"
classNamestring—

undo applies to "low", timeout to "medium" and duration to "high". scope is only sent when scopes is set.

ToolApprovalBatch

Takes the same props as ToolApproval, with parts in place of part.

PropTypeDefault
partsToolApprovalPart[]required
onRespond({ id, approved, reason?, scope? }) => void | PromiseLike<unknown>required
riskToolApprovalRisk | (part) => ToolApprovalRisk"medium"
phrasestring"approve all"
approveLabelReactNode"Approve all", or "Hold to approve all" for "high"
denyLabelReactNode"Deny all"
approvedLabelReactNode"Approved"
deniedLabelReactNode"Denied"
errorLabelReactNode—
onConfirmError(error: unknown) => void—
undoboolean | number | "manual"true
timeoutnumber3000
durationnumber1200
armDelaynumber0
scopes("once" | "session" | "always")[]—
scopeLabels{ group?, once?, session?, always? }{ group: "Remember", once: "Once", session: "This session", always: "Always" }
notebooleanfalse
noteLabelstring"Add a note for the agent"
classNamestring—

phrase applies when the riskiest call is "critical". The outcome shows once every part is answered the same way. When the pending calls change, a gesture in progress starts over.

Data attributes

The root sets data-slot="tool-approval" (or "tool-approval-batch") and data-state. The scope choice sets data-slot="tool-approval-scope", and the note field sets data-slot="tool-approval-note".

ValueWhen
requestedWaiting for Approve or Deny.
approvedThe part was approved.
deniedThe part was denied.
  • Approve uses the same announcements as Confirm Button and Type to Confirm, including the second-click fallback for a hold.
  • Only one response is ever sent per call. Deny is disabled during a low-risk undo window and while a response is pending, and both buttons come back if the response fails. In a batch, only the calls whose response failed come back.
  • The scope choice is a radio group named by scopeLabels.group, so arrow keys move between options.
  • The note field is named by noteLabel, and it is disabled with the buttons while a response is pending.

WCAG