From 5c8246a62d51283d1bd2603148fc29718f78ee13 Mon Sep 17 00:00:00 2001 From: Clayton Date: Fri, 10 Jul 2026 14:12:49 -0500 Subject: [PATCH 1/2] feat(ui): add first-session onboarding preview card to maintainer dashboard Closes #2217 Adds OnboardingPreviewCard, which auto-runs the existing settings-preview simulator (buildSettingsPreviewRequest) against a repo's most recently cached pull request on first visit, so a maintainer sees "here's what Gittensory would have flagged" without filling out SurfacePreview's form. Reuses PreviewResult (now exported) for the decision/checklist/comment rendering rather than duplicating it. Dismissible via the same localStorage first-visit idiom app.index.tsx's OnboardingChecklist uses. Distinct from ActivationPreview (#701/#708): that shows aggregate multi-PR evaluation stats from a dedicated endpoint; this shows one demo run's full policy decision from the settings-preview pipeline. --- .../site/app-panels/maintainer-panel.tsx | 8 +- .../onboarding-preview-card.test.tsx | 147 +++++++++++++++++ .../app-panels/onboarding-preview-card.tsx | 149 ++++++++++++++++++ 3 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.test.tsx create mode 100644 apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.tsx diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx index 4f48615d63..01f8f856f6 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx @@ -20,6 +20,7 @@ import { import { ActivationPreview } from "@/components/site/app-panels/activation-preview"; import { AiReviewSettings } from "@/components/site/app-panels/ai-review-settings"; import { MaintainerSettings } from "@/components/site/app-panels/maintainer-settings"; +import { OnboardingPreviewCard } from "@/components/site/app-panels/onboarding-preview-card"; import { CheckRunReadinessTable } from "@/components/site/check-run-readiness-table"; import type { CheckRunReadinessTableData } from "@/components/site/check-run-readiness-model"; import { StatCard } from "@/components/site/primitives"; @@ -119,7 +120,7 @@ type InstallPreview = { }>; }; -type SettingsPreviewResponse = { +export type SettingsPreviewResponse = { repoFullName: string; generatedAt: string; installation: { @@ -224,6 +225,9 @@ function MaintainerDashboardView({
+ + +
{data.metrics.map((metric) => ( ({ apiFetch: vi.fn() })); +vi.mock("@/lib/api/request", () => ({ apiFetch: (...args: unknown[]) => apiFetch(...args) })); +vi.mock("@/lib/api/origin", () => ({ getApiOrigin: () => "https://api.test" })); + +import { OnboardingPreviewCard } from "@/components/site/app-panels/onboarding-preview-card"; +import type { SettingsPreviewResponse } from "@/components/site/app-panels/maintainer-panel"; + +const INSTALL_PREVIEW = { + status: "ready" as const, + summary: "All checks pass.", + readScope: [], + computedContext: [], + previewBehavior: [], + permissions: { + status: "ready" as const, + required: [], + missing: [], + missingEvents: [], + summary: "ok", + }, + publicOutputs: [], + privateOnlyContext: [], + commandAuthorization: [], + auditBehavior: [], + sanitizerBoundaries: [], + manualControls: [], + checklist: [], +}; + +function preview(overrides: Partial = {}): SettingsPreviewResponse { + return { + repoFullName: "acme/widgets", + generatedAt: "2026-07-10T00:00:00.000Z", + installation: null, + sample: { + authorLogin: "octocat", + authorType: "User", + authorAssociation: "NONE", + minerStatus: "confirmed", + title: "Add cursor pagination", + labels: [], + linkedIssues: [], + }, + decision: { + willComment: true, + willLabel: true, + willCheckRun: true, + skipped: false, + skipReason: null, + actions: ["comment", "label", "check_run"], + summary: "Would comment and label this PR.", + }, + previewComment: "Thanks for the PR! A couple of notes...", + appliedLabel: "gittensor:reviewed", + checkRun: { willCreate: true, title: "Gittensory review", detailLevel: "full" }, + installPreview: INSTALL_PREVIEW, + warnings: [], + summary: "Would comment and label this PR.", + ...overrides, + }; +} + +const REVIEWABILITY = [ + { pr: "acme/widgets#12", title: "Add cursor pagination", reason: "linked issue #7" }, +]; + +describe("OnboardingPreviewCard", () => { + beforeEach(() => { + apiFetch.mockReset(); + window.localStorage.clear(); + }); + + it("auto-runs the demo against the most recent PR and shows flagged findings (comment+label+check-run)", async () => { + apiFetch.mockResolvedValue({ ok: true, data: preview() }); + render(); + + await waitFor(() => expect(screen.getByText("Would comment and label this PR.")).toBeTruthy()); + expect(screen.getByText("acme/widgets#12")).toBeTruthy(); + expect(screen.getByText("comment")).toBeTruthy(); + expect(screen.getByText("gittensor:reviewed")).toBeTruthy(); + + // Real title + real linked-issue number scraped from `reason` reach the request. + const [url, options] = apiFetch.mock.calls[0] as [string, { body: string; method: string }]; + expect(url).toContain("/v1/repos/acme/widgets/settings-preview"); + expect(options.method).toBe("POST"); + const body = JSON.parse(options.body) as { sample: { title: string; linkedIssues: number[] } }; + expect(body.sample.title).toBe("Add cursor pagination"); + expect(body.sample.linkedIssues).toEqual([7]); + }); + + it("shows a clean/skip verdict for a PR the policy would not act on", async () => { + apiFetch.mockResolvedValue({ + ok: true, + data: preview({ + decision: { + willComment: false, + willLabel: false, + willCheckRun: false, + skipped: true, + skipReason: "author is a trusted maintainer", + actions: ["skip"], + summary: "Nothing to flag on this PR.", + }, + summary: "Nothing to flag on this PR.", + }), + }); + render(); + await waitFor(() => expect(screen.getByText("Nothing to flag on this PR.")).toBeTruthy()); + expect(screen.getByText("author is a trusted maintainer")).toBeTruthy(); + }); + + it("renders an EmptyState when the repo has no recent pull requests, without calling the API", async () => { + render(); + await waitFor(() => expect(screen.getByText(/No recent pull requests yet/i)).toBeTruthy()); + expect(apiFetch).not.toHaveBeenCalled(); + }); + + it("renders an error state when the demo run fails", async () => { + apiFetch.mockResolvedValue({ ok: false, message: "503 Service Unavailable" }); + render(); + await waitFor(() => + expect(screen.getByText(/Couldn't run the onboarding preview/i)).toBeTruthy(), + ); + expect(screen.getByText("503 Service Unavailable")).toBeTruthy(); + }); + + it("dismisses the card and keeps it hidden (and skips the API call) across a remount", async () => { + apiFetch.mockResolvedValue({ ok: true, data: preview() }); + const { unmount } = render(); + await waitFor(() => expect(screen.getByText("Would comment and label this PR.")).toBeTruthy()); + + fireEvent.click(screen.getByRole("button", { name: "Dismiss onboarding preview" })); + expect(screen.queryByText(/Here's what Gittensory would have flagged/)).toBeNull(); + + apiFetch.mockClear(); + unmount(); + // render() flushes the synchronous hydration + load effects within its own act() wrapping (same + // assumption ActivationPreview's tests already rely on for its own initial-load effect), so both of + // these are safe to assert immediately rather than under waitFor. + render(); + expect(screen.queryByText(/Here's what Gittensory would have flagged/)).toBeNull(); + expect(apiFetch).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.tsx b/apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.tsx new file mode 100644 index 0000000000..6c2df935a2 --- /dev/null +++ b/apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.tsx @@ -0,0 +1,149 @@ +import { useCallback, useEffect, useState } from "react"; +import { X } from "lucide-react"; + +import { StateBoundary } from "@/components/site/state-views"; +import { + PreviewResult, + type SettingsPreviewResponse, +} from "@/components/site/app-panels/maintainer-panel"; +import { apiFetch } from "@/lib/api/request"; +import { getApiOrigin } from "@/lib/api/origin"; +import { + buildSettingsPreviewRequest, + splitRepoFullName, + type PreviewFormState, +} from "@/lib/maintainer-settings-preview"; +import { useLocalStorage } from "@/lib/use-local-storage"; + +type ReviewabilityRow = { pr: string; title: string; reason: string }; + +const DISMISS_KEY = "gittensory_maintainer_onboarding_preview_dismissed"; + +/** Builds a settings-preview form from a REAL cached PR (title, and a linked-issue number scraped from + * `reason` when present) — everything else (author identity, labels, body) isn't in the reviewability + * projection (see src/api/routes.ts's `reviewability` mapper), so it's filled from a representative + * scenario, same as SurfacePreview's own default. Returns null if the PR string doesn't parse. */ +function reviewabilityRowToForm(row: ReviewabilityRow): PreviewFormState | null { + const repoFullName = row.pr.split("#")[0] ?? ""; + if (!splitRepoFullName(repoFullName)) return null; + const linkedIssueMatch = row.reason.match(/linked issue #(\d+)/); + return { + repoFullName, + scenarioId: "confirmed-miner", + title: row.title, + labels: "", + linkedIssues: linkedIssueMatch ? linkedIssueMatch[1] : "", + body: "", + }; +} + +/** + * First-session onboarding preview card (#2217, part of #701): auto-runs the same settings-preview + * simulator SurfacePreview drives manually, against this repo's most recently cached pull request, so a + * maintainer sees "here's what Gittensory would have flagged" without filling out a form. Dismissible via + * localStorage, matching this codebase's established first-visit-card idiom (app.index.tsx's + * OnboardingChecklist). Renders through PreviewResult — the same decision/checklist/comment-preview UI + * SurfacePreview already uses — rather than a new findings UI: the settings-preview response has no + * discrete findings array, so "flagged" here means decision.willComment / willLabel / willCheckRun. + */ +export function OnboardingPreviewCard({ reviewability }: { reviewability: ReviewabilityRow[] }) { + const [state, setState, hydrated] = useLocalStorage<{ dismissed: boolean }>(DISMISS_KEY, { + dismissed: false, + }); + const target = reviewability[0] ?? null; + const [preview, setPreview] = useState(null); + const [loading, setLoading] = useState(Boolean(target)); + const [error, setError] = useState(null); + + const load = useCallback(async () => { + if (!target) { + setPreview(null); + setError(null); + return; + } + const form = reviewabilityRowToForm(target); + const repoParts = form ? splitRepoFullName(form.repoFullName) : null; + if (!form || !repoParts) { + setPreview(null); + setError(`Couldn't parse a repository from ${target.pr}.`); + return; + } + setLoading(true); + setError(null); + const result = await apiFetch( + `${getApiOrigin().replace(/\/$/, "")}/v1/repos/${encodeURIComponent(repoParts.owner)}/${encodeURIComponent(repoParts.repo)}/settings-preview`, + { + method: "POST", + label: "Onboarding preview", + credentials: "include", + headers: { Accept: "application/json", "Content-Type": "application/json" }, + body: JSON.stringify(buildSettingsPreviewRequest(form)), + }, + ); + setLoading(false); + if (result.ok) { + setPreview(result.data); + } else { + setPreview(null); + setError(result.message); + } + }, [target]); + + useEffect(() => { + if (!hydrated || state.dismissed) return; + void load(); + }, [load, hydrated, state.dismissed]); + + if (!hydrated || state.dismissed) return null; + + return ( +
+
+
+

+ Here's what Gittensory would have flagged +

+

+ {target ? ( + <> + A live demo run of the review policy against{" "} + {target.pr}, your most recently cached pull + request. + + ) : ( + "A live demo run against this repo's most recent pull request." + )} +

+
+ +
+ +
+ + + +
+
+ ); +} From d80b1a6762495f41191057f1112eddee0cfc6b18 Mon Sep 17 00:00:00 2001 From: Clayton Date: Fri, 10 Jul 2026 14:28:56 -0500 Subject: [PATCH 2/2] test(ui): add checkRunReadiness to the onboarding-preview-card fixture Required field added by the merged check-run-readiness-table PR (#2216); keeps the SettingsPreviewResponse test fixture in sync post-rebase. --- .../components/site/app-panels/onboarding-preview-card.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.test.tsx b/apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.test.tsx index bafc48331f..4516ef2f9c 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.test.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/onboarding-preview-card.test.tsx @@ -56,6 +56,7 @@ function preview(overrides: Partial = {}): SettingsPrev previewComment: "Thanks for the PR! A couple of notes...", appliedLabel: "gittensor:reviewed", checkRun: { willCreate: true, title: "Gittensory review", detailLevel: "full" }, + checkRunReadiness: null, installPreview: INSTALL_PREVIEW, warnings: [], summary: "Would comment and label this PR.",