Repositories
Reusable git repositories available to projects and agents in this company.
@@ -182,16 +231,23 @@ export function Repositories() {
{connections.map((connection) => {
const tone = connectionHealthTone(connection);
return (
-
-
+
+
-
-
+
+
{connection.accountName ?? connection.host}
-
- {connectionHealthLabel(connection)}
- {connection.syncError ? ` — ${connection.syncError}` : ""}
+
+ {syncMutation.isError && syncMutation.variables === connection.id
+ ? "Sync failed — status refreshed; try again"
+ : connectionHealthLabel(connection)}
+ {connection.syncError && !(syncMutation.isError && syncMutation.variables === connection.id)
+ ? ` — ${connection.syncError}`
+ : ""}
@@ -201,7 +257,7 @@ export function Repositories() {
onClick={() => syncMutation.mutate(connection.id)}
disabled={connection.status === "disconnected" || syncMutation.isPending}
>
- {syncMutation.isPending ? (
+ {syncMutation.isPending && syncMutation.variables === connection.id ? (
) : (
@@ -215,14 +271,19 @@ export function Repositories() {
) : null}
+ {catalogNotice ? {catalogNotice}
: null}
+
{repositories.length === 0 ? (
0
+ ? "Add a repository by clone URL, or connect GitHub to import from a provider."
+ : "Add a repository by clone URL. Provider connections appear here when configured."}
+ action="Add manually"
+ actionRef={manualTriggerRef}
hideActionIcon
- onAction={() => openGithubDialog(defaultGithubHost)}
+ onAction={() => setManualOpen(true)}
/>
) : (
@@ -243,7 +304,7 @@ export function Repositories() {
@@ -254,7 +315,7 @@ export function Repositories() {
{repo.host}
-
+
{repo.state !== "active" ? (
{repo.state}
) : null}
@@ -278,11 +339,11 @@ export function Repositories() {
companyId={selectedCompanyId!}
open={githubOpen}
host={githubHost}
- onOpenChange={setGithubOpen}
+ onOpenChange={handleGithubOpenChange}
onImported={() => refresh()}
/>
-
diff --git a/ui/src/pages/RepositoryDetail.test.tsx b/ui/src/pages/RepositoryDetail.test.tsx
index 479e79f4a03..46963a9903b 100644
--- a/ui/src/pages/RepositoryDetail.test.tsx
+++ b/ui/src/pages/RepositoryDetail.test.tsx
@@ -88,8 +88,9 @@ describe("RepositoryDetail", () => {
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
expect(container.textContent).toContain("This is guidance, not a boundary");
+ expect(container.textContent).toContain("Direct grant · Also inherited through Project One");
+ expect(container.textContent).toContain("Inherited through projects (1)");
expect(container.textContent).toContain("Inherited through Project One · managed on those projects");
- expect(container.textContent).toContain("Inherited access");
const revoke = [...container.querySelectorAll("button")].find((button) => button.textContent?.includes("Revoke direct grant"));
await act(() => revoke!.click());
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
diff --git a/ui/src/pages/RepositoryDetail.tsx b/ui/src/pages/RepositoryDetail.tsx
index 984db4c3722..1347a795fc0 100644
--- a/ui/src/pages/RepositoryDetail.tsx
+++ b/ui/src/pages/RepositoryDetail.tsx
@@ -15,12 +15,13 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
+import { Input } from "@/components/ui/input";
function MetadataRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
-
-
{label}
-
{value}
+
+ {label}
+ {value}
);
}
@@ -37,24 +38,42 @@ function SectionPicker({ label, options, onSelect }: {
onSelect: (id: string) => void;
}) {
const [open, setOpen] = useState(false);
+ const [search, setSearch] = useState("");
+ const filtered = options.filter((option) => option.label.toLowerCase().includes(search.trim().toLowerCase()));
+ function handleOpenChange(next: boolean) {
+ setOpen(next);
+ if (!next) setSearch("");
+ }
return (
-
+
-
- {options.map((option) => (
-
- ))}
+
+ setSearch(event.target.value)}
+ placeholder="Search"
+ />
+
+ {filtered.map((option) => (
+
+ ))}
+ {filtered.length === 0 ? (
+
No options match.
+ ) : null}
+
);
@@ -128,6 +147,8 @@ export function RepositoryDetail() {
const agentOptions = (agentsQuery.data ?? [])
.filter((agent) => !directAgentIds.has(agent.id))
.map((agent) => ({ id: agent.id, label: agent.name }));
+ const directAgents = relationships?.agents.filter((agent) => agent.sources.direct) ?? [];
+ const inheritedAgents = relationships?.agents.filter((agent) => !agent.sources.direct) ?? [];
if (repositoryQuery.isLoading || relationshipsQuery.isLoading) return ;
if (repositoryQuery.isError || relationshipsQuery.isError || !repository || !relationships) {
@@ -142,10 +163,10 @@ export function RepositoryDetail() {
return (
-
+
-
-
+
+
{repository.owner}/{repository.name}
{repository.host}
@@ -173,8 +194,8 @@ export function RepositoryDetail() {
-
-
+
+
Project hints
Attach this repository to every project likely to touch it. This is guidance, not a boundary.
@@ -185,8 +206,8 @@ export function RepositoryDetail() {
{relationships.projects.length === 0 ? (
No projects currently hint at this repository.
) : relationships.projects.map((project) => (
-
-
{project.name}
+
+
{project.name}
@@ -197,34 +218,47 @@ export function RepositoryDetail() {
-
-
-
Direct agent access
-
These grants are in addition to access inherited through projects.
+
+
+
Direct agent access ({directAgents.length})
+
Explicit grants managed on this repository.
grantAgent.mutate(id)} />
- {relationships.agents.length === 0 ? (
-
No agents currently have effective access.
- ) : relationships.agents.map((agent) => (
-
+ {directAgents.length === 0 ? (
+
No agents have a direct grant.
+ ) : directAgents.map((agent) => (
+
{agent.name}
{agent.sources.projects.length > 0
- ? `Inherited through ${agent.sources.projects.map((project) => project.name).join(", ")} · managed on those projects`
+ ? `Direct grant · Also inherited through ${agent.sources.projects.map((project) => project.name).join(", ")}`
: "Direct grant"}
- {agent.sources.direct ? (
-
- ) : (
-
Inherited access
- )}
+
))}
+ {inheritedAgents.length > 0 ? (
+
+
+ Inherited through projects ({inheritedAgents.length})
+
+
+ {inheritedAgents.map((agent) => (
+
+
{agent.name}
+
+ Inherited through {agent.sources.projects.map((project) => project.name).join(", ")} · managed on those projects
+
+
+ ))}
+
+
+ ) : null}
);