Redesign project review proposals

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-27 20:38:23 -07:00
parent e0e3327ead
commit fe2082543c
9 changed files with 1049 additions and 213 deletions

View File

@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import test from "node:test";
import { getShortestUniquePathLabels } from "./parseDiff.ts";
test("uses filenames when diff paths have distinct basenames", () => {
assert.deepEqual(
getShortestUniquePathLabels([
"desktop/src/features/channels/ui/ChannelPane.tsx",
"desktop/src/features/messages/ui/MessageTimeline.tsx",
]),
["ChannelPane.tsx", "MessageTimeline.tsx"],
);
});
test("adds only enough parent directories to disambiguate duplicate names", () => {
assert.deepEqual(
getShortestUniquePathLabels([
"desktop/src/features/channels/ui/index.ts",
"desktop/src/features/messages/ui/index.ts",
"desktop/src/features/messages/lib/parseDiff.ts",
]),
["channels/ui/index.ts", "messages/ui/index.ts", "parseDiff.ts"],
);
});
test("expands the longer path when one path is another path's suffix", () => {
assert.deepEqual(
getShortestUniquePathLabels(["src/shared/index.ts", "shared/index.ts"]),
["src/shared/index.ts", "shared/index.ts"],
);
});

View File

@@ -55,6 +55,49 @@ export function getDiffFileLabel(
return newPath || oldPath || fallbackFilePath || "diff";
}
/** Return the resulting file path, preferring the new side of a rename. */
export function getDiffFilePath(
file: FileData,
fallbackFilePath?: string,
): string {
const oldPath = file.oldPath === "/dev/null" ? undefined : file.oldPath;
const newPath = file.newPath === "/dev/null" ? undefined : file.newPath;
return newPath || oldPath || fallbackFilePath || "diff";
}
/** Reduce paths to the shortest unique suffix, usually just the filename. */
export function getShortestUniquePathLabels(paths: string[]): string[] {
const pathParts = paths.map((path) => {
const parts = path.split(/[\\/]/).filter(Boolean);
return parts.length > 0 ? parts : [path];
});
const depths = pathParts.map(() => 1);
while (true) {
const labels = pathParts.map((parts, index) =>
parts.slice(-depths[index]).join("/"),
);
const collisions = new Set<string>();
const counts = new Map<string, number>();
for (const label of labels) {
counts.set(label, (counts.get(label) ?? 0) + 1);
}
for (const [label, count] of counts) {
if (count > 1) collisions.add(label);
}
if (collisions.size === 0) return labels;
let expanded = false;
for (const [index, label] of labels.entries()) {
if (collisions.has(label) && depths[index] < pathParts[index].length) {
depths[index] += 1;
expanded = true;
}
}
if (!expanded) return labels;
}
}
export function shouldShowDiffFileHeader(
label: string,
fileCount: number,

View File

@@ -21,6 +21,8 @@ type DiffViewerProps = {
viewType?: ViewType;
className?: string;
searchQuery?: string;
fileIndex?: number;
hideFileHeader?: boolean;
};
function FileChangeBadge({
@@ -50,11 +52,15 @@ export function DiffViewer({
viewType = "unified",
className,
searchQuery,
fileIndex,
hideFileHeader = false,
}: DiffViewerProps) {
const { files, parseError } = useMemo(
() => parseUnifiedDiff(content),
[content],
);
const visibleFiles =
fileIndex === undefined ? files : files.slice(fileIndex, fileIndex + 1);
const searchPreview = searchQuery
? buildSearchResultPreview(content, searchQuery, 160)
: null;
@@ -89,15 +95,13 @@ export function DiffViewer({
</pre>
) : null}
<div className="space-y-3">
{files.map((file) => {
{visibleFiles.map((file) => {
const label = getDiffFileLabel(file, fallbackFilePath);
const { additions, deletions } = countDiffFileChanges(file);
const diffType = normalizeDiffType(file.type);
const showFileHeader = shouldShowDiffFileHeader(
label,
files.length,
fallbackFilePath,
);
const showFileHeader =
!hideFileHeader &&
shouldShowDiffFileHeader(label, files.length, fallbackFilePath);
const fileKey = [
file.oldPath || "",
file.newPath || "",

View File

@@ -14,9 +14,17 @@ export type ProjectReviewCheckFinding = {
line: number | null;
};
export type ProjectReviewCheckProposalReference = {
title: string;
summary: string;
diffEventId: string;
};
export type ProjectReviewCheckResult = {
requestId?: string;
/** Legacy single-proposal reference. New results use `proposals`. */
diffEventId?: string;
proposals?: ProjectReviewCheckProposalReference[];
conclusion: ProjectReviewCheckConclusion;
summary: string;
findings: ProjectReviewCheckFinding[];
@@ -32,6 +40,12 @@ export type ProjectReviewCheckDiffProposal = {
truncated: boolean;
};
export type ProjectReviewCheckResolvedProposal =
ProjectReviewCheckDiffProposal & {
title: string;
summary: string;
};
export const PROJECT_REVIEW_CHECK_RESULT_MARKER: string;
export const DEFAULT_PROJECT_REVIEW_CHECKS: ProjectReviewCheckDefinition[];

View File

@@ -75,6 +75,22 @@ function normalizedText(value, maxLength) {
: null;
}
function normalizedDecisionCopy(value) {
const text = normalizedText(value, 1_000)?.replace(/\s+/g, " ");
if (!text) return null;
const sentenceBounded = text
.split(/(?<=[.!?])\s+/)
.slice(0, 2)
.join(" ");
if (sentenceBounded.length <= 240) return sentenceBounded;
const visible = sentenceBounded.slice(0, 239);
const wordBoundary = visible.lastIndexOf(" ");
const cutoff = wordBoundary >= 160 ? wordBoundary : visible.length;
return `${visible.slice(0, cutoff).trimEnd()}…`;
}
function normalizedConclusion(value) {
if (typeof value !== "string") return null;
const normalized = value.trim().toLowerCase();
@@ -117,6 +133,18 @@ function normalizedFinding(value) {
};
}
function normalizedProposalReference(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const title = normalizedText(value.title, 160);
const summary = normalizedDecisionCopy(value.summary);
const diffEventId = normalizedEventId(
value.diff_event_id ?? value.diffEventId,
);
return title && summary && diffEventId
? { title, summary, diffEventId }
: null;
}
function jsonObjectsAfterMarker(content) {
const markerIndex = content.lastIndexOf(PROJECT_REVIEW_CHECK_RESULT_MARKER);
if (markerIndex < 0) return [];
@@ -147,7 +175,7 @@ export function parseProjectReviewCheckResult(content) {
continue;
}
const conclusion = normalizedConclusion(parsed.conclusion);
const summary = normalizedText(parsed.summary, 4_000);
const summary = normalizedDecisionCopy(parsed.summary);
if (!conclusion || !summary) continue;
const requestId = normalizedText(
parsed.request_id ?? parsed.requestId,
@@ -155,11 +183,23 @@ export function parseProjectReviewCheckResult(content) {
);
const rawDiffEventId = parsed.diff_event_id ?? parsed.diffEventId;
const diffEventId = normalizedEventId(rawDiffEventId);
const rawProposals = parsed.proposals;
if (rawProposals !== undefined && !Array.isArray(rawProposals)) {
continue;
}
const proposalCandidates = Array.isArray(rawProposals)
? rawProposals.slice(0, 4).map(normalizedProposalReference)
: [];
const proposals = proposalCandidates.filter(Boolean);
if (
(rawDiffEventId !== undefined &&
rawDiffEventId !== null &&
!diffEventId) ||
(conclusion === "approved" && diffEventId)
proposals.length !== proposalCandidates.length ||
new Set(proposals.map((proposal) => proposal.diffEventId)).size !==
proposals.length ||
(diffEventId && proposals.length > 0) ||
(conclusion === "approved" && (diffEventId || proposals.length > 0))
) {
continue;
}
@@ -171,6 +211,9 @@ export function parseProjectReviewCheckResult(content) {
...(conclusion === "fix-recommended" && diffEventId
? { diffEventId }
: {}),
...(conclusion === "fix-recommended" && proposals.length > 0
? { proposals }
: {}),
conclusion,
summary,
findings,
@@ -306,8 +349,19 @@ export function buildProjectReviewCheckPrompt({
JSON.stringify({
request_id: requestId,
conclusion: "fix-recommended",
summary: "short explanation",
diff_event_id: null,
summary: "one short sentence framing the decision",
proposals: [
{
title: "first viable direction",
summary: "one plain-language sentence about the outcome",
diff_event_id: null,
},
{
title: "countering viable direction",
summary: "one plain-language sentence about the tradeoff",
diff_event_id: null,
},
],
findings: [
{
title: "concise actionable fix",
@@ -321,15 +375,19 @@ export function buildProjectReviewCheckPrompt({
"Return an empty findings array when approved. For every recommended fix, return one finding object.",
"Use null for file or line when the finding is not tied to a precise source location.",
"Use approved only when no material fix is recommended. Use fix-recommended otherwise.",
"Treat a fix-recommended result as a decision to negotiate, not a verdict. Frame the concern neutrally and look for countering viewpoints that could each be a defensible implementation.",
"When code can safely express alternatives, return two genuinely distinct proposal directions. Do not manufacture a second option when only one is safe.",
"DECISION COPY CONTRACT: The top-level summary and each proposal summary MUST be no more than two short sentences and 240 characters total; prefer one sentence. Proposal titles must be action-oriented and no more than eight words. State only the user-visible outcome or essential tradeoff in plain language. Put implementation details, identifiers, evidence, and reasoning in the finding or diff, never in decision copy.",
"A human may reply in this request's thread with their reasoning. Treat that as a continuation of this review negotiation: reconsider the concern, inspect any newly suggested direction, and publish a revised structured result in the same thread with the same request_id. Create new diff events for revised code proposals; never reuse an earlier diff event id.",
...(repoUrl && commit
? [
"When recommending a fix and you can express it safely as code, create one valid unified diff against the exact commit without modifying or committing the review branch.",
`Pipe that patch into this command before the final result to publish one native Buzz diff message (kind 40008) in this request's thread: buzz messages send-diff --channel ${JSON.stringify(channelId)} --diff - --repo ${JSON.stringify(repoUrl)} --commit ${JSON.stringify(commit)} --description ${JSON.stringify(`Review check ${requestId}`)} --reply-to <THIS_REQUEST_EVENT_ID>`,
"Only after send-diff succeeds, copy its exact 64-character event_id into diff_event_id in the final JSON. Never invent or reuse an event id. Do not paste the patch into the JSON.",
"Use diff_event_id: null when there is no safe code patch. Approved results must use diff_event_id: null.",
"For each proposed direction you can express safely as code, create a valid unified diff against the exact commit without modifying or committing the review branch.",
`Pipe each patch into this command before the final result to publish a native Buzz diff message (kind 40008) in this request's thread: buzz messages send-diff --channel ${JSON.stringify(channelId)} --diff - --repo ${JSON.stringify(repoUrl)} --commit ${JSON.stringify(commit)} --description <PROPOSAL_TITLE> --reply-to <THIS_REQUEST_EVENT_ID>`,
"Only after each send-diff succeeds, copy its exact 64-character event_id into that proposal's diff_event_id in the final JSON. Never invent or reuse an event id. Do not paste patches into the JSON.",
"Omit any proposal that has no safe code patch. Return proposals: [] when no direction can be expressed safely as code. Approved results must return proposals: [].",
]
: [
"Use diff_event_id: null because this review does not expose the repository URL and exact commit required for a native diff proposal.",
"Return proposals: [] because this review does not expose the repository URL and exact commit required for native diff proposals.",
]),
].join("\n");
}

View File

@@ -150,6 +150,94 @@ test("parses structured findings with bounded source locations", () => {
});
});
test("parses competing proposal directions with exact diff references", () => {
const firstDiffEventId = "d".repeat(64);
const secondDiffEventId = "e".repeat(64);
const result = parseProjectReviewCheckResult(
`${PROJECT_REVIEW_CHECK_RESULT_MARKER}\n${JSON.stringify({
request_id: "request-123",
conclusion: "fix-recommended",
summary: "Choose where the read boundary should live.",
proposals: [
{
title: "Follow the rendered timeline",
summary: "Only mark messages read once they are visible.",
diff_event_id: firstDiffEventId,
},
{
title: "Follow the loaded query",
summary: "Treat loaded messages as read at the physical bottom.",
diff_event_id: secondDiffEventId,
},
],
findings: [],
})}`,
);
assert.deepEqual(result, {
requestId: "request-123",
conclusion: "fix-recommended",
summary: "Choose where the read boundary should live.",
proposals: [
{
title: "Follow the rendered timeline",
summary: "Only mark messages read once they are visible.",
diffEventId: firstDiffEventId,
},
{
title: "Follow the loaded query",
summary: "Treat loaded messages as read at the physical bottom.",
diffEventId: secondDiffEventId,
},
],
findings: [],
});
});
test("bounds proposal copy to two short sentences", () => {
const result = parseProjectReviewCheckResult(`
${PROJECT_REVIEW_CHECK_RESULT_MARKER}
${JSON.stringify({
request_id: "request-123",
conclusion: "fix-recommended",
summary: "Choose a read boundary.",
proposals: [
{
title: "Use rendered rows",
summary:
"Keep unread state tied to what the user can see. This preserves the navigation signal. This implementation detail should not appear in the decision copy.",
diff_event_id: "a".repeat(64),
},
],
findings: [],
})}
`);
assert.equal(
result?.proposals?.[0]?.summary,
"Keep unread state tied to what the user can see. This preserves the navigation signal.",
);
});
test("bounds the shared decision summary", () => {
const result = parseProjectReviewCheckResult(`
${PROJECT_REVIEW_CHECK_RESULT_MARKER}
${JSON.stringify({
request_id: "request-123",
conclusion: "fix-recommended",
summary:
"Choose whether visibility or loaded state owns the read boundary. Both preserve valid behavior. This extra analysis belongs in the finding.",
proposals: [],
findings: [],
})}
`);
assert.equal(
result?.summary,
"Choose whether visibility or loaded state owns the read boundary. Both preserve valid behavior.",
);
});
test("resolves only the exact agent diff for the reviewed repository and commit", () => {
const eventId = "d".repeat(64);
const agentPubkey = "a".repeat(64);
@@ -204,6 +292,45 @@ test("resolves only the exact agent diff for the reviewed repository and commit"
}),
null,
);
assert.equal(
parseProjectReviewCheckResult(
`${PROJECT_REVIEW_CHECK_RESULT_MARKER}\n${JSON.stringify({
conclusion: "fix-recommended",
summary: "Proposal references must be unique.",
proposals: [
{
title: "First",
summary: "First direction.",
diff_event_id: "f".repeat(64),
},
{
title: "Second",
summary: "Second direction.",
diff_event_id: "f".repeat(64),
},
],
findings: [],
})}`,
),
null,
);
assert.equal(
parseProjectReviewCheckResult(
`${PROJECT_REVIEW_CHECK_RESULT_MARKER}\n${JSON.stringify({
conclusion: "fix-recommended",
summary: "Every proposal needs a verified patch reference.",
proposals: [
{
title: "Missing reference",
summary: "This direction cannot be correlated to a patch.",
diff_event_id: null,
},
],
findings: [],
})}`,
),
null,
);
});
test("does not treat ordinary agent prose as a completed check", () => {
@@ -275,7 +402,13 @@ test("builds a review-only prompt pinned to the exact commit", () => {
);
assert.match(prompt, /buzz messages send-diff/);
assert.match(prompt, /send-diff[^\n]+--reply-to <THIS_REQUEST_EVENT_ID>/);
assert.match(prompt, /"diff_event_id":null/);
assert.match(prompt, /"proposals":\[/);
assert.match(prompt, /countering viable direction/);
assert.match(prompt, /two genuinely distinct proposal directions/);
assert.match(prompt, /DECISION COPY CONTRACT/);
assert.match(prompt, /no more than two short sentences and 240 characters/);
assert.match(prompt, /implementation details, identifiers, evidence/);
assert.match(prompt, /continuation of this review negotiation/);
assert.match(prompt, /copy its exact 64-character event_id/);
assert.match(prompt, new RegExp(PROJECT_REVIEW_CHECK_RESULT_MARKER));
assert.doesNotMatch(prompt, /Superseded request id:/);

View File

@@ -1,179 +1,411 @@
import { Check, CheckCircle2, CircleAlert, FileDiff } from "lucide-react";
import { Check, ChevronRight, FileDiff, LoaderCircle, X } from "lucide-react";
import * as React from "react";
import {
countDiffFileChanges,
getDiffFilePath,
getShortestUniquePathLabels,
parseUnifiedDiff,
} from "@/features/messages/lib/parseDiff";
import { DiffViewer } from "@/features/messages/ui/DiffViewer";
import type {
ProjectReviewCheckDiffProposal,
ProjectReviewCheckResolvedProposal,
ProjectReviewCheckResult,
} from "@/features/projects/lib/projectReviewChecks.mjs";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Textarea } from "@/shared/ui/textarea";
const DiffMessage = React.lazy(
() => import("@/features/messages/ui/DiffMessage"),
);
const DiffMessageExpanded = React.lazy(
() => import("@/features/messages/ui/DiffMessageExpanded"),
);
function ProposalDiffFiles({
files,
parseError,
proposal,
}: {
files: ReturnType<typeof parseUnifiedDiff>["files"];
parseError: boolean;
proposal: ProjectReviewCheckResolvedProposal;
}) {
if (parseError || files.length === 0) {
const label = getShortestUniquePathLabels([
proposal.filePath ?? "Proposed patch",
])[0];
function ReviewCheckDiffProposal({
return (
<details className="group overflow-hidden rounded-lg border border-border/60 bg-background/50">
<summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-xs [&::-webkit-details-marker]:hidden">
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform group-open:rotate-90" />
<FileDiff className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 truncate font-mono text-foreground/85">
{label}
</span>
</summary>
<div className="overflow-auto border-t border-border/50 p-2">
<DiffViewer
content={proposal.content}
fallbackFilePath={proposal.filePath ?? undefined}
/>
</div>
</details>
);
}
const labels = getShortestUniquePathLabels(
files.map((file) => getDiffFilePath(file, proposal.filePath ?? undefined)),
);
return (
<div className="space-y-1.5">
{files.map((file, index) => {
const label = labels[index];
const { additions, deletions } = countDiffFileChanges(file);
const key = `${file.oldPath ?? ""}:${file.newPath ?? ""}:${index}`;
return (
<details
className="group overflow-hidden rounded-lg border border-border/60 bg-background/50"
data-testid="project-review-check-proposal-file"
key={key}
>
<summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-xs [&::-webkit-details-marker]:hidden">
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform group-open:rotate-90" />
<span className="min-w-0 flex-1 truncate font-mono text-foreground/85">
{label}
</span>
<span className="flex shrink-0 items-center gap-1.5 font-mono text-2xs font-semibold">
<span className="text-status-added">+{additions}</span>
<span className="text-status-deleted">-{deletions}</span>
</span>
</summary>
<div className="max-h-[24rem] overflow-auto border-t border-border/50 p-2">
<DiffViewer
content={proposal.content}
fallbackFilePath={proposal.filePath ?? undefined}
fileIndex={index}
hideFileHeader
/>
</div>
</details>
);
})}
</div>
);
}
function ProposalOption({
accepted,
canAccept,
index,
onAccept,
proposal,
}: {
accepted: boolean;
canAccept: boolean;
index: number;
onAccept: () => void;
proposal: ProjectReviewCheckDiffProposal;
proposal: ProjectReviewCheckResolvedProposal;
}) {
const [expanded, setExpanded] = React.useState(false);
const acceptEnabled = canAccept && !proposal.truncated;
const parsedDiff = React.useMemo(
() => parseUnifiedDiff(proposal.content),
[proposal.content],
);
const totals = parsedDiff.files.reduce(
(sum, file) => {
const changes = countDiffFileChanges(file);
return {
additions: sum.additions + changes.additions,
deletions: sum.deletions + changes.deletions,
};
},
{ additions: 0, deletions: 0 },
);
return (
<div
className="space-y-3 border-t border-border/55 px-4 py-4"
data-testid="project-review-check-diff-proposal"
<article
className={cn(
"space-y-3 rounded-xl p-3.5",
accepted ? "bg-emerald-500/12" : "bg-emerald-500/8",
)}
data-testid="project-review-check-proposal"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<FileDiff className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="min-w-0">
<h6 className="text-xs font-semibold text-foreground">
Proposed code changes
</h6>
<p className="truncate text-2xs text-muted-foreground">
Native Buzz diff · {proposal.commitSha.slice(0, 7)}
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-2xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400">
Direction {index + 1}
</p>
<h6 className="mt-0.5 line-clamp-2 text-sm font-semibold text-foreground">
{proposal.title}
</h6>
</div>
<div className="flex shrink-0 items-center gap-3">
{!parsedDiff.parseError && parsedDiff.files.length > 0 ? (
<span className="flex items-center gap-1.5 font-mono text-2xs font-semibold">
<span className="text-status-added">+{totals.additions}</span>
<span className="text-status-deleted">-{totals.deletions}</span>
</span>
) : null}
<Button
aria-pressed={accepted}
className={cn(
"shrink-0",
accepted
? "border border-emerald-500/35 bg-emerald-500/10 text-emerald-700 shadow-none hover:bg-emerald-500/15 dark:text-emerald-300"
: "bg-emerald-600 text-white hover:bg-emerald-600/90",
)}
disabled={accepted || !acceptEnabled}
onClick={onAccept}
size="sm"
type="button"
variant={accepted ? "secondary" : "default"}
>
{accepted ? <Check /> : null}
{accepted ? "Accepted" : "Accept"}
</Button>
</div>
</div>
<details
className="group"
data-testid="project-review-check-proposal-details"
>
<summary className="flex w-fit cursor-pointer list-none items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground [&::-webkit-details-marker]:hidden">
<ChevronRight className="h-3.5 w-3.5 transition-transform group-open:rotate-90" />
<span className="group-open:hidden">Read more</span>
<span className="hidden group-open:inline">Hide details</span>
</summary>
<div className="space-y-3 pt-3">
<p className="text-xs leading-relaxed text-muted-foreground">
{proposal.summary}
</p>
{proposal.truncated || !canAccept ? (
<p className="text-2xs text-muted-foreground">
{proposal.truncated
? "The patch is incomplete and cannot be selected."
: "Run this check again to select a current proposal."}
</p>
</div>
) : null}
<ProposalDiffFiles
files={parsedDiff.files}
parseError={parsedDiff.parseError}
proposal={proposal}
/>
</div>
</details>
</article>
);
}
function ReviewResponse({
canRespond,
onRespond,
}: {
canRespond: boolean;
onRespond?: (response: string) => Promise<void>;
}) {
const [response, setResponse] = React.useState("");
const [expanded, setExpanded] = React.useState(false);
const [isSending, setIsSending] = React.useState(false);
const responseId = React.useId();
const canSubmit =
canRespond && Boolean(onRespond) && Boolean(response.trim());
if (!expanded) {
return (
<article
className="flex flex-wrap items-center justify-between gap-3 rounded-xl bg-muted/20 p-3.5"
data-testid="project-review-check-response"
>
<div>
<p className="text-sm font-semibold text-foreground">
Propose another direction
</p>
<p className="text-2xs text-muted-foreground">
Suggest a different approach for the agent to consider.
</p>
</div>
<Button
disabled={accepted || !acceptEnabled}
onClick={onAccept}
size="sm"
disabled={!canRespond || !onRespond}
onClick={() => setExpanded(true)}
type="button"
variant={accepted ? "secondary" : "default"}
variant="secondary"
>
{accepted ? <Check /> : null}
{accepted ? "Accepted" : "Accept changes"}
Propose
</Button>
</article>
);
}
return (
<form
className="space-y-2 rounded-xl bg-muted/20 p-3.5"
data-testid="project-review-check-response"
onSubmit={(event) => {
event.preventDefault();
if (!canSubmit || !onRespond) return;
setIsSending(true);
void onRespond(response.trim())
.then(() => {
setResponse("");
setExpanded(false);
})
.catch(() => undefined)
.finally(() => setIsSending(false));
}}
>
<div>
<label
className="text-sm font-semibold text-foreground"
htmlFor={responseId}
>
Propose another direction
</label>
<p className="text-2xs text-muted-foreground">
Suggest a different approach for the agent to consider.
</p>
</div>
<React.Suspense
fallback={
<div className="p-3 text-xs text-muted-foreground">
Loading proposed changes…
</div>
}
>
<DiffMessage
commitSha={proposal.commitSha}
content={proposal.content}
description={proposal.description ?? undefined}
filePath={proposal.filePath ?? undefined}
onExpand={() => setExpanded(true)}
repoUrl={proposal.repoUrl}
truncated={proposal.truncated}
<div className="flex items-end gap-2">
<Textarea
className="min-h-16 resize-none text-sm"
disabled={!canRespond || isSending}
id={responseId}
onChange={(event) => setResponse(event.target.value)}
placeholder="Share your reasoning or suggest a different fix…"
rows={2}
value={response}
/>
</React.Suspense>
<p className="text-2xs text-muted-foreground">
{accepted
? "Accepted for this prototype. No repository files or review state were changed."
: proposal.truncated
? "This proposal is truncated and cannot be accepted. No code was changed."
: acceptEnabled
? "Preview only. Accepting records your choice in this dev build and does not change code."
: "This proposal is outdated. Run the check again before accepting it. No code was changed."}
</p>
{expanded ? (
<React.Suspense fallback={null}>
<DiffMessageExpanded
content={proposal.content}
filePath={proposal.filePath ?? undefined}
onClose={() => setExpanded(false)}
/>
</React.Suspense>
) : null}
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
disabled={isSending}
onClick={() => setExpanded(false)}
size="sm"
type="button"
variant="ghost"
>
Cancel
</Button>
<Button
disabled={!canSubmit || isSending}
size="sm"
type="submit"
variant="secondary"
>
{isSending ? <LoaderCircle className="animate-spin" /> : null}
Respond
</Button>
</div>
</div>
</form>
);
}
export function ProjectReviewCheckResultCard({
acceptedProposalEventId,
canAcceptProposal,
canDecide,
keptCurrentApproach,
onAcceptProposal,
proposal,
onKeepCurrentApproach,
onRespond,
proposals,
result,
}: {
acceptedProposalEventId?: string;
canAcceptProposal: boolean;
canDecide: boolean;
keptCurrentApproach: boolean;
onAcceptProposal: (eventId: string) => void;
proposal?: ProjectReviewCheckDiffProposal;
onKeepCurrentApproach: () => void;
onRespond?: (response: string) => Promise<void>;
proposals: ProjectReviewCheckResolvedProposal[];
result: ProjectReviewCheckResult;
}) {
const isApproved = result.conclusion === "approved";
const findingCount = result.findings.length;
const title = isApproved
? "Approved"
: findingCount === 0
? "Fix recommended"
: `${findingCount} ${findingCount === 1 ? "fix" : "fixes"} recommended`;
? "Aligned"
: proposals.length > 0
? "Choose a direction"
: "Review the concern";
return (
<section
className="overflow-hidden rounded-xl border border-border/70 bg-background/70"
data-testid="project-review-check-result"
>
<div className="flex items-start gap-3 px-4 py-3.5">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-muted/70">
{isApproved ? (
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
) : (
<CircleAlert className="h-5 w-5 text-amber-500" />
)}
</div>
<div className="min-w-0 flex-1">
<h5 className="text-sm font-semibold text-foreground">{title}</h5>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{result.summary}
</p>
</div>
</div>
{findingCount > 0 ? (
<div className="divide-y divide-border/55 border-t border-border/55">
<section className="space-y-3" data-testid="project-review-check-result">
<header>
<h5 className="text-sm font-semibold text-foreground">{title}</h5>
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
{result.summary}
</p>
</header>
{!isApproved && proposals.length === 0 && result.findings.length > 0 ? (
<div className="divide-y divide-border/45 border-t border-border/55 px-4">
{result.findings.map((finding) => (
<article
className="px-4 py-3"
<div
className="py-2.5"
data-testid="project-review-check-finding"
key={JSON.stringify(finding)}
>
<div className="flex items-start justify-between gap-4">
<p className="min-w-0 flex-1 text-xs font-medium text-foreground">
{finding.title}
</p>
{finding.file ? (
<span
className="max-w-[45%] truncate font-mono text-2xs text-muted-foreground"
title={`${finding.file}${finding.line ? `:${finding.line}` : ""}`}
>
{finding.file}
{finding.line ? `:${finding.line}` : ""}
</span>
) : null}
</div>
<p className="text-xs font-medium text-foreground">
{finding.title}
</p>
{finding.detail ? (
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
<p className="mt-0.5 text-xs text-muted-foreground">
{finding.detail}
</p>
) : null}
</article>
</div>
))}
</div>
) : null}
{proposal ? (
<ReviewCheckDiffProposal
accepted={acceptedProposalEventId === proposal.eventId}
canAccept={canAcceptProposal}
onAccept={() => onAcceptProposal(proposal.eventId)}
proposal={proposal}
/>
{proposals.length > 0 ? (
<>
<p className="text-2xs text-muted-foreground">
Preview only — selecting a direction records the decision without
changing code.
</p>
{proposals.map((proposal, index) => (
<ProposalOption
accepted={acceptedProposalEventId === proposal.eventId}
canAccept={canDecide}
index={index}
key={proposal.eventId}
onAccept={() => onAcceptProposal(proposal.eventId)}
proposal={proposal}
/>
))}
<article
className={cn(
"flex flex-wrap items-center justify-between gap-3 rounded-xl bg-muted/20 p-3.5",
keptCurrentApproach && "bg-muted/35",
)}
>
<div>
<p className="text-sm font-semibold text-foreground">
Keep it as-is
</p>
<p className="text-2xs text-muted-foreground">
Reject the proposals and leave the review unchanged.
</p>
</div>
<Button
aria-pressed={keptCurrentApproach}
className={cn(
"border-rose-500/35 text-rose-600 hover:bg-rose-500/10 hover:text-rose-700 dark:text-rose-400",
keptCurrentApproach && "bg-rose-500/10",
)}
disabled={!canDecide || keptCurrentApproach}
onClick={onKeepCurrentApproach}
size="sm"
type="button"
variant="outline"
>
{keptCurrentApproach ? <Check /> : <X />}
{keptCurrentApproach ? "Keeping it as-is" : "Keep it as-is"}
</Button>
</article>
</>
) : null}
{!isApproved ? (
<ReviewResponse canRespond={canDecide} onRespond={onRespond} />
) : null}
</section>
);

View File

@@ -29,7 +29,7 @@ import {
readProjectReviewCheckRuns,
writeProjectReviewCheckRuns,
type ProjectReviewCheckDefinition,
type ProjectReviewCheckDiffProposal,
type ProjectReviewCheckResolvedProposal,
type ProjectReviewCheckResult,
} from "@/features/projects/lib/projectReviewChecks.mjs";
import { isAtOrAfterConversationOpener } from "@/features/projects/lib/projectAgentConversation";
@@ -63,8 +63,10 @@ type ReviewCheckRun = {
channelId?: string;
opener?: ProjectsConversationOpener;
result?: ProjectReviewCheckResult;
proposal?: ProjectReviewCheckDiffProposal;
proposals?: ProjectReviewCheckResolvedProposal[];
acceptedProposalEventId?: string;
keptCurrentApproach?: boolean;
resultEventIds?: string[];
resultCreatedAt?: number;
error?: string;
};
@@ -83,7 +85,9 @@ function isReviewCheckStatus(value: unknown): value is ReviewCheckStatus {
function normalizeStoredProposal(
value: unknown,
agentPubkey: string | null,
): ProjectReviewCheckDiffProposal | undefined {
fallbackTitle: string,
fallbackSummary: string,
): ProjectReviewCheckResolvedProposal | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
@@ -106,23 +110,33 @@ function normalizeStoredProposal(
: []),
...(raw.truncated === true ? [["truncated", "true"]] : []),
];
return (
parseProjectReviewCheckDiffEvent(
{
id: raw.eventId,
pubkey: agentPubkey,
kind: KIND_STREAM_MESSAGE_DIFF,
content: raw.content,
tags,
},
{
eventId: raw.eventId,
agentPubkey,
repoUrl: raw.repoUrl,
commit: raw.commitSha,
},
) ?? undefined
const proposal = parseProjectReviewCheckDiffEvent(
{
id: raw.eventId,
pubkey: agentPubkey,
kind: KIND_STREAM_MESSAGE_DIFF,
content: raw.content,
tags,
},
{
eventId: raw.eventId,
agentPubkey,
repoUrl: raw.repoUrl,
commit: raw.commitSha,
},
);
if (!proposal) return undefined;
return {
...proposal,
title:
typeof raw.title === "string" && raw.title.trim()
? raw.title.trim().slice(0, 160)
: fallbackTitle,
summary:
typeof raw.summary === "string" && raw.summary.trim()
? raw.summary.trim().slice(0, 1_000)
: fallbackSummary,
};
}
function normalizeStoredRuns(value: Record<string, unknown>): ReviewCheckRuns {
@@ -138,13 +152,35 @@ function normalizeStoredRuns(value: Record<string, unknown>): ReviewCheckRuns {
const raw = candidate as Record<string, unknown>;
const agentPubkey =
typeof raw.agentPubkey === "string" ? raw.agentPubkey : null;
const proposal = normalizeStoredProposal(raw.proposal, agentPubkey);
const status = isReviewCheckStatus(raw.status) ? raw.status : "idle";
const parsedResult = parseProjectReviewCheckResult(
raw.result && typeof raw.result === "object"
? `BUZZ_CHECK_RESULT_V1\n${JSON.stringify(raw.result)}`
: "",
);
const fallbackTitle =
parsedResult?.findings[0]?.title ?? "Apply the recommended change";
const fallbackSummary =
parsedResult?.findings[0]?.detail ??
parsedResult?.summary ??
"Apply the agent's proposed implementation.";
const storedProposals = Array.isArray(raw.proposals)
? raw.proposals
: raw.proposal
? [raw.proposal]
: [];
const proposals = storedProposals
.map((proposal) =>
normalizeStoredProposal(
proposal,
agentPubkey,
fallbackTitle,
fallbackSummary,
),
)
.filter((proposal): proposal is ProjectReviewCheckResolvedProposal =>
Boolean(proposal),
);
const opener =
raw.opener &&
typeof raw.opener === "object" &&
@@ -169,9 +205,22 @@ function normalizeStoredRuns(value: Record<string, unknown>): ReviewCheckRuns {
: {}),
...(opener ? { opener } : {}),
...(parsedResult ? { result: parsedResult } : {}),
...(proposal ? { proposal } : {}),
...(proposal && raw.acceptedProposalEventId === proposal.eventId
? { acceptedProposalEventId: proposal.eventId }
...(proposals.length > 0 ? { proposals } : {}),
...(proposals.some(
(proposal) => raw.acceptedProposalEventId === proposal.eventId,
)
? { acceptedProposalEventId: raw.acceptedProposalEventId as string }
: {}),
...(raw.keptCurrentApproach === true
? { keptCurrentApproach: true }
: {}),
...(Array.isArray(raw.resultEventIds)
? {
resultEventIds: raw.resultEventIds.filter(
(eventId): eventId is string =>
typeof eventId === "string" && /^[0-9a-f]{64}$/i.test(eventId),
),
}
: {}),
...(typeof raw.resultCreatedAt === "number"
? { resultCreatedAt: raw.resultCreatedAt }
@@ -204,6 +253,7 @@ function ReviewCheckResultObserver({
opener,
repoUrl,
requestId,
resultEventIds,
targetCommit,
}: {
agentPubkey: string;
@@ -213,11 +263,13 @@ function ReviewCheckResultObserver({
checkId: string,
result: ProjectReviewCheckResult,
createdAt: number,
proposal?: ProjectReviewCheckDiffProposal,
resultEventId: string,
proposals: ProjectReviewCheckResolvedProposal[],
) => void;
opener: ProjectsConversationOpener;
repoUrl: string | null;
requestId: string | null;
resultEventIds: string[];
targetCommit: string | null;
}) {
useChannelSubscription(channel);
@@ -245,7 +297,11 @@ function ReviewCheckResultObserver({
getThreadReference(event.tags).rootId === opener.eventId,
);
const resultEvents = agentEvents
.filter((event) => isAgentMessage(event, agentPubkey))
.filter(
(event) =>
isAgentMessage(event, agentPubkey) &&
!resultEventIds.includes(event.id),
)
.sort(
(left, right) =>
left.created_at - right.created_at || right.id.localeCompare(left.id),
@@ -253,27 +309,55 @@ function ReviewCheckResultObserver({
for (const event of resultEvents) {
const result = parseProjectReviewCheckResult(event.content);
if (result && (!requestId || result.requestId === requestId)) {
let proposal: ProjectReviewCheckDiffProposal | undefined;
if (result.diffEventId) {
if (!repoUrl) continue;
const proposalReferences =
result.proposals ??
(result.diffEventId
? [
{
diffEventId: result.diffEventId,
title:
result.findings[0]?.title ?? "Apply the recommended change",
summary: result.findings[0]?.detail ?? result.summary,
},
]
: []);
const proposals: ProjectReviewCheckResolvedProposal[] = [];
if (proposalReferences.length > 0 && !repoUrl) continue;
for (const reference of proposalReferences) {
const diffEvent = agentEvents.find(
(candidate) =>
candidate.kind === KIND_STREAM_MESSAGE_DIFF &&
candidate.id.toLowerCase() === result.diffEventId,
candidate.id.toLowerCase() === reference.diffEventId,
);
proposal = diffEvent
? (parseProjectReviewCheckDiffEvent(diffEvent, {
eventId: result.diffEventId,
agentPubkey,
repoUrl,
commit: targetCommit,
}) ?? undefined)
: undefined;
// The agent is instructed to publish the native diff before its
// result. Keep observing if relay delivery arrives out of order.
if (!proposal) continue;
const parsedProposal =
diffEvent && repoUrl
? parseProjectReviewCheckDiffEvent(diffEvent, {
eventId: reference.diffEventId,
agentPubkey,
repoUrl,
commit: targetCommit,
})
: null;
if (!parsedProposal) {
// Each diff is published before the structured result, but relay
// delivery may arrive out of order. Keep observing until every
// referenced alternative is available and verified.
proposals.length = 0;
break;
}
proposals.push({
...parsedProposal,
title: reference.title,
summary: reference.summary,
});
}
return { createdAt: event.created_at, proposal, result };
if (proposals.length !== proposalReferences.length) continue;
return {
createdAt: event.created_at,
proposals,
result,
resultEventId: event.id,
};
}
}
return null;
@@ -283,6 +367,7 @@ function ReviewCheckResultObserver({
opener,
repoUrl,
requestId,
resultEventIds,
targetCommit,
threadReplies.events,
]);
@@ -293,7 +378,8 @@ function ReviewCheckResultObserver({
checkId,
observedResult.result,
observedResult.createdAt,
observedResult.proposal,
observedResult.resultEventId,
observedResult.proposals,
);
}
}, [checkId, observedResult, onResult]);
@@ -325,7 +411,7 @@ function CheckStatus({
return <Badge variant="success">Approved</Badge>;
}
if (run.result?.conclusion === "fix-recommended") {
return <Badge variant="warning">Fix recommended</Badge>;
return <Badge variant="warning">Proposal ready</Badge>;
}
return null;
}
@@ -412,7 +498,8 @@ export function ProjectReviewChecks({
checkId: string,
result: ProjectReviewCheckResult,
createdAt: number,
proposal?: ProjectReviewCheckDiffProposal,
resultEventId: string,
proposals: ProjectReviewCheckResolvedProposal[],
) => {
const expectedStorageKey = storageKeyRef.current;
if (!expectedStorageKey) return;
@@ -420,15 +507,19 @@ export function ProjectReviewChecks({
...current,
error: undefined,
acceptedProposalEventId: undefined,
proposal,
keptCurrentApproach: undefined,
proposals,
result,
resultEventIds: [
...new Set([...(current.resultEventIds ?? []), resultEventId]),
],
resultCreatedAt: createdAt,
status: "completed",
}));
toast.success(
result.conclusion === "approved"
? "Review check approved."
: "Review check recommends a fix.",
: "Review proposals are ready.",
);
},
[updateRun],
@@ -439,8 +530,12 @@ export function ProjectReviewChecks({
const expectedStorageKey = storageKeyRef.current;
if (!expectedStorageKey) return;
updateRun(expectedStorageKey, checkId, (current) =>
current.proposal?.eventId === eventId
? { ...current, acceptedProposalEventId: eventId }
current.proposals?.some((proposal) => proposal.eventId === eventId)
? {
...current,
acceptedProposalEventId: eventId,
keptCurrentApproach: undefined,
}
: current,
);
toast.success("Proposal accepted locally. No code was changed.");
@@ -448,6 +543,72 @@ export function ProjectReviewChecks({
[updateRun],
);
const keepCurrentApproach = React.useCallback(
(checkId: string) => {
const expectedStorageKey = storageKeyRef.current;
if (!expectedStorageKey) return;
updateRun(expectedStorageKey, checkId, (current) => ({
...current,
acceptedProposalEventId: undefined,
keptCurrentApproach: true,
}));
toast.success("The current approach is being kept locally.");
},
[updateRun],
);
const respondToCheck = React.useCallback(
async (checkId: string, response: string) => {
const expectedStorageKey = storageKeyRef.current;
const run = runs[checkId];
const trimmed = response.trim();
if (
!expectedStorageKey ||
!trimmed ||
!run?.channelId ||
!run.opener ||
!run.agentPubkey ||
!relayScope ||
!signerScope
) {
throw new Error("This review conversation is no longer available.");
}
try {
await sendChannelMessage(
run.channelId,
trimmed,
run.opener.eventId,
undefined,
[run.agentPubkey],
undefined,
undefined,
undefined,
undefined,
undefined,
relayScope,
signerScope,
run.opener.eventId,
);
updateRun(expectedStorageKey, checkId, (current) => ({
...current,
acceptedProposalEventId: undefined,
keptCurrentApproach: undefined,
status: "running",
}));
toast.success("Reasoning sent. Waiting for revised proposals.");
} catch (error) {
const message =
error instanceof Error
? error.message
: "Failed to continue the review conversation.";
toast.error(message);
throw error;
}
},
[relayScope, runs, signerScope, updateRun],
);
const runCheck = React.useCallback(
async (
check: ProjectReviewCheckDefinition,
@@ -561,8 +722,10 @@ export function ProjectReviewChecks({
},
requestId,
acceptedProposalEventId: undefined,
proposal: undefined,
keptCurrentApproach: undefined,
proposals: undefined,
result: undefined,
resultEventIds: [],
resultCreatedAt: undefined,
status: "running",
targetCommit,
@@ -716,11 +879,14 @@ export function ProjectReviewChecks({
{run.result ? (
<ProjectReviewCheckResultCard
acceptedProposalEventId={run.acceptedProposalEventId}
canAcceptProposal={!isStale}
canDecide={!isStale && run.status === "completed"}
keptCurrentApproach={run.keptCurrentApproach === true}
onAcceptProposal={(eventId) =>
acceptProposal(check.id, eventId)
}
proposal={run.proposal}
onKeepCurrentApproach={() => keepCurrentApproach(check.id)}
onRespond={(response) => respondToCheck(check.id, response)}
proposals={run.proposals ?? []}
result={run.result}
/>
) : null}
@@ -733,6 +899,7 @@ export function ProjectReviewChecks({
opener={run.opener}
repoUrl={diffRepoUrl}
requestId={run.requestId ?? null}
resultEventIds={run.resultEventIds ?? []}
targetCommit={run.targetCommit ?? targetCommit}
/>
) : null}

View File

@@ -12,6 +12,8 @@ const RELAY_REVIEW_AGENT_PUBKEY =
const LOCAL_REVIEW_AGENT_PUBKEY = "c".repeat(64);
const STOPPED_LOCAL_REVIEW_AGENT_PUBKEY = "d".repeat(64);
const PROPOSED_DIFF_EVENT_ID = "e".repeat(64);
const ALTERNATE_DIFF_EVENT_ID = "f".repeat(64);
const REVISED_DIFF_EVENT_ID = "1".repeat(64);
async function expectSinglePrimaryTextColumn(row: Locator) {
const primary = row.locator('[data-projects-text-priority="primary"]');
@@ -227,6 +229,7 @@ test("review checks dispatch to an agent authorized for the relay identity", asy
await page.evaluate(
({
agentPubkey,
alternateDiffEventId,
diffEventId,
diffRepoUrl,
openerEventId,
@@ -256,25 +259,57 @@ test("review checks dispatch to an agent authorized for the relay identity", asy
parentEventId: openerEventId,
pubkey: agentPubkey,
});
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "DM",
content: [
"diff --git a/desktop/src/shared/ui/button.tsx b/desktop/src/shared/ui/button.tsx",
"--- a/desktop/src/shared/ui/button.tsx",
"+++ b/desktop/src/shared/ui/button.tsx",
"@@ -1,3 +1,3 @@",
"-const focusStyle = 'outline-none';",
"+const focusStyle = 'outline-offset-2';",
" export { focusStyle };",
].join("\n"),
createdAt: Math.floor(Date.now() / 1_000) + 1,
extraTags: [
["repo", diffRepoUrl],
["commit", reviewedCommit],
["file", "desktop/src/shared/ui/button.tsx"],
["description", "Preserve the custom control"],
],
id: alternateDiffEventId,
kind: 40008,
parentEventId: openerEventId,
pubkey: agentPubkey,
});
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "DM",
content: `BUZZ_CHECK_RESULT_V1\n${JSON.stringify({
request_id: requestId,
conclusion: "fix-recommended",
summary: "Two interface fixes are needed before approval.",
diff_event_id: diffEventId,
findings: [
summary:
"The focus behavior can follow the shared primitive or preserve the custom control's semantics.",
proposals: [
{
title: "Use the shared button primitive",
detail: "The custom control misses the standard focus treatment.",
file: "desktop/src/features/projects/ui/ProjectReviewChecks.tsx",
line: 742,
title: "Use the shared focus treatment",
summary:
"Align this control with the standard keyboard focus behavior.",
diff_event_id: diffEventId,
},
{
title: "Cover the disabled interaction state",
detail: "Add a UI assertion for the pending check state.",
file: "desktop/tests/e2e/project-pr-review.spec.ts",
line: 118,
title: "Preserve the custom control",
summary:
"Keep the specialized control and make its focus boundary explicit.",
diff_event_id: alternateDiffEventId,
},
],
findings: [
{
title: "Choose a consistent focus boundary",
detail:
"The current control has no visible keyboard focus treatment.",
file: "desktop/src/features/projects/ui/ProjectReviewChecks.tsx",
line: 742,
},
],
})}`,
@@ -286,6 +321,7 @@ test("review checks dispatch to an agent authorized for the relay identity", asy
},
{
agentPubkey: RELAY_REVIEW_AGENT_PUBKEY,
alternateDiffEventId: ALTERNATE_DIFF_EVENT_ID,
diffEventId: PROPOSED_DIFF_EVENT_ID,
diffRepoUrl: diffRepoUrl ?? "",
openerEventId,
@@ -297,37 +333,154 @@ test("review checks dispatch to an agent authorized for the relay identity", asy
const result = interfaceCheck.getByTestId("project-review-check-result");
await expect(
result.getByRole("heading", {
name: "2 fixes recommended",
name: "Choose a direction",
exact: true,
}),
).toBeVisible();
await expect(result).toContainText(
"Two interface fixes are needed before approval.",
"The focus behavior can follow the shared primitive or preserve the custom control's semantics.",
);
await expect(result.getByTestId("project-review-check-finding")).toHaveCount(
2,
const proposals = result.getByTestId("project-review-check-proposal");
await expect(proposals).toHaveCount(2);
const sharedDirection = proposals.nth(0);
const customDirection = proposals.nth(1);
await expect(sharedDirection).toContainText("Use the shared focus treatment");
await expect(customDirection).toContainText("Preserve the custom control");
const sharedFile = sharedDirection.getByTestId(
"project-review-check-proposal-file",
);
await expect(result).toContainText("ProjectReviewChecks.tsx:742");
const proposal = result.getByTestId("project-review-check-diff-proposal");
await expect(proposal).toContainText("Proposed code changes");
await expect(proposal).toContainText("button.tsx");
await expect(proposal).toContainText("Use the shared focus treatment");
await expect(proposal).toContainText("focus-visible:ring-1");
await expect(sharedDirection).toContainText("+1");
await expect(sharedDirection).toContainText("-1");
const sharedDetails = sharedDirection.getByTestId(
"project-review-check-proposal-details",
);
await expect(sharedDetails).not.toHaveAttribute("open", "");
await sharedDetails.getByText("Read more").click();
await expect(sharedDetails).toHaveAttribute("open", "");
await expect(sharedFile).toContainText("button.tsx");
await expect(sharedFile).not.toHaveAttribute("open", "");
await sharedFile.locator("summary").click();
await expect(sharedFile).toHaveAttribute("open", "");
await expect(
sharedDirection.getByText("const focusStyle = 'focus-visible:ring-1';"),
).toBeVisible();
const commandCountBeforeAccept = await page.evaluate(
() => window.__BUZZ_E2E_COMMANDS__?.length ?? 0,
);
await proposal.getByRole("button", { name: "Accept changes" }).click();
await sharedDirection
.getByRole("button", { name: "Accept", exact: true })
.click();
await expect(
proposal.getByRole("button", { name: "Accepted" }),
sharedDirection.getByRole("button", { name: "Accepted" }),
).toBeVisible();
await expect(proposal).toContainText(
"No repository files or review state were changed.",
);
await expect
.poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__?.length ?? 0))
.toBe(commandCountBeforeAccept);
await waitForAnimations(page);
await result.screenshot({ path: `${SHOTS}/review-check-result.png` });
await result.getByRole("button", { name: "Keep it as-is" }).click();
await expect(
result.getByRole("button", { name: "Keeping it as-is" }),
).toBeVisible();
await result.getByRole("button", { name: "Propose", exact: true }).click();
await result
.getByRole("textbox", { name: "Propose another direction" })
.fill("Can we keep the custom control but use the shared focus token?");
await result.getByRole("button", { name: "Respond" }).click();
await expect(interfaceCheck).toContainText("Running");
const response = await page.evaluate(() => {
const entry = window.__BUZZ_E2E_COMMAND_PAYLOADS__?.findLast(
(candidate) =>
candidate.command === "send_channel_message" &&
(candidate.payload as { content?: unknown })?.content ===
"Can we keep the custom control but use the shared focus token?",
);
return entry?.payload as
| {
mentionPubkeys?: string[];
parentEventId?: string;
rootEventId?: string;
}
| undefined;
});
expect(response?.mentionPubkeys).toContain(RELAY_REVIEW_AGENT_PUBKEY);
expect(response?.parentEventId).toBe(openerEventId);
expect(response?.rootEventId).toBe(openerEventId);
await page.evaluate(
({
agentPubkey,
diffEventId,
diffRepoUrl,
openerEventId,
requestId,
reviewedCommit,
}) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "DM",
content: [
"diff --git a/desktop/src/shared/ui/button.tsx b/desktop/src/shared/ui/button.tsx",
"--- a/desktop/src/shared/ui/button.tsx",
"+++ b/desktop/src/shared/ui/button.tsx",
"@@ -1,3 +1,3 @@",
"-const focusStyle = 'outline-none';",
"+const focusStyle = sharedFocusToken;",
" export { focusStyle };",
].join("\n"),
createdAt: Math.floor(Date.now() / 1_000) + 4,
extraTags: [
["repo", diffRepoUrl],
["commit", reviewedCommit],
["file", "desktop/src/shared/ui/button.tsx"],
["description", "Use the shared token in the custom control"],
],
id: diffEventId,
kind: 40008,
parentEventId: openerEventId,
pubkey: agentPubkey,
});
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "DM",
content: `BUZZ_CHECK_RESULT_V1\n${JSON.stringify({
request_id: requestId,
conclusion: "fix-recommended",
summary:
"Your hybrid direction preserves the custom control while sharing the established focus token.",
proposals: [
{
title: "Share the focus token",
summary:
"Keep the specialized control and reuse the common visual contract.",
diff_event_id: diffEventId,
},
],
findings: [],
})}`,
createdAt: Math.floor(Date.now() / 1_000) + 5,
kind: 9,
parentEventId: openerEventId,
pubkey: agentPubkey,
});
},
{
agentPubkey: RELAY_REVIEW_AGENT_PUBKEY,
diffEventId: REVISED_DIFF_EVENT_ID,
diffRepoUrl: diffRepoUrl ?? "",
openerEventId,
requestId,
reviewedCommit: reviewedCommit ?? "",
},
);
await expect(interfaceCheck).toContainText("Proposal ready");
await expect(result).toContainText(
"Your hybrid direction preserves the custom control while sharing the established focus token.",
);
await expect(result.getByTestId("project-review-check-proposal")).toHaveCount(
1,
);
await expect(result).toContainText("Share the focus token");
});
test("review checks dispatch to a running local managed agent", async ({