mirror of
https://github.com/block/buzz.git
synced 2026-11-04 16:31:38 +00:00
feat(desktop): add review check debug harness
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Bot,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
CircleAlert,
|
||||
LoaderCircle,
|
||||
Play,
|
||||
@@ -49,6 +48,10 @@ 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 { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { listManagedAgents } from "@/shared/api/tauri";
|
||||
import { revalidateRelayAgents } from "@/shared/api/tauriRelayAgents";
|
||||
@@ -63,13 +66,6 @@ import {
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
|
||||
type ReviewCheckStatus = "idle" | "running" | "completed" | "failed";
|
||||
|
||||
@@ -89,13 +85,6 @@ type ReviewCheckRun = {
|
||||
|
||||
type ReviewCheckRuns = Record<string, ReviewCheckRun>;
|
||||
|
||||
type ReviewCheckAgent = {
|
||||
pubkey: string;
|
||||
name: string;
|
||||
isManaged: boolean;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
function isReviewCheckStatus(value: unknown): value is ReviewCheckStatus {
|
||||
return (
|
||||
value === "idle" ||
|
||||
@@ -457,66 +446,6 @@ function CheckStatus({
|
||||
return null;
|
||||
}
|
||||
|
||||
function AgentPicker({
|
||||
candidates,
|
||||
disabled,
|
||||
onSelect,
|
||||
selected,
|
||||
}: {
|
||||
candidates: ReviewCheckAgent[];
|
||||
disabled: boolean;
|
||||
onSelect: (pubkey: string) => void;
|
||||
selected: ReviewCheckAgent | null;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="min-w-44 justify-between"
|
||||
data-testid="project-review-check-agent-picker"
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
<span className="truncate">
|
||||
{selected?.name ?? "Assign an agent"}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-64">
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={onSelect}
|
||||
value={selected?.pubkey ?? ""}
|
||||
>
|
||||
{candidates.map((candidate) => (
|
||||
<DropdownMenuRadioItem
|
||||
className="flex items-center justify-between gap-3"
|
||||
key={candidate.pubkey}
|
||||
value={candidate.pubkey}
|
||||
>
|
||||
<span className="truncate">{candidate.name}</span>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">
|
||||
{candidate.isManaged
|
||||
? candidate.isActive
|
||||
? "Running here"
|
||||
: "Can start here"
|
||||
: candidate.isActive
|
||||
? "Online on relay"
|
||||
: "Offline"}
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectReviewChecks({
|
||||
project,
|
||||
pullRequest,
|
||||
@@ -548,6 +477,9 @@ 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(
|
||||
() =>
|
||||
@@ -566,6 +498,7 @@ export function ProjectReviewChecks({
|
||||
);
|
||||
setRuns(normalizeStoredRuns(stored));
|
||||
setPendingCheckIds(new Set());
|
||||
setSelectedAgentPubkey(null);
|
||||
}, [storageKey]);
|
||||
|
||||
const updateRun = React.useCallback(
|
||||
@@ -593,17 +526,6 @@ export function ProjectReviewChecks({
|
||||
[],
|
||||
);
|
||||
|
||||
const assignAgent = React.useCallback(
|
||||
(checkId: string, pubkey: string) => {
|
||||
if (!storageKey) return;
|
||||
updateRun(storageKey, checkId, () => ({
|
||||
agentPubkey: normalizePubkey(pubkey),
|
||||
status: "idle",
|
||||
}));
|
||||
},
|
||||
[storageKey, updateRun],
|
||||
);
|
||||
|
||||
const recordResult = React.useCallback(
|
||||
(
|
||||
checkId: string,
|
||||
@@ -806,13 +728,19 @@ export function ProjectReviewChecks({
|
||||
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;
|
||||
|
||||
return (
|
||||
<div className="-mx-6" data-testid="project-review-checks">
|
||||
<div className="border-b border-border/55 px-6 pb-3 text-xs text-muted-foreground">
|
||||
The first running agent is selected automatically; you can choose a
|
||||
different local managed agent or relay agent authorized for your
|
||||
signed-in identity. Definitions come from the target branch
|
||||
Definitions come from the target branch
|
||||
{checkDefinitions.source === "project"
|
||||
? ` (${PROJECT_REVIEW_CHECKS_CONFIG_PATH})`
|
||||
: checkDefinitions.source === "starter"
|
||||
@@ -826,13 +754,13 @@ export function ProjectReviewChecks({
|
||||
agentPubkey: null,
|
||||
status: "idle" as const,
|
||||
};
|
||||
const selectedAgent = run.agentPubkey
|
||||
const runAgent = run.agentPubkey
|
||||
? (reviewCheckAgents.candidates.find(
|
||||
(candidate) =>
|
||||
normalizePubkey(candidate.pubkey) ===
|
||||
normalizePubkey(run.agentPubkey ?? ""),
|
||||
) ?? null)
|
||||
: defaultAgent;
|
||||
: null;
|
||||
const isPending = pendingCheckIds.has(check.id);
|
||||
const isStale =
|
||||
run.status === "completed" && run.targetCommit !== targetCommit;
|
||||
@@ -870,12 +798,6 @@ export function ProjectReviewChecks({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<AgentPicker
|
||||
candidates={reviewCheckAgents.candidates}
|
||||
disabled={isPending || run.status === "running"}
|
||||
onSelect={(pubkey) => assignAgent(check.id, pubkey)}
|
||||
selected={selectedAgent}
|
||||
/>
|
||||
<Button
|
||||
disabled={
|
||||
!selectedAgent ||
|
||||
@@ -905,8 +827,8 @@ export function ProjectReviewChecks({
|
||||
</div>
|
||||
{run.status === "running" ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Waiting for {selectedAgent?.name ?? "the assigned agent"} to
|
||||
return a structured conclusion…
|
||||
Waiting for {runAgent?.name ?? "the assigned agent"} to return
|
||||
a structured conclusion…
|
||||
</p>
|
||||
) : null}
|
||||
{run.error ? (
|
||||
@@ -960,6 +882,13 @@ export function ProjectReviewChecks({
|
||||
: "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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Bug } from "lucide-react";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
|
||||
export type ReviewCheckAgent = {
|
||||
pubkey: string;
|
||||
name: string;
|
||||
isManaged: boolean;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
function agentAvailabilityLabel(agent: ReviewCheckAgent) {
|
||||
return agent.isManaged ? "Running here" : "Online on relay";
|
||||
}
|
||||
|
||||
export function ProjectReviewDebugHarness({
|
||||
candidates,
|
||||
hasError,
|
||||
isLoading,
|
||||
onSelect,
|
||||
selected,
|
||||
}: {
|
||||
candidates: ReviewCheckAgent[];
|
||||
hasError: boolean;
|
||||
isLoading: boolean;
|
||||
onSelect: (pubkey: string) => void;
|
||||
selected: ReviewCheckAgent | null;
|
||||
}) {
|
||||
const activeCandidates = candidates.filter((candidate) => candidate.isActive);
|
||||
const offlineAgentCount = candidates.length - activeCandidates.length;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-5 right-5 z-40">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
aria-label={`Review check 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"
|
||||
type="button"
|
||||
>
|
||||
<Bug className="h-5 w-5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="w-72 space-y-3"
|
||||
data-testid="project-review-debug-harness-menu"
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<label className="block space-y-1.5 text-xs font-medium">
|
||||
<span>Agent</span>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-input bg-background px-3 text-sm font-normal outline-hidden focus:ring-1 focus:ring-ring disabled:opacity-50"
|
||||
data-testid="project-review-debug-agent-select"
|
||||
disabled={activeCandidates.length === 0}
|
||||
onChange={(event) => onSelect(event.target.value)}
|
||||
value={selected?.pubkey ?? ""}
|
||||
>
|
||||
{activeCandidates.length === 0 ? (
|
||||
<option value="">
|
||||
{isLoading
|
||||
? "Loading available agents…"
|
||||
: hasError
|
||||
? "Agents could not be loaded"
|
||||
: "No running agents are available"}
|
||||
</option>
|
||||
) : null}
|
||||
{activeCandidates.map((candidate) => (
|
||||
<option key={candidate.pubkey} value={candidate.pubkey}>
|
||||
{candidate.name} · {agentAvailabilityLabel(candidate)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{offlineAgentCount > 0 ? (
|
||||
<span
|
||||
className="block font-normal text-muted-foreground"
|
||||
data-testid="project-review-debug-offline-agent-count"
|
||||
>
|
||||
{offlineAgentCount} offline{" "}
|
||||
{offlineAgentCount === 1 ? "agent" : "agents"}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -169,8 +169,10 @@ test("review checks dispatch to an agent authorized for the relay identity", asy
|
||||
await expect(checks).toBeVisible();
|
||||
await expect(checks.getByTestId("project-review-check")).toHaveCount(3);
|
||||
const interfaceCheck = checks.getByTestId("project-review-check").first();
|
||||
await interfaceCheck.getByTestId("project-review-check-agent-picker").click();
|
||||
await page.getByRole("menuitemradio", { name: /charlie/i }).click();
|
||||
await page.getByTestId("project-review-debug-harness-trigger").click();
|
||||
await page
|
||||
.getByTestId("project-review-debug-agent-select")
|
||||
.selectOption(RELAY_REVIEW_AGENT_PUBKEY);
|
||||
await interfaceCheck
|
||||
.getByRole("button", { name: "Run", exact: true })
|
||||
.click();
|
||||
@@ -350,6 +352,13 @@ test("review checks dispatch to a running local managed agent", async ({
|
||||
respondTo: "allowlist",
|
||||
respondToAllowlist: [],
|
||||
},
|
||||
...Array.from({ length: 16 }, (_, index) => ({
|
||||
pubkey: (index + 1).toString(16).padStart(64, "0"),
|
||||
name: `Stopped prototype ${index + 1}`,
|
||||
status: "stopped" as const,
|
||||
respondTo: "allowlist" as const,
|
||||
respondToAllowlist: [],
|
||||
})),
|
||||
],
|
||||
});
|
||||
await openBuzzProject(page);
|
||||
@@ -362,12 +371,28 @@ test("review checks dispatch to a running local managed agent", async ({
|
||||
|
||||
const checks = page.getByTestId("project-review-checks");
|
||||
const interfaceCheck = checks.getByTestId("project-review-check").first();
|
||||
const debugHarness = page.getByTestId("project-review-debug-harness-trigger");
|
||||
await expect(debugHarness).toHaveCount(1);
|
||||
await expect(debugHarness).toHaveAccessibleName(
|
||||
"Review check debug harness, agent Roof",
|
||||
);
|
||||
await debugHarness.click();
|
||||
await expect(
|
||||
checks.getByTestId("project-review-check-agent-picker"),
|
||||
).toHaveCount(3);
|
||||
page.getByTestId("project-review-debug-agent-select"),
|
||||
).toHaveValue(LOCAL_REVIEW_AGENT_PUBKEY);
|
||||
await expect(
|
||||
interfaceCheck.getByTestId("project-review-check-agent-picker"),
|
||||
).toContainText("Roof");
|
||||
page
|
||||
.getByTestId("project-review-debug-agent-select")
|
||||
.locator("option", { hasText: "Stopped prototype" }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("project-review-debug-offline-agent-count"),
|
||||
).toHaveText("17 offline agents");
|
||||
expect(
|
||||
(await page.getByTestId("project-review-debug-harness-menu").boundingBox())
|
||||
?.height,
|
||||
).toBeLessThan(220);
|
||||
await page.keyboard.press("Escape");
|
||||
const revalidationCountBefore = await page.evaluate(
|
||||
() =>
|
||||
window.__BUZZ_E2E_COMMANDS__?.filter(
|
||||
|
||||
Reference in New Issue
Block a user