Early bird: 30% off Dashral ProSee pricing

Dashral

Approval inboxPro

Everything your AI agents are waiting on you for, each with its risk: the one in focus open with exactly what it will do, approve or deny it from the keyboard (J/K, A, D) or several at once, and always allow a tool for an agent, for its details or for an hour. Decided rows fold away and the next opens, on motion.

Installation

When to use

  • One place for everything your AI agents are waiting on a person for, across agents and tools.
  • Products where agents act on real things (emails, refunds, records) and someone answers for what they do.

When not to use

  • One request inside a conversation: Approval Card asks right where the assistant is working.
  • Requests from people rather than agents (access, invites): Notifications center.

Usage

Approval inbox with your own data: every prop has the sample as its default, so pass only what you have.

example.tsx
"use client"

import { useRef, useState } from "react"
import { ApprovalInbox, type ApprovalRequest, type ApprovalInboxHandle } from "@/components/dashral/blocks/approval-inbox"

export default function Approvals() {
  const inbox = useRef<ApprovalInboxHandle>(null)
  // Kept in state: a new array replaces what the inbox shows, so don't build the list inline.
  const [waiting] = useState<ApprovalRequest[]>([
    {
      id: "r-1",
      agent: "Billing agent",
      tool: "email.send",
      title: "Send 3 payment reminders",
      description: "Each customer gets the first reminder, with their invoice attached.",
      details: [{ label: "To", value: "Acme, Globex, Initech" }],
      risk: "low",
      time: "2 min ago",
    },
    { id: "r-2", agent: "Support agent", tool: "orders.refund", title: "Refund order #5821", risk: "high", destructive: true, time: "6 min ago" },
  ])
  return (
    <ApprovalInbox
      ref={inbox}
      requests={waiting}
      history={[]}
      rules={[]}
      // Tell your agent runtime; while the promise is pending the button shows it, and a thrown Error keeps the request.
      onDecide={async (request, decision, by) => console.log(request.id, decision, by)}
      onRuleAdd={(rule) => console.log("always allow", rule.agent, rule.tool, rule.scope ?? "tool")}
      onRuleRemove={(rule) => console.log("ask again", rule.agent, rule.tool)}
    />
  )
  // A request raised while the page is open: inbox.current?.add({ id, agent, tool, title, time: "Just now" })
}

API reference

ApprovalInbox

An approval inbox for AI agents: every request waiting on you, the one in focus open and the rest one line each, each with its risk. Approve, deny or always allow it (for its tool, with the same details, or for an hour), move with J and K, decide with A and D (Enter approves), or tick several and decide them from the bar. A rule approves what it covers, one after another; anything that can't be undone always asks. Decided, folded under the queue, keeps what was approved or denied and by whom; Rules lists the rules, to remove. Your requests come in as requests, new ones through the handle's add, and every decision goes to onDecide.

PropTypeDefault
titleReact.ReactNode"Approvals"
requestsApprovalRequest[]

What's waiting, newest first. A new list (another array) replaces what's shown, so keep yours in state or outside the component rather than building it inline on every render.

sample
historyApprovalRecord[]

What was decided before, newest first, for the Decided list, at first: decisions made here are added on top.

sample
rulesApprovalRule[]

The rules your agents work under, at first: rules added and removed here change it.

sample
onDecide(request: ApprovalRequest, decision: ApprovalDecision, by: "You" | "Rule") => Promise<void> | void

A decision, by a button, a key or a new rule (by is then "Rule"). While its promise is pending the button shows it's working; if it throws, the request stays and its message is shown.

—
onRuleAdd(rule: ApprovalRule) => Promise<void> | void—
onRuleRemove(rule: ApprovalRule) => Promise<void> | void—
refReact.Ref<ApprovalInboxHandle>—
classNamestring—

PlainMark

A tool without a brand, for your own requests' toolIcon: a plain icon on the muted ground, the same size as a mark.

