From 37b3eb45e9af8e1b301455424f1a8398b26359b8 Mon Sep 17 00:00:00 2001 From: mptyl Date: Fri, 3 Jul 2026 13:17:54 +0200 Subject: [PATCH] feat(gate): render reviewer artifacts in a 90% modal (fix empty artifact.data gate) --- .../src/widgets/ArtifactGateWidget.test.tsx | 10 +- frontend/src/widgets/ArtifactGateWidget.tsx | 97 +++++++++++-------- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/frontend/src/widgets/ArtifactGateWidget.test.tsx b/frontend/src/widgets/ArtifactGateWidget.test.tsx index 1facd7da..7c7eb102 100644 --- a/frontend/src/widgets/ArtifactGateWidget.test.tsx +++ b/frontend/src/widgets/ArtifactGateWidget.test.tsx @@ -2,19 +2,25 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ArtifactGateWidget } from "./ArtifactGateWidget"; -test("renders artifact content in a pre block", () => { +vi.mock("../viewers/mermaid", () => ({ + renderMermaid: vi.fn().mockResolvedValue(''), +})); + +test("renders inside a dialog and shows the artifact via ArtifactView (data path)", () => { const onRespond = vi.fn(); render( ); + expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByText("SELECT 42")).toBeInTheDocument(); }); diff --git a/frontend/src/widgets/ArtifactGateWidget.tsx b/frontend/src/widgets/ArtifactGateWidget.tsx index 641ac6b0..cd2adff0 100644 --- a/frontend/src/widgets/ArtifactGateWidget.tsx +++ b/frontend/src/widgets/ArtifactGateWidget.tsx @@ -1,9 +1,18 @@ import { useState } from "react"; import type { WidgetProps } from "./types"; import type { UiResponse, WidgetDescriptor } from "../api/types"; +import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog"; +import { ArtifactView } from "../viewers/ArtifactView"; import { ReservedControls } from "./ReservedControls"; import { LinkageHost } from "./LinkageHost"; +/** + * Artifact review gate rendered as a full-screen (90%) modal: the artifact fills + * the top (scrollable) area via ArtifactView; the action bar (options + reserved + * controls) sits at the bottom. The gate contract forbids silent dismissal, so the + * dialog has no close button and is not closeable by Esc/backdrop — the only way + * out is an action or a reserved control, both of which call onRespond. + */ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) { const [pendingLinkage, setPendingLinkage] = useState<{ parentResponse: UiResponse; @@ -13,48 +22,56 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) { function handleOption(optionId: string) { const option = descriptor.options?.find((o) => o.id === optionId); const parentResponse: UiResponse = { id: descriptor.id, kind: "artifact-gate", choices: [optionId] }; - - if (option?.opens) { - // Route through LinkageHost to collect child text first - setPendingLinkage({ parentResponse, childDescriptor: option.opens }); - } else { - onRespond(parentResponse); - } - } - - if (pendingLinkage) { - return ( - - ); + if (option?.opens) setPendingLinkage({ parentResponse, childDescriptor: option.opens }); + else onRespond(parentResponse); } return ( -
- {descriptor.title &&

{descriptor.title}

} - {descriptor.artifact?.content && ( -
-          {descriptor.artifact.content}
-        
- )} -
- {descriptor.options?.map((o) => ( - - ))} -
- onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) })} - /> -
+ + + {descriptor.title ?? "Artifact review"} + +
+ {descriptor.artifact ? ( + + ) : ( +

No artifact.

+ )} +
+ +
+ {pendingLinkage ? ( + + ) : ( + <> +
+ {descriptor.options?.map((o) => ( + + ))} +
+ + onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) }) + } + /> + + )} +
+
+
); }