Skip to content

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.

ApprovalBar on the fixture host

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#

ComponentShows
ApprovalBarThe 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
ApprovalCardThe inline card: the tool, what worried the decision model, the command (or the input as JSON), and Deny, Always allow, Allow once
FileEditApprovalThe 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
SafelistPanelThe "Always allow" entries of each project, with Revoke
ApprovalShellThe panel the review and the safelist draw in when your app gives them no shell of its own
KeysInline cardFile-edit review
Allow onceCommand ReturnA
Allow for sessionS
Always allowCommand Shift ReturnP
DenyEsc (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.

MemberDoesWithout it
pendingThe approvals waiting for an answer, oldest first. The host keeps it current: new requests, requests answered elsewhere, a new read after a reconnectRequired
resolve(requestId, decision)Answers one approval. It leaves pending at onceThe card shows the request without its buttons
denyAll(sessionId?)Denies every pending approval, or those of one sessionNo bulk deny
safelistThe "Always allow" entries: state, refresh(), remove(project, entry)SafelistPanel renders nothing
TSX
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.

HostFromOffers
zidane-servercreateServerApprovalsHost({ 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 appIts IPC host (gui/src/renderer/src/hosts/electron-approvals-host.ts)Every member
In memorycreateFixtureApprovalsHost (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).

TypeScript
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 pointHolds
zidane-ui/approvalsApprovalsProvider and its hooks, ApprovalBar, ApprovalCard, FileEditApproval, SafelistPanel, ApprovalShell
zidane-ui/approvals/headlessApprovalsHost, createPendingApprovals, createApprovalHeads, createSafelist, the preview helpers and the labels
zidane-ui/approvals/testingcreateFixtureApprovalsHost and the demo approvals
zidane-ui/approvals/testing/contractdescribeApprovalsHostContract, the shared host suite