PropTypeDefault
iconLucideIcon

Required.

—

ApprovalRiskBadge

A request's risk: a shield in its colour and its word; "High risk" where there's room.

PropTypeDefault
riskApprovalRisk

Required.

—
longbooleanfalse

ApprovalRequestRow

One request in the queue. Closed: the agent, the tool's mark, what it wants to do and its risk, on one line. Open: who asks with which tool and when, why, exactly what it will do, then Deny, Always allow (for this tool, with these details, or for an hour) and Approve. Opening and closing glide; on a phone the buttons go under the words.

PropTypeDefault
requestApprovalRequest

Required.

—
expandedboolean

The request in focus: open, with its details and buttons. Closed, it's one line you can open.

true
onExpand() => void—
selectedbooleanfalse
onSelectedChange(selected: boolean) => void—
busyApprovalDecision | null

The decision being sent: its button shows it's working and the others wait.

null
decidedApprovalDecision | null

Once it went through: "Approved" or "Denied" in place of its risk, for the beat before it folds away.

null
onDecide(decision: ApprovalDecision) => void

Required.

—
onAlwaysAllow(scope: NonNullable<ApprovalRule["scope"]>) => void

Offers Always allow, with its scopes; left out, and for a request that can't be undone, it isn't offered.

—
savingRuleboolean

The rule is being saved: Always allow shows it's working.

false

type ApprovalDecision

= "approved" | "denied"

type ApprovalRisk

= "low" | "medium" | "high"

type ApprovalRequest

PropTypeDefault
idnumber | string—
agentstring

Which agent asks, e.g. "Billing agent".

—
toolstring

The tool it wants to use, as your code names it, e.g. "email.send". A rule allows a tool for an agent.

—
toolIconReact.ReactNode

The tool's mark (your integration's logo), beside the request.

—
titlestring

What it wants to do, as an action: "Send 3 payment reminders".

—
descriptionstring

Why, or what will happen.

—
details{ label: string; value: string }[]

Exactly what it will do, as rows; shown when the request is open.

—
riskApprovalRisk

How much harm a mistake would do. Left out: high when it can't be undone, otherwise none is shown.

—
destructiveboolean

Can't be undone: said so, approved in red, never covered by a rule, and A only takes you to its Approve.

—
timestring

When it asked, e.g. "2 min ago".

—

type ApprovalRecord

A request once decided, for the Decided list.

PropTypeDefault
idnumber | string—
agentstring

Which agent asks, e.g. "Billing agent".

—
toolstring

The tool it wants to use, as your code names it, e.g. "email.send". A rule allows a tool for an agent.

—
toolIconReact.ReactNode

The tool's mark (your integration's logo), beside the request.

—
titlestring

What it wants to do, as an action: "Send 3 payment reminders".

—
descriptionstring

Why, or what will happen.

—
details{ label: string; value: string }[]

Exactly what it will do, as rows; shown when the request is open.

—
riskApprovalRisk

How much harm a mistake would do. Left out: high when it can't be undone, otherwise none is shown.

—
destructiveboolean

Can't be undone: said so, approved in red, never covered by a rule, and A only takes you to its Approve.

—
timestring

When it asked, e.g. "2 min ago".

—
decisionApprovalDecision—
bystring

Who decided: a person's name, "You", or "Rule" for one a rule let through.

—
decidedAtstring

When it was decided, e.g. "Yesterday".

—

type ApprovalRule

An agent that may use a tool without asking. scope narrows it: "tool" (the default) for any use, "details" only with the same details as the request it came from (kept in details), "hour" until until (epoch milliseconds).

PropTypeDefault
agentstring—
toolstring—
scope"tool" | "details" | "hour"—
detailsstring—
untilnumber—

type ApprovalInboxHandle

Brings in requests raised while the page is open.

PropTypeDefault
add(request: ApprovalRequest) => void

A new request: it grows in at the top of the queue, or goes straight to Decided when a rule covers it.

—