# useAnsweringApprovals

The guard every chat with permission prompts needs: while an answer is being sent, that request's buttons wait, so Allow can't be pressed twice; and an answer that fails to send leaves the prompt answerable again and says so, instead of an unhandled rejection. `respond` is the chat's own (`useCandorChat`, `useClaudeAgent`, `useAgUi`).

```
const { answering, answer, failed } = useAnsweringApprovals(respond)
<PermissionPrompt onAllow={() => answer(a.id, true)} onDeny={() => answer(a.id, false)} busy={answering.has(a.id)} />
{(error ?? failed) && <p role="alert">{error ?? failed}</p>}
```

- Registry name: `use-answering-approvals` (`@candor/use-answering-approvals`)
- 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/use-answering-approvals

## Install

```bash
npx candor-ui add use-answering-approvals
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/use-answering-approvals`.
It brings in: locale, tokens.

## Example

```tsx
"use client"

import * as React from "react"
import { PermissionPrompt } from "@/components/candor/permission-prompt"
import { useAnsweringApprovals } from "@/lib/candor/use-answering-approvals"

const request = { id: "ap-1", tool: "set_flag", title: "The agent wants to turn on new_checkout in production", input: { flag: "new_checkout", environment: "production", on: true } }

export default function Example() {
  const [answered, setAnswered] = React.useState<string>()
  // Stands in for the chat's own respond (useCandorChat, useClaudeAgent): here the first answer fails to send.
  const tries = React.useRef(0)
  const respond = React.useCallback(async (id: string, approved: boolean) => {
    await new Promise((r) => setTimeout(r, 800))
    if (tries.current++ === 0) throw new Error("The network dropped.")
    setAnswered(`${id}: ${approved ? "allowed" : "denied"}`)
  }, [])
  const { answering, answer, failed } = useAnsweringApprovals(respond)
  if (answered) return <p className="text-sm text-muted-foreground">Sent. {answered}</p>
  return (
    <div className="flex max-w-xl flex-col gap-2">
      <PermissionPrompt request={request} onAllow={() => answer(request.id, true)} onDeny={() => answer(request.id, false, "Not now")} busy={answering.has(request.id)} />
      {failed && (
        <p role="alert" className="text-sm text-destructive-text">
          {failed}
        </p>
      )}
    </div>
  )
}
```

## Functions

### useAnsweringApprovals

```ts
useAnsweringApprovals(respond: (id: string, approved: boolean, reason?: string) => unknown)
```

## Strings (`answeringApprovals`)

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

- `failed`: Your answer wasn’t sent, so the request is still waiting. Answer it again.

## Types

### AnsweringApprovals

What `useAnsweringApprovals` gives back.

```ts
{
  /** The ids of the requests whose answer is being sent. Pass `answering.has(id)` as the prompt's `busy`. */
  answering: ReadonlySet<string>;
  /**
   * Sends one answer through `respond`. While it's being sent, a second answer to the same request is ignored, so a
   * double press can't answer twice. When it settles the request leaves `answering`, whether it was sent or not.
   */
  answer: (id: string, approved: boolean, reason?: string | undefined) => void;
  /**
   * The sentence to show when the last answer failed to send (`respond` threw or its promise rejected), in
   * LocaleProvider's words; undefined otherwise. The request is still waiting and can be answered again. It clears
   * when the next answer is sent.
   */
  failed: string | undefined;
}
```

## 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`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
