// An `Approval` row. The daemon exposes exactly three decisions // (docs/STRUCTURED-VIEW.md) and mints the nonce; this only solicits one of // them. A decision the request offers no matching ACP option for is not shown // — its button would "allow" something that silently resolves `Cancelled`. import { useState } from "react"; import type { AcpPermissionOption, AcpToolCallLike, StructuredApprovalDecision } from "../../types.js"; import { offeredDecisions, toolKindLabel } from "./model.js"; const SPECS: [StructuredApprovalDecision, string, string][] = [ ["allow_once", "Allow once", "primary"], ["allow_for_session", "Allow for session", ""], ["deny", "Deny", "deny"], ]; export interface ApprovalViewProps { /** The `RequestPermissionRequest` the row carries — serialized without its * nonce, which never leaves the daemon's live wire. */ request: { toolCall?: AcpToolCallLike; options?: AcpPermissionOption[] }; /** Whether this is the one live approval. A restored/answered row is not. */ pending: boolean; /** Resolves once the decision reached the daemon; rejects only while this * approval is still the pending one, so the buttons can come back. */ onDecide: (decision: StructuredApprovalDecision) => Promise; } export function ApprovalView({ request, pending, onDecide }: ApprovalViewProps) { const [busy, setBusy] = useState(false); const toolCall = request.toolCall ?? {}; const what = toolCall.title ?? toolKindLabel(toolCall.kind); // Resolved approvals render as a compact one-liner inside a tool group (or // are dropped entirely when standalone — see model.ts's transcriptItems). if (!pending) { return
{`${what} — approved`}
; } const offered = offeredDecisions(request.options ?? []); const path = toolCall.locations?.[0]?.path; const decide = (decision: StructuredApprovalDecision) => { setBusy(true); onDecide(decision).catch(() => { // Rejects only while this approval is still pending — restore the // controls so the operator can retry. The error itself is surfaced by // the pane's banner. setBusy(false); }); }; return (
permission requested
{what}
{path &&
{path}
} {offered.length > 0 ? (
{SPECS.filter(([decision]) => offered.includes(decision)).map(([decision, label, cls]) => ( ))}
) : (
no decision offered
)}
); }