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 ? (