feat: render schema approval in chat

This commit is contained in:
lda
2026-07-09 05:45:41 +07:00 Verified
parent 22386f7e2d
commit 7594ba4b0d
6 changed files with 128 additions and 8 deletions
@@ -74,7 +74,43 @@ describe("OperatorChat", () => {
expect(screen.getByText(/Found prepared workflow recipe/)).toBeInTheDocument();
});
it("renders approval controls and wires decisions", async () => {
it("renders schema approval surface inside chat approval request", async () => {
const user = userEvent.setup();
const onApprove = vi.fn();
const onDeny = vi.fn();
const messages: ReadonlyArray<AgentMessage> = [
{
id: "approval",
role: "assistant",
parts: [
{
type: "approval-request",
callId: "call-1",
name: "resumeIssueReview",
prompt: "Approve resuming?",
contract: {
kind: "issue_review",
outcomes: ["submitted", "cancelled"],
resumeSchema: { type: "object", properties: { comment: { type: "string" } } },
resumePayloadPreview: { comment: "Looks good." },
runId: "run_recorded_lda_report",
},
},
],
},
];
render(<OperatorChat state={initialPresentationState} messages={messages} onApprove={onApprove} onDeny={onDeny} />);
expect(screen.getByRole("group", { name: /issue_review resume/i })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /submit/i }));
await user.click(screen.getByRole("button", { name: /cancel/i }));
expect(onApprove).toHaveBeenCalledTimes(1);
expect(onDeny).toHaveBeenCalledTimes(1);
});
it("falls back to plain approve/deny buttons when approval request has no contract", async () => {
const user = userEvent.setup();
const onApprove = vi.fn();
const onDeny = vi.fn();
@@ -95,6 +131,7 @@ describe("OperatorChat", () => {
render(<OperatorChat state={initialPresentationState} messages={messages} onApprove={onApprove} onDeny={onDeny} />);
expect(screen.queryByRole("group", { name: /issue_review resume/i })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Approve" }));
await user.click(screen.getByRole("button", { name: "Deny" }));
expect(onApprove).toHaveBeenCalledTimes(1);
@@ -3,6 +3,7 @@ import { PREPARE_THESIS_REPORT_RECIPE } from "../demo/agent/recipes.js";
import type { AgentMessage, AgentMessagePart } from "../demo/agent/events.js";
import type { PresentationState } from "./presentation-state.js";
import { compositionForState } from "./presentation-state.js";
import { SchemaApprovalSurface } from "./approval/SchemaApprovalSurface.js";
type OperatorChatProps = {
readonly state: PresentationState;
@@ -78,10 +79,22 @@ const renderPart = (
<span>Approval required</span>
<code>{part.name}</code>
<p>{part.prompt}</p>
<div className="chat-approval-actions">
<button type="button" onClick={onApprove} disabled={!onApprove}>Approve</button>
<button type="button" onClick={onDeny} disabled={!onDeny}>Deny</button>
</div>
{part.contract ? (
<SchemaApprovalSurface
title={`${part.contract.kind} resume`}
schema={part.contract.resumeSchema}
payload={part.contract.resumePayloadPreview}
outcomes={part.contract.outcomes}
runId={part.contract.runId}
onSubmit={onApprove}
onCancel={onDeny}
/>
) : (
<div className="chat-approval-actions">
<button type="button" onClick={onApprove} disabled={!onApprove}>Approve</button>
<button type="button" onClick={onDeny} disabled={!onDeny}>Deny</button>
</div>
)}
</div>
);
case "error":