"use client" // The approval queue: what a run's tool asked a person to decide. The panel renders the EXACT // stored proposed arguments — a pretty-printed JSON of what the tool was called with — never a // model's narration of them, and it carries the AI disclosure a reviewer reads before deciding. // // The queue is live: the page's view hands over the approvals it rendered as the seed, and this reads them // again whenever the live stream announces a change of a run (`agent.run`) -- a run that parks an approval // or leaves one changes its status -- and after this person's own decision, so nothing reloads the page. import { AiDisclosureBadge, type ProjectSlugs } from "@conusai-kit/ui" import { describeError, useRead, useWrite } from "@conusai-kit/ui/data" import { PageSection, PageState } from "@conusai-kit/ui/patterns" import { Button, ItemActions } from "@conusai-kit/ui/primitives" import { type ReactElement, useState } from "react" import { type ApprovalView, RUN_COLLECTION } from "../contract/index.js" export interface ApprovalQueueLabels { readonly heading: string readonly empty: string readonly proposed: string readonly review: string readonly approve: string readonly reject: string readonly decided: string readonly disclosure: string readonly failed: string } export function ApprovalQueue({ slugs, seed, labels, }: { slugs: ProjectSlugs /** The approvals the server rendered: shown at once, then kept current. */ seed: readonly ApprovalView[] labels: ApprovalQueueLabels }): ReactElement { const queue = useRead<{ approvals: readonly ApprovalView[] }>( "agent.approval.list", { ...slugs }, { live: { org: slugs.org, topic: { ws: slugs.workspace, project: slugs.project }, entities: [RUN_COLLECTION.entity], }, initial: { approvals: seed }, }, ) const approvals = queue.data?.approvals ?? seed const review = useWrite( "agent.approval.review", ) const [busy, setBusy] = useState(null) const [message, setMessage] = useState(null) const decide = async (approval: ApprovalView, decision: "approve" | "reject"): Promise => { setBusy(`${approval.runId}:${approval.stepKey}`) setMessage(null) try { await review.mutate({ ...slugs, runId: approval.runId, stepKey: approval.stepKey, decision }) setMessage(labels.decided) // The decision is a step's, which the stream does not announce until the run moves: read it again now. await queue.refetch() } catch (error) { setMessage(labels.failed.replace("{detail}", describeError(error))) } finally { setBusy(null) } } return ( {approvals.length === 0 ? : null} {approvals.map((approval) => (

{labels.proposed}

{JSON.stringify(approval.arguments, null, 2)}
{approval.reviewUrl !== null ? ( ) : null}
))} {message !== null ? (

{message}

) : null}
) }