fix: clarify schema approval presentation
This commit is contained in:
@@ -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"] {
|
||||
|
||||
Reference in New Issue
Block a user