fix: clarify schema approval presentation

This commit is contained in:
lda
2026-07-09 06:00:05 +07:00 Verified
parent 28a720b998
commit a86f54d14a
14 changed files with 47 additions and 39 deletions
@@ -95,7 +95,7 @@ export async function* runPreparedRecipeReplay(
: undefined;
const approvalParts: AgentMessagePart[] = [
agentToolCallPart(callId, step.toolName, { runId }),
approvalRequestPart(callId, step.toolName, "Approve resuming the workflow run with the selected issues?", approvalContract),
approvalRequestPart(callId, step.toolName, "Submit the resume request with the selected issues?", approvalContract),
];
yield { id: step.id, role: "assistant", parts: approvalParts };
const decision = await requestApproval(signal);
@@ -55,7 +55,7 @@ export const DemoOutcomePanel = ({
<strong>Product state, not chat-only text</strong>
<ul>
<li><b>Report markdown</b><small>Generated from selected documents.</small></li>
<li><b>Issue board changes</b><small>Created only after approval.</small></li>
<li><b>Issue board changes</b><small>Created only after resume submission.</small></li>
</ul>
</aside>
);
@@ -112,7 +112,7 @@ describe("DemoWorkflowScene", () => {
expect(stage).toHaveAttribute("data-demo-layout", "approval");
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
expect(screen.getByLabelText("typed interrupt contract")).toHaveTextContent("Operator decision");
expect(screen.getByLabelText("typed interrupt contract")).toHaveTextContent("Resume outcomes");
expect(screen.getByRole("group", { name: /issue review resume/i })).toHaveTextContent("submitted / cancelled");
expect(screen.getByLabelText("workflow graph")).toBeInTheDocument();
});
@@ -143,11 +143,11 @@ describe("DemoWorkflowScene", () => {
});
it("marks outcome-panel layouts so CSS can clear the receipt row", () => {
renderBeat("approval", "interrupt-evidence");
renderBeat("resume", "interrupt-evidence");
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "approval");
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "operation");
expect(screen.getByLabelText("demo outcome proof")).toBeInTheDocument();
expect(screen.getByLabelText("workflow.runs.start execution receipt")).toBeInTheDocument();
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
});
it("shows the continuity rail across Scene 9 operation, graph, and interrupt beats", () => {
@@ -187,16 +187,17 @@ describe("DemoWorkflowScene", () => {
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
expect(screen.getByLabelText("workflow graph")).toHaveAttribute("data-graph-variant", "compact");
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
expect(screen.queryByLabelText("demo outcome proof")).not.toBeInTheDocument();
});
it("shows a schema approval surface for the approval beat instead of raw schema as the primary visual", () => {
renderBeat("approval", "interrupt-evidence");
const approval = screen.getByRole("group", { name: /issue_review resume/i });
const approval = screen.getByRole("group", { name: /issue review resume/i });
expect(within(approval).getByText("Schema-backed decision")).toBeInTheDocument();
expect(within(approval).getByText("selected_issue_ids")).toBeInTheDocument();
expect(within(approval).getByText("[\"risk-1\"]")).toBeInTheDocument();
expect(within(approval).queryByText("approved")).not.toBeInTheDocument();
expect(within(approval).getByRole("button", { name: /submit/i })).toBeDisabled();
expect(within(approval).getByRole("button", { name: /cancel/i })).toBeDisabled();
});
@@ -210,11 +211,7 @@ describe("DemoWorkflowScene", () => {
expect(screen.queryByText("Resume schema")).not.toBeInTheDocument();
});
it("adds outcome proof to approval, resume, output, and trace beats", () => {
const approval = renderBeat("approval", "interrupt-evidence");
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
approval.unmount();
it("adds outcome proof to resume, output, and trace beats", () => {
const resume = renderBeat("resume", "interrupt-evidence");
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("Same persisted run");
resume.unmount();
@@ -73,7 +73,7 @@ export const DemoWorkflowScene = ({
const lens = demoBeatLensForBeat(beat.id);
const currentOperation = currentEvent ? projectOperationPresentation(currentEvent) : null;
const showOutcomePanel = beat.id === "approval" || beat.id === "resume" || beat.id === "output" || beat.id === "trace";
const showOutcomePanel = beat.id === "resume" || beat.id === "output" || beat.id === "trace";
const showExpandedOperation = beat.id === "operation" || beat.id === "resume" || beat.id === "trace";
const showGraph = beat.id === "graph" || beat.id === "interrupt" || beat.id === "approval" || beat.id === "output";
@@ -9,6 +9,8 @@ type InterruptContractPreviewProps = {
readonly hero?: boolean;
};
const titleForKind = (kind: string): string => `${kind.replaceAll("_", " ")} resume`;
export const InterruptContractPreview = ({
contract,
mode,
@@ -27,19 +29,21 @@ export const InterruptContractPreview = ({
<span>{mode === "approval" ? "Operator decision" : "Execution paused"}</span>
<strong>{contract.kind}</strong>
</header>
<dl>
<div>
<dt>Persisted run</dt>
<dd><code>{contract.runId ?? "unavailable"}</code></dd>
</div>
<div>
<dt>Resume outcomes</dt>
<dd>{contract.outcomes.join(" / ")}</dd>
</div>
</dl>
{mode === "preview" ? (
<dl>
<div>
<dt>Persisted run</dt>
<dd><code>{contract.runId ?? "unavailable"}</code></dd>
</div>
<div>
<dt>Resume outcomes</dt>
<dd>{contract.outcomes.join(" / ")}</dd>
</div>
</dl>
) : null}
{mode === "approval" ? (
<SchemaApprovalSurface
title={`${contract.kind} resume`}
title={titleForKind(contract.kind)}
schema={contract.resumeSchema}
payload={contract.resumePayloadPreview}
outcomes={contract.outcomes}
@@ -87,7 +87,7 @@ describe("OperatorChat", () => {
type: "approval-request",
callId: "call-1",
name: "resumeIssueReview",
prompt: "Approve resuming?",
prompt: "Submit resume request?",
contract: {
kind: "issue_review",
outcomes: ["submitted", "cancelled"],
@@ -102,7 +102,7 @@ describe("OperatorChat", () => {
render(<OperatorChat state={initialPresentationState} messages={messages} onApprove={onApprove} onDeny={onDeny} />);
expect(screen.getByRole("group", { name: /issue_review resume/i })).toBeInTheDocument();
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 }));
@@ -123,7 +123,7 @@ describe("OperatorChat", () => {
type: "approval-request",
callId: "call-1",
name: "resumeIssueReview",
prompt: "Approve resuming?",
prompt: "Submit resume request?",
},
],
},
@@ -131,7 +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();
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);
@@ -81,7 +81,7 @@ const renderPart = (
<p>{part.prompt}</p>
{part.contract ? (
<SchemaApprovalSurface
title={`${part.contract.kind} resume`}
title={`${part.contract.kind.replaceAll("_", " ")} resume`}
schema={part.contract.resumeSchema}
payload={part.contract.resumePayloadPreview}
outcomes={part.contract.outcomes}
@@ -51,7 +51,7 @@ describe("SchemaApprovalSurface", () => {
/>,
);
expect(screen.getByText("No additional resume fields are declared by this schema.")).toBeInTheDocument();
expect(screen.getByText("Recorded resume payload for this decision.")).toBeInTheDocument();
expect(screen.getByText("selected_issue_ids")).toBeInTheDocument();
expect(screen.getByText("[\"risk-1\"]")).toBeInTheDocument();
});
@@ -50,7 +50,7 @@ export const SchemaApprovalSurface = ({
</dl>
) : (
<div className="schema-approval-surface__loose">
<p>No additional resume fields are declared by this schema.</p>
<p>Recorded resume payload for this decision.</p>
<dl>
{model.payloadPreview.map((entry) => (
<div key={entry.key}>
@@ -59,6 +59,7 @@ describe("buildSchemaApprovalModel", () => {
const model = buildSchemaApprovalModel({
schema: { type: "object" },
payload: {
approved: true,
selected_issue_ids: ["risk-1"],
comment: "Create the selected issue.",
},
@@ -51,10 +51,16 @@ const labelFor = (name: string): string => name.replaceAll("_", " ");
const payloadEntries = (payload: unknown): ReadonlyArray<{ readonly key: string; readonly value: string }> => {
if (!isObject(payload)) return [];
return Object.entries(payload).map(([key, value]) => ({
key,
value: formatValue(value) ?? "undefined",
}));
return Object.entries(payload)
// Loose resume schemas often carry implementation flags such as
// `approved: true`; the product surface should show the workflow outcome
// (`submitted/cancelled`) and the operator payload, not duplicate flag
// vocabulary that reads like a second outcome system.
.filter(([key]) => key !== "approved")
.map(([key, value]) => ({
key,
value: formatValue(value) ?? "undefined",
}));
};
export const buildSchemaApprovalModel = ({
@@ -91,7 +91,7 @@ const demoBeatLensByBeat: Readonly<Record<string, DemoBeatLens>> = {
resume: {
phase: "resume",
eyebrow: "Same run resumes",
headline: "The approved payload continues the persisted run",
headline: "The submitted payload continues the persisted run",
proofLabel: "workflow.runs.resume",
speakerLine: "Resume is not a restart; it continues the stopped run with a validated payload.",
},
@@ -196,7 +196,7 @@ export const mainScenes = defineScenes([
view: "demo",
beats: [
sceneBeat("approval", "Approval", "The operator reviews a schema-backed resume request.", { chatMode: "hidden", chatTheme: "light" }),
sceneBeat("resume", "Resume", "The approved payload resumes the same persisted run.", { chatMode: "hidden", chatTheme: "light" }),
sceneBeat("resume", "Resume", "The submitted payload resumes the same persisted run.", { chatMode: "hidden", chatTheme: "light" }),
sceneBeat("output", "Output", "The workflow produces the report and issue-board changes.", { chatMode: "hidden", chatTheme: "light" }),
sceneBeat("trace", "Evidence", "Trace frames and protocol evidence remain inspectable.", { chatMode: "dock", chatTheme: "light", evidencePresentation: "receipt" }),
],
@@ -729,7 +729,7 @@
}
.demo-workflow-stage[data-demo-layout="approval"] {
grid-template-columns: minmax(0, 1.35fr) minmax(18rem, 0.65fr);
grid-template-columns: minmax(0, 1fr);
}
.demo-workflow-stage[data-demo-layout="evidence"] {