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 } : {}) })}
- />
-
+
);
}