# Approval queue

Approval requests by what they need from the viewer. "Needs you" lists what they can decide, soonest to expire first; production requests are flagged.

- Registry name: `approval-queue` (`@candor/approval-queue`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/approval-queue

## Install

```bash
npx candor-ui add approval-queue
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/approval-queue`.
It brings in: agent-avatar, approval-request, badge, locale, relative-time, work, utils.
npm packages: lucide-react@^1.67.0, radix-ui@^1.8.1.

## Example

```tsx
"use client"

import * as React from "react"
import type { AgentIdentity } from "@/lib/chat"
import type { ApprovalRequest } from "@/lib/work"
import { ApprovalQueue } from "@/components/approval-queue"

const ago = (m: number) => new Date(Date.now() - m * 60_000).toISOString()
const ahead = (h: number) => new Date(Date.now() + h * 3_600_000).toISOString()
const keel: AgentIdentity = { id: "a-orch", name: "Keel", role: "orchestrator" }

const requests: ApprovalRequest[] = [
  {
    id: "ap1",
    title: "Add region to customers",
    summary: "A region column on customers, backfilled from billing country.",
    requester: { agent: keel },
    environment: "Acme web",
    production: true,
    artifactHash: "9f2c4e81b7a03d55c6e1f0a9b8d7c6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9",
    impact: { components: [{ kind: "migration", name: "0142_add_customer_region" }], rows: "unknown" },
    rollback: { available: true, how: "Run the migration's down step." },
    createdAt: ago(25),
    expiresAt: ahead(7 * 24),
    state: "pending",
  },
  {
    id: "ap2",
    title: "Turn on plan-change-review",
    summary: "Only billing admins can change a customer's plan while an invoice is open.",
    requester: { agent: keel },
    environment: "Acme web",
    detail: "Staging",
    production: false,
    artifactHash: "77aa1c0e5d4b3a29180f7e6d5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3a2918",
    impact: { components: [{ kind: "flag", name: "plan-change-review" }], rows: 0 },
    rollback: { available: true, how: "Turn the flag off." },
    createdAt: ago(60 * 3),
    expiresAt: ahead(10),
    state: "pending",
  },
  {
    id: "ap3",
    title: "Drop partner_tier_old",
    summary: "A migration drops customers.partner_tier_old.",
    requester: { name: "You" },
    environment: "Acme web",
    production: true,
    artifactHash: "3b1d9c8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c",
    impact: { components: [{ kind: "column", name: "customers.partner_tier_old" }], rows: "unknown" },
    rollback: { available: false, reason: "a dropped column's data only comes back from a backup restore" },
    createdAt: ago(60 * 20),
    expiresAt: ahead(30),
    state: "pending",
    cannotDecide: "You asked for this change, so another approver must decide it.",
  },
  {
    id: "ap4",
    title: "Merge signup source values",
    summary: "Merge “web” and “website” into one value.",
    requester: { agent: keel },
    environment: "Acme web",
    production: true,
    artifactHash: "c0de4f3e2d1c0b9a8f7e6d5c4b3a29180f7e6d5c4b3a29180f7e6d5c4b3a2918",
    impact: { components: [{ kind: "column", name: "customers.signup_source" }], rows: 4210 },
    rollback: { available: false, reason: "merged values can't be split back" },
    createdAt: ago(60 * 26),
    expiresAt: ahead(6 * 24),
    state: "approved",
    decision: { by: "Priya", at: ago(60 * 3) },
  },
]

export default function Example() {
  const [active, setActive] = React.useState("ap1")
  return <ApprovalQueue className="max-w-md" requests={requests} activeId={active} onOpen={setActive} />
}
```

## <ApprovalQueue>

Props:

- `requests: ApprovalRequest[]`: Every request; the tabs sort them into needs you, waiting on others and decided.
- `onOpen?: ((id: string) => void) | undefined`: Called with a request's id when its row is clicked.
- `activeId?: string | undefined`: The id of the request open beside the queue, marked as current.
- `className?: string | undefined`

## Strings (`approvalQueue`)

The fixed words, English by default. Override any of them under `approvalQueue` in `<LocaleProvider strings={{ approvalQueue: { … } }}>`; the table is exported as `approvalQueueStrings`.

- `label`: Approval requests
- `tabs`: Requests
- `mine`: Needs you
- `others`: Waiting on others
- `decided`: Decided
- `emptyMine`: Nothing waiting on you.
- `emptyOthers`: Nothing waiting on others.
- `emptyDecided`: No decisions yet.
- `production`: Production
- `expires`: expires

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
