Show GitHub connection completion over bot settings

This commit is contained in:
Dotta committed 2026-10-08 06:53:03 -05:00
1 parent 91d2a17b78
commit 1d9152ace4
9 files changed
+211 -122

No files matched your search

+5 -5
View File
@@ -30,11 +30,11 @@ the GitHub Code Review Bot connector. This default-off setting is independent of
4. Paperclip reuses your verified GitHub identity when available. Otherwise,
authorize the dedicated App and confirm the observed account once. Bot work
uses installation credentials, never your personal GitHub token.
5. Connection verification and completion happen automatically. The connected
page shows the actual App identity and enabled repository count, with a
copyable optional review mention. Open **Bot settings** to manage the
bot; normal connection navigation is already available. Runtime and isolation
readiness remain separate prerequisites.
5. Connection verification and completion happen automatically. Paperclip opens
the bot's Settings with a connected confirmation modal showing the actual App
identity, enabled repository count, and a copyable optional review mention.
Dismiss it with **Done**, the close button, or Escape to stay in Settings.
Runtime and isolation readiness remain separate prerequisites.
Installation may return before GitHub's separately delivered signed webhook
ping. Paperclip keeps the same draft and credentials, shows a waiting state,
@@ -57,6 +57,7 @@ vi.mock("@/context/SidebarContext", () => ({
}));
vi.mock("@/lib/router", () => ({
useNavigate: () => vi.fn(),
useLocation: () => ({ state: null }),
useParams: () => ({ endpointId: "endpoint-a", tab: mocks.tab }),
useSearchParams: () => [new URLSearchParams(mocks.search), mocks.setParams],
Link: ({ children }: { children: React.ReactNode }) => (
@@ -24,6 +24,7 @@ vi.mock("./EmailEndpointSetup", () => ({ EmailEndpointSettings: () => <h1>Email
vi.mock("@/context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: () => {} }) }));
vi.mock("@/context/ToastContext", () => ({ useToast: () => ({ pushToast: () => {} }) }));
vi.mock("@/lib/router", () => ({
useLocation: () => ({ state: null }),
useParams: () => ({ endpointId: "inbox", tab: mocks.tab }), useNavigate: () => mocks.navigate,
Link: ({ children, to, ...props }: React.ComponentProps<"a"> & { to: string }) => <a href={to} {...props}>{children}</a>,
Navigate: () => null,
+20 -3
View File
@@ -9,11 +9,12 @@ import { AgentAvatar } from "@/components/AgentAvatar";
import { ChatConversationList } from "./ChatConversationList";
import { GitHubBotManagement, GitHubReviews } from "./GitHubBotManagement";
import { GitHubBotMention } from "./GitHubAppIdentity";
import { GitHubConnectionComplete, type GitHubConnectionCompleteLocationState } from "./GitHubConnectionComplete";
import { EmailEndpointSettings } from "./EmailEndpointSetup";
import { EmailConnectionAccess } from "@/components/EmailConnectionAccess";
import { emailApi } from "@/api/email";
import { toolsApi } from "@/api/tools";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
AlertTriangle,
@@ -57,7 +58,7 @@ import { useToast } from "@/context/ToastContext";
import { formatDateTime } from "@/lib/utils";
import { queryKeys } from "@/lib/queryKeys";
import { copyTextToClipboard } from "@/lib/clipboard";
import { Link, Navigate, useNavigate, useParams } from "@/lib/router";
import { Link, Navigate, useLocation, useNavigate, useParams } from "@/lib/router";
const tabs = ["settings", "access", "reviews", "conversations", "activity"] as const;
type ChatTab = (typeof tabs)[number];
@@ -236,6 +237,9 @@ export function ChatEndpointDetail() {
const tab = reviewId ? "reviews" : (routeTab ?? "settings");
const activeTab = tabs.includes(tab as ChatTab) ? (tab as ChatTab) : null;
const navigate = useNavigate();
const location = useLocation();
const heading = useRef<HTMLHeadingElement>(null);
const completion = (location.state as GitHubConnectionCompleteLocationState | null)?.githubConnectionComplete;
const { setBreadcrumbs } = useBreadcrumbs();
const endpointQuery = useQuery({
queryKey: queryKeys.chatEndpoints.detail(endpointId),
@@ -304,7 +308,7 @@ export function ChatEndpointDetail() {
<div className="flex min-w-0 items-center gap-3">
{endpoint.provider === "github" && <AgentAvatar agent={avatarAgent.data ?? { id: endpoint.assignedAgentId, name: endpoint.assignedAgentName }} size={40} />}
<div className="min-w-0">
<h1 className="text-xl font-bold">
<h1 ref={heading} tabIndex={-1} className="text-xl font-bold">
{endpoint.provider === "github" ? <Link to={`/agents/${endpoint.assignedAgentId}`} className="hover:underline">{endpoint.assignedAgentName}</Link> : `${endpoint.assignedAgentName} in ${providerNames[endpoint.provider]}`}
</h1>
<p className="mt-1 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
@@ -359,6 +363,19 @@ export function ChatEndpointDetail() {
{activeTab === "activity" && (
<Activity endpointId={endpoint.id} endpoint={endpoint} />
)}
{endpoint.provider === "github" && activeTab === "settings" && endpoint.status === "active" &&
endpoint.setup?.step === "complete" && completion?.endpointId === endpoint.id && (
<GitHubConnectionComplete
endpoint={endpoint}
agent={avatarAgent.data}
runtimeChecks={completion.runtimeChecks}
onClose={() => {
const { githubConnectionComplete: _complete, ...rest } = location.state as GitHubConnectionCompleteLocationState;
navigate({ pathname: location.pathname, search: location.search, hash: location.hash }, { replace: true, state: rest });
}}
onCloseAutoFocus={() => heading.current?.focus()}
/>
)}
</div>
);
}
@@ -69,6 +69,7 @@ vi.mock("@/lib/router", () => ({
reviewId: mocks.reviewId,
}),
useNavigate: () => vi.fn(),
useLocation: () => ({ state: null }),
Link: ({
children,
to,
+62 -20
View File
@@ -2,9 +2,12 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter, useLocation } from "react-router-dom";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { GitHubChatSetup, gitHubAppManifestAction } from "./GitHubChatSetup";
import { defaultGitHubReviewPolicy } from "@paperclipai/shared";
import { ChatEndpointDetail } from "./ChatEndpointDetail";
import { TooltipProvider } from "@/components/ui/tooltip";
import { agentsApi } from "@/api/agents";
import { chatEndpointsApi } from "@/api/chatEndpoints";
import { githubChatApi } from "@/api/githubChat";
@@ -71,6 +74,7 @@ vi.mock("@/api/chatEndpoints", () => ({
}));
vi.mock("@/api/githubChat", () => ({
githubChatApi: {
configuration: vi.fn(),
advance: vi.fn(),
repositories: vi.fn(),
registration: vi.fn(),
@@ -161,6 +165,11 @@ describe("GitHub App wizard", () => {
endpointId: "draft-1",
state: "create",
});
vi.mocked(githubChatApi.configuration).mockResolvedValue({
revision: 1,
configuration: { version: 1, toolsEnabled: true, responsibleUserId: "board", memberAccess: "selected", people: [],
defaults: defaultGitHubReviewPolicy(), repositories: {} },
});
vi.mocked(githubChatApi.repositories).mockResolvedValue({
items: [], nextOffset: null, totalCount: 1, enabledCount: 1, availableCount: 1,
});
@@ -168,7 +177,7 @@ describe("GitHub App wizard", () => {
vi.mocked(githubChatApi.personalConnections).mockResolvedValue([]);
vi.mocked(copyTextToClipboard).mockResolvedValue();
client = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } },
defaultOptions: { queries: { retry: false, gcTime: 60_000 } },
});
container = document.createElement("div");
document.body.append(container);
@@ -266,13 +275,22 @@ describe("GitHub App wizard", () => {
<MemoryRouter
initialEntries={[`/BOT/apps/chat/connect?provider=github&${query}`]}
>
<GitHubChatSetup />
<Location />
<TooltipProvider>
<Routes>
<Route path="/BOT/apps/chat/connect" element={<GitHubChatSetup />} />
<Route path="/apps/chat/:endpointId/:tab" element={<ChatEndpointDetail />} />
<Route path="/apps" element={<div>Connectors</div>} />
</Routes>
<Location />
</TooltipProvider>
</MemoryRouter>
</QueryClientProvider>,
),
);
await settle();
// Completion mounts Settings and its own queries after the wizard redirect.
await settle();
await settle();
}
async function click(label: string) {
const button = Array.from(document.querySelectorAll("button")).find(
@@ -570,6 +588,8 @@ describe("GitHub App wizard", () => {
it("completes automatically while reporting runtime readiness separately", async () => {
fixture.endpoint = {
...fixture.endpoint,
status: "active",
setup: { step: "complete" },
botLabel: "Actual GitHub Name",
botUsername: "actual-agent[bot]",
resources: [{ enabled: true, label: "acme/repo" }],
@@ -587,43 +607,65 @@ describe("GitHub App wizard", () => {
],
});
await render("resume=draft-1");
expect(container.querySelector("h1")?.textContent).toBe("GitHub connected");
expect(container.textContent).toContain("Actual GitHub Name");
expect(container.textContent).toContain("GitHub App name and logo");
expect(container.textContent).toContain("1 repository enabled");
expect(container.querySelector("footer a")?.getAttribute("href")).toBe("/apps/chat/draft-1/settings");
expect(container.querySelector("details")?.hasAttribute("open")).toBe(false);
expect(container.textContent).toContain("Before your first review");
expect(container.textContent).toContain("Configure a runtime");
expect(container.textContent).not.toContain("Finish");
expect(document.querySelector('[role="dialog"] h2')?.textContent).toBe("GitHub connected");
expect(document.body.textContent).toContain("Actual GitHub Name");
expect(document.body.textContent).toContain("GitHub App name and logo");
expect(document.body.textContent).toContain("1 repository enabled");
expect(container.querySelector("output")?.textContent).toBe("/apps/chat/draft-1/settings");
expect(document.querySelector('[role="dialog"] details')?.hasAttribute("open")).toBe(false);
expect(document.body.textContent).toContain("Before your first review");
expect(document.body.textContent).toContain("Configure a runtime");
expect(document.body.textContent).not.toContain("Finish");
await click("Copy mention");
expect(copyTextToClipboard).toHaveBeenCalledWith(
"@actual-agent review this pull request",
);
});
it.each(["Done", "Close", "Escape"])("dismisses with %s onto the same mounted Settings page", async (action) => {
fixture.endpoint = { ...fixture.endpoint, status: "active", setup: { step: "complete" } };
vi.mocked(githubChatApi.advance).mockResolvedValue({ endpointId: "draft-1", state: "connected" });
await render("resume=draft-1");
const settings = container.querySelector("textarea");
expect(settings).not.toBeNull();
expect(document.querySelector('[role="dialog"]')).not.toBeNull();
if (action === "Escape") {
await act(async () => {
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
});
await settle();
} else await click(action);
expect(document.querySelector('[role="dialog"]')).toBeNull();
expect(container.querySelector("output")?.textContent).toBe("/apps/chat/draft-1/settings");
expect(container.querySelector("textarea")).toBe(settings);
expect(container.querySelector("h1")?.textContent).toBe("Reviewer");
expect(document.activeElement).toBe(container.querySelector("h1"));
expect(githubChatApi.saveDraft).not.toHaveBeenCalled();
expect(chatEndpointsApi.create).not.toHaveBeenCalled();
});
it("reports copy failure without undoing the completed connection", async () => {
fixture.endpoint = { ...fixture.endpoint, status: "active", botUsername: "actual-agent[bot]" };
fixture.endpoint = { ...fixture.endpoint, status: "active", setup: { step: "complete" }, botUsername: "actual-agent[bot]" };
vi.mocked(githubChatApi.advance).mockResolvedValue({ endpointId: "draft-1", state: "connected" });
vi.mocked(copyTextToClipboard).mockRejectedValueOnce(new Error("Clipboard unavailable"));
await render("resume=draft-1");
await click("Copy mention");
expect(container.textContent).toContain("Copy failed");
expect(container.querySelector("h1")?.textContent).toBe("GitHub connected");
expect(document.body.textContent).toContain("Copy failed");
expect(document.querySelector('[role="dialog"] h2')?.textContent).toBe("GitHub connected");
expect(githubChatApi.registration).not.toHaveBeenCalled();
});
it("shows a useful next step when a connected bot has no enabled repositories", async () => {
fixture.endpoint = { ...fixture.endpoint, status: "active", setup: { step: "complete" } };
vi.mocked(githubChatApi.advance).mockResolvedValue({ endpointId: "draft-1", state: "connected" });
vi.mocked(githubChatApi.repositories).mockResolvedValue({
items: [], nextOffset: null, totalCount: 2, enabledCount: 0, availableCount: 2,
});
await render("resume=draft-1");
expect(container.textContent).toContain("No repositories enabled");
expect(container.textContent).toContain("Enable a repository in Access to try your bot");
expect(container.querySelector('a[href="/apps/chat/draft-1/access"]')).not.toBeNull();
expect(document.body.textContent).toContain("No repositories enabled");
expect(document.body.textContent).toContain("Enable a repository in Access to try your bot");
expect(document.querySelector('[role="dialog"] a[href="/apps/chat/draft-1/access"]')).not.toBeNull();
});
it("does not flash App creation controls while checking a resumed connection", async () => {
fixture.endpoint = { ...fixture.endpoint, status: "active", botUsername: "actual-agent[bot]" };
fixture.endpoint = { ...fixture.endpoint, status: "active", setup: { step: "complete" }, botUsername: "actual-agent[bot]" };
vi.mocked(githubChatApi.advance).mockReturnValue(new Promise(() => {}));
await render("resume=draft-1");
expect(container.textContent).toContain("Checking your GitHub connection");
+18 -15
View File
@@ -21,7 +21,8 @@ import { useCompany } from "@/context/CompanyContext";
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
import { useNavigate, useSearchParams, Link } from "@/lib/router";
import { buildPermissionsForTrustPreset } from "@/lib/trust-policy-ui";
import { GitHubConnectionComplete } from "./GitHubConnectionComplete";
import { queryKeys } from "@/lib/queryKeys";
import type { GitHubConnectionCompleteLocationState } from "./GitHubConnectionComplete";
/** Restrict native manifest submission to GitHub registration endpoints. */
export function gitHubAppManifestAction(
@@ -142,6 +143,17 @@ export function GitHubChatSetup() {
const existingIdentityMethod =
identityOnly || state?.identityMethod === "existing_connection";
const connected = state?.state === "connected" && !existing;
useEffect(() => {
if (!connected || !bot) return;
queryClient.setQueryData(queryKeys.chatEndpoints.detail(bot.id), bot);
if (selectedAgent) queryClient.setQueryData(queryKeys.agents.detail(selectedAgent.id), selectedAgent);
navigate(`/apps/chat/${bot.id}/settings`, {
replace: true,
state: {
githubConnectionComplete: { endpointId: bot.id, runtimeChecks: state.runtimeChecks },
} satisfies GitHubConnectionCompleteLocationState,
});
}, [connected, bot, selectedAgent, state, queryClient, navigate]);
useEffect(() => {
setBreadcrumbs([
{ label: "Connectors", href: "/apps" },
@@ -238,11 +250,10 @@ export function GitHubChatSetup() {
})}
</div>
);
if (connected) return null;
return (
<div className={connected
? "mx-auto w-full max-w-3xl space-y-6 py-6"
: "mx-auto w-full max-w-3xl space-y-6 px-4 py-6 sm:px-6"}>
{!identityOnly && !connected && (
<div className="mx-auto w-full max-w-3xl space-y-6 px-4 py-6 sm:px-6">
{!identityOnly && (
<SetupWizardNavigation
labels={["Choose agent", "Connect GitHub"]}
step={bot ? 1 : 0}
@@ -252,13 +263,13 @@ export function GitHubChatSetup() {
takeover
/>
)}
{!connected && <h1 className="text-2xl font-semibold">
<h1 className="text-2xl font-semibold">
{!bot
? "Choose agent"
: identityOnly || state?.state === "identity"
? "Connect your account"
: "Connect GitHub"}
</h1>}
</h1>
{(error || (!identityOnly && progress.error) || accounts.error) && (
<p role="alert" className="text-sm text-destructive">
{error ||
@@ -331,14 +342,6 @@ export function GitHubChatSetup() {
</p>
{footer()}
</>
) : connected ? (
<GitHubConnectionComplete
endpoint={bot}
agent={selectedAgent}
runtimeChecks={state.runtimeChecks}
onExit={exit}
pending={busy}
/>
) : existing ? (
<>
<p className="text-sm text-muted-foreground">
@@ -6,24 +6,31 @@ import { githubChatApi } from "@/api/githubChat";
import { AgentAvatar, type AvatarAgent } from "@/components/AgentAvatar";
import { AppLogo } from "../AppLogo";
import { CopyText } from "@/components/CopyText";
import { SetupWizardFooter } from "@/components/SetupWizard";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogTitle } from "@/components/ui/dialog";
import { agentAvatarUrl } from "@/lib/agent-avatar-url";
import { Link } from "@/lib/router";
import { GitHubAppBranding, gitHubBotMention } from "./GitHubAppIdentity";
export type GitHubConnectionCompleteLocationState = {
githubConnectionComplete?: {
endpointId: string;
runtimeChecks?: GitHubAppWizardState["runtimeChecks"];
};
};
export function GitHubConnectionComplete({
endpoint,
agent,
runtimeChecks,
onExit,
pending = false,
onClose,
onCloseAutoFocus,
}: {
endpoint: ChatEndpoint;
agent?: AvatarAgent;
runtimeChecks?: GitHubAppWizardState["runtimeChecks"];
onExit: () => void;
pending?: boolean;
onClose: () => void;
onCloseAutoFocus?: () => void;
}) {
const repositories = useQuery({
queryKey: ["github-connected-repositories", endpoint.id],
@@ -34,73 +41,82 @@ export function GitHubConnectionComplete({
const count = repositories.data?.enabledCount;
const incomplete = runtimeChecks?.filter((check) => !check.ok) ?? [];
return (
<div className="@container space-y-8">
<header className="space-y-5">
<div className="flex items-center gap-3">
<CheckCircle2 className="size-6 shrink-0 text-(--status-task-done)" />
<h1 className="text-xl font-semibold @md:text-2xl">GitHub connected</h1>
<Dialog open onOpenChange={(open) => { if (!open) onClose(); }}>
<DialogContent
className="max-h-(--sz-85dvh) overflow-y-auto sm:max-w-xl"
aria-describedby={undefined}
onCloseAutoFocus={onCloseAutoFocus ? (event) => {
event.preventDefault();
onCloseAutoFocus();
} : undefined}
>
<div className="@container space-y-6">
<header className="space-y-5">
<div className="flex items-center gap-3">
<CheckCircle2 className="size-6 shrink-0 text-(--status-task-done)" />
<DialogTitle className="text-xl leading-normal">GitHub connected</DialogTitle>
</div>
<div className="flex items-center gap-3">
<AgentAvatar agent={avatar} size={48} />
<div className="min-w-0 space-y-1">
<h2 className="break-words text-base font-semibold">
{endpoint.botLabel ?? endpoint.botUsername ?? endpoint.assignedAgentName}
</h2>
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground">
<Link to={`/agents/${endpoint.assignedAgentId}`} className="hover:text-foreground hover:underline">
{endpoint.assignedAgentName}
</Link>
{endpoint.providerAccountLabel && <>
<span aria-hidden="true">·</span>
<span className="inline-flex items-center gap-1.5">
<AppLogo name="GitHub" brandKey="github" compact className="size-4! rounded-sm bg-transparent" />
{endpoint.providerAccountLabel}
</span>
</>}
</p>
<Link to={`/apps/chat/${endpoint.id}/access`} className="inline-block text-xs text-muted-foreground underline underline-offset-4">
{repositories.isError ? "Repository access unavailable" : count === undefined ? "Repository access" : count === 0 ? "No repositories enabled" : `${count} ${count === 1 ? "repository" : "repositories"} enabled`}
</Link>
</div>
</div>
</header>
{incomplete.length > 0 && (
<section className="space-y-2">
<h2 className="text-sm font-medium">Before your first review</h2>
{incomplete.map((check) => (
<p key={check.key} className="text-sm text-muted-foreground">{check.detail}</p>
))}
</section>
)}
<section className="space-y-3">
<div className="space-y-1">
<h2 className="text-base font-semibold">Try your bot</h2>
<p className="text-sm text-muted-foreground">
{count === 0
? "Enable a repository in Access to try your bot."
: "Post this on a pull request in an enabled repository."}
</p>
</div>
<CopyText
text={mention}
ariaLabel="Copy mention"
title="Copy mention"
containerClassName="w-full"
className="flex w-full items-center gap-3 rounded-md border border-border bg-muted/30 p-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<code className="min-w-0 flex-1 break-words text-sm">{mention}</code>
<Copy className="size-4 shrink-0 text-muted-foreground" />
</CopyText>
</section>
<GitHubAppBranding
endpoint={endpoint}
avatarUrl={agentAvatarUrl(resolveAgentAppearance(avatar.appearance, endpoint.assignedAgentId), 512, 1, "rest")}
/>
<DialogFooter>
<Button onClick={onClose}>Done</Button>
</DialogFooter>
</div>
<div className="flex items-center gap-3">
<AgentAvatar agent={avatar} size={48} />
<div className="min-w-0 space-y-1">
<h2 className="break-words text-base font-semibold">
{endpoint.botLabel ?? endpoint.botUsername ?? endpoint.assignedAgentName}
</h2>
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground">
<Link to={`/agents/${endpoint.assignedAgentId}`} className="hover:text-foreground hover:underline">
{endpoint.assignedAgentName}
</Link>
{endpoint.providerAccountLabel && <>
<span aria-hidden="true">·</span>
<span className="inline-flex items-center gap-1.5">
<AppLogo name="GitHub" brandKey="github" compact className="size-4! rounded-sm bg-transparent" />
{endpoint.providerAccountLabel}
</span>
</>}
</p>
<Link to={`/apps/chat/${endpoint.id}/access`} className="inline-block text-xs text-muted-foreground underline underline-offset-4">
{repositories.isError ? "Repository access unavailable" : count === undefined ? "Repository access" : count === 0 ? "No repositories enabled" : `${count} ${count === 1 ? "repository" : "repositories"} enabled`}
</Link>
</div>
</div>
</header>
{incomplete.length > 0 && (
<section className="space-y-2">
<h2 className="text-sm font-medium">Before your first review</h2>
{incomplete.map((check) => (
<p key={check.key} className="text-sm text-muted-foreground">{check.detail}</p>
))}
</section>
)}
<section className="space-y-3">
<div className="space-y-1">
<h2 className="text-base font-semibold">Try your bot</h2>
<p className="text-sm text-muted-foreground">
{count === 0
? "Enable a repository in Access to try your bot."
: "Post this on a pull request in an enabled repository."}
</p>
</div>
<CopyText
text={mention}
ariaLabel="Copy mention"
title="Copy mention"
containerClassName="w-full"
className="flex w-full items-center gap-3 rounded-md border border-border bg-muted/30 p-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<code className="min-w-0 flex-1 break-words text-sm">{mention}</code>
<Copy className="size-4 shrink-0 text-muted-foreground" />
</CopyText>
</section>
<GitHubAppBranding
endpoint={endpoint}
avatarUrl={agentAvatarUrl(resolveAgentAppearance(avatar.appearance, endpoint.assignedAgentId), 512, 1, "rest")}
/>
<SetupWizardFooter onSaveExit={onExit} disabled={pending}>
<Button asChild>
<Link to={`/apps/chat/${endpoint.id}/settings`}>Bot settings</Link>
</Button>
</SetupWizardFooter>
</div>
</DialogContent>
</Dialog>
);
}
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useState, type ComponentProps } from "react";
import { Button } from "@/components/ui/button";
import { useQueryClient } from "@tanstack/react-query";
import { githubChatApi } from "@/api/githubChat";
import { chatEndpointsApi } from "@/api/chatEndpoints";
@@ -42,7 +43,7 @@ const meta = {
docs: {
description: {
component:
"Production pages and components. Start with **01 Journey / Settings**, then use the actual contextual sidebar to visit Access, Reviews, and Conversations. Try editing instructions, switch tabs, and save or discard the shared draft. **01 Journey / Connected** uses the production completion page, then opens the same bot Settings. **02 States** covers empty, loading, recoverable error, long content, and mobile. **03 Components** isolates the policy editor, people access, repository access, review rows and detail, pending review, and conversations. Provider operations use isolated fixture APIs; these stories are not live GitHub evidence.",
"Production pages and components. Start with **01 Journey / Settings**, then use the actual contextual sidebar to visit Access, Reviews, and Conversations. Try editing instructions, switch tabs, and save or discard the shared draft. **01 Journey / Connected** opens the production confirmation modal over the same bot Settings. Close it with Done, the ×, or Escape and continue editing Settings. **02 States** covers empty, loading, recoverable error, long content, and mobile. **03 Components** isolates the policy editor, people access, repository access, review rows and detail, pending review, and conversations. Provider operations use isolated fixture APIs; these stories are not live GitHub evidence.",
},
},
},
@@ -79,7 +80,7 @@ const route = (tab: string) => ({
initialEntries: [`/PAP/apps/chat/${endpoint.id}/${tab}`],
});
export const ConnectedJourney: Story = {
name: "01 Journey / Connected — copy a mention or open settings",
name: "01 Journey / Connected — dismiss to settings",
parameters: { initialEntries: [`/PAP/apps/chat/connect?provider=github&resume=${endpoint.id}`] },
};
export const ConnectedMobile: Story = {
@@ -205,20 +206,27 @@ export const RepositoryAccess: Story = {
name: "03 Components / Repository access",
render: () => component(<Repositories />),
};
function ConnectedExample(props: Omit<ComponentProps<typeof GitHubConnectionComplete>, "onClose">) {
const [open, setOpen] = useState(true);
return <>
<Button onClick={() => setOpen(true)}>Show connected confirmation</Button>
{open && <GitHubConnectionComplete {...props} onClose={() => setOpen(false)} />}
</>;
}
export const ConnectedSummary: Story = {
name: "03 Components / Connected summary",
render: () => component(<FixtureApi><GitHubConnectionComplete endpoint={endpoint} agent={agent} onExit={() => {}} /></FixtureApi>),
render: () => component(<FixtureApi><ConnectedExample endpoint={endpoint} agent={agent} /></FixtureApi>),
};
export const ConnectedRuntimeNotReady: Story = {
name: "03 Components / Connected — runtime needs setup",
render: () => component(<FixtureApi><GitHubConnectionComplete endpoint={endpoint} agent={agent} onExit={() => {}}
render: () => component(<FixtureApi><ConnectedExample endpoint={endpoint} agent={agent}
runtimeChecks={[{ key: "runtime", label: "Runtime", ok: false, detail: "Configure an isolated runtime before the first review." }]} /></FixtureApi>),
};
export const ConnectedLongName: Story = {
name: "03 Components / Connected — long App name",
render: () => component(<FixtureApi><GitHubConnectionComplete endpoint={{ ...endpoint,
render: () => component(<FixtureApi><ConnectedExample endpoint={{ ...endpoint,
botLabel: "Maya Platform Accessibility Reviews", botUsername: "maya-platform-accessibility-reviews[bot]",
providerAccountLabel: "acme-platform-engineering" }} agent={agent} onExit={() => {}} /></FixtureApi>),
providerAccountLabel: "acme-platform-engineering" }} agent={agent} /></FixtureApi>),
};
export const AppIdentityAndLogo: Story = {
name: "03 Components / App identity and logo",