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 ( + + ); +} 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 ( -
-
-