diff --git a/desktop/src/features/projects/ui/ProjectPullRequestsPanel.tsx b/desktop/src/features/projects/ui/ProjectPullRequestsPanel.tsx index 24ab8a590f..46f9e6f883 100644 --- a/desktop/src/features/projects/ui/ProjectPullRequestsPanel.tsx +++ b/desktop/src/features/projects/ui/ProjectPullRequestsPanel.tsx @@ -48,6 +48,10 @@ import { } from "./ProjectProfileIdentity"; import { ProjectRichContent } from "./ProjectRichContent"; import { ProjectReviewChecks } from "./ProjectReviewChecks"; +import { + ProjectReviewAgentSelectionHarness, + useProjectReviewAgentSelection, +} from "./ProjectReviewAgentSelection"; import { ProjectReviewMemory } from "./ProjectReviewMemory"; import { PullRequestMetaHeader } from "./PullRequestMetaRail"; import { PullRequestReviewCard } from "./PullRequestReviewCard"; @@ -423,6 +427,7 @@ export function ProjectPullRequestDetail({ pullRequest: ProjectPullRequest; }) { const identityQuery = useIdentityQuery(); + const reviewAgents = useProjectReviewAgentSelection(); const commentMutation = useCreateProjectPullRequestCommentMutation(project); const [ expandedReviewHistoryPullRequestIds, @@ -617,14 +622,23 @@ export function ProjectPullRequestDetail({ - + - + +
void; + selected: ReviewAgent | null; +}; + +export function useProjectReviewAgentSelection(): ProjectReviewAgentSelection { + const identityQuery = useIdentityQuery(); + const managedAgentsQuery = useManagedAgentsQuery(); + const relayAgentsQuery = useRelayAgentsQuery(); + const channelsQuery = useChannelsQuery(); + const [selectedAgentPubkey, setSelectedAgentPubkey] = React.useState< + string | null + >(null); + + const candidates = React.useMemo(() => { + const managedAgents = managedAgentsQuery.data ?? []; + const managedByPubkey = new Map( + managedAgents.map((agent) => [normalizePubkey(agent.pubkey), agent]), + ); + const relayAgents = relayAgentsQuery.data ?? []; + const allowedPubkeys = getMentionableAgentPubkeys({ + currentPubkey: identityQuery.data?.pubkey, + eligibilityScope: { type: "community" }, + managedAgentPubkeys: managedByPubkey.keys(), + relayAgents, + sharedChannelIds: getSharedChannelIds(channelsQuery.data), + }); + const available: ReviewAgent[] = managedAgents.map((agent) => ({ + pubkey: normalizePubkey(agent.pubkey), + name: agent.name, + isManaged: true, + isActive: isManagedAgentActive(agent), + })); + for (const relayAgent of relayAgents) { + const pubkey = normalizePubkey(relayAgent.pubkey); + if (managedByPubkey.has(pubkey) || !allowedPubkeys.has(pubkey)) continue; + available.push({ + pubkey, + name: relayAgent.name, + isManaged: false, + isActive: relayAgent.status !== "offline", + }); + } + return available.sort((left, right) => { + if (left.isActive !== right.isActive) return left.isActive ? -1 : 1; + if (left.isManaged !== right.isManaged) return left.isManaged ? -1 : 1; + return left.name.localeCompare(right.name); + }); + }, [ + channelsQuery.data, + identityQuery.data?.pubkey, + managedAgentsQuery.data, + relayAgentsQuery.data, + ]); + const activeCandidates = candidates.filter((candidate) => candidate.isActive); + const defaultAgent = activeCandidates[0] ?? null; + const selected = selectedAgentPubkey + ? (activeCandidates.find( + (candidate) => + normalizePubkey(candidate.pubkey) === + normalizePubkey(selectedAgentPubkey), + ) ?? defaultAgent) + : defaultAgent; + const isLoading = + identityQuery.isLoading || + managedAgentsQuery.isLoading || + relayAgentsQuery.isLoading || + channelsQuery.isLoading; + const isError = managedAgentsQuery.isError || relayAgentsQuery.isError; + return React.useMemo( + () => ({ + candidates, + isError, + isLoading, + select: setSelectedAgentPubkey, + selected, + }), + [candidates, isError, isLoading, selected], + ); +} + +export function ProjectReviewAgentSelectionHarness({ + selection, +}: { + selection: ProjectReviewAgentSelection; +}) { + return ( + + ); +} diff --git a/desktop/src/features/projects/ui/ProjectReviewChecks.tsx b/desktop/src/features/projects/ui/ProjectReviewChecks.tsx index ce14ef9f36..35d57976cc 100644 --- a/desktop/src/features/projects/ui/ProjectReviewChecks.tsx +++ b/desktop/src/features/projects/ui/ProjectReviewChecks.tsx @@ -9,15 +9,7 @@ import { import * as React from "react"; import { toast } from "sonner"; -import { - useManagedAgentsQuery, - useRelayAgentsQuery, - useStartManagedAgentMutation, -} from "@/features/agents/hooks"; -import { - getMentionableAgentPubkeys, - getSharedChannelIds, -} from "@/features/agents/lib/agentAutocompleteEligibility"; +import { useStartManagedAgentMutation } from "@/features/agents/hooks"; import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions"; import { useChannelsQuery, useOpenDmMutation } from "@/features/channels/hooks"; import { normalizeRelayUrl } from "@/features/communities/communityStorage"; @@ -45,10 +37,8 @@ import type { ProjectsConversationOpener } from "@/features/projects/lib/project import { pullRequestShareLink } from "@/features/projects/lib/projectShareLinks"; import type { ProjectPullRequest, Repository } from "@/features/projects/hooks"; import { ProjectReviewCheckResultCard } from "@/features/projects/ui/ProjectReviewCheckResultCard"; -import { - ProjectReviewDebugHarness, - type ReviewCheckAgent, -} from "@/features/projects/ui/ProjectReviewDebugHarness"; +import type { ProjectReviewAgentSelection } from "@/features/projects/ui/ProjectReviewAgentSelection"; +import type { ReviewAgent } from "@/features/projects/ui/ProjectReviewDebugHarness"; import { useIdentityQuery } from "@/shared/api/hooks"; import { listManagedAgents } from "@/shared/api/tauri"; import { revalidateRelayAgents } from "@/shared/api/tauriRelayAgents"; @@ -183,67 +173,6 @@ function normalizeStoredRuns(value: Record): ReviewCheckRuns { return runs; } -/** Local managed agents can run on this dev build; relay agents must pass the - * active identity's community-level respond-to policy. */ -function useReviewCheckAgents() { - const identityQuery = useIdentityQuery(); - const managedAgentsQuery = useManagedAgentsQuery(); - const relayAgentsQuery = useRelayAgentsQuery(); - const channelsQuery = useChannelsQuery(); - - const candidates = React.useMemo(() => { - const managedAgents = managedAgentsQuery.data ?? []; - const managedByPubkey = new Map( - managedAgents.map((agent) => [normalizePubkey(agent.pubkey), agent]), - ); - const relayAgents = relayAgentsQuery.data ?? []; - const sharedChannelIds = getSharedChannelIds(channelsQuery.data); - const allowedPubkeys = getMentionableAgentPubkeys({ - currentPubkey: identityQuery.data?.pubkey, - eligibilityScope: { type: "community" }, - managedAgentPubkeys: managedByPubkey.keys(), - relayAgents, - sharedChannelIds, - }); - const assigned: ReviewCheckAgent[] = managedAgents.map((agent) => ({ - pubkey: normalizePubkey(agent.pubkey), - name: agent.name, - isManaged: true, - isActive: isManagedAgentActive(agent), - })); - for (const relayAgent of relayAgents) { - const pubkey = normalizePubkey(relayAgent.pubkey); - if (managedByPubkey.has(pubkey) || !allowedPubkeys.has(pubkey)) continue; - assigned.push({ - pubkey, - name: relayAgent.name, - isManaged: false, - isActive: relayAgent.status !== "offline", - }); - } - return assigned.sort((left, right) => { - if (left.isActive !== right.isActive) return left.isActive ? -1 : 1; - if (left.isManaged !== right.isManaged) return left.isManaged ? -1 : 1; - return left.name.localeCompare(right.name); - }); - }, [ - channelsQuery.data, - identityQuery.data?.pubkey, - managedAgentsQuery.data, - relayAgentsQuery.data, - ]); - - return { - candidates, - isLoading: - identityQuery.isLoading || - managedAgentsQuery.isLoading || - relayAgentsQuery.isLoading || - channelsQuery.isLoading, - isError: managedAgentsQuery.isError || relayAgentsQuery.isError, - }; -} - function useProjectReviewCheckDefinitions() { // Future project-owned definitions can be loaded from the target branch here. // Keep the prototype synchronous and local so opening Checks never clones a repo. @@ -395,16 +324,17 @@ function CheckStatus({ export function ProjectReviewChecks({ project, pullRequest, + reviewAgents, }: { project: Repository; pullRequest: ProjectPullRequest; + reviewAgents: ProjectReviewAgentSelection; }) { const { activeCommunity } = useCommunities(); const identityQuery = useIdentityQuery(); const channelsQuery = useChannelsQuery(); const openDmMutation = useOpenDmMutation(); const startAgentMutation = useStartManagedAgentMutation(); - const reviewCheckAgents = useReviewCheckAgents(); const checkDefinitions = useProjectReviewCheckDefinitions(); const relayScope = activeCommunity?.relayUrl ? normalizeRelayUrl(activeCommunity.relayUrl) @@ -423,9 +353,6 @@ export function ProjectReviewChecks({ const [pendingCheckIds, setPendingCheckIds] = React.useState>( () => new Set(), ); - const [selectedAgentPubkey, setSelectedAgentPubkey] = React.useState< - string | null - >(null); const targetCommit = pullRequest.commit ?? pullRequest.initialCommit; const diffRepoUrl = React.useMemo( () => @@ -444,7 +371,6 @@ export function ProjectReviewChecks({ ); setRuns(normalizeStoredRuns(stored)); setPendingCheckIds(new Set()); - setSelectedAgentPubkey(null); }, [storageKey]); const updateRun = React.useCallback( @@ -516,7 +442,7 @@ export function ProjectReviewChecks({ const runCheck = React.useCallback( async ( check: ProjectReviewCheckDefinition, - agent: ReviewCheckAgent, + agent: ReviewAgent, supersededRun?: ReviewCheckRun, ) => { const expectedStorageKey = storageKey; @@ -679,17 +605,7 @@ export function ProjectReviewChecks({ ), [channelsQuery.data], ); - const defaultAgent = - reviewCheckAgents.candidates.find((candidate) => candidate.isActive) ?? - null; - const selectedAgent = selectedAgentPubkey - ? (reviewCheckAgents.candidates.find( - (candidate) => - candidate.isActive && - normalizePubkey(candidate.pubkey) === - normalizePubkey(selectedAgentPubkey), - ) ?? defaultAgent) - : defaultAgent; + const selectedAgent = reviewAgents.selected; return (
@@ -705,7 +621,7 @@ export function ProjectReviewChecks({ }; const runningAgent = run.status === "running" && run.agentPubkey - ? (reviewCheckAgents.candidates.find( + ? (reviewAgents.candidates.find( (candidate) => normalizePubkey(candidate.pubkey) === normalizePubkey(run.agentPubkey ?? ""), @@ -815,21 +731,13 @@ export function ProjectReviewChecks({ ); })}
- {!reviewCheckAgents.isLoading && - reviewCheckAgents.candidates.length === 0 ? ( + {!reviewAgents.isLoading && reviewAgents.candidates.length === 0 ? (
- {reviewCheckAgents.isError + {reviewAgents.isError ? "Local managed agents or the relay agent directory could not be loaded." : "No local managed or authorized relay agents are available."}
) : null} -
); } diff --git a/desktop/src/features/projects/ui/ProjectReviewDebugHarness.tsx b/desktop/src/features/projects/ui/ProjectReviewDebugHarness.tsx index b7d0326c3d..4ab9df8cd9 100644 --- a/desktop/src/features/projects/ui/ProjectReviewDebugHarness.tsx +++ b/desktop/src/features/projects/ui/ProjectReviewDebugHarness.tsx @@ -3,14 +3,14 @@ import { Bug } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; -export type ReviewCheckAgent = { +export type ReviewAgent = { pubkey: string; name: string; isManaged: boolean; isActive: boolean; }; -function agentAvailabilityLabel(agent: ReviewCheckAgent) { +function agentAvailabilityLabel(agent: ReviewAgent) { return agent.isManaged ? "Running here" : "Online on relay"; } @@ -21,11 +21,11 @@ export function ProjectReviewDebugHarness({ onSelect, selected, }: { - candidates: ReviewCheckAgent[]; + candidates: ReviewAgent[]; hasError: boolean; isLoading: boolean; onSelect: (pubkey: string) => void; - selected: ReviewCheckAgent | null; + selected: ReviewAgent | null; }) { const activeCandidates = candidates.filter((candidate) => candidate.isActive); const offlineAgentCount = candidates.length - activeCandidates.length; @@ -35,10 +35,10 @@ export function ProjectReviewDebugHarness({ - {run.error ? (

{run.error}

) : null} diff --git a/desktop/tests/e2e/project-pr-memory.spec.ts b/desktop/tests/e2e/project-pr-memory.spec.ts index 460ac79fb4..e3445737b6 100644 --- a/desktop/tests/e2e/project-pr-memory.spec.ts +++ b/desktop/tests/e2e/project-pr-memory.spec.ts @@ -97,8 +97,12 @@ test("review context is discovered by an agent and restored as sourced evidence" await expect( memory.getByRole("button", { name: "Record outcome" }), ).toHaveCount(0); - await memory - .getByRole("combobox", { name: "Context agent" }) + await expect( + memory.getByRole("combobox", { name: "Context agent" }), + ).toHaveCount(0); + await page.getByTestId("project-review-debug-harness-trigger").click(); + await page + .getByTestId("project-review-debug-agent-select") .selectOption(RELAY_REVIEW_AGENT_PUBKEY); await memory .getByRole("button", { name: "Find context", exact: true }) diff --git a/desktop/tests/e2e/project-pr-review.spec.ts b/desktop/tests/e2e/project-pr-review.spec.ts index c6e94c8191..3cc05b9e29 100644 --- a/desktop/tests/e2e/project-pr-review.spec.ts +++ b/desktop/tests/e2e/project-pr-review.spec.ts @@ -374,7 +374,7 @@ test("review checks dispatch to a running local managed agent", async ({ const debugHarness = page.getByTestId("project-review-debug-harness-trigger"); await expect(debugHarness).toHaveCount(1); await expect(debugHarness).toHaveAccessibleName( - "Review check debug harness, agent Roof", + "Review debug harness, agent Roof", ); await debugHarness.click(); await expect(