Approval cards
The approval card, the file-edit review and the safelist from zidane-ui, on the desktop app's IPC, on zidane-server or in memory.
zidane-ui/approvals shows the tool calls that wait for your answer when Tool approvals is not Full access. The desktop app renders these components (its keys are on desktop approvals). Your app renders them on the same host interface.
The preview runs on demo data. Request three approvals: a command, a one-hunk edit, then a three-hunk edit from a subagent. Answer each one to see the next.
What it shows#
| Component | Shows |
|---|---|
ApprovalBar | The first pending approval of a session, above its composer. A tool that writes files (edit, multi_edit, write_file) gets the file-edit review. Every other tool gets the inline card. Renders nothing when the session has no pending approval |
ApprovalCard | The inline card: the tool, what worried the decision model, the command (or the input as JSON), and Deny, Always allow, Allow once |
FileEditApproval | The file-edit review: the path, the subagent that asked, the diff, and Deny, Allow for session, Always allow, Allow. With several hunks, one checkbox per hunk. Only the hunks found in the file start checked, and Allow answers the hunks you keep |
SafelistPanel | The "Always allow" entries of each project, with Revoke |
ApprovalShell | The panel the review and the safelist draw in when your app gives them no shell of its own |
| Keys | Inline card | File-edit review |
|---|---|---|
| Allow once | Command Return | A |
| Allow for session | S | |
| Always allow | Command Shift Return | P |
| Deny | Esc (not from inside the code editor) | D, Esc, a click on the modal scrim |
The card takes focus when it shows, so a stray Enter meant for the composer never allows a command. If you are typing in a field or the code editor, the focus stays there. The review starts on Allow.
In your app#
Your app passes an ApprovalsHost to ApprovalsProvider, or as approvals to AppHostProvider. The host holds the pending approvals as a readable store, and answers them. If you leave out a member, its control does not show.
| Member | Does | Without it |
|---|---|---|
pending | The approvals waiting for an answer, oldest first. The host keeps it current: new requests, requests answered elsewhere, a new read after a reconnect | Required |
resolve(requestId, decision) | Answers one approval. It leaves pending at once | The card shows the request without its buttons |
denyAll(sessionId?) | Denies every pending approval, or those of one session | No bulk deny |
safelist | The "Always allow" entries: state, refresh(), remove(project, entry) | SafelistPanel renders nothing |
import { ApprovalBar, ApprovalsProvider, SafelistPanel } from 'zidane-ui/approvals'
import { createServerApprovalsHost, createServerClient, createServerHost } from 'zidane-ui/adapters'
const client = createServerClient({ url: 'wss://agent.example.com', token: 'zds_…' })
await client.ready // the approvals host reads from the handshake whether the token may answer
const chat = createServerHost({ client })
const approvals = createServerApprovalsHost({ client })
export function Composer({ sessionId }: { sessionId: string }) {
return (
<ApprovalsProvider host={approvals}>
<ApprovalBar sessionId={sessionId} />
</ApprovalsProvider>
)
}To open the file-edit review on your own modal layer, pass onFileEdit to ApprovalBar. It is called once per request. Then render FileEditApproval there with onDone to close the modal, and renderShell to draw it in your modal's panel. ApprovalCard takes renderFrame for your own card surface. The desktop app does both.
| Host | From | Offers |
|---|---|---|
| zidane-server | createServerApprovalsHost({ client }) (zidane-ui/adapters) | pending, resolve, denyAll. Answering needs a chat or admin scope. Built after the handshake (await client.ready), the host follows client.can('approvals.resolve'): a connection that may not call it shows approvals without buttons. canApprove overrides it. safelist over safelist.list, its remove over safelist.remove (a chat token) |
| Desktop app | Its IPC host (gui/src/renderer/src/hosts/electron-approvals-host.ts) | Every member |
| In memory | createFixtureApprovalsHost (zidane-ui/approvals/testing) | Every member, for demos and tests |
Your own host can build pending with createPendingApprovals, which keeps a queue the way every host above does, and safelist with createSafelist. Its tests run the shared suite: describeApprovalsHostContract from zidane-ui/approvals/testing/contract (vitest).
import { describeApprovalsHostContract } from 'zidane-ui/approvals/testing/contract'
describeApprovalsHostContract('my host', backend => ({ host: makeHost(backend), raise, resolveElsewhere }), ['resolve', 'denyAll'])Every string comes from the labels (DEFAULT_APPROVALS_LABELS). Override them on ApprovalsProvider or LabelsProvider.
| Entry point | Holds |
|---|---|
zidane-ui/approvals | ApprovalsProvider and its hooks, ApprovalBar, ApprovalCard, FileEditApproval, SafelistPanel, ApprovalShell |
zidane-ui/approvals/headless | ApprovalsHost, createPendingApprovals, createApprovalHeads, createSafelist, the preview helpers and the labels |
zidane-ui/approvals/testing | createFixtureApprovalsHost and the demo approvals |
zidane-ui/approvals/testing/contract | describeApprovalsHostContract, the shared host suite |