mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
Show GitHub connection completion over bot settings
This commit is contained in:
1 parent
91d2a17b78
commit
1d9152ace4
9 files changed
+211
-122
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user