@@ -0,0 +1,112 @@
|
||||
"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<ProjectSlugs & { runId: string; stepKey: string; decision: "approve" | "reject" }, unknown>(
|
||||
"agent.approval.review",
|
||||
)
|
||||
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 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 (
|
||||
<PageSection title={labels.heading}>
|
||||
{approvals.length === 0 ? <PageState title={labels.empty} /> : null}
|
||||
{approvals.map((approval) => (
|
||||
<PageSection
|
||||
key={`${approval.runId}:${approval.stepKey}`}
|
||||
title={approval.tool}
|
||||
description={approval.goal}
|
||||
level={3}
|
||||
variant="card"
|
||||
>
|
||||
<AiDisclosureBadge label={labels.disclosure} />
|
||||
<h4>{labels.proposed}</h4>
|
||||
<pre>{JSON.stringify(approval.arguments, null, 2)}</pre>
|
||||
<ItemActions>
|
||||
<Button disabled={busy !== null} onClick={() => void decide(approval, "approve")}>
|
||||
{labels.approve}
|
||||
</Button>
|
||||
<Button variant="outline" disabled={busy !== null} onClick={() => void decide(approval, "reject")}>
|
||||
{labels.reject}
|
||||
</Button>
|
||||
{approval.reviewUrl !== null ? (
|
||||
<Button asChild variant="link">
|
||||
<a href={approval.reviewUrl} target="_blank" rel="noreferrer">
|
||||
{labels.review}
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
</ItemActions>
|
||||
</PageSection>
|
||||
))}
|
||||
{message !== null ? (
|
||||
<p role="status" aria-live="polite">
|
||||
{message}
|
||||
</p>
|
||||
) : null}
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user