From 7c1d2503dbfc9c06a78c0bb6a2a34e58e0eafad3 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 7 Oct 2026 15:39:06 -0500 Subject: [PATCH] Improve GitHub bot management hierarchy Separate behavior and access, retain drafts across tabs, and make current reviews and task conversations easier to scan. Add production Storybook journeys and regression coverage. Co-Authored-By: Paperclip --- doc/connections/GITHUB-REVIEW-BOT.md | 23 +- .../2026-10-07-github-app-live-test-drive.md | 32 + .../pages/apps/chat/ChatConversationList.tsx | 110 ++ ui/src/pages/apps/chat/ChatEndpointDetail.tsx | 60 +- .../apps/chat/GitHubBotConfiguration.tsx | 1040 ++++++++++------- .../apps/chat/GitHubBotManagement.test.tsx | 418 +++++++ .../pages/apps/chat/GitHubBotManagement.tsx | 721 +++++++----- .../pages/apps/chat/chat-ui-contract.test.ts | 2 - .../github-bot-management/fixture-api.tsx | 130 +++ .../stories/github-bot-management/fixtures.ts | 200 ++++ .../management.stories.tsx | 180 +++ ui/storybook/stories/github-chat.stories.tsx | 2 +- 12 files changed, 2129 insertions(+), 789 deletions(-) create mode 100644 ui/src/pages/apps/chat/ChatConversationList.tsx create mode 100644 ui/src/pages/apps/chat/GitHubBotManagement.test.tsx create mode 100644 ui/storybook/stories/github-bot-management/fixture-api.tsx create mode 100644 ui/storybook/stories/github-bot-management/fixtures.ts create mode 100644 ui/storybook/stories/github-bot-management/management.stories.tsx diff --git a/doc/connections/GITHUB-REVIEW-BOT.md b/doc/connections/GITHUB-REVIEW-BOT.md index 146ba8f920c..894de711da9 100644 --- a/doc/connections/GITHUB-REVIEW-BOT.md +++ b/doc/connections/GITHUB-REVIEW-BOT.md @@ -33,8 +33,8 @@ required GitHub checks, read New connections default to authorized mentions, advisory reviews, linked-member access, guests off, and enabled bot GitHub tools. Existing instructions, explicit behavior, and narrower saved repository restrictions remain intact. Later -repository additions require enablement in connection settings. Advanced review -rules and prompts also live in settings. +repository additions require enablement in Access. Advanced review rules and +prompts live in Settings. Local instances receive public callbacks and signed events through an enrolled Paperclip Cloud connector, using outbound requests instead of a public tunnel. @@ -57,6 +57,25 @@ Connection setup reports tool/runtime support and isolation separately; an actua task is still required to prove that the chosen provider can execute in the 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. +- **Access:** choose enabled repositories, allowed people, each person’s automatic + events, and the bot’s GitHub tools. Repository switches save immediately. Other + changes use **Save changes**. A linked account alone does not grant selected-member + access. External contributors still require a sponsor and restricted permissions. +- **Reviews:** see the latest review for each pull request. The result names the + reviewed commit; earlier reviews remain in history. A previous passing result + does not stand in for a pending review of a newer commit. +- **Conversations:** follow the task title to Paperclip or the repository/thread + label to GitHub. + +Settings and Access share unsaved edits while you switch connection tabs. Save +before reloading or leaving the connection. No permissions change merely by +opening a tab or configuring an implicit linked member’s individual event settings. + ## Who can start work Linked members may be allowed together or selected individually. Teammates diff --git a/doc/plans/2026-10-07-github-app-live-test-drive.md b/doc/plans/2026-10-07-github-app-live-test-drive.md index 008dc861753..454dee4663e 100644 --- a/doc/plans/2026-10-07-github-app-live-test-drive.md +++ b/doc/plans/2026-10-07-github-app-live-test-drive.md @@ -253,3 +253,35 @@ Qualification limits: personal-account setup, delayed organization approval and ### Cleanup completed Closed disposable issues 1, 2 and 4 and PR 3 without merging. Deleted only the test branch. Retained the App, original connection, bot, vault and test-drive data. The public relay/tunnel was stopped. The private test drive remains available on port 3110 for inspection. Fixes and qualification record were pushed to draft PR #15416; current-head CI/review remain outstanding. + + +### Management page hierarchy qualification + +The connected bot now separates instructions, triggers, and review output in +Settings from repository, people, and tool permissions in Access. Each linked +person appears once. Advanced controls are disclosed. Settings and Access share +one versioned draft across all connection tabs. Reviews show the latest assessment +per repository/PR and preserve earlier commit evidence in history. Conversation +titles link directly to tasks; provider labels link directly to their threads. + +Walkthrough: Connectors → search GitHub → Animal Bot → Settings → Access → Reviews +→ Conversations. A harmless trailing newline was saved from Access after editing +Settings, persisted after a reload, and then was removed through the UI. Redacted +API snapshots confirm the full configuration returned exactly to its original +value (revision 4 → 6). Repository and people permissions were not changed. + +The real instance was inspected at its desktop width, 820px, and 390px with the +actual mobile shell. Review history expanded correctly and the ASCII animal +rendered as a code block. Production Storybook fixtures verified a saved edit, +a pending current commit with its previous passing score hidden, a recoverable +configuration failure, and a mobile empty conversation state. These fixtures do +not qualify any new provider execution. Screenshots and configuration comparisons +are in the existing local E2E evidence directory. + +Focused UI checks cover draft retention, revision conflicts, isolated repository +updates, override preservation/unique field IDs, implicit linked-member event +configuration, mentions-only trigger preservation, latest-review ordering, pending +head presentation, and GitHub thread labels. The affected management, setup, +AgentMail, sidebar, and shared UI-contract group has 47 passing tests. Repository +typecheck/build, Storybook build, and token gates pass. The earlier full-suite +failure and outstanding current-head CI/review remain as recorded above. diff --git a/ui/src/pages/apps/chat/ChatConversationList.tsx b/ui/src/pages/apps/chat/ChatConversationList.tsx new file mode 100644 index 00000000000..68022b96450 --- /dev/null +++ b/ui/src/pages/apps/chat/ChatConversationList.tsx @@ -0,0 +1,110 @@ +import { MessageSquare } from "lucide-react"; +import type { ChatConversation, ChatProvider } from "@/api/chatEndpoints"; +import { EmptyState } from "@/components/EmptyState"; +import { StatusBadge } from "@/components/StatusBadge"; +import { Link } from "@/lib/router"; +import { formatDateTime } from "@/lib/utils"; +import { AppLogo } from "../AppLogo"; + +export function conversationDestination( + row: ChatConversation, + provider: ChatProvider, +) { + if (provider === "github" && row.externalUrl) { + try { + const url = new URL(row.externalUrl); + const parts = url.pathname.split("/"); + if ( + url.hostname === "github.com" && + ["issues", "pull"].includes(parts[3]) && + /^\d+$/.test(parts[4]) + ) + return `${parts[1]}/${parts[2]} #${parts[4]}`; + } catch { + /* Preserve the provider label when no thread URL is available. */ + } + } + return row.externalLabel; +} + +export function ChatConversationList({ + rows, + provider, +}: { + rows: ChatConversation[]; + provider: ChatProvider; +}) { + if (!rows.length) + return ( + + ); + return ( +
    + {rows.map((row) => ( +
  • + +
    +
    + {row.issueIdentifier && ( + + {row.issueIdentifier} + + )} + {row.issueId ? ( + + {row.issueTitle ?? row.issueIdentifier ?? "Paperclip task"} + + ) : ( + + {row.issueTitle ?? "Waiting for task"} + + )} +
    +
    + {row.externalUrl ? ( + + {conversationDestination(row, provider)} + + ) : ( + {row.externalLabel} + )} + {row.updatedAt && ( + + )} + {row.state === "completed" && Completed} +
    +
    + {row.state !== "active" && row.state !== "completed" && ( + + )} +
  • + ))} +
