From 91d2a17b784da53485efd0ba218fadc3db8389b1 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 8 Oct 2026 06:39:44 -0500 Subject: [PATCH] Simplify GitHub bot completion and hide repository override editing --- doc/connections/GITHUB-REVIEW-BOT.md | 12 +- ui/src/components/AppsSidebar.production.tsx | 2 +- .../chat/ChatDetailSidebar.test.tsx | 33 ++++++ .../components/chat/ChatSetupNavigation.tsx | 24 +++- ui/src/pages/apps/chat/GitHubAppIdentity.tsx | 60 ++++------ .../apps/chat/GitHubBotManagement.test.tsx | 33 +++--- .../pages/apps/chat/GitHubBotManagement.tsx | 84 +------------- .../pages/apps/chat/GitHubChatSetup.test.tsx | 40 ++++++- ui/src/pages/apps/chat/GitHubChatSetup.tsx | 74 ++++-------- .../apps/chat/GitHubConnectionComplete.tsx | 106 ++++++++++++++++++ .../github-bot-management/fixture-api.tsx | 3 + .../management.stories.tsx | 34 +++++- 12 files changed, 302 insertions(+), 203 deletions(-) create mode 100644 ui/src/pages/apps/chat/GitHubConnectionComplete.tsx diff --git a/doc/connections/GITHUB-REVIEW-BOT.md b/doc/connections/GITHUB-REVIEW-BOT.md index 86b38906d3e..8458e532596 100644 --- a/doc/connections/GITHUB-REVIEW-BOT.md +++ b/doc/connections/GITHUB-REVIEW-BOT.md @@ -31,8 +31,10 @@ the GitHub Code Review Bot connector. This default-off setting is independent of 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 repositories; a mention test is - optional. Runtime and isolation readiness remain separate prerequisites. + 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. Installation may return before GitHub's separately delivered signed webhook ping. Paperclip keeps the same draft and credentials, shows a waiting state, @@ -88,8 +90,10 @@ selected environment. ## Manage a connected bot - **Settings:** edit instructions, choose when the bot runs, and set review output. - Event-specific instructions, filters, formal approvals, and repository overrides - are available in disclosures. + Event-specific instructions, filters, and formal approvals are available in + disclosures. Repository-override editing is temporarily hidden in the UI; + existing overrides and the configuration API remain supported. Changing + defaults preserves saved overrides. - **Access:** choose enabled repositories, allowed people, and each person's **Run automatically** setting. Repository switches save immediately. Other changes use **Save changes**. A linked account alone does not grant selected-member diff --git a/ui/src/components/AppsSidebar.production.tsx b/ui/src/components/AppsSidebar.production.tsx index b9859b97a42..bf50e5d4b2c 100644 --- a/ui/src/components/AppsSidebar.production.tsx +++ b/ui/src/components/AppsSidebar.production.tsx @@ -38,7 +38,7 @@ export function AppsSidebar() { (tab) => !isExperimentalToolTab(tab.key) || smokeLabEnabled, ); - if (pathname.endsWith("/apps/chat/connect")) return ; + if (pathname.endsWith("/apps/chat/connect")) return ; const chatDetail = pathname.match(/\/apps\/chat\/([^/]+)(?:\/(?:settings|access|conversations|activity)|\/reviews(?:\/[^/]+)?)?\/?$/); if (chatDetail) return ; diff --git a/ui/src/components/chat/ChatDetailSidebar.test.tsx b/ui/src/components/chat/ChatDetailSidebar.test.tsx index 2d6cecbfe98..9a129098ea3 100644 --- a/ui/src/components/chat/ChatDetailSidebar.test.tsx +++ b/ui/src/components/chat/ChatDetailSidebar.test.tsx @@ -5,6 +5,11 @@ import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; import { describe, expect, it, vi } from "vitest"; import { ChatDetailSidebar } from "./ChatDetailSidebar"; +import { ChatSetupSidebar } from "./ChatSetupNavigation"; +import { githubChatApi } from "@/api/githubChat"; +import { queryKeys } from "@/lib/queryKeys"; +const setup = vi.hoisted(() => ({ params: "provider=github&resume=endpoint-a", advance: vi.fn() })); +vi.mock("@/api/githubChat", () => ({ githubChatApi: { advance: setup.advance } })); import { SidebarNavItem, SidebarNavExpandedProvider } from "../SidebarNavItem"; import { SidebarNavItem as ProductionNavItem, SidebarNavExpandedProvider as ProductionProvider } from "../SidebarNavItem.production"; import { TooltipProvider } from "../ui/tooltip"; @@ -13,6 +18,7 @@ vi.mock("@/context/SidebarContext", () => ({ useSidebar: () => ({ collapsed: true, peeking: false, isMobile: false, setSidebarOpen: vi.fn() }), })); vi.mock("@/lib/router", () => ({ + useSearchParams: () => [new URLSearchParams(setup.params)], NavLink: ({ to, children, className }: { to: string; children: ReactNode; className?: string | ((state: { isActive: boolean }) => string) }) => {children}, })); @@ -34,3 +40,30 @@ describe("chat detail sidebar with collapsed global navigation", () => { } finally { flushSync(() => root.unmount()); } }); }); + +describe("completed GitHub setup navigation", () => { + it.each([ + ["provider=github&resume=endpoint-a", true], + ["provider=github&resume=endpoint-a&reconnect=1", false], + ["provider=github&resume=endpoint-a&stage=identity", false], + ])("uses the right sidebar for %s without advancing setup", (params, complete) => { + setup.params = params; + setup.advance.mockClear(); + const client = new QueryClient({ defaultOptions: { queries: { enabled: false, retry: false } } }); + client.setQueryData(["github-wizard", "endpoint-a"], { endpointId: "endpoint-a", state: "connected" }); + client.setQueryData(queryKeys.chatEndpoints.detail("endpoint-a"), { provider: "github" }); + const container = document.createElement("div"); + const root = createRoot(container); + try { + flushSync(() => root.render( + + )); + expect(container.querySelector('nav[aria-label="Chat connection"]') !== null).toBe(complete); + if (complete) { + expect(container.textContent).toContain("Settings"); + expect(container.textContent).toContain("Reviews"); + } + expect(githubChatApi.advance).not.toHaveBeenCalled(); + } finally { flushSync(() => root.unmount()); client.clear(); } + }); +}); diff --git a/ui/src/components/chat/ChatSetupNavigation.tsx b/ui/src/components/chat/ChatSetupNavigation.tsx index 95e9cc47b6c..6e24a7e1fdb 100644 --- a/ui/src/components/chat/ChatSetupNavigation.tsx +++ b/ui/src/components/chat/ChatSetupNavigation.tsx @@ -1,5 +1,27 @@ +import { useQuery } from "@tanstack/react-query"; +import type { GitHubAppWizardState } from "@paperclipai/shared"; +import { githubChatApi } from "@/api/githubChat"; +import { useSearchParams } from "@/lib/router"; import { SetupWizardNavigation, SetupWizardSidebar } from "../SetupWizard"; -export { SetupWizardSidebar as ChatSetupSidebar }; +import { SidebarNavItem } from "../SidebarNavItem"; +import { ChatDetailSidebar } from "./ChatDetailSidebar"; + +/** Once setup completes, reuse the connection's normal management navigation. */ +export function ChatSetupSidebar({ NavItem = SidebarNavItem }: { NavItem?: typeof SidebarNavItem }) { + const [params] = useSearchParams(); + const resume = params.get("resume"); + // Observe progress owned and fetched by the wizard; the sidebar never advances setup. + const progress = useQuery({ + queryKey: ["github-wizard", resume], + queryFn: () => githubChatApi.advance(resume!), + enabled: false, + }); + return params.get("provider") === "github" && resume && + params.get("stage") !== "identity" && params.get("reconnect") !== "1" && + progress.data?.state === "connected" + ? + : ; +} export function ChatSetupNavigation(props: { labels?: string[]; step: number; availableStep: number; disabled?: boolean; onSelect: (step: number) => void; }) { diff --git a/ui/src/pages/apps/chat/GitHubAppIdentity.tsx b/ui/src/pages/apps/chat/GitHubAppIdentity.tsx index a2b0d1f2037..0b68258ca3f 100644 --- a/ui/src/pages/apps/chat/GitHubAppIdentity.tsx +++ b/ui/src/pages/apps/chat/GitHubAppIdentity.tsx @@ -1,7 +1,8 @@ -import { ExternalLink } from "lucide-react"; +import { ChevronRight, ExternalLink } from "lucide-react"; import type { ChatEndpoint } from "@/api/chatEndpoints"; import { AgentAvatarDownload } from "@/components/AgentAvatarDownload"; import { CopyText } from "@/components/CopyText"; +import { Button } from "@/components/ui/button"; import { Link } from "@/lib/router"; export function gitHubBotMention(endpoint: Pick) { @@ -44,61 +45,42 @@ export function GitHubAppBranding({ avatarUrl?: string; }) { return ( -
- +
+ + GitHub App name and logo
-

- This is your custom GitHub App - {endpoint.botLabel ? ( - <> - : {endpoint.botLabel}. - - ) : ( - "." - )} - {endpoint.providerAccountLabel && ( - <> - {" "} - Owned by {endpoint.providerAccountLabel}. - - )} +

+ Change the name or upload a logo in the App’s Display information settings on GitHub.

-

- GitHub derives the @mention from the App name. Upload your logo in the - App’s Display information settings. -

- {avatarUrl && ( -
+
+ {avatarUrl && <> {`${endpoint.assignedAgentName}’s -
- )} - - Edit App name and logo on GitHub - + } + +

- After renaming the App,{" "} + Renaming also changes its @mention.{" "} - reconnect this App - {" "} - to refresh its mention in Paperclip. + Reconnect afterward + {" "}to update it here.

diff --git a/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx b/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx index bef921f25ab..13b0d9563b2 100644 --- a/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx +++ b/ui/src/pages/apps/chat/GitHubBotManagement.test.tsx @@ -314,9 +314,9 @@ describe("GitHub bot management", () => { container.querySelector('button[aria-label="Copy GitHub mention"]') ?.textContent, ).toBe("@maya-reviews"); - expect(container.textContent).toContain("This is your custom GitHub App"); + expect(container.textContent).toContain("Maya Reviews"); const branding = [...container.querySelectorAll("a")].find((a) => - a.textContent?.includes("Edit App name and logo"), + a.getAttribute("aria-label") === "Edit App name and logo on GitHub", ); expect(branding?.href).toBe( "https://github.com/organizations/acme/settings/apps/maya-reviews", @@ -404,27 +404,22 @@ describe("GitHub bot management", () => { await render("settings"); expect(container.querySelector("textarea")?.value).toBe("Unsaved behavior"); }); - it("creates repository overrides without overwriting defaults or duplicating field IDs", async () => { + it("hides repository overrides while preserving saved overrides when other settings change", async () => { + const overrides = { "100": { instructions: "Retain repository guidance", events: ["opened"] } }; + mocks.config.mockResolvedValue({ + revision: 4, + configuration: { ...base, repositories: overrides }, + }); await render(); - await input( - container.querySelector("#github-policy-repository")!, - "100", - ); - await click("Use custom settings for this repository"); - const areas = [ - ...container.querySelectorAll( - 'textarea[placeholder="What should this agent do on GitHub?"]', - ), - ]; - expect(areas).toHaveLength(2); - await input(areas[1], "Repository guidance"); - const ids = [...container.querySelectorAll("[id]")].map((e) => e.id); - expect(new Set(ids).size).toBe(ids.length); + expect(container.textContent).not.toContain("Repository overrides"); + expect(container.querySelector("#github-policy-repository")).toBeNull(); + expect(mocks.resources).not.toHaveBeenCalled(); + await input(container.querySelector("textarea")!, "Updated default instructions"); await click("Save changes"); await vi.waitFor(() => expect(mocks.save).toHaveBeenCalled()); const saved = mocks.save.mock.calls[0][2]; - expect(saved.defaults).toEqual(base.defaults); - expect(saved.repositories["100"].instructions).toBe("Repository guidance"); + expect(saved.defaults.instructions).toBe("Updated default instructions"); + expect(saved.repositories).toEqual(overrides); }); it("configures implicit linked-member events without switching to a narrower selected-member list", async () => { mocks.config.mockResolvedValue({ diff --git a/ui/src/pages/apps/chat/GitHubBotManagement.tsx b/ui/src/pages/apps/chat/GitHubBotManagement.tsx index f08d9458bab..02d9a8c8b06 100644 --- a/ui/src/pages/apps/chat/GitHubBotManagement.tsx +++ b/ui/src/pages/apps/chat/GitHubBotManagement.tsx @@ -35,7 +35,6 @@ import { GitHubAccessEditor, GitHubPolicyEditor, GitHubToggle, - githubSelectClass, } from "./GitHubBotConfiguration"; import { GitHubAppBranding } from "./GitHubAppIdentity"; @@ -179,13 +178,7 @@ export function GitHubBotManagement({ queryKey: ["github-bot-configuration", endpoint.id], queryFn: () => githubChatApi.configuration(endpoint.id), }); - const resources = useQuery({ - queryKey: ["github-bot-repositories", endpoint.id], - queryFn: () => chatEndpointsApi.listResources(endpoint.id), - enabled: view === "settings", - }); const [draft, setDraft] = useState(null); - const [repository, setRepository] = useState(""); const [pending, setPending] = useState(false); const [notice, setNotice] = useState(""); const [error, setError] = useState(""); @@ -210,7 +203,7 @@ export function GitHubBotManagement({ setPending(false); } }; - if (query.isError || (view === "settings" && resources.isError)) + if (query.isError) return (

Could not load the bot configuration.{" "} @@ -218,24 +211,19 @@ export function GitHubBotManagement({ variant="link" onClick={() => { void query.refetch(); - void resources.refetch(); }} > Try again

); - if (!record || (view === "settings" && resources.isPending)) + if (!record) return (

Loading configuration…

); const config = record.configuration; - const repositories = (resources.data ?? []).filter( - (resource) => resource.type === "repository", - ); - const override = repository ? config.repositories[repository] : undefined; return (
@@ -309,74 +297,6 @@ export function GitHubBotManagement({ policy={config.defaults} onChange={(defaults) => edit({ ...config, defaults })} /> -
- - Repository overrides - {Object.keys(config.repositories).length > 0 - ? ` (${Object.keys(config.repositories).length})` - : ""} - -
- - - {repository && ( - { - const overrides = { ...config.repositories }; - if (enabled) - overrides[repository] = { ...config.defaults }; - else delete overrides[repository]; - edit({ ...config, repositories: overrides }); - }} - /> - )} - {repository && override && ( - - edit({ - ...config, - repositories: { - ...config.repositories, - [repository]: policy, - }, - }) - } - /> - )} - {repository && !override && ( -

- Uses the settings above. -

- )} -
-
)} diff --git a/ui/src/pages/apps/chat/GitHubChatSetup.test.tsx b/ui/src/pages/apps/chat/GitHubChatSetup.test.tsx index c06ef1a06f4..10e2ee0a4f1 100644 --- a/ui/src/pages/apps/chat/GitHubChatSetup.test.tsx +++ b/ui/src/pages/apps/chat/GitHubChatSetup.test.tsx @@ -72,6 +72,7 @@ vi.mock("@/api/chatEndpoints", () => ({ vi.mock("@/api/githubChat", () => ({ githubChatApi: { advance: vi.fn(), + repositories: vi.fn(), registration: vi.fn(), restartRegistration: vi.fn(), saveDraft: vi.fn(), @@ -160,6 +161,9 @@ describe("GitHub App wizard", () => { endpointId: "draft-1", state: "create", }); + vi.mocked(githubChatApi.repositories).mockResolvedValue({ + items: [], nextOffset: null, totalCount: 1, enabledCount: 1, availableCount: 1, + }); vi.mocked(githubChatApi.saveDraft).mockResolvedValue({ saved: true }); vi.mocked(githubChatApi.personalConnections).mockResolvedValue([]); vi.mocked(copyTextToClipboard).mockResolvedValue(); @@ -586,7 +590,10 @@ describe("GitHub App wizard", () => { 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("acme/repo"); + 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"); await click("Copy mention"); @@ -594,4 +601,35 @@ describe("GitHub App wizard", () => { "@actual-agent review this pull request", ); }); + it("reports copy failure without undoing the completed connection", async () => { + fixture.endpoint = { ...fixture.endpoint, status: "active", 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(githubChatApi.registration).not.toHaveBeenCalled(); + }); + it("shows a useful next step when a connected bot has no enabled repositories", async () => { + 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(); + }); + + it("does not flash App creation controls while checking a resumed connection", async () => { + fixture.endpoint = { ...fixture.endpoint, status: "active", botUsername: "actual-agent[bot]" }; + vi.mocked(githubChatApi.advance).mockReturnValue(new Promise(() => {})); + await render("resume=draft-1"); + expect(container.textContent).toContain("Checking your GitHub connection"); + expect(container.querySelector("#github-app-name")).toBeNull(); + expect(container.textContent).not.toContain("I already have an App"); + expect(container.textContent).not.toContain("GitHub connected"); + }); + }); diff --git a/ui/src/pages/apps/chat/GitHubChatSetup.tsx b/ui/src/pages/apps/chat/GitHubChatSetup.tsx index 98d2f1ab770..57c3bc8bf6a 100644 --- a/ui/src/pages/apps/chat/GitHubChatSetup.tsx +++ b/ui/src/pages/apps/chat/GitHubChatSetup.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { CheckCircle2, ExternalLink, Loader2 } from "lucide-react"; +import { ExternalLink, Loader2 } from "lucide-react"; import type { GitHubAppWizardState } from "@paperclipai/shared"; import { agentsApi } from "@/api/agents"; import { chatEndpointsApi } from "@/api/chatEndpoints"; @@ -21,10 +21,7 @@ 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 { copyTextToClipboard } from "@/lib/clipboard"; -import { resolveAgentAppearance } from "@paperclipai/shared"; -import { agentAvatarUrl } from "@/lib/agent-avatar-url"; -import { GitHubAppBranding, gitHubBotMention } from "./GitHubAppIdentity"; +import { GitHubConnectionComplete } from "./GitHubConnectionComplete"; /** Restrict native manifest submission to GitHub registration endpoints. */ export function gitHubAppManifestAction( @@ -98,7 +95,6 @@ export function GitHubChatSetup() { webhookSecret: "", }); const [appNotCreated, setAppNotCreated] = useState(false); - const [copied, setCopied] = useState(false); const [legacyAccount, setLegacyAccount] = useState(""); const [identityLinked, setIdentityLinked] = useState(false); const [legacyIdentity, setLegacyIdentity] = useState ); - const mention = `${(bot && gitHubBotMention(bot)) || "@your-bot"} review this pull request`; return ( -
+
{!identityOnly && !connected && ( )} -

+ {!connected &&

{!bot ? "Choose agent" - : connected - ? "GitHub connected" - : identityOnly || state?.state === "identity" + : identityOnly || state?.state === "identity" ? "Connect your account" : "Connect GitHub"} -

+ } {(error || (!identityOnly && progress.error) || accounts.error) && (

{error || @@ -329,52 +324,21 @@ export function GitHubChatSetup() { !agentId || !selectedCompanyId, )} - ) : connected ? ( + ) : !identityOnly && !existing && progress.isPending ? ( <> -

- - - {bot.botLabel ?? bot.botUsername} · {bot.assignedAgentName} - -
-

- {bot.resources - ?.filter((resource) => resource.enabled) - .map((resource) => resource.label) - .join(", ")} +

+ Checking your GitHub connection…

- {state.runtimeChecks - ?.filter((check) => !check.ok) - .map((check) => ( -

- {check.detail} -

- ))} -

Try a mention on a pull request:

-
- {mention} - -
- - - Connection settings - {footer()} + ) : connected ? ( + ) : existing ? ( <>

diff --git a/ui/src/pages/apps/chat/GitHubConnectionComplete.tsx b/ui/src/pages/apps/chat/GitHubConnectionComplete.tsx new file mode 100644 index 00000000000..97fb088f0f5 --- /dev/null +++ b/ui/src/pages/apps/chat/GitHubConnectionComplete.tsx @@ -0,0 +1,106 @@ +import { useQuery } from "@tanstack/react-query"; +import { CheckCircle2, Copy } from "lucide-react"; +import { resolveAgentAppearance, type GitHubAppWizardState } from "@paperclipai/shared"; +import type { ChatEndpoint } from "@/api/chatEndpoints"; +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 { agentAvatarUrl } from "@/lib/agent-avatar-url"; +import { Link } from "@/lib/router"; +import { GitHubAppBranding, gitHubBotMention } from "./GitHubAppIdentity"; + +export function GitHubConnectionComplete({ + endpoint, + agent, + runtimeChecks, + onExit, + pending = false, +}: { + endpoint: ChatEndpoint; + agent?: AvatarAgent; + runtimeChecks?: GitHubAppWizardState["runtimeChecks"]; + onExit: () => void; + pending?: boolean; +}) { + const repositories = useQuery({ + queryKey: ["github-connected-repositories", endpoint.id], + queryFn: () => githubChatApi.repositories(endpoint.id, { limit: 1 }), + }); + const avatar = agent ?? { id: endpoint.assignedAgentId, name: endpoint.assignedAgentName }; + const mention = `${gitHubBotMention(endpoint) || "@your-bot"} review this pull request`; + const count = repositories.data?.enabledCount; + const incomplete = runtimeChecks?.filter((check) => !check.ok) ?? []; + return ( +

+
+
+ +

GitHub connected

+
+
+ +
+

+ {endpoint.botLabel ?? endpoint.botUsername ?? endpoint.assignedAgentName} +

+

+ + {endpoint.assignedAgentName} + + {endpoint.providerAccountLabel && <> + + + + {endpoint.providerAccountLabel} + + } +

+ + {repositories.isError ? "Repository access unavailable" : count === undefined ? "Repository access" : count === 0 ? "No repositories enabled" : `${count} ${count === 1 ? "repository" : "repositories"} enabled`} + +
+
+
+ {incomplete.length > 0 && ( +
+

Before your first review

+ {incomplete.map((check) => ( +

{check.detail}

+ ))} +
+ )} +
+
+

Try your bot

+

+ {count === 0 + ? "Enable a repository in Access to try your bot." + : "Post this on a pull request in an enabled repository."} +

+
+ + {mention} + + +
+ + + + +
+ ); +} diff --git a/ui/storybook/stories/github-bot-management/fixture-api.tsx b/ui/storybook/stories/github-bot-management/fixture-api.tsx index baf56bef1f9..28d2d5a6e2a 100644 --- a/ui/storybook/stories/github-bot-management/fixture-api.tsx +++ b/ui/storybook/stories/github-bot-management/fixture-api.tsx @@ -27,6 +27,8 @@ export function FixtureApi({ }); client.setQueryData(queryKeys.chatEndpoints.detail(endpoint.id), endpoint); client.setQueryData(queryKeys.agents.detail(agent.id), agent); + client.setQueryData(["github-setup", endpoint.id], endpoint); + client.setQueryData(["github-wizard", endpoint.id], { endpointId: endpoint.id, state: "connected" }); client.setQueryData(["github-members", endpoint.companyId], members); return client; }); @@ -65,6 +67,7 @@ export function FixtureApi({ ); } const body = init?.body ? JSON.parse(String(init.body)) : {}; + if (path.endsWith("/github/setup")) return Response.json({ endpointId: endpoint.id, state: "connected" }); if (path.endsWith("/repositories/access")) { repos = repos.map((r) => (!body.enabled || r.availability === "available") ? { ...r, enabled: body.enabled } : r); return Response.json({ success: true }); diff --git a/ui/storybook/stories/github-bot-management/management.stories.tsx b/ui/storybook/stories/github-bot-management/management.stories.tsx index ac59c71105a..033f9bb27d0 100644 --- a/ui/storybook/stories/github-bot-management/management.stories.tsx +++ b/ui/storybook/stories/github-bot-management/management.stories.tsx @@ -6,6 +6,8 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { Routes, Route } from "@/lib/router"; import { PluginLauncherProvider } from "@/plugins/launchers"; import { Layout } from "@/components/Layout"; +import { GitHubChatSetup } from "@/pages/apps/chat/GitHubChatSetup"; +import { GitHubConnectionComplete } from "@/pages/apps/chat/GitHubConnectionComplete"; import { ChatEndpointDetail } from "@/pages/apps/chat/ChatEndpointDetail"; import { GitHubPolicyEditor, @@ -40,7 +42,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. **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** 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.", }, }, }, @@ -56,6 +58,7 @@ const meta = { }> + } /> } @@ -75,6 +78,20 @@ type Story = StoryObj; 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", + parameters: { initialEntries: [`/PAP/apps/chat/connect?provider=github&resume=${endpoint.id}`] }, +}; +export const ConnectedMobile: Story = { + ...ConnectedJourney, + name: "02 States / Mobile connected", + globals: { viewport: { value: "mobile1", isRotated: false } }, +}; +export const ConnectedNoRepositories: Story = { + ...ConnectedJourney, + name: "02 States / Connected without enabled repositories", + args: { state: "empty" }, +}; export const SettingsJourney: Story = { name: "01 Journey / Settings" }; export const AccessJourney: Story = { name: "01 Journey / Access", @@ -188,6 +205,21 @@ export const RepositoryAccess: Story = { name: "03 Components / Repository access", render: () => component(), }; +export const ConnectedSummary: Story = { + name: "03 Components / Connected summary", + render: () => component( {}} />), +}; +export const ConnectedRuntimeNotReady: Story = { + name: "03 Components / Connected — runtime needs setup", + render: () => component( {}} + runtimeChecks={[{ key: "runtime", label: "Runtime", ok: false, detail: "Configure an isolated runtime before the first review." }]} />), +}; +export const ConnectedLongName: Story = { + name: "03 Components / Connected — long App name", + render: () => component( {}} />), +}; export const AppIdentityAndLogo: Story = { name: "03 Components / App identity and logo", render: () =>