mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
fix: separate GitHub tools and code review bot connections (#14750)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Connectors catalog lets people give agents tools or connect agents to conversations. > - GitHub put these two uses behind one card and an extra choice. > - People should choose the connection they need from the catalog. > - This pull request keeps GitHub for tools and adds GitHub Code Review Bot as a separate card. > - Each card opens its setup directly. Both use the existing connection code. ## Linked Issues or Issue Description **What existing behavior does this improve?** GitHub connector discovery and setup. **Current behavior** With chat connectors enabled, GitHub opens a menu that asks whether to use tools or create a bot. Saved tools and bots share the same catalog entry. **Proposed behavior** GitHub opens tool account access. GitHub Code Review Bot opens agent selection. Saved bots and drafts appear under the bot card. Chat-disabled instances show only GitHub tools. **Reason and benefit** The catalog names the two uses and removes an extra setup choice. The bot keeps the existing GitHub provider, credentials, endpoint IDs, setup steps, and runtime. **Additional context** Related work: https://github.com/paperclipai/paperclip/pull/12843 and https://github.com/paperclipai/paperclip/pull/14594 established GitHub account identity. This change preserves that tool flow. No duplicate catalog split was found. ## What Changed - Split the generated app definitions into GitHub tools and GitHub Code Review Bot. Reuse the existing GitHub logo and channel method. - Open bot setup directly, including old resume and reconnect links. - Put existing bot endpoints and drafts under the bot card. Hide duplicate internal chat applications. - Keep pasted GitHub URLs mapped to the tool connection. - Add seven Storybook states for the catalog, saved connections, disabled chat, both setup paths, mobile, and light mode. - Fix narrow-screen bot rows so the label cannot overlap status and setup actions. - Update catalog, route, browser, and API tests, plus the GitHub connector guide. ## Verification - [Hosted Storybook](https://d1p6rlowie26tp.cloudfront.net/storybook/branches/codex~2Fgithub-review-connection/?path=/story/connections-github-and-code-review-bot--catalog): seven states built from this branch. The deployment passed its public-file verification. - All GitHub checks pass on `d13a2cd53561645bb2a15c6f8e75a61a936d6459`. Two optional Storybook jobs skip under their normal trigger rules; the manual Storybook deployment passes. The branch has no merge conflicts. - Greptile: 5/5 on the current head, with no review comments or unresolved threads. - `pnpm -r typecheck`, `pnpm build`, `pnpm check:token-gates`, and `pnpm build-storybook` passed. The final Storybook fixture also passed UI typecheck and the hosted build. - Targeted catalog, URL matching, routing, grouping, brand, and chat UI contract tests passed. - GitHub provider browser tests: 2 passed. These cover direct tool setup and the bot setup and management lifecycle with provider responses mocked. - Embedded-browser test on an isolated local instance: opened both cards, selected an agent, saved a bot draft, and resumed the same endpoint under the bot card after a reload. - Storybook Tool Setup and Bot Setup assertions pass in the published preview. Chat Disabled assertions pass locally. Inspected mobile and light mode, including the draft-row layout and official GitHub marks. - Local full-suite limitation: `pnpm test:run` was not clean. A cross-company route assertion failed in the aggregate run and passed in isolation; a workspace-runtime test reached its 30-second hook timeout. Some isolated database reruns skipped when the embedded-PostgreSQL availability probe failed. The local aggregate was stopped after CI completed. The corresponding full CI suites pass all 360 tool-access tests and all 162 workspace-runtime tests. - No live GitHub authorization or installation was performed. The isolated instance correctly stopped at the cloud enrollment or public HTTPS prerequisites. ## Risks - Low scope: catalog presentation and routing change. There is no database migration or provider credential change. - Existing GitHub bot URLs now open bot setup directly. The tool route remains `/apps/connect?source=github`. - The bot remains behind the existing chat-connectors feature flag. Existing endpoints retain `provider: github`. - Channel applications are represented by endpoint rows. Regression tests cover legacy bot applications, tools, active bots, and drafts together. ## Model Used OpenAI GPT-6 through Codex, with reasoning, code execution, and embedded-browser tools. The exact deployed model ID, context window size, and reasoning setting are not exposed to this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
018993140f
commit
33f2b3a159
20 files changed
+431
-252
No files matched your search
@@ -5,6 +5,18 @@ compatibility method. Cloud owns the fixed public OAuth callback and signed
|
||||
webhook inbox; provider tokens are sealed to the enrolled instance and stored
|
||||
only in its existing encrypted secret system.
|
||||
|
||||
## Catalog entries
|
||||
|
||||
**GitHub** connects an account for repository tools, Git, and `gh`, and opens
|
||||
Access → Connect directly. **GitHub Code Review Bot** connects one agent to a
|
||||
GitHub App for pull-request reviews and mentions, and opens Choose agent directly.
|
||||
The bot entry follows the Chat Connectors experimental setting.
|
||||
|
||||
Both entries reuse the existing GitHub integrations. Bot endpoints retain the
|
||||
`github` provider identity and existing setup, reconnect, and management URLs;
|
||||
saved bot connections and drafts appear under GitHub Code Review Bot. GitHub
|
||||
repository and MCP URLs still resolve to the ordinary GitHub tool connection.
|
||||
|
||||
## Self-hosted setup
|
||||
|
||||
The Access step uses **Continue** to open the local setup screen.
|
||||
|
||||
@@ -8,6 +8,7 @@ describe("tool app gallery URL matching", () => {
|
||||
it("matches pasted links against gallery URL patterns", () => {
|
||||
expect(getAppDefinitionForUrl("https://mcp.zapier.com/api/mcp")?.slug).toBe("zapier");
|
||||
expect(getAppDefinitionForUrl("https://api.githubcopilot.com/mcp/")?.slug).toBe("github");
|
||||
expect(getAppDefinitionForUrl("https://github.com/paperclipai/paperclip/pull/1")?.slug).toBe("github");
|
||||
expect(getAppDefinitionForUrl("https://docs.google.com/spreadsheets/d/sheet_123/edit")?.slug).toBe("google-sheets");
|
||||
expect(getAppDefinitionForUrl("https://gmailmcp.googleapis.com/mcp/v1")?.slug).toBe("gmail");
|
||||
});
|
||||
@@ -40,8 +41,8 @@ describe("tool app gallery URL matching", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps every gallery entry reachable through at least one pattern", () => {
|
||||
for (const app of CONNECTABLE_APP_DEFINITIONS) {
|
||||
it("keeps tool gallery entries reachable through at least one pattern", () => {
|
||||
for (const app of CONNECTABLE_APP_DEFINITIONS.filter((app) => app.methods.some((method) => method.purpose !== "channel"))) {
|
||||
const example = app.urlPatterns[0]?.replace("*", "example");
|
||||
expect(example, `${app.slug} has a pattern`).toBeTruthy();
|
||||
expect(getAppDefinitionForUrl(example!)?.slug).toBe(app.slug);
|
||||
|
||||
@@ -4,79 +4,80 @@ import a2 from "./app-definitions/arcade.json" with { type: "json" };
|
||||
import a3 from "./app-definitions/executor.json" with { type: "json" };
|
||||
import a4 from "./app-definitions/railway.json" with { type: "json" };
|
||||
import a5 from "./app-definitions/github.json" with { type: "json" };
|
||||
import a6 from "./app-definitions/slack.json" with { type: "json" };
|
||||
import a7 from "./app-definitions/microsoft-teams.json" with { type: "json" };
|
||||
import a8 from "./app-definitions/imessage-photon.json" with { type: "json" };
|
||||
import a9 from "./app-definitions/telegram.json" with { type: "json" };
|
||||
import a10 from "./app-definitions/discord.json" with { type: "json" };
|
||||
import a11 from "./app-definitions/notion.json" with { type: "json" };
|
||||
import a12 from "./app-definitions/posthog.json" with { type: "json" };
|
||||
import a13 from "./app-definitions/linear.json" with { type: "json" };
|
||||
import a14 from "./app-definitions/context7.json" with { type: "json" };
|
||||
import a15 from "./app-definitions/shopify.json" with { type: "json" };
|
||||
import a16 from "./app-definitions/composio.json" with { type: "json" };
|
||||
import a17 from "./app-definitions/oauth-generic.json" with { type: "json" };
|
||||
import a18 from "./app-definitions/api-key-generic.json" with { type: "json" };
|
||||
import a19 from "./app-definitions/sentry.json" with { type: "json" };
|
||||
import a20 from "./app-definitions/vercel.json" with { type: "json" };
|
||||
import a21 from "./app-definitions/anthropic.json" with { type: "json" };
|
||||
import a22 from "./app-definitions/browser-use-cloud.json" with { type: "json" };
|
||||
import a23 from "./app-definitions/cognee.json" with { type: "json" };
|
||||
import a24 from "./app-definitions/jira.json" with { type: "json" };
|
||||
import a25 from "./app-definitions/airtable.json" with { type: "json" };
|
||||
import a26 from "./app-definitions/beehiiv.json" with { type: "json" };
|
||||
import a27 from "./app-definitions/bitly.json" with { type: "json" };
|
||||
import a28 from "./app-definitions/candid.json" with { type: "json" };
|
||||
import a29 from "./app-definitions/cloudflare.json" with { type: "json" };
|
||||
import a30 from "./app-definitions/cloudinary.json" with { type: "json" };
|
||||
import a31 from "./app-definitions/coda.json" with { type: "json" };
|
||||
import a32 from "./app-definitions/hugging-face.json" with { type: "json" };
|
||||
import a33 from "./app-definitions/kernel.json" with { type: "json" };
|
||||
import a34 from "./app-definitions/local-falcon.json" with { type: "json" };
|
||||
import a35 from "./app-definitions/make.json" with { type: "json" };
|
||||
import a36 from "./app-definitions/manufact.json" with { type: "json" };
|
||||
import a37 from "./app-definitions/miro.json" with { type: "json" };
|
||||
import a38 from "./app-definitions/netlify.json" with { type: "json" };
|
||||
import a39 from "./app-definitions/oreilly.json" with { type: "json" };
|
||||
import a40 from "./app-definitions/planetscale.json" with { type: "json" };
|
||||
import a41 from "./app-definitions/resend.json" with { type: "json" };
|
||||
import a42 from "./app-definitions/ticktick.json" with { type: "json" };
|
||||
import a43 from "./app-definitions/todoist.json" with { type: "json" };
|
||||
import a44 from "./app-definitions/webflow.json" with { type: "json" };
|
||||
import a45 from "./app-definitions/wix.json" with { type: "json" };
|
||||
import a46 from "./app-definitions/brex.json" with { type: "json" };
|
||||
import a47 from "./app-definitions/clickhouse.json" with { type: "json" };
|
||||
import a48 from "./app-definitions/egnyte.json" with { type: "json" };
|
||||
import a49 from "./app-definitions/embat.json" with { type: "json" };
|
||||
import a50 from "./app-definitions/mixpanel.json" with { type: "json" };
|
||||
import a51 from "./app-definitions/postman.json" with { type: "json" };
|
||||
import a52 from "./app-definitions/razorpay.json" with { type: "json" };
|
||||
import a53 from "./app-definitions/sanity.json" with { type: "json" };
|
||||
import a54 from "./app-definitions/stripe.json" with { type: "json" };
|
||||
import a55 from "./app-definitions/supabase.json" with { type: "json" };
|
||||
import a56 from "./app-definitions/ticket-tailor.json" with { type: "json" };
|
||||
import a57 from "./app-definitions/asana.json" with { type: "json" };
|
||||
import a58 from "./app-definitions/box.json" with { type: "json" };
|
||||
import a59 from "./app-definitions/mem0.json" with { type: "json" };
|
||||
import a60 from "./app-definitions/pagerduty.json" with { type: "json" };
|
||||
import a61 from "./app-definitions/similarweb.json" with { type: "json" };
|
||||
import a62 from "./app-definitions/xero.json" with { type: "json" };
|
||||
import a63 from "./app-definitions/youcom.json" with { type: "json" };
|
||||
import a64 from "./app-definitions/fireflies.json" with { type: "json" };
|
||||
import a65 from "./app-definitions/zep.json" with { type: "json" };
|
||||
import a66 from "./app-definitions/supermemory.json" with { type: "json" };
|
||||
import a67 from "./app-definitions/honcho.json" with { type: "json" };
|
||||
import a68 from "./app-definitions/gmail.json" with { type: "json" };
|
||||
import a69 from "./app-definitions/google-drive.json" with { type: "json" };
|
||||
import a70 from "./app-definitions/google-docs.json" with { type: "json" };
|
||||
import a71 from "./app-definitions/google-sheets.json" with { type: "json" };
|
||||
import a72 from "./app-definitions/google-slides.json" with { type: "json" };
|
||||
import a73 from "./app-definitions/google-calendar.json" with { type: "json" };
|
||||
import a74 from "./app-definitions/google-chat.json" with { type: "json" };
|
||||
import a75 from "./app-definitions/google-people.json" with { type: "json" };
|
||||
import a76 from "./app-definitions/google-workspace-search.json" with { type: "json" };
|
||||
import a77 from "./app-definitions/openai.json" with { type: "json" };
|
||||
import a78 from "./app-definitions/openrouter.json" with { type: "json" };
|
||||
import a79 from "./app-definitions/xai.json" with { type: "json" };
|
||||
import a6 from "./app-definitions/github-code-review-bot.json" with { type: "json" };
|
||||
import a7 from "./app-definitions/slack.json" with { type: "json" };
|
||||
import a8 from "./app-definitions/microsoft-teams.json" with { type: "json" };
|
||||
import a9 from "./app-definitions/imessage-photon.json" with { type: "json" };
|
||||
import a10 from "./app-definitions/telegram.json" with { type: "json" };
|
||||
import a11 from "./app-definitions/discord.json" with { type: "json" };
|
||||
import a12 from "./app-definitions/notion.json" with { type: "json" };
|
||||
import a13 from "./app-definitions/posthog.json" with { type: "json" };
|
||||
import a14 from "./app-definitions/linear.json" with { type: "json" };
|
||||
import a15 from "./app-definitions/context7.json" with { type: "json" };
|
||||
import a16 from "./app-definitions/shopify.json" with { type: "json" };
|
||||
import a17 from "./app-definitions/composio.json" with { type: "json" };
|
||||
import a18 from "./app-definitions/oauth-generic.json" with { type: "json" };
|
||||
import a19 from "./app-definitions/api-key-generic.json" with { type: "json" };
|
||||
import a20 from "./app-definitions/sentry.json" with { type: "json" };
|
||||
import a21 from "./app-definitions/vercel.json" with { type: "json" };
|
||||
import a22 from "./app-definitions/anthropic.json" with { type: "json" };
|
||||
import a23 from "./app-definitions/browser-use-cloud.json" with { type: "json" };
|
||||
import a24 from "./app-definitions/cognee.json" with { type: "json" };
|
||||
import a25 from "./app-definitions/jira.json" with { type: "json" };
|
||||
import a26 from "./app-definitions/airtable.json" with { type: "json" };
|
||||
import a27 from "./app-definitions/beehiiv.json" with { type: "json" };
|
||||
import a28 from "./app-definitions/bitly.json" with { type: "json" };
|
||||
import a29 from "./app-definitions/candid.json" with { type: "json" };
|
||||
import a30 from "./app-definitions/cloudflare.json" with { type: "json" };
|
||||
import a31 from "./app-definitions/cloudinary.json" with { type: "json" };
|
||||
import a32 from "./app-definitions/coda.json" with { type: "json" };
|
||||
import a33 from "./app-definitions/hugging-face.json" with { type: "json" };
|
||||
import a34 from "./app-definitions/kernel.json" with { type: "json" };
|
||||
import a35 from "./app-definitions/local-falcon.json" with { type: "json" };
|
||||
import a36 from "./app-definitions/make.json" with { type: "json" };
|
||||
import a37 from "./app-definitions/manufact.json" with { type: "json" };
|
||||
import a38 from "./app-definitions/miro.json" with { type: "json" };
|
||||
import a39 from "./app-definitions/netlify.json" with { type: "json" };
|
||||
import a40 from "./app-definitions/oreilly.json" with { type: "json" };
|
||||
import a41 from "./app-definitions/planetscale.json" with { type: "json" };
|
||||
import a42 from "./app-definitions/resend.json" with { type: "json" };
|
||||
import a43 from "./app-definitions/ticktick.json" with { type: "json" };
|
||||
import a44 from "./app-definitions/todoist.json" with { type: "json" };
|
||||
import a45 from "./app-definitions/webflow.json" with { type: "json" };
|
||||
import a46 from "./app-definitions/wix.json" with { type: "json" };
|
||||
import a47 from "./app-definitions/brex.json" with { type: "json" };
|
||||
import a48 from "./app-definitions/clickhouse.json" with { type: "json" };
|
||||
import a49 from "./app-definitions/egnyte.json" with { type: "json" };
|
||||
import a50 from "./app-definitions/embat.json" with { type: "json" };
|
||||
import a51 from "./app-definitions/mixpanel.json" with { type: "json" };
|
||||
import a52 from "./app-definitions/postman.json" with { type: "json" };
|
||||
import a53 from "./app-definitions/razorpay.json" with { type: "json" };
|
||||
import a54 from "./app-definitions/sanity.json" with { type: "json" };
|
||||
import a55 from "./app-definitions/stripe.json" with { type: "json" };
|
||||
import a56 from "./app-definitions/supabase.json" with { type: "json" };
|
||||
import a57 from "./app-definitions/ticket-tailor.json" with { type: "json" };
|
||||
import a58 from "./app-definitions/asana.json" with { type: "json" };
|
||||
import a59 from "./app-definitions/box.json" with { type: "json" };
|
||||
import a60 from "./app-definitions/mem0.json" with { type: "json" };
|
||||
import a61 from "./app-definitions/pagerduty.json" with { type: "json" };
|
||||
import a62 from "./app-definitions/similarweb.json" with { type: "json" };
|
||||
import a63 from "./app-definitions/xero.json" with { type: "json" };
|
||||
import a64 from "./app-definitions/youcom.json" with { type: "json" };
|
||||
import a65 from "./app-definitions/fireflies.json" with { type: "json" };
|
||||
import a66 from "./app-definitions/zep.json" with { type: "json" };
|
||||
import a67 from "./app-definitions/supermemory.json" with { type: "json" };
|
||||
import a68 from "./app-definitions/honcho.json" with { type: "json" };
|
||||
import a69 from "./app-definitions/gmail.json" with { type: "json" };
|
||||
import a70 from "./app-definitions/google-drive.json" with { type: "json" };
|
||||
import a71 from "./app-definitions/google-docs.json" with { type: "json" };
|
||||
import a72 from "./app-definitions/google-sheets.json" with { type: "json" };
|
||||
import a73 from "./app-definitions/google-slides.json" with { type: "json" };
|
||||
import a74 from "./app-definitions/google-calendar.json" with { type: "json" };
|
||||
import a75 from "./app-definitions/google-chat.json" with { type: "json" };
|
||||
import a76 from "./app-definitions/google-people.json" with { type: "json" };
|
||||
import a77 from "./app-definitions/google-workspace-search.json" with { type: "json" };
|
||||
import a78 from "./app-definitions/openai.json" with { type: "json" };
|
||||
import a79 from "./app-definitions/openrouter.json" with { type: "json" };
|
||||
import a80 from "./app-definitions/xai.json" with { type: "json" };
|
||||
import type { AppDefinition } from "./types/app-definition.js";
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79] as AppDefinition[];
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80] as AppDefinition[];
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
appSupportsCatalogSetup,
|
||||
getAvailableConnectionMethod,
|
||||
getAppDefinitionForUrl,
|
||||
getConnectableAppDefinition,
|
||||
getRecommendedConnectionMethod,
|
||||
recommendedDefaultsForApp,
|
||||
resolveConnectionMethodServerUrl,
|
||||
@@ -284,6 +285,17 @@ describe("AppDefinition catalog", () => {
|
||||
for (const entry of BLOCKED_MCP_PROVIDERS)
|
||||
expect(connectableSlugs.has(entry.slug)).toBe(false);
|
||||
});
|
||||
it("separates GitHub tools from the review bot without changing the provider identity", () => {
|
||||
const github = getConnectableAppDefinition("github")!;
|
||||
const bot = getConnectableAppDefinition("github-code-review-bot")!;
|
||||
expect(github.methods.map((method) => method.key)).toEqual(["managed", "mcp-key"]);
|
||||
expect(github.methods.every((method) => method.purpose === "tool")).toBe(true);
|
||||
expect(bot.name).toBe("GitHub Code Review Bot");
|
||||
expect(bot.methods).toHaveLength(1);
|
||||
expect(bot.methods[0]).toMatchObject({ provider: "github", purpose: "channel", transport: "chat_sdk" });
|
||||
expect(bot.branding).toEqual(github.branding);
|
||||
expect(bot.urlPatterns).toEqual([]);
|
||||
});
|
||||
it("registers the five native chat providers with only required setup credentials", () => {
|
||||
const expected = {
|
||||
slack: {
|
||||
@@ -292,11 +304,11 @@ describe("AppDefinition catalog", () => {
|
||||
resources: ["workspace", "channel"],
|
||||
tool: true,
|
||||
},
|
||||
github: {
|
||||
"github-code-review-bot": {
|
||||
credentials: ["appId", "privateKey"],
|
||||
publicFields: ["appId"],
|
||||
resources: ["organization", "repository"],
|
||||
tool: true,
|
||||
tool: false,
|
||||
},
|
||||
discord: {
|
||||
credentials: ["botToken", "applicationId", "guildId"],
|
||||
@@ -329,7 +341,7 @@ describe("AppDefinition catalog", () => {
|
||||
key: "chat-agent",
|
||||
label: "Chat with an agent",
|
||||
purpose: "channel",
|
||||
provider: slug,
|
||||
provider: slug === "github-code-review-bot" ? "github" : slug,
|
||||
transport: "chat_sdk",
|
||||
auth: "api_key",
|
||||
ownershipModes: ["customer"],
|
||||
@@ -374,20 +386,20 @@ describe("AppDefinition catalog", () => {
|
||||
APP_DEFINITIONS.find((app) => app.slug === slug)?.methods.find(
|
||||
(method) => method.purpose === "channel",
|
||||
);
|
||||
expect(channel("github")?.guidanceMd).toContain("issue_comment");
|
||||
expect(channel("github-code-review-bot")?.guidanceMd).toContain("issue_comment");
|
||||
expect(channel("discord")?.guidanceMd).toContain("Message Content intent");
|
||||
expect(channel("discord")?.guidanceMd).toContain("Discord thread");
|
||||
expect(channel("github")?.guidanceMd).toContain("pull_request");
|
||||
expect(channel("github")?.guidanceMd).toContain(
|
||||
expect(channel("github-code-review-bot")?.guidanceMd).toContain("pull_request");
|
||||
expect(channel("github-code-review-bot")?.guidanceMd).toContain(
|
||||
"pull_request_review_comment",
|
||||
);
|
||||
expect(channel("github")?.guidanceMd).toContain(
|
||||
expect(channel("github-code-review-bot")?.guidanceMd).toContain(
|
||||
"installation_repositories",
|
||||
);
|
||||
expect(channel("github")?.guidanceMd).toContain(
|
||||
expect(channel("github-code-review-bot")?.guidanceMd).toContain(
|
||||
"Generate the webhook secret in Paperclip",
|
||||
);
|
||||
expect(channel("github")?.guidanceMd).toContain("SSL-verified");
|
||||
expect(channel("github-code-review-bot")?.guidanceMd).toContain("SSL-verified");
|
||||
expect(channel("microsoft-teams")?.guidanceMd).toContain(
|
||||
"resource-specific",
|
||||
);
|
||||
@@ -707,7 +719,7 @@ describe("AppDefinition catalog", () => {
|
||||
"ticktick",
|
||||
"xero",
|
||||
]);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(57);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(58);
|
||||
const connectableSlugs = new Set(
|
||||
CONNECTABLE_APP_DEFINITIONS.map((entry) => entry.slug),
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ export const CONNECTABLE_APP_SLUGS = new Set([
|
||||
"google-people",
|
||||
"google-workspace-search",
|
||||
"github",
|
||||
"github-code-review-bot",
|
||||
"discord",
|
||||
"microsoft-teams",
|
||||
"telegram",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "github-code-review-bot",
|
||||
"name": "GitHub Code Review Bot",
|
||||
"description": "Have an agent review pull requests and respond to GitHub mentions.",
|
||||
"categories": [
|
||||
"developer"
|
||||
],
|
||||
"featured": true,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/github.svg",
|
||||
"darkLogoUrl": "/brands/apps/github-dark.svg"
|
||||
},
|
||||
"urlPatterns": [],
|
||||
"methods": [
|
||||
{
|
||||
"key": "chat-agent",
|
||||
"label": "Chat with an agent",
|
||||
"purpose": "channel",
|
||||
"provider": "github",
|
||||
"transport": "chat_sdk",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Let people in GitHub start and continue work with one Paperclip agent.",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "appId",
|
||||
"label": "GitHub App ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "123456",
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"key": "privateKey",
|
||||
"label": "Private key (PEM)",
|
||||
"type": "textarea",
|
||||
"required": true,
|
||||
"placeholder": "-----BEGIN RSA PRIVATE KEY-----",
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"guidanceMd": "Generate the webhook secret in Paperclip, then create one private GitHub App with active SSL-verified webhooks, Issues and Pull requests read/write permission, and the selectable issue_comment and pull_request_review_comment events. GitHub sends installation and installation_repositories automatically. Install the App only on repositories where people may mention the agent.",
|
||||
"consoleLinks": {
|
||||
"register": "https://github.com/settings/apps/new",
|
||||
"docs": "https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app"
|
||||
},
|
||||
"riskTier": "S3",
|
||||
"requiredResourceFilters": [
|
||||
"organization",
|
||||
"repository"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"slug": "github",
|
||||
"name": "GitHub",
|
||||
"description": "Give agents repository tools or let people work with an agent from GitHub issues and pull requests.",
|
||||
"description": "Give agents access to GitHub repositories, issues, and pull requests.",
|
||||
"categories": [
|
||||
"developer"
|
||||
],
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"guidanceMd": "Authorize Paperclip, then choose selected repositories in GitHub. You can edit repository access later from GitHub's installation settings.",
|
||||
"riskTier": "S3",
|
||||
"label": "Use this connection as an agent tool",
|
||||
"label": "Connect GitHub",
|
||||
"purpose": "tool",
|
||||
"oauthStrategy": "paperclip_cloud_connector",
|
||||
"connectorProfile": "github.code",
|
||||
@@ -80,46 +80,6 @@
|
||||
"organization",
|
||||
"repository"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "chat-agent",
|
||||
"label": "Chat with an agent",
|
||||
"purpose": "channel",
|
||||
"provider": "github",
|
||||
"transport": "chat_sdk",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Let people in GitHub start and continue work with one Paperclip agent.",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "appId",
|
||||
"label": "GitHub App ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "123456",
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"key": "privateKey",
|
||||
"label": "Private key (PEM)",
|
||||
"type": "textarea",
|
||||
"required": true,
|
||||
"placeholder": "-----BEGIN RSA PRIVATE KEY-----",
|
||||
"secret": true
|
||||
}
|
||||
],
|
||||
"guidanceMd": "Generate the webhook secret in Paperclip, then create one private GitHub App with active SSL-verified webhooks, Issues and Pull requests read/write permission, and the selectable issue_comment and pull_request_review_comment events. GitHub sends installation and installation_repositories automatically. Install the App only on repositories where people may mention the agent.",
|
||||
"consoleLinks": {
|
||||
"register": "https://github.com/settings/apps/new",
|
||||
"docs": "https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app"
|
||||
},
|
||||
"riskTier": "S3",
|
||||
"requiredResourceFilters": [
|
||||
"organization",
|
||||
"repository"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -258,7 +258,7 @@ const apps = [
|
||||
[
|
||||
"github",
|
||||
"GitHub",
|
||||
"Give agents repository tools or let people work with an agent from GitHub issues and pull requests.",
|
||||
"Give agents access to GitHub repositories, issues, and pull requests.",
|
||||
"developer",
|
||||
"github.com",
|
||||
["https://api.githubcopilot.com/mcp/*", "https://github.com/*"],
|
||||
@@ -271,7 +271,7 @@ const apps = [
|
||||
"S3",
|
||||
"Authorize Paperclip, then choose selected repositories in GitHub. You can edit repository access later from GitHub's installation settings.",
|
||||
{
|
||||
label: "Use this connection as an agent tool",
|
||||
label: "Connect GitHub",
|
||||
purpose: "tool",
|
||||
oauthStrategy: "paperclip_cloud_connector",
|
||||
connectorProfile: "github.code",
|
||||
@@ -306,32 +306,41 @@ const apps = [
|
||||
requiredResourceFilters: ["organization", "repository"],
|
||||
},
|
||||
),
|
||||
channelMethod(
|
||||
"github",
|
||||
[
|
||||
{
|
||||
...field("appId", "GitHub App ID", "123456"),
|
||||
type: "text",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
...field(
|
||||
"privateKey",
|
||||
"Private key (PEM)",
|
||||
"-----BEGIN RSA PRIVATE KEY-----",
|
||||
),
|
||||
type: "textarea",
|
||||
},
|
||||
],
|
||||
["organization", "repository"],
|
||||
"Generate the webhook secret in Paperclip, then create one private GitHub App with active SSL-verified webhooks, Issues and Pull requests read/write permission, and the selectable issue_comment and pull_request_review_comment events. GitHub sends installation and installation_repositories automatically. Install the App only on repositories where people may mention the agent.",
|
||||
{
|
||||
register: "https://github.com/settings/apps/new",
|
||||
docs: "https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app",
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
[
|
||||
"github-code-review-bot",
|
||||
"GitHub Code Review Bot",
|
||||
"Have an agent review pull requests and respond to GitHub mentions.",
|
||||
"developer",
|
||||
"github.com",
|
||||
[],
|
||||
channelMethod(
|
||||
"github",
|
||||
[
|
||||
{
|
||||
...field("appId", "GitHub App ID", "123456"),
|
||||
type: "text",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
...field(
|
||||
"privateKey",
|
||||
"Private key (PEM)",
|
||||
"-----BEGIN RSA PRIVATE KEY-----",
|
||||
),
|
||||
type: "textarea",
|
||||
},
|
||||
],
|
||||
["organization", "repository"],
|
||||
"Generate the webhook secret in Paperclip, then create one private GitHub App with active SSL-verified webhooks, Issues and Pull requests read/write permission, and the selectable issue_comment and pull_request_review_comment events. GitHub sends installation and installation_repositories automatically. Install the App only on repositories where people may mention the agent.",
|
||||
{
|
||||
register: "https://github.com/settings/apps/new",
|
||||
docs: "https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app",
|
||||
},
|
||||
),
|
||||
{ featured: true },
|
||||
],
|
||||
[
|
||||
"slack",
|
||||
"Slack",
|
||||
|
||||
@@ -5119,10 +5119,11 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
"google-people",
|
||||
"google-workspace-search",
|
||||
"github",
|
||||
"github-code-review-bot",
|
||||
"youcom",
|
||||
]),
|
||||
);
|
||||
expect(res.body.apps).toHaveLength(57);
|
||||
expect(res.body.apps).toHaveLength(58);
|
||||
expect(
|
||||
res.body.apps.find((app: { slug: string }) => app.slug === "gmail")
|
||||
.ownershipAvailability,
|
||||
|
||||
@@ -137,7 +137,7 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
const connector = page.locator(
|
||||
`[role="listitem"][data-app-slug="${github.slug}"]`,
|
||||
'[role="listitem"][data-app-slug="github"]',
|
||||
);
|
||||
await expect(connector).toBeVisible();
|
||||
await connector.getByRole("button", { name: "Connect GitHub" }).click();
|
||||
@@ -216,8 +216,15 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
`[role="listitem"][data-app-slug="${provider.slug}"]`,
|
||||
);
|
||||
await expect(connector).toBeVisible({ timeout: 30_000 });
|
||||
if (provider.provider === "github") {
|
||||
const tools = page.locator('[role="listitem"][data-app-slug="github"]');
|
||||
await tools.getByRole("button", { name: "Connect GitHub", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Connect GitHub as" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Choose how to connect" })).toHaveCount(0);
|
||||
await page.goto(`/${seed.prefix}/apps`);
|
||||
}
|
||||
await connector
|
||||
.getByRole("button", { name: `Connect ${provider.name}` })
|
||||
.getByRole("button", { name: `Connect ${provider.provider === "github" ? "GitHub Code Review Bot" : provider.name}` })
|
||||
.click();
|
||||
|
||||
if (provider.chatAndTool) {
|
||||
@@ -244,14 +251,6 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
expect(new URL(page.url()).searchParams.get("source")).toBe(
|
||||
provider.provider,
|
||||
);
|
||||
if (provider.provider === "github") {
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Connect GitHub as" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Chat with an agent", { exact: true }),
|
||||
).toHaveCount(0);
|
||||
}
|
||||
await page.goto(chatSetupUrl);
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Choose how to connect" }),
|
||||
|
||||
@@ -59,7 +59,7 @@ export const PROVIDERS: ProviderCase[] = [
|
||||
},
|
||||
{
|
||||
provider: "github",
|
||||
slug: "github",
|
||||
slug: "github-code-review-bot",
|
||||
name: "GitHub",
|
||||
accountLabel: "paperclip-ai",
|
||||
botLabel: "Maya",
|
||||
@@ -72,7 +72,7 @@ export const PROVIDERS: ProviderCase[] = [
|
||||
externalUrl: "https://github.com/paperclip-ai/paperclip/issues/123",
|
||||
setupHeading: /Create or connect a GitHub App/i,
|
||||
setupButton: "Connect and verify",
|
||||
chatAndTool: true,
|
||||
chatAndTool: false,
|
||||
},
|
||||
{
|
||||
provider: "microsoft-teams",
|
||||
|
||||
@@ -135,6 +135,13 @@
|
||||
"localAsset": "/brands/apps/github.svg",
|
||||
"darkAsset": "/brands/apps/github-dark.svg"
|
||||
},
|
||||
{
|
||||
"slug": "github-code-review-bot",
|
||||
"provider": "GitHub Code Review Bot",
|
||||
"catalogVisible": true,
|
||||
"localAsset": "/brands/apps/github.svg",
|
||||
"darkAsset": "/brands/apps/github-dark.svg"
|
||||
},
|
||||
{
|
||||
"slug": "gmail",
|
||||
"provider": "Gmail",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useChatConnectorsEnabled } from "@/hooks/useChatConnectorsEnabled";
|
||||
|
||||
const providerNames: Record<ChatProvider, string> = {
|
||||
slack: "Slack",
|
||||
github: "GitHub",
|
||||
github: "GitHub Code Review Bot",
|
||||
discord: "Discord",
|
||||
"microsoft-teams": "Microsoft Teams",
|
||||
telegram: "Telegram",
|
||||
@@ -57,7 +57,7 @@ export function AgentChannelsPanel({
|
||||
<div className="rounded-lg border border-dashed border-border p-5">
|
||||
<p className="text-sm font-medium">No channels connected</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Connect AgentMail, Slack, GitHub, Discord, Microsoft Teams, or Telegram from
|
||||
Connect AgentMail, Slack, GitHub Code Review Bot, Discord, Microsoft Teams, or Telegram from
|
||||
Connectors.
|
||||
</p>
|
||||
<Button asChild className="mt-3" variant="outline" size="sm">
|
||||
|
||||
@@ -231,14 +231,14 @@ describe("Connectors landing page", () => {
|
||||
|
||||
it("defaults to tools-only GitHub and hides chat-only catalog and existing chat accounts", async () => {
|
||||
experimentalMock.mockResolvedValue({});
|
||||
listGalleryMock.mockResolvedValue({ apps: ["github", "discord", "telegram", "microsoft-teams"].map(getAppStoreDefinition) });
|
||||
listGalleryMock.mockResolvedValue({ apps: ["github", "github-code-review-bot", "discord", "telegram", "microsoft-teams"].map(getAppStoreDefinition) });
|
||||
listApplicationsMock.mockResolvedValue({ applications: [application({
|
||||
id: "chat-app", type: "chat", name: "Private bot", applicationKey: "chat:github:endpoint-1", metadata: { purpose: "channel" },
|
||||
})] });
|
||||
await renderBrowse();
|
||||
expect(chatListMock).not.toHaveBeenCalled();
|
||||
expect(container.querySelector('[data-app-slug="github"]')).not.toBeNull();
|
||||
for (const slug of ["discord", "telegram", "microsoft-teams", "slack"]) {
|
||||
for (const slug of ["github-code-review-bot", "discord", "telegram", "microsoft-teams", "slack"]) {
|
||||
expect(container.querySelector(`[data-app-slug="${slug}"]`)).toBeNull();
|
||||
}
|
||||
expect(container.textContent).not.toContain("Private bot");
|
||||
@@ -247,21 +247,52 @@ describe("Connectors landing page", () => {
|
||||
expect(navigateMock).toHaveBeenLastCalledWith("/apps/connect?source=github");
|
||||
});
|
||||
|
||||
it("restores the GitHub intent chooser when enabled and hides cached chat rows immediately when disabled", async () => {
|
||||
listGalleryMock.mockResolvedValue({ apps: [getAppStoreDefinition("github")] });
|
||||
chatListMock.mockResolvedValue([{ id: "endpoint-1", provider: "github", status: "active", assignedAgentName: "Chat agent", botLabel: "Chat bot", assignedAgentId: "agent-1" }]);
|
||||
it("separates tools and saved bots and hides only bots when chat connectors are disabled", async () => {
|
||||
listGalleryMock.mockResolvedValue({ apps: ["github", "github-code-review-bot"].map(getAppStoreDefinition) });
|
||||
listApplicationsMock.mockResolvedValue({ applications: [
|
||||
application({ id: "github-tools", name: "GitHub", metadata: { sourceTemplateKey: "github" } }),
|
||||
application({ id: "chat-app", type: "chat", name: "Legacy bot", metadata: { sourceTemplateKey: "github", purpose: "channel" } }),
|
||||
] });
|
||||
listConnectionsMock.mockResolvedValue({ connections: [
|
||||
connection({ id: "github-account", applicationId: "github-tools", name: "My GitHub" }),
|
||||
connection({ id: "chat-tools", applicationId: "chat-app", connectionPurpose: "channel" }),
|
||||
] });
|
||||
chatListMock.mockResolvedValue([
|
||||
{ id: "endpoint-1", provider: "github", status: "active", assignedAgentName: "Review agent", botLabel: "Review bot", assignedAgentId: "agent-1" },
|
||||
{ id: "endpoint-2", provider: "github", status: "draft", assignedAgentName: "Draft agent", assignedAgentId: "agent-2" },
|
||||
]);
|
||||
const client = await renderBrowse();
|
||||
expect(chatListMock).toHaveBeenCalledWith("company-1");
|
||||
expect(container.querySelector('[data-app-slug="telegram"]')).not.toBeNull();
|
||||
await act(() => void container.querySelector<HTMLButtonElement>('button[aria-label="Add connection GitHub"]')!.click());
|
||||
expect(navigateMock).toHaveBeenLastCalledWith("/apps/chat/connect?provider=github&toolHref=%2Fapps%2Fconnect%3Fsource%3Dgithub");
|
||||
const tools = container.querySelector('[data-app-slug="github"]')!;
|
||||
const bots = container.querySelector('[data-app-slug="github-code-review-bot"]')!;
|
||||
expect(tools.textContent).toContain("My GitHub");
|
||||
expect(tools.textContent).not.toContain("Review agent");
|
||||
expect(bots.textContent).toContain("Review agent · Code review bot");
|
||||
expect(bots.textContent).toContain("Draft agent");
|
||||
expect(bots.textContent).not.toContain("My GitHub");
|
||||
expect(container.textContent).not.toContain("Legacy bot");
|
||||
await act(() => void tools.querySelector<HTMLButtonElement>('button[aria-label="Add account GitHub"]')!.click());
|
||||
expect(navigateMock).toHaveBeenLastCalledWith("/apps/connect?source=github&applicationId=github-tools&name=GitHub&new=1");
|
||||
await act(() => void bots.querySelector<HTMLButtonElement>('button[aria-label="Add connection GitHub Code Review Bot"]')!.click());
|
||||
expect(navigateMock).toHaveBeenLastCalledWith("/apps/chat/connect?provider=github&purpose=chat");
|
||||
const finish = [...bots.querySelectorAll("button")].find((button) => button.textContent === "Finish setup")!;
|
||||
await act(() => finish.click());
|
||||
expect(navigateMock).toHaveBeenLastCalledWith("/apps/chat/connect?provider=github&purpose=chat&resume=endpoint-2");
|
||||
await act(() => { client.setQueryData(queryKeys.instance.experimentalSettings, { enableChatConnectors: false }); });
|
||||
await flushReact();
|
||||
expect(container.querySelector('[data-app-slug="telegram"]')).toBeNull();
|
||||
expect(container.textContent).not.toContain("Chat agent");
|
||||
expect(container.querySelector('a[href*="/apps/chat/"]')).toBeNull();
|
||||
expect(container.querySelector('[data-app-slug="github-code-review-bot"]')).toBeNull();
|
||||
expect(container.textContent).not.toContain("Review agent");
|
||||
expect(container.querySelector('[data-app-slug="github"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the GitHub tool card when the chat catalog needs its local fallback", async () => {
|
||||
listGalleryMock.mockResolvedValue({ apps: [getAppStoreDefinition("github")] });
|
||||
await renderBrowse();
|
||||
expect(container.querySelector('[data-app-slug="github"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-app-slug="github-code-review-bot"]')).not.toBeNull();
|
||||
await act(() => void container.querySelector<HTMLButtonElement>('button[aria-label="Connect GitHub"]')!.click());
|
||||
expect(navigateMock).toHaveBeenLastCalledWith("/apps/connect?source=github");
|
||||
await act(() => void container.querySelector<HTMLButtonElement>('button[aria-label="Connect GitHub Code Review Bot"]')!.click());
|
||||
expect(navigateMock).toHaveBeenLastCalledWith("/apps/chat/connect?provider=github&purpose=chat");
|
||||
});
|
||||
|
||||
it("renders one connector list with the requested header and no gallery sections", async () => {
|
||||
@@ -294,7 +325,7 @@ describe("Connectors landing page", () => {
|
||||
).map((row) => row.dataset.appSlug),
|
||||
).toEqual([
|
||||
"discord",
|
||||
"github",
|
||||
"github-code-review-bot",
|
||||
"gmail",
|
||||
"imessage-photon",
|
||||
"jira",
|
||||
|
||||
@@ -388,9 +388,8 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne
|
||||
).filter(
|
||||
(application) =>
|
||||
application.status !== "archived" &&
|
||||
(chatConnectorsEnabled ||
|
||||
(application.type !== "chat" &&
|
||||
application.metadata?.purpose !== "channel")),
|
||||
application.type !== "chat" &&
|
||||
application.metadata?.purpose !== "channel",
|
||||
);
|
||||
const connectionsByApplicationId = new Map<string, ToolConnection[]>();
|
||||
for (const connection of activeConnections) {
|
||||
@@ -429,51 +428,46 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne
|
||||
chatEndpoints: [],
|
||||
});
|
||||
}
|
||||
const nativeChatProviders = [
|
||||
{ provider: "imessage-photon", name: "iMessage Photon", description: "Message agents and share photos from Apple Messages with a dedicated Photon number." },
|
||||
const nativeChatApps = [
|
||||
{ slug: "imessage-photon", name: "iMessage Photon", description: "Message agents and share photos from Apple Messages with a dedicated Photon number." },
|
||||
{
|
||||
provider: "slack",
|
||||
slug: "slack",
|
||||
name: "Slack",
|
||||
description:
|
||||
"Chat with agents from Slack channels and direct messages.",
|
||||
},
|
||||
{
|
||||
provider: "github",
|
||||
name: "GitHub",
|
||||
slug: "github-code-review-bot",
|
||||
name: "GitHub Code Review Bot",
|
||||
description:
|
||||
"Chat with agents from issues, pull requests, and review threads.",
|
||||
"Have an agent review pull requests and respond to GitHub mentions.",
|
||||
},
|
||||
{
|
||||
provider: "discord",
|
||||
slug: "discord",
|
||||
name: "Discord",
|
||||
description:
|
||||
"Chat with agents from Discord channels, threads, and direct messages.",
|
||||
},
|
||||
{
|
||||
provider: "microsoft-teams",
|
||||
slug: "microsoft-teams",
|
||||
name: "Microsoft Teams",
|
||||
description: "Chat with agents from Teams channels and conversations.",
|
||||
},
|
||||
{
|
||||
provider: "telegram",
|
||||
slug: "telegram",
|
||||
name: "Telegram",
|
||||
description:
|
||||
"Chat with agents from Telegram direct messages, groups, and topics.",
|
||||
},
|
||||
] as const;
|
||||
for (const item of chatConnectorsEnabled ? nativeChatProviders : []) {
|
||||
if (
|
||||
[...rowsBySlug.values()].some(
|
||||
(row) => chatProviderForSlug(row.slug) === item.provider,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
rowsBySlug.set(item.provider, {
|
||||
key: `native-chat:${item.provider}`,
|
||||
slug: item.provider,
|
||||
for (const item of chatConnectorsEnabled ? nativeChatApps : []) {
|
||||
if (rowsBySlug.has(item.slug)) continue;
|
||||
rowsBySlug.set(item.slug, {
|
||||
key: `native-chat:${item.slug}`,
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
brandKey: item.provider,
|
||||
brandKey: item.slug,
|
||||
entry: null,
|
||||
applications: [],
|
||||
connections: [],
|
||||
@@ -857,17 +851,17 @@ export function ConnectorCard({
|
||||
{row.chatEndpoints.map((endpoint) => (
|
||||
<div
|
||||
key={endpoint.id}
|
||||
className="flex flex-wrap items-center gap-3 px-4 py-3"
|
||||
className="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center sm:gap-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
className="truncate text-left text-sm font-medium hover:underline"
|
||||
className="block max-w-full truncate text-left text-sm font-medium hover:underline"
|
||||
onClick={() =>
|
||||
onNavigate(`/apps/chat/${endpoint.id}/settings`)
|
||||
}
|
||||
>
|
||||
{endpoint.assignedAgentName} · {endpoint.provider === "agentmail" ? "Email" : "Chat"}
|
||||
{endpoint.assignedAgentName} · {endpoint.provider === "agentmail" ? "Email" : endpoint.provider === "github" ? "Code review bot" : "Chat"}
|
||||
</button>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{endpoint.providerAccountLabel ??
|
||||
@@ -875,10 +869,10 @@ export function ConnectorCard({
|
||||
"Provider identity"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{endpoint.status.replace(/_/g, " ")}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{endpoint.status.replace(/_/g, " ")}
|
||||
</span>
|
||||
{endpoint.status === "draft" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
@@ -27,7 +26,7 @@ function Location() {
|
||||
);
|
||||
}
|
||||
|
||||
describe("GitHub connection purpose routing", () => {
|
||||
describe("GitHub code review bot routing", () => {
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
beforeEach(() => {
|
||||
@@ -52,33 +51,10 @@ describe("GitHub connection purpose routing", () => {
|
||||
),
|
||||
);
|
||||
}
|
||||
async function click(label: string) {
|
||||
const button = [...container.querySelectorAll("button")].find((node) =>
|
||||
node.textContent?.includes(label),
|
||||
);
|
||||
expect(button).toBeDefined();
|
||||
await act(async () => {
|
||||
button!.click();
|
||||
});
|
||||
}
|
||||
it("offers personal/tool connections from the catalog instead of forcing bot setup", async () => {
|
||||
render("provider=github&toolHref=%2Fapps%2Fconnect%3Fsource%3Dgithub");
|
||||
expect(container.textContent).toContain("Choose how to connect");
|
||||
expect(container.textContent).not.toContain("GitHub bot setup");
|
||||
await click("Use this connection as an agent tool");
|
||||
expect(container.querySelector("output")?.textContent).toBe(
|
||||
"/apps/connect?source=github",
|
||||
);
|
||||
});
|
||||
it("opens the bot wizard when chat is chosen and preserves the agent preselection", async () => {
|
||||
render("provider=github&agentId=agent-a");
|
||||
await click("Chat with an agent");
|
||||
expect(container.textContent).toContain("GitHub bot setup");
|
||||
expect(container.querySelector("output")?.textContent).toContain(
|
||||
"agentId=agent-a&purpose=chat",
|
||||
);
|
||||
});
|
||||
it.each([
|
||||
"",
|
||||
"agentId=agent-a",
|
||||
"toolHref=%2Fapps%2Fconnect%3Fsource%3Dgithub",
|
||||
"purpose=chat",
|
||||
"resume=endpoint-a",
|
||||
"resume=endpoint-a&reconnect=1",
|
||||
@@ -86,5 +62,6 @@ describe("GitHub connection purpose routing", () => {
|
||||
render(`provider=github&${search}`);
|
||||
expect(container.textContent).toContain("GitHub bot setup");
|
||||
expect(container.textContent).not.toContain("Choose how to connect");
|
||||
expect(container.querySelector("output")?.textContent).toBe(`/apps/chat/connect?provider=github&${search}`);
|
||||
});
|
||||
});
|
||||
@@ -157,16 +157,8 @@ function ChatConnectionPurpose({ provider, onChat, onTools }: {
|
||||
}
|
||||
|
||||
export function ChatEndpointSetup() {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
if (params.get("provider") === "github") {
|
||||
if (params.get("purpose") === "chat" || params.get("resume")) return <GitHubChatSetup />;
|
||||
return <ChatConnectionPurpose provider="github" onChat={() => {
|
||||
const next = new URLSearchParams(params);
|
||||
next.set("purpose", "chat");
|
||||
setParams(next);
|
||||
}} onTools={() => navigate(params.get("toolHref") || "/apps/connect?source=github")} />;
|
||||
}
|
||||
const [params] = useSearchParams();
|
||||
if (params.get("provider") === "github") return <GitHubChatSetup />;
|
||||
return params.get("provider") === "agentmail" ? <EmailEndpointSetup /> : <ChatSdkEndpointSetup />;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export function GitHubChatSetup() {
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([
|
||||
{ label: "Connectors", href: "/apps" },
|
||||
{ label: "Connect GitHub bot" },
|
||||
{ label: "GitHub Code Review Bot" },
|
||||
]);
|
||||
return () => setBreadcrumbs([]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("chat connector UI contract", () => {
|
||||
it("lists every supported provider in the agent channel empty state", () => {
|
||||
const panel = source("../../../components/chat/AgentChannelsPanel.tsx");
|
||||
expect(panel).toContain(
|
||||
"Connect AgentMail, Slack, GitHub, Discord, Microsoft Teams, or Telegram from",
|
||||
"Connect AgentMail, Slack, GitHub Code Review Bot, Discord, Microsoft Teams, or Telegram from",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, userEvent, within } from "storybook/test";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { getAppStoreDefinition } from "@paperclipai/shared";
|
||||
import { Link, Route, Routes, useLocation } from "@/lib/router";
|
||||
import { SetupWizardSidebarProvider } from "@/context/SetupWizardSidebarContext";
|
||||
import { SetupWizardSidebar } from "@/components/SetupWizard";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { Browse } from "@/pages/apps/Browse";
|
||||
import { AppsConnect } from "@/pages/apps/AppsConnect";
|
||||
import { ChatEndpointSetup } from "@/pages/apps/chat/ChatEndpointSetup";
|
||||
import { storybookAgents } from "../fixtures/paperclipData";
|
||||
|
||||
const companyId = "company-storybook";
|
||||
// Hosted Storybooks live below a branch path; reuse their bundled GitHub marks.
|
||||
const githubBranding = {
|
||||
logoUrl: "./brands/apps/github.svg",
|
||||
darkLogoUrl: "./brands/apps/github-dark.svg",
|
||||
};
|
||||
|
||||
function GitHubConnections({ saved = false, chatEnabled = true }) {
|
||||
const location = useLocation();
|
||||
const [client] = useState(() => {
|
||||
const result = new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: Infinity, retry: false, refetchOnMount: false } },
|
||||
});
|
||||
const github = getAppStoreDefinition("github")!;
|
||||
result.setQueryData(queryKeys.apps.gallery(companyId), {
|
||||
apps: [
|
||||
{ ...github, branding: githubBranding, ownershipAvailability: { platform_shared: true, customer: true } },
|
||||
{ ...getAppStoreDefinition("github-code-review-bot"), branding: githubBranding },
|
||||
],
|
||||
capabilities: { canSetCompanyInstall: true, canCreatePersonalConnection: true, canCreateOrganizationConnection: true },
|
||||
});
|
||||
result.setQueryData(queryKeys.instance.experimentalSettings, { enableChatConnectors: chatEnabled });
|
||||
result.setQueryData(queryKeys.tools.applications(companyId), { applications: saved ? [
|
||||
{ id: "github-tools", companyId, name: "GitHub", status: "active", metadata: { sourceTemplateKey: "github" } },
|
||||
// Existing bots retain their provider/source identity after the catalog split.
|
||||
{ id: "github-bot", companyId, name: "Review bot", status: "active", type: "chat", metadata: { sourceTemplateKey: "github", purpose: "channel" } },
|
||||
] : [] });
|
||||
result.setQueryData(queryKeys.tools.connections(companyId), { connections: saved ? [{
|
||||
id: "github-account", applicationId: "github-tools", companyId, name: "octocat",
|
||||
status: "active", enabled: true, healthStatus: "healthy", transport: "mcp_remote",
|
||||
authKind: "oauth", config: { sourceTemplateKey: "github" }, transportConfig: {},
|
||||
}] : [] });
|
||||
result.setQueryData(queryKeys.chatEndpoints.list(companyId), saved ? [
|
||||
{ id: "github-review", companyId, provider: "github", status: "active", assignedAgentId: "reviewer", assignedAgentName: "Reviewer", botLabel: "Paperclip Review", allowUnlinkedPeople: false },
|
||||
{ id: "github-draft", companyId, provider: "github", status: "draft", assignedAgentId: "backup", assignedAgentName: "Backup reviewer", allowUnlinkedPeople: false },
|
||||
] : []);
|
||||
result.setQueryData(queryKeys.access.companyUserDirectory(companyId), { users: [] });
|
||||
result.setQueryData(["github-setup-agents", companyId], storybookAgents);
|
||||
return result;
|
||||
});
|
||||
return (
|
||||
<QueryClientProvider client={client}>
|
||||
<SetupWizardSidebarProvider>
|
||||
<div className="flex min-h-screen">
|
||||
{location.pathname.endsWith("/apps/chat/connect") && (
|
||||
<div className="hidden w-64 shrink-0 md:block">
|
||||
<SetupWizardSidebar />
|
||||
</div>
|
||||
)}
|
||||
<main className="mx-auto min-w-0 w-full max-w-4xl space-y-6 p-6">
|
||||
<Link to="/apps" className="text-sm text-muted-foreground hover:text-foreground">
|
||||
Connectors
|
||||
</Link>
|
||||
<Routes>
|
||||
<Route path="/:companyPrefix/apps/connect" element={<AppsConnect />} />
|
||||
<Route path="/:companyPrefix/apps/chat/connect" element={<ChatEndpointSetup />} />
|
||||
<Route path="*" element={<Browse />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</SetupWizardSidebarProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Connections/GitHub and Code Review Bot",
|
||||
component: GitHubConnections,
|
||||
play: async ({ canvasElement }) => {
|
||||
await userEvent.type(await within(canvasElement).findByRole("searchbox", { name: "Search connectors" }), "GitHub");
|
||||
},
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: { description: { component: "Production catalog cards and setup pages with local API query fixtures. GitHub opens account/tool access; GitHub Code Review Bot opens Choose agent. These stories cover discovery and entry into setup, without live GitHub consent or app installation." } },
|
||||
},
|
||||
} satisfies Meta<typeof GitHubConnections>;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Catalog: Story = {};
|
||||
export const SavedConnections: Story = { args: { saved: true } };
|
||||
export const ChatDisabled: Story = {
|
||||
args: { chatEnabled: false },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(await canvas.findByRole("button", { name: /^Connect GitHub$/ })).toBeVisible();
|
||||
await expect(canvas.queryByRole("heading", { name: "GitHub Code Review Bot" })).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
export const ToolSetup: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(await canvas.findByRole("button", { name: /^Connect GitHub$/ }));
|
||||
await expect(await canvas.findByRole("heading", { name: "Connect GitHub as" })).toBeVisible();
|
||||
await expect(canvas.queryByRole("heading", { name: "Choose how to connect" })).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
export const BotSetup: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(await canvas.findByRole("button", { name: /^Connect GitHub Code Review Bot$/ }));
|
||||
await expect(await canvas.findByRole("heading", { name: /^Choose agent$/ })).toBeVisible();
|
||||
await expect(canvas.queryByRole("heading", { name: "Choose how to connect" })).not.toBeInTheDocument();
|
||||
await expect(canvas.getByRole("button", { name: /^Continue$/ })).toBeDisabled();
|
||||
},
|
||||
};
|
||||
export const Mobile: Story = {
|
||||
args: { saved: true },
|
||||
globals: { viewport: { value: "mobile", isRotated: false } },
|
||||
};
|
||||
export const Light: Story = { globals: { theme: "light" } };
|
||||
Reference in new issue
Block a user