84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
"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.
|
|
|
|
import { AiDisclosureBadge, type ProjectSlugs, useTransport } from "@conusai-kit/ui"
|
|
import { type ReactElement, useState } from "react"
|
|
import type { ApprovalView } 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,
|
|
approvals,
|
|
labels,
|
|
}: {
|
|
slugs: ProjectSlugs
|
|
approvals: readonly ApprovalView[]
|
|
labels: ApprovalQueueLabels
|
|
}): ReactElement {
|
|
const { call, describe } = useTransport()
|
|
const [busy, setBusy] = useState<string | null>(null)
|
|
const [message, setMessage] = useState<string | null>(null)
|
|
|
|
const decide = async (approval: ApprovalView, decision: "approve" | "reject"): Promise<void> => {
|
|
setBusy(`${approval.runId}:${approval.stepKey}`)
|
|
setMessage(null)
|
|
try {
|
|
await call("agent.approval.review", { ...slugs, runId: approval.runId, stepKey: approval.stepKey, decision })
|
|
setMessage(labels.decided)
|
|
window.location.reload()
|
|
} catch (error) {
|
|
setMessage(labels.failed.replace("{detail}", describe(error)))
|
|
} finally {
|
|
setBusy(null)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section aria-labelledby="approvals-heading">
|
|
<h2 id="approvals-heading">{labels.heading}</h2>
|
|
{approvals.length === 0 ? <p>{labels.empty}</p> : null}
|
|
{approvals.map((approval) => (
|
|
<article key={`${approval.runId}:${approval.stepKey}`} aria-label={approval.tool}>
|
|
<h3>{approval.tool}</h3>
|
|
<p>{approval.goal}</p>
|
|
<AiDisclosureBadge label={labels.disclosure} />
|
|
<h4>{labels.proposed}</h4>
|
|
<pre>{JSON.stringify(approval.arguments, null, 2)}</pre>
|
|
<div>
|
|
<button type="button" disabled={busy !== null} onClick={() => void decide(approval, "approve")}>
|
|
{labels.approve}
|
|
</button>
|
|
<button type="button" disabled={busy !== null} onClick={() => void decide(approval, "reject")}>
|
|
{labels.reject}
|
|
</button>
|
|
{approval.reviewUrl !== null ? (
|
|
<a href={approval.reviewUrl} target="_blank" rel="noreferrer">
|
|
{labels.review}
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
))}
|
|
{message !== null ? (
|
|
<p role="status" aria-live="polite">
|
|
{message}
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
)
|
|
}
|