chore: commit remaining worktree changes
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
import { apiFetch } from "./client";
|
||||
import type { PreviewResult } from "./types";
|
||||
|
||||
export const sqlPreview = (
|
||||
id: string,
|
||||
opts: { limit?: number; offset?: number } = {}
|
||||
) =>
|
||||
apiFetch<PreviewResult>(`/sessions/${id}/sql/preview`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(opts),
|
||||
});
|
||||
|
||||
export const sqlExport = (id: string) =>
|
||||
apiFetch<{ path: string }>(`/sessions/${id}/sql/export`, { method: "POST" });
|
||||
@@ -1,38 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
|
||||
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
|
||||
return (
|
||||
<RadioGroupPrimitive
|
||||
data-slot="radio-group"
|
||||
className={cn("grid w-full gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
|
||||
return (
|
||||
<RadioPrimitive.Root
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="flex size-4 items-center justify-center"
|
||||
>
|
||||
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
|
||||
</RadioPrimitive.Indicator>
|
||||
</RadioPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base shadow-xs transition-[border-color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -1,120 +0,0 @@
|
||||
// frontend/src/shell/NewSessionDialog.test.tsx
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { server } from "../test/msw";
|
||||
import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
|
||||
import { NewSessionDialog } from "./NewSessionDialog";
|
||||
import { workspacePreferences } from "../workspaces/preferences";
|
||||
|
||||
function renderDialog() {
|
||||
const onCreated = vi.fn();
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(
|
||||
<QueryClientProvider client={client}>
|
||||
<NewSessionDialog onCreated={onCreated} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return { onCreated };
|
||||
}
|
||||
|
||||
test("the form has only a question field (no workspace/model/provider/thinking)", async () => {
|
||||
renderDialog();
|
||||
await userEvent.click(screen.getByRole("button", { name: /new/i }));
|
||||
expect(await screen.findByLabelText(/question/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/workspace/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/model/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/provider/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/thinking/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("submitting includes ephemeral migrated preferences and calls onCreated", async () => {
|
||||
localStorage.clear();
|
||||
let body: unknown = null;
|
||||
server.use(
|
||||
http.get("/api/settings", () => HttpResponse.json({
|
||||
workspace: "default", provider: "zai", model: "glm-5.2", thinking: "low",
|
||||
})),
|
||||
http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
...workspaceSummaryFixture("default", {
|
||||
displayName: "Default",
|
||||
revision: workspaceRevisionFixture("default"),
|
||||
}),
|
||||
}])),
|
||||
http.get("/api/workspaces/default", () => HttpResponse.json({
|
||||
workspace: canonicalWorkspaceFixture("default", ["zai/glm-5.2"], "zai/glm-5.2"),
|
||||
revision: workspaceRevisionFixture("default"),
|
||||
})),
|
||||
http.post("/api/sessions", async ({ request }) => {
|
||||
body = await request.json();
|
||||
return HttpResponse.json({ id: "s1" });
|
||||
}),
|
||||
);
|
||||
const { onCreated } = renderDialog();
|
||||
await userEvent.click(screen.getByRole("button", { name: /new/i }));
|
||||
await userEvent.type(await screen.findByLabelText(/question/i), "Quante vendite nel 2025?");
|
||||
await userEvent.click(screen.getByRole("button", { name: /^create$/i }));
|
||||
|
||||
await waitFor(() => expect(body).toEqual({
|
||||
question: "Quante vendite nel 2025?",
|
||||
workspaceId: "default", provider: "zai", model: "glm-5.2", thinking: "low",
|
||||
}));
|
||||
await waitFor(() => expect(onCreated).toHaveBeenCalledWith("s1"));
|
||||
});
|
||||
|
||||
test("first-run direct dialog creation waits for registry policy without a mounted footer", async () => {
|
||||
let body: unknown;
|
||||
let releasePolicy!: () => void;
|
||||
let summaryRequestStarted = false;
|
||||
let policyRequestStarted = false;
|
||||
const policyMayFinish = new Promise<void>((resolve) => { releasePolicy = resolve; });
|
||||
const revision = {
|
||||
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
|
||||
};
|
||||
workspacePreferences.save({
|
||||
workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
|
||||
});
|
||||
server.use(
|
||||
http.get("/api/workspaces", () => {
|
||||
summaryRequestStarted = true;
|
||||
return HttpResponse.json([{
|
||||
...workspaceSummaryFixture("psd-clinical", { displayName: "PSD Clinical", revision: revision as any }),
|
||||
}]);
|
||||
}),
|
||||
http.get("/api/workspaces/psd-clinical", async () => {
|
||||
policyRequestStarted = true;
|
||||
await policyMayFinish;
|
||||
return HttpResponse.json({
|
||||
workspace: canonicalWorkspaceFixture("psd-clinical", ["zai/glm-5.2"], "zai/glm-5.2"), revision,
|
||||
});
|
||||
}),
|
||||
http.post("/api/sessions", async ({ request }) => {
|
||||
body = await request.json();
|
||||
return HttpResponse.json({ id: "s1" });
|
||||
}),
|
||||
);
|
||||
|
||||
const { onCreated } = renderDialog();
|
||||
await userEvent.click(screen.getByRole("button", { name: /new/i }));
|
||||
await userEvent.type(await screen.findByLabelText(/question/i), "q");
|
||||
await userEvent.click(screen.getByRole("button", { name: /^create$/i }));
|
||||
|
||||
await waitFor(() => expect(summaryRequestStarted).toBe(true));
|
||||
await waitFor(() => expect(policyRequestStarted).toBe(true));
|
||||
expect(body).toBeUndefined();
|
||||
expect(screen.getByRole("button", { name: /creating/i })).toBeDisabled();
|
||||
releasePolicy();
|
||||
|
||||
await waitFor(() => expect(body).toEqual({
|
||||
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
|
||||
}));
|
||||
await waitFor(() => expect(onCreated).toHaveBeenCalledWith("s1"));
|
||||
});
|
||||
|
||||
test("empty question shows a validation error and does not submit", async () => {
|
||||
renderDialog();
|
||||
await userEvent.click(screen.getByRole("button", { name: /new/i }));
|
||||
await userEvent.click(screen.getByRole("button", { name: /^create$/i }));
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent(/empty/i);
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
// frontend/src/shell/NewSessionDialog.tsx
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../components/ui/dialog";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { createSession } from "../api/sessions";
|
||||
import { captureAuthOperation, isAuthOperationCurrent } from "../auth/authOperation";
|
||||
|
||||
interface Props {
|
||||
onCreated: (id: string) => void;
|
||||
}
|
||||
|
||||
export function NewSessionDialog({ onCreated }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [question, setQuestion] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const operationEpochRef = useRef(0);
|
||||
useEffect(() => () => { operationEpochRef.current += 1; }, []);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (!question.trim()) {
|
||||
setError("The question cannot be empty.");
|
||||
return;
|
||||
}
|
||||
const operation = captureAuthOperation({ disposalEpoch: operationEpochRef.current });
|
||||
setBusy(true);
|
||||
try {
|
||||
const { id } = await createSession({ question: question.trim() }, operation ? {
|
||||
operation,
|
||||
isCurrent: () => isAuthOperationCurrent(operation, {
|
||||
sessionId: null,
|
||||
disposalEpoch: operationEpochRef.current,
|
||||
}),
|
||||
} : undefined);
|
||||
if (operation && !isAuthOperationCurrent(operation, {
|
||||
sessionId: null,
|
||||
disposalEpoch: operationEpochRef.current,
|
||||
})) return;
|
||||
setOpen(false);
|
||||
setQuestion("");
|
||||
onCreated(id);
|
||||
} catch (err) {
|
||||
if (operation && !isAuthOperationCurrent(operation, {
|
||||
sessionId: null,
|
||||
disposalEpoch: operationEpochRef.current,
|
||||
})) return;
|
||||
setError(err instanceof Error ? err.message : "Failed to create the session.");
|
||||
} finally {
|
||||
if (!operation || isAuthOperationCurrent(operation, {
|
||||
sessionId: null,
|
||||
disposalEpoch: operationEpochRef.current,
|
||||
})) setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
New session
|
||||
</Button>
|
||||
<DialogContent showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New session</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-muted-foreground" htmlFor="nsd-question">
|
||||
Question
|
||||
</label>
|
||||
<textarea
|
||||
id="nsd-question"
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full resize-none rounded-lg border border-input bg-card px-3 py-2 text-sm shadow-xs outline-none transition-[border-color,box-shadow] placeholder:text-muted-foreground focus:border-primary/50 focus:ring-3 focus:ring-ring/15"
|
||||
placeholder="Enter your question…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" size="sm" disabled={busy}>
|
||||
{busy ? "Creating…" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/** The rotating "assistant is working" icon. Spins while `spinning`; used as the
|
||||
* inline activity trigger in CentralStatus. */
|
||||
export function WorkingSpinner({
|
||||
className,
|
||||
spinning = true,
|
||||
}: {
|
||||
className?: string;
|
||||
spinning?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="status"
|
||||
aria-label="Assistant is working"
|
||||
className={["size-4 text-primary", spinning ? "animate-spin" : "", className]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" opacity="0.2" />
|
||||
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { server } from "../test/msw";
|
||||
import { ResultsPanel } from "./ResultsPanel";
|
||||
|
||||
const BASE = "/api";
|
||||
|
||||
function makeClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
}
|
||||
|
||||
function wrap(ui: React.ReactElement) {
|
||||
return (
|
||||
<QueryClientProvider client={makeClient()}>{ui}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// (a) 2-col × 2-row preview → AGGrid renders cell values
|
||||
test("(a) renders grid cells for a 2×2 preview result", async () => {
|
||||
server.use(
|
||||
http.post(`${BASE}/sessions/sess-1/sql/preview`, () =>
|
||||
HttpResponse.json({
|
||||
columns: ["name", "age"],
|
||||
rows: [
|
||||
["Alice", 30],
|
||||
["Bob", 25],
|
||||
],
|
||||
execution_ms: 5,
|
||||
truncated: true,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
render(wrap(<ResultsPanel sessionId="sess-1" />));
|
||||
|
||||
// Wait for data to load; check known cell values
|
||||
await screen.findByText("Alice");
|
||||
expect(screen.getByText("Bob")).toBeInTheDocument();
|
||||
expect(screen.getByText("30")).toBeInTheDocument();
|
||||
expect(screen.getByText("25")).toBeInTheDocument();
|
||||
|
||||
// Grid must be present, not bold scalar
|
||||
expect(screen.queryByTestId("scalar-value")).not.toBeInTheDocument();
|
||||
|
||||
// truncated:true → show the "results truncated" indicator
|
||||
expect(screen.getByText(/results truncated/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// (b) 1×1 preview → bold scalar shown, no grid
|
||||
test("(b) renders bold scalar for a 1×1 result", async () => {
|
||||
server.use(
|
||||
http.post(`${BASE}/sessions/sess-2/sql/preview`, () =>
|
||||
HttpResponse.json({
|
||||
columns: ["count"],
|
||||
rows: [[42]],
|
||||
execution_ms: 2,
|
||||
truncated: false,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
render(wrap(<ResultsPanel sessionId="sess-2" />));
|
||||
|
||||
// Should show the scalar value in bold
|
||||
const bold = await screen.findByTestId("scalar-value");
|
||||
expect(bold).toHaveTextContent("42");
|
||||
|
||||
// No grid
|
||||
expect(screen.queryByRole("treegrid")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("grid")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// (c) clicking "Esporta CSV" calls the export endpoint
|
||||
test("(c) Esporta CSV button calls the export endpoint", async () => {
|
||||
let exportCalled = false;
|
||||
|
||||
server.use(
|
||||
http.post(`${BASE}/sessions/sess-3/sql/preview`, () =>
|
||||
HttpResponse.json({
|
||||
columns: ["id"],
|
||||
rows: [[1], [2]],
|
||||
execution_ms: 3,
|
||||
truncated: false,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
})
|
||||
),
|
||||
http.post(`${BASE}/sessions/sess-3/sql/export`, () => {
|
||||
exportCalled = true;
|
||||
return HttpResponse.json({ path: "/tmp/export.csv" });
|
||||
})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(wrap(<ResultsPanel sessionId="sess-3" />));
|
||||
|
||||
// Wait for preview to load (use role to avoid ambiguity with paging panel)
|
||||
await waitFor(() => expect(screen.getAllByRole("gridcell")).not.toHaveLength(0));
|
||||
|
||||
const btn = screen.getByRole("button", { name: /export csv/i });
|
||||
await user.click(btn);
|
||||
|
||||
await waitFor(() => expect(exportCalled).toBe(true));
|
||||
});
|
||||
|
||||
// (d) selecting "tutti" drives sqlPreview with the large/unlimited limit
|
||||
test("(d) selecting tutti re-fetches preview with the large limit", async () => {
|
||||
const limits: unknown[] = [];
|
||||
|
||||
server.use(
|
||||
http.post(`${BASE}/sessions/sess-4/sql/preview`, async ({ request }) => {
|
||||
const body = (await request.json()) as { limit?: number };
|
||||
limits.push(body.limit);
|
||||
return HttpResponse.json({
|
||||
columns: ["id"],
|
||||
rows: [[1], [2]],
|
||||
execution_ms: 3,
|
||||
truncated: false,
|
||||
limit: body.limit ?? 10,
|
||||
offset: 0,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(wrap(<ResultsPanel sessionId="sess-4" />));
|
||||
|
||||
// Initial fetch uses limit 10
|
||||
await waitFor(() => expect(limits).toContain(10));
|
||||
|
||||
// Select "all"
|
||||
const select = screen.getByLabelText(/rows to display/i);
|
||||
await user.selectOptions(select, "all");
|
||||
|
||||
// A re-fetch with the large/unlimited limit must occur
|
||||
await waitFor(() => expect(limits.some((l) => (l as number) > 10)).toBe(true));
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { sqlPreview, sqlExport } from "../api/sql";
|
||||
import { PreviewGrid } from "./PreviewGrid";
|
||||
|
||||
interface Props {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export function ResultsPanel({ sessionId }: Props) {
|
||||
const [limit, setLimit] = useState<number>(10);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["preview", sessionId, limit],
|
||||
queryFn: () => sqlPreview(sessionId, { limit, offset: 0 }),
|
||||
});
|
||||
|
||||
async function handleExport() {
|
||||
await sqlExport(sessionId);
|
||||
}
|
||||
|
||||
const isScalar =
|
||||
data != null && data.columns.length === 1 && data.rows.length === 1;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Limit selector */}
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<select
|
||||
className="rounded-lg border border-input bg-card px-2 py-1 text-sm shadow-xs outline-none transition-[border-color,box-shadow] focus:border-primary/50 focus:ring-2 focus:ring-ring/20"
|
||||
value={limit}
|
||||
onChange={(e) =>
|
||||
setLimit(e.target.value === "all" ? 1_000_000 : Number(e.target.value))
|
||||
}
|
||||
aria-label="Rows to display"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value="all">all</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
className="rounded-md border border-border/70 px-3 py-1 text-sm shadow-xs transition-colors hover:bg-accent"
|
||||
onClick={handleExport}
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Loading / error states */}
|
||||
{isLoading && <div>Loading…</div>}
|
||||
{error && <div>Error: {String(error)}</div>}
|
||||
|
||||
{/* Scalar result: 1 col × 1 row → bold value */}
|
||||
{data && isScalar && (
|
||||
<strong data-testid="scalar-value">{String(data.rows[0][0])}</strong>
|
||||
)}
|
||||
|
||||
{/* Grid for everything else */}
|
||||
{data && !isScalar && (
|
||||
<>
|
||||
<PreviewGrid columns={data.columns} rows={data.rows} />
|
||||
{data.truncated && <div>(results truncated)</div>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// TypeScript mirrors of the v2 artifact payload contracts (see
|
||||
// .superpowers/sdd/review-gates-v2/contracts.md). Detection everywhere is
|
||||
// TypeScript mirrors of the persisted v2 artifact payload contracts. Detection everywhere is
|
||||
// `data.schema_version === 2` + a shape-guard; anything else falls back to the
|
||||
// legacy renderers in ArtifactView.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user