From db85bf4b7abec2be51dab97a8b7afa5d9e7fef5b Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:28:23 -0500 Subject: [PATCH] fix(ui): simplify GitHub repository access controls (#13047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip helps people manage AI agents and their connected apps. > - GitHub permissions show which repositories an account can access. > - The page adds an account dropdown and search above the repository list. > - These controls add clutter to a view meant to show the full access list. > - This change removes both controls and their filtering state. > - People see all repositories directly, with the existing scrolling and access controls. ## Linked Issues or Issue Description Refs #12998. **Current behavior** GitHub permissions show an “All accounts” dropdown and a “Search repositories” input above the repository list. **Proposed behavior** Show the full repository list directly. Keep the scroll limit, repository links, private-repository icons, refresh button, and GitHub configuration links. **Reason and benefit** Remove unnecessary controls from the access summary. Users manage repository permissions on GitHub. ## What Changed - Remove the account dropdown, search input, filtering state, and unused imports. - Rename the GitHub configuration button to “Add More Repos on GitHub”. - Render every returned repository and simplify the empty-list message. - Update the existing multi-account test and permissions documentation. ## Verification - All 55 AppDetail tests passed. - `pnpm check:token-gates` passed. - Full `pnpm build` and `pnpm -r typecheck` passed. All 31 CI checks passed on `cb9b8bb14a880d926393c62821265fbd2d8c5d31`. Storybook visual regression was correctly skipped. - Fresh Greptile review on the same commit: 5/5, with no unresolved findings. ## Risks Users can no longer narrow this list by account or search text. The list still scrolls and shows each repository’s full owner/name. This change does not alter GitHub permissions or credential selection. ## Model Used OpenAI GPT-6 through Codex, with code editing and shell verification tools. The exact model variant and context-window size are not exposed in 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 --- doc/execution-github-identity.md | 2 + ui/src/pages/apps/AppDetail.test.tsx | 43 ++++++----------- .../apps/app-detail/IdentitiesSection.tsx | 46 ++----------------- 3 files changed, 20 insertions(+), 71 deletions(-) diff --git a/doc/execution-github-identity.md b/doc/execution-github-identity.md index 1832e9d0a1..bf85604c1f 100644 --- a/doc/execution-github-identity.md +++ b/doc/execution-github-identity.md @@ -28,6 +28,8 @@ An explicit dedicated-agent grant overrides personal selection. Revoked, disable Connection setup and permissions display: “This agent uses this GitHub account for everyone's work, instead of the person giving instructions.” +The GitHub permissions page shows repositories across all connected accounts in one scrollable list. It has no account filter or repository search. Repository icons, private-repository indicators, refresh, and GitHub configuration links remain available. The “Add More Repos on GitHub” button opens GitHub’s app installation and repository-access setup. + Multiple eligible connections for the same GitHub account are treated as one identity, using GitHub's stable account ID rather than its login. The resolver selects an available grant, preferring the newest authorization with a stable diff --git a/ui/src/pages/apps/AppDetail.test.tsx b/ui/src/pages/apps/AppDetail.test.tsx index 131f328d5b..044806fddb 100644 --- a/ui/src/pages/apps/AppDetail.test.tsx +++ b/ui/src/pages/apps/AppDetail.test.tsx @@ -1508,19 +1508,19 @@ describe("AppDetail", () => { await act(async () => { findButton("Load GitHub configuration")!.click(); }); await flushReact(); expect(checkConnectionHealthMock).toHaveBeenCalledWith("conn-1"); - expect(container.querySelector('a[href="https://github.com/apps/paperclip-staging/installations/new"]')?.textContent).toBe("Configure on GitHub"); + expect(container.querySelector('a[href="https://github.com/apps/paperclip-staging/installations/new"]')?.textContent).toBe("Add More Repos on GitHub"); expect(findButton("Load GitHub configuration")).toBeUndefined(); }); - it("filters the combined GitHub repository list by owner and search without changing access", async () => { + it.each([false, true])("shows repositories across accounts without filter controls (empty: %s)", async (empty) => { mockParams.tab = "permissions"; getConnectionMock.mockResolvedValue(perUserConnection()); listConnectionGrantsMock.mockResolvedValue({ connection: { id: "conn-1", uid: "conn-1" }, grants: [dedicatedGitHubGrant({ kind: "user", subjectAgentId: null, subjectUserId: "user-1" }, { - repositoryCount: 3, + repositoryCount: empty ? 0 : 3, installationOwnerLogins: ["paperclipai", "dottabot", "empty-org"], - repositories: [ + repositories: empty ? [] : [ { id: "1", fullName: "paperclipai/first", installationId: "456" }, { id: "2", fullName: "paperclipai/second", installationId: "456" }, { id: "3", fullName: "dottabot/first", installationId: "789" }, @@ -1530,31 +1530,14 @@ describe("AppDetail", () => { }); await renderAppDetail(); const repositoryNames = () => [...container.querySelectorAll('ul[aria-label="Accessible GitHub repositories"] a')].map((link) => link.textContent); - const selectOwner = async (label: string) => { - await act(async () => { - container.querySelector('[role="combobox"][aria-label="Filter repositories by account or organization"]')! - .dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })); - }); - const option = [...document.querySelectorAll('[role="option"]')].find((item) => item.textContent === label); - expect(option).toBeTruthy(); - await act(async () => { - option!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); - }); - }; - expect(repositoryNames()).toEqual(["paperclipai/first", "paperclipai/second", "dottabot/first"]); - await selectOwner("paperclipai"); - expect(repositoryNames()).toEqual(["paperclipai/first", "paperclipai/second"]); - const search = container.querySelector('input[aria-label="Search GitHub repositories"]')!; - await act(async () => { setInputValue(search, "FIRST"); }); - expect(repositoryNames()).toEqual(["paperclipai/first"]); - await selectOwner("All accounts"); - expect(repositoryNames()).toEqual(["paperclipai/first", "dottabot/first"]); - await act(async () => { setInputValue(search, "missing"); }); - expect(container.textContent).toContain("No repositories match your search."); - await act(async () => { setInputValue(search, ""); }); - await selectOwner("empty-org"); - expect(container.textContent).toContain("No accessible repositories for this account or organization."); - expect(container.querySelector('a[href="https://github.com/apps/paperclip-test/installations/new"]')?.textContent).toBe("Configure on GitHub"); + expect(repositoryNames()).toEqual(empty ? [] : ["paperclipai/first", "paperclipai/second", "dottabot/first"]); + if (empty) { + expect(container.querySelector('p[role="status"]')?.textContent?.trim()).toBe("No accessible repositories."); + expect(container.textContent).not.toContain("Refresh access to load the current repository list."); + } + expect(container.querySelector('[aria-label="Filter repositories by account or organization"]')).toBeNull(); + expect(container.querySelector('input[aria-label="Search GitHub repositories"]')).toBeNull(); + expect(container.querySelector('a[href="https://github.com/apps/paperclip-test/installations/new"]')?.textContent).toBe("Add More Repos on GitHub"); expect(updateConnectionMock).not.toHaveBeenCalled(); }); @@ -1581,7 +1564,7 @@ describe("AppDetail", () => { expect(container.querySelector('a[href="https://github.com/paperclipai/test-repo"]')?.textContent).toBe("paperclipai/test-repo"); expect(container.querySelector( 'a[href="https://github.com/apps/paperclip-test/installations/new"]', - )?.textContent).toBe("Configure on GitHub"); + )?.textContent).toBe("Add More Repos on GitHub"); expect(container.querySelector('button[aria-label="Refresh access"]')).toBeTruthy(); expect(container.textContent).not.toContain("Installation"); expect(container.textContent).not.toContain("Token continuity"); diff --git a/ui/src/pages/apps/app-detail/IdentitiesSection.tsx b/ui/src/pages/apps/app-detail/IdentitiesSection.tsx index 2105ade531..38a47ec956 100644 --- a/ui/src/pages/apps/app-detail/IdentitiesSection.tsx +++ b/ui/src/pages/apps/app-detail/IdentitiesSection.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState, type ReactNode } from "react"; -import { Building2, Loader2, Lock, RefreshCw, Search, TriangleAlert, UserRound } from "lucide-react"; +import { Building2, Loader2, Lock, RefreshCw, TriangleAlert, UserRound } from "lucide-react"; import type { ConnectionAudienceMember, ConnectionGrant, @@ -9,8 +9,6 @@ import type { import { Button } from "@/components/ui/button"; import { Identity } from "@/components/Identity"; import { GithubIcon } from "@/components/icons/github-icon"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { InlineBanner } from "@/components/InlineBanner"; import { MemberMultiSelect } from "@/components/MemberMultiSelect"; @@ -295,24 +293,8 @@ function GitHubConnectionSummary({ onRefreshAccess?: () => void; refreshPending: boolean; }) { - const [repositoryOwner, setRepositoryOwner] = useState("*"); - const [repositorySearch, setRepositorySearch] = useState(""); - useEffect(() => { - setRepositoryOwner("*"); - setRepositorySearch(""); - }, [grant.id]); const github = grant.providerTenant?.github; if (!github) return null; - const owners = [...new Set([ - ...(github.installationOwnerLogins ?? []), - ...(github.repositories ?? []).map((repository) => repository.fullName.split("/")[0]), - ])].sort((a, b) => a.localeCompare(b)); - const selectedOwner = owners.includes(repositoryOwner) ? repositoryOwner : "*"; - const search = repositorySearch.trim().toLowerCase(); - const visibleRepositories = github.repositories?.filter((repository) => ( - (selectedOwner === "*" || repository.fullName.split("/")[0] === selectedOwner) - && repository.fullName.toLowerCase().includes(search) - )); const configurationUrl = github.appSlug ? `https://github.com/apps/${encodeURIComponent(github.appSlug)}/installations/new` : /^https:\/\/github\.com\/apps\/[a-z0-9-]+\/installations\/new$/.test(github.installationUrl ?? "") @@ -361,7 +343,7 @@ function GitHubConnectionSummary({ ) : null} {configurationUrl ? ( ) : onRefreshAccess ? (