+ ); +} diff --git a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx index 3203d0207ec..20e35fdfb37 100644 --- a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx +++ b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx @@ -5,6 +5,8 @@ import { SlackAvatarSettings } from "./SlackAvatarStep"; import { agentsApi } from "@/api/agents"; import { agentAvatarUrl } from "@/lib/agent-avatar-url"; import { resolveAgentAppearance } from "@paperclipai/shared"; +import { AgentAvatar } from "@/components/AgentAvatar"; +import { ChatConversationList } from "./ChatConversationList"; import { GitHubBotManagement, GitHubReviews } from "./GitHubBotManagement"; import { EmailEndpointSettings } from "./EmailEndpointSetup"; import { EmailConnectionAccess } from "@/components/EmailConnectionAccess"; @@ -243,6 +245,11 @@ export function ChatEndpointDetail() { : false, }); const endpoint = endpointQuery.data; + const avatarAgent = useQuery({ + queryKey: queryKeys.agents.detail(endpoint?.assignedAgentId ?? ""), + queryFn: () => agentsApi.get(endpoint!.assignedAgentId), + enabled: endpoint?.provider === "github", + }); const [copyStatus, setCopyStatus] = useState(null); useEffect(() => { @@ -291,12 +298,14 @@ export function ChatEndpointDetail() { return (
-
+
+ {endpoint.provider === "github" && } +

- {endpoint.assignedAgentName} in {providerNames[endpoint.provider]} + {endpoint.provider === "github" ? {endpoint.assignedAgentName} : `${endpoint.assignedAgentName} in ${providerNames[endpoint.provider]}`}

-

- {endpoint.providerAccountLabel ?? (endpoint.provider === "agentmail" ? endpoint.botExternalId ?? "Email connection" : "Chat connection")} +

+ {endpoint.provider === "github" ? <>{endpoint.botLabel ?? endpoint.botUsername ?? "GitHub"}{endpoint.providerAccountLabel && · {endpoint.providerAccountLabel}} : endpoint.providerAccountLabel ?? (endpoint.provider === "agentmail" ? endpoint.botExternalId ?? "Email connection" : "Chat connection")}

{endpoint.provider === "imessage-photon" && endpoint.botExternalId && endpoint.photonAllocation !== "shared" && (
@@ -308,6 +317,7 @@ export function ChatEndpointDetail() { {copyStatus}
)} +
{setupIncomplete ? ( @@ -325,14 +335,11 @@ export function ChatEndpointDetail() { {endpoint.status !== "active" && }
- {activeTab === "settings" && ( - <> -{endpoint.provider === "github" && } -{endpoint.provider !== "github" && } - - )} + {endpoint.provider === "github" && } + {activeTab === "settings" && endpoint.provider !== "github" && } {activeTab === "reviews" && endpoint.provider === "github" && } -{activeTab === "access" && endpoint.provider === "github" && } {activeTab === "access" && endpoint.provider === "agentmail" && } {activeTab === "access" && endpoint.provider !== "github" && endpoint.provider !== "agentmail" && ( chatEndpointsApi.listConversations(endpointId), ...liveChatQueryOptions, }); - const rows = query.data ?? []; return (
-
-

Conversations

-
{query.isPending ?

Loading conversations…

: query.isError ? (

Conversations could not be loaded.

- ) : rows.length === 0 ? ( -

- {provider === "agentmail" - ? "No email conversations yet. Send an email to this agent’s address to start one." - : "No conversations yet. Address the agent in an enabled destination to start one."} -

- ) : ( -
    - {rows.map((row) => ( -
  • - -
    - {row.externalLabel} - {row.externalUrl && Open {providerNames[provider]}} -
    - -
    - {row.issueTitle ?? "Waiting for task"} - {row.issueId && Open task} -
    - {row.issueIdentifier} - {row.state !== "active" && } -
  • - ))} -
- )} + ) : }
); } diff --git a/ui/src/pages/apps/chat/GitHubBotConfiguration.tsx b/ui/src/pages/apps/chat/GitHubBotConfiguration.tsx index eaf1558d8c6..f5ba5d0db25 100644 --- a/ui/src/pages/apps/chat/GitHubBotConfiguration.tsx +++ b/ui/src/pages/apps/chat/GitHubBotConfiguration.tsx @@ -1,5 +1,5 @@ import { copyTextToClipboard } from "@/lib/clipboard"; -import { useRef, useState } from "react"; +import { useId, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { GITHUB_REVIEW_EVENTS, @@ -11,6 +11,13 @@ import { accessApi } from "@/api/access"; import { chatEndpointsApi } from "@/api/chatEndpoints"; import { githubChatApi } from "@/api/githubChat"; import { Button } from "@/components/ui/button"; +import { Copy, MoreHorizontal } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; @@ -32,22 +39,27 @@ export function GitHubToggle({ description, checked, onChange, + ariaLabel, + disabled, }: { label: string; description?: string; checked: boolean; onChange: (checked: boolean) => void; + ariaLabel?: string; + disabled?: boolean; }) { return (
-
-

{label}

+
+

{label}

{description && (

{description}

)}
@@ -61,20 +73,92 @@ export function GitHubPolicyEditor({ policy: GitHubReviewPolicy; onChange: (policy: GitHubReviewPolicy) => void; }) { - const [prompt, setPrompt] = - useState<(typeof GITHUB_REVIEW_EVENTS)[number]>("opened"); + const id = useId(); + const [prompt, setPrompt] = useState< + (typeof GITHUB_REVIEW_EVENTS)[number] | "issue_opened" + >("opened"); const set = ( key: K, value: GitHubReviewPolicy[K], ) => onChange({ ...policy, [key]: value }); return ( -
-
-