mirror of
https://github.com/block/buzz.git
synced 2026-11-09 02:41:57 +00:00
Move review agent selection to debug harness
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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({
|
||||
</div>
|
||||
</ProjectDetailSection>
|
||||
<ProjectDetailSection defaultOpen={false} title="Checks">
|
||||
<ProjectReviewChecks project={project} pullRequest={pullRequest} />
|
||||
<ProjectReviewChecks
|
||||
project={project}
|
||||
pullRequest={pullRequest}
|
||||
reviewAgents={reviewAgents}
|
||||
/>
|
||||
</ProjectDetailSection>
|
||||
<ProjectDetailSection
|
||||
defaultOpen={false}
|
||||
title="Prior art and future vision"
|
||||
>
|
||||
<ProjectReviewMemory project={project} pullRequest={pullRequest} />
|
||||
<ProjectReviewMemory
|
||||
project={project}
|
||||
pullRequest={pullRequest}
|
||||
reviewAgents={reviewAgents}
|
||||
/>
|
||||
</ProjectDetailSection>
|
||||
<ProjectReviewAgentSelectionHarness selection={reviewAgents} />
|
||||
<ProjectDetailSection defaultOpen title="Activity">
|
||||
<div className="space-y-3">
|
||||
<DiscussedInChannels
|
||||
|
||||
118
desktop/src/features/projects/ui/ProjectReviewAgentSelection.tsx
Normal file
118
desktop/src/features/projects/ui/ProjectReviewAgentSelection.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
useManagedAgentsQuery,
|
||||
useRelayAgentsQuery,
|
||||
} from "@/features/agents/hooks";
|
||||
import {
|
||||
getMentionableAgentPubkeys,
|
||||
getSharedChannelIds,
|
||||
} from "@/features/agents/lib/agentAutocompleteEligibility";
|
||||
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
|
||||
import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import {
|
||||
ProjectReviewDebugHarness,
|
||||
type ReviewAgent,
|
||||
} from "@/features/projects/ui/ProjectReviewDebugHarness";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
|
||||
export type ProjectReviewAgentSelection = {
|
||||
candidates: ReviewAgent[];
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
select: (pubkey: string) => 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 (
|
||||
<ProjectReviewDebugHarness
|
||||
candidates={selection.candidates}
|
||||
hasError={selection.isError}
|
||||
isLoading={selection.isLoading}
|
||||
onSelect={selection.select}
|
||||
selected={selection.selected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<string, unknown>): 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<Set<string>>(
|
||||
() => 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 (
|
||||
<div className="-mx-6" data-testid="project-review-checks">
|
||||
@@ -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({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!reviewCheckAgents.isLoading &&
|
||||
reviewCheckAgents.candidates.length === 0 ? (
|
||||
{!reviewAgents.isLoading && reviewAgents.candidates.length === 0 ? (
|
||||
<div className="border-t border-border/55 px-6 py-3 text-xs text-muted-foreground">
|
||||
{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."}
|
||||
</div>
|
||||
) : null}
|
||||
<ProjectReviewDebugHarness
|
||||
candidates={reviewCheckAgents.candidates}
|
||||
hasError={reviewCheckAgents.isError}
|
||||
isLoading={reviewCheckAgents.isLoading}
|
||||
onSelect={setSelectedAgentPubkey}
|
||||
selected={selectedAgent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
aria-label={`Review check debug harness${selected ? `, agent ${selected.name}` : ""}`}
|
||||
aria-label={`Review debug harness${selected ? `, agent ${selected.name}` : ""}`}
|
||||
className="h-12 w-12 rounded-full p-0 shadow-lg ring-1 ring-border/60"
|
||||
data-testid="project-review-debug-harness-trigger"
|
||||
title="Review check debug harness"
|
||||
title="Review debug harness"
|
||||
type="button"
|
||||
>
|
||||
<Bug className="h-5 w-5" />
|
||||
@@ -54,7 +54,7 @@ export function ProjectReviewDebugHarness({
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-semibold">Review debug harness</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose the agent used for new checks.
|
||||
Choose the agent used for checks and context discovery.
|
||||
</p>
|
||||
</div>
|
||||
<label className="block space-y-1.5 text-xs font-medium">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Bot,
|
||||
History,
|
||||
LoaderCircle,
|
||||
Play,
|
||||
@@ -10,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";
|
||||
@@ -43,7 +34,8 @@ import {
|
||||
} from "@/features/projects/lib/projectReviewMemory";
|
||||
import { pullRequestShareLink } from "@/features/projects/lib/projectShareLinks";
|
||||
import type { ProjectPullRequest, Repository } from "@/features/projects/hooks";
|
||||
import 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";
|
||||
@@ -116,64 +108,6 @@ function normalizeStoredRun(
|
||||
};
|
||||
}
|
||||
|
||||
function useReviewContextAgents() {
|
||||
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 allowedPubkeys = getMentionableAgentPubkeys({
|
||||
currentPubkey: identityQuery.data?.pubkey,
|
||||
eligibilityScope: { type: "community" },
|
||||
managedAgentPubkeys: managedByPubkey.keys(),
|
||||
relayAgents,
|
||||
sharedChannelIds: getSharedChannelIds(channelsQuery.data),
|
||||
});
|
||||
const available: 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;
|
||||
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,
|
||||
]);
|
||||
|
||||
return {
|
||||
candidates,
|
||||
isLoading:
|
||||
identityQuery.isLoading ||
|
||||
managedAgentsQuery.isLoading ||
|
||||
relayAgentsQuery.isLoading ||
|
||||
channelsQuery.isLoading,
|
||||
isError: managedAgentsQuery.isError || relayAgentsQuery.isError,
|
||||
};
|
||||
}
|
||||
|
||||
function isAgentMessage(event: RelayEvent, agentPubkey: string) {
|
||||
return (
|
||||
(event.kind === KIND_STREAM_MESSAGE ||
|
||||
@@ -387,16 +321,17 @@ function ReviewContextResultCard({
|
||||
export function ProjectReviewMemory({
|
||||
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 reviewContextAgents = useReviewContextAgents();
|
||||
const relayScope = activeCommunity?.relayUrl
|
||||
? normalizeRelayUrl(activeCommunity.relayUrl)
|
||||
: null;
|
||||
@@ -415,9 +350,6 @@ export function ProjectReviewMemory({
|
||||
status: "idle",
|
||||
});
|
||||
const [isPending, setIsPending] = React.useState(false);
|
||||
const [selectedAgentPubkey, setSelectedAgentPubkey] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const targetCommit = pullRequest.commit ?? pullRequest.initialCommit;
|
||||
const repoUrl = React.useMemo(
|
||||
() =>
|
||||
@@ -438,7 +370,6 @@ export function ProjectReviewMemory({
|
||||
);
|
||||
setRun(stored ?? { agentPubkey: null, status: "idle" });
|
||||
setIsPending(false);
|
||||
setSelectedAgentPubkey(null);
|
||||
}, [storageKey]);
|
||||
|
||||
const updateRun = React.useCallback(
|
||||
@@ -477,7 +408,7 @@ export function ProjectReviewMemory({
|
||||
);
|
||||
|
||||
const runDiscovery = React.useCallback(
|
||||
async (agent: ReviewCheckAgent, supersededRun?: ReviewContextRun) => {
|
||||
async (agent: ReviewAgent, supersededRun?: ReviewContextRun) => {
|
||||
const expectedStorageKey = storageKey;
|
||||
if (!expectedStorageKey || !relayScope || !signerScope) return;
|
||||
const requestId = crypto.randomUUID();
|
||||
@@ -623,20 +554,10 @@ export function ProjectReviewMemory({
|
||||
),
|
||||
[channelsQuery.data],
|
||||
);
|
||||
const activeCandidates = reviewContextAgents.candidates.filter(
|
||||
(candidate) => candidate.isActive,
|
||||
);
|
||||
const defaultAgent = activeCandidates[0] ?? null;
|
||||
const selectedAgent = selectedAgentPubkey
|
||||
? (activeCandidates.find(
|
||||
(candidate) =>
|
||||
normalizePubkey(candidate.pubkey) ===
|
||||
normalizePubkey(selectedAgentPubkey),
|
||||
) ?? defaultAgent)
|
||||
: defaultAgent;
|
||||
const selectedAgent = reviewAgents.selected;
|
||||
const runningAgent =
|
||||
run.status === "running" && run.agentPubkey
|
||||
? (reviewContextAgents.candidates.find(
|
||||
? (reviewAgents.candidates.find(
|
||||
(candidate) =>
|
||||
normalizePubkey(candidate.pubkey) ===
|
||||
normalizePubkey(run.agentPubkey ?? ""),
|
||||
@@ -704,33 +625,6 @@ export function ProjectReviewMemory({
|
||||
{hasRun ? null : "Find context"}
|
||||
</Button>
|
||||
</div>
|
||||
<label className="mt-3 flex max-w-sm items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
<Bot className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="shrink-0">Agent</span>
|
||||
<select
|
||||
aria-label="Context agent"
|
||||
className="h-8 min-w-0 flex-1 rounded-lg border border-input bg-background px-2.5 text-xs font-normal text-foreground outline-hidden focus:ring-1 focus:ring-ring disabled:opacity-50"
|
||||
disabled={activeCandidates.length === 0 || run.status === "running"}
|
||||
onChange={(event) => setSelectedAgentPubkey(event.target.value)}
|
||||
value={selectedAgent?.pubkey ?? ""}
|
||||
>
|
||||
{activeCandidates.length === 0 ? (
|
||||
<option value="">
|
||||
{reviewContextAgents.isLoading
|
||||
? "Loading available agents…"
|
||||
: reviewContextAgents.isError
|
||||
? "Agents could not be loaded"
|
||||
: "No running agents are available"}
|
||||
</option>
|
||||
) : null}
|
||||
{activeCandidates.map((candidate) => (
|
||||
<option key={candidate.pubkey} value={candidate.pubkey}>
|
||||
{candidate.name}
|
||||
{candidate.isManaged ? " · Running here" : " · Online"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{run.error ? (
|
||||
<p className="mt-2 text-xs text-destructive">{run.error}</p>
|
||||
) : null}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user