Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/src/components/BriefingFindings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ interface BriefingFindingsProps {
* Briefing tab list.
*/
readonly limit?: number;
/**
* Adopt a finding as a review comment (explicit click; outward posting still
* rides the guarded Mirror / Submit-review flows). Omitted by read-only
* surfaces (the board preview rail) and when the gate is not InReview.
*/
readonly onAdopt?: ((finding: ReviewFinding) => void) | undefined;
/** Ids of findings that already have an adopted comment. */
readonly adoptedIds?: ReadonlySet<string> | undefined;
}

// ---------------------------------------------------------------------------
Expand All @@ -55,9 +63,13 @@ function locationLabel(finding: ReviewFinding): string {
function FindingRow({
finding,
onJumpTo,
onAdopt,
adopted,
}: {
readonly finding: ReviewFinding;
readonly onJumpTo: (path: string, side: DiffSide, line: number) => void;
readonly onAdopt?: ((finding: ReviewFinding) => void) | undefined;
readonly adopted: boolean;
}) {
const meta = severityMeta(finding.severity);
const [start] = finding.range;
Expand Down Expand Up @@ -94,6 +106,27 @@ function FindingRow({
>
{locationLabel(finding)}
</span>
{onAdopt !== undefined &&
hasLine &&
(adopted ? (
<span
className="shrink-0 rounded border border-border/60 px-1.5 py-0.5 text-[10px] text-muted-foreground/70"
title="A review comment for this finding already exists"
>
Added ✓
</span>
) : (
<button
type="button"
onClick={() => {
onAdopt(finding);
}}
className="shrink-0 cursor-pointer rounded border border-border bg-transparent px-1.5 py-0.5 text-[10px] text-muted-foreground hover:border-primary/50 hover:text-foreground"
title="Add this finding as a review comment (posts to GitHub only via Mirror / Submit review)"
>
Add to review
</button>
))}
{hasLine ? (
<button
type="button"
Expand Down Expand Up @@ -134,6 +167,8 @@ export function BriefingFindings({
findings,
onJumpTo,
limit,
onAdopt,
adoptedIds,
}: BriefingFindingsProps) {
if (findings.length === 0) return null;
const ranked = sortFindingsBySeverity(findings);
Expand All @@ -157,6 +192,8 @@ export function BriefingFindings({
key={finding.id}
finding={finding}
onJumpTo={onJumpTo}
onAdopt={onAdopt}
adopted={adoptedIds?.has(finding.id) ?? false}
/>
))}
</ul>
Expand Down
26 changes: 26 additions & 0 deletions app/src/components/BriefingTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type { EvidenceSummary } from "../bindings/EvidenceSummary";
import type { RecapPayload } from "../bindings/RecapPayload";
import { useAppStore } from "../store";
import { recapStale } from "@/lib/recap";
import type { ReviewFinding } from "../bindings/ReviewFinding";
import { adoptedFindingIds, findingCommentBody } from "@/lib/finding-comment";
import { findingsBreakdown } from "./FindingsPanel";
import { IntentHero } from "./IntentHero";
import { InstrumentRow } from "./InstrumentRow";
Expand All @@ -44,6 +46,7 @@ export function BriefingTab({ review, active, onJumpToDiff }: BriefingTabProps)
const fetchEvidence = useAppStore((s) => s.fetchEvidence);
const fetchRecap = useAppStore((s) => s.fetchRecap);
const preReview = useAppStore((s) => s.preReview);
const addComment = useAppStore((s) => s.addComment);
const recap = useAppStore((s) => s.recaps[review.pr] ?? null);
const appTheme = useAppStore((s) => s.config?.app_theme ?? "dark");
const dark = appTheme !== "light";
Expand Down Expand Up @@ -119,6 +122,27 @@ export function BriefingTab({ review, active, onJumpToDiff }: BriefingTabProps)
};
const showRecap = hasRecapContent || stale;

// Adopt a finding as a local review comment (explicit click; outward posting
// still rides the guarded Mirror / Submit-review flows — §9). Commenting is
// only meaningful while the gate is InReview, matching the Diff tab's rule.
const canAdopt = review.gate_state === "InReview";
const adoptedFindings = useMemo(
() => adoptedFindingIds(review.review_findings, review.comments),
[review.review_findings, review.comments],
);
const handleAdoptFinding = useCallback(
(finding: ReviewFinding) => {
void addComment(
finding.path,
finding.range[0],
finding.range[1],
findingCommentBody(finding),
finding.side,
);
},
[addComment],
);

return (
<div className="h-full overflow-y-auto">
<div className="mx-auto max-w-4xl space-y-5 px-6 py-6">
Expand All @@ -132,6 +156,8 @@ export function BriefingTab({ review, active, onJumpToDiff }: BriefingTabProps)
<BriefingFindings
findings={review.review_findings}
onJumpTo={onJumpToDiff}
onAdopt={canAdopt ? handleAdoptFinding : undefined}
adoptedIds={adoptedFindings}
/>
)}

Expand Down
28 changes: 28 additions & 0 deletions app/src/components/DiffView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ import {
import type { FileDiff } from "../diff-parser";
import { pairRequests } from "@/lib/request-pairing";
import { findingDecorations } from "@/lib/finding-decorations";
import {
adoptedFindingIds,
findingCommentBody,
} from "@/lib/finding-comment";
import { elapsedSince } from "@/lib/relative-time";
import { useAppStore } from "../store";
import { registerCustomThemes } from "@/lib/monaco-themes";
Expand Down Expand Up @@ -882,6 +886,28 @@ export function DiffView({
// Only open the input form when the review is InReview
const effectiveActiveComment = canAddComments ? activeComment : null;

// Findings already adopted as review comments (idempotent Add-to-review).
const adoptedFindings = useMemo(
() => adoptedFindingIds(review.review_findings, review.comments),
[review.review_findings, review.comments],
);

// Adopt a finding: create a local comment at the finding's anchor with the
// agent-attributed body. Outward posting still rides the guarded Mirror /
// Submit-review flows (§9) — this only drafts the comment locally.
const handleAdoptFinding = useCallback(
(finding: ReviewFinding) => {
void onAddComment(
finding.path,
finding.range[0],
finding.range[1],
findingCommentBody(finding),
finding.side,
);
},
[onAddComment],
);

// -- Relocated PR-info: external reference links --
const prHref = useMemo(() => prUrl(review.pr), [review.pr]);
const issueHref = useMemo(() => issueUrl(review.issue), [review.issue]);
Expand Down Expand Up @@ -2348,6 +2374,8 @@ export function DiffView({
});
}}
onJumpTo={handleJumpTo}
onAdopt={canAddComments ? handleAdoptFinding : undefined}
adoptedIds={adoptedFindings}
/>

{/* ----------------------------------------------------------------- */}
Expand Down
50 changes: 50 additions & 0 deletions app/src/components/FindingsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,53 @@ describe("FindingsPanel", () => {
).toBeInTheDocument();
});
});

describe("FindingsPanel — adopt as review comment", () => {
it("calls onAdopt with the finding and shows Added once adopted", () => {
const finding = makeFinding({ id: "a", title: "Missing tenant scope" });
const onAdopt = vi.fn();
const { rerender } = render(
<FindingsPanel
findings={[finding]}
dismissed={new Set()}
open={true}
onToggle={noop}
onDismiss={noop}
onJumpTo={noop}
onAdopt={onAdopt}
adoptedIds={new Set()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Add to review" }));
expect(onAdopt).toHaveBeenCalledWith(finding);

rerender(
<FindingsPanel
findings={[finding]}
dismissed={new Set()}
open={true}
onToggle={noop}
onDismiss={noop}
onJumpTo={noop}
onAdopt={onAdopt}
adoptedIds={new Set(["a"])}
/>,
);
expect(screen.queryByRole("button", { name: "Add to review" })).toBeNull();
expect(screen.getByText("Added ✓")).toBeDefined();
});

it("hides the affordance entirely when onAdopt is absent", () => {
render(
<FindingsPanel
findings={[makeFinding({ id: "a" })]}
dismissed={new Set()}
open={true}
onToggle={noop}
onDismiss={noop}
onJumpTo={noop}
/>,
);
expect(screen.queryByRole("button", { name: "Add to review" })).toBeNull();
});
});
39 changes: 39 additions & 0 deletions app/src/components/FindingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ interface FindingsPanelProps {
readonly onDismiss: (id: string) => void;
/** Scroll the diff to a finding's line, side-aware. */
readonly onJumpTo: (path: string, side: DiffSide, line: number) => void;
/**
* Adopt a finding as a review comment (explicit click; the comment reaches
* GitHub only through the guarded Mirror / Submit-review flows). Omit when
* commenting is unavailable (gate not InReview) to hide the affordance.
*/
readonly onAdopt?: ((finding: ReviewFinding) => void) | undefined;
/** Ids of findings that already have an adopted comment. */
readonly adoptedIds?: ReadonlySet<string> | undefined;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -192,10 +200,16 @@ function FindingRow({
finding,
onDismiss,
onJumpTo,
onAdopt,
adopted,
}: {
readonly finding: ReviewFinding;
readonly onDismiss: (id: string) => void;
readonly onJumpTo: (path: string, side: DiffSide, line: number) => void;
/** Adopt this finding as a review comment; absent when commenting is off. */
readonly onAdopt?: ((finding: ReviewFinding) => void) | undefined;
/** Whether a comment for this finding already exists. */
readonly adopted: boolean;
}) {
const [expanded, setExpanded] = useState(false);
const meta = severityMeta(finding.severity);
Expand Down Expand Up @@ -236,6 +250,27 @@ function FindingRow({
>
{locationLabel(finding)}
</span>
{onAdopt !== undefined &&
hasLine &&
(adopted ? (
<span
className="shrink-0 rounded border border-border/60 px-1.5 py-0.5 text-[10px] text-muted-foreground/70"
title="A review comment for this finding already exists"
>
Added ✓
</span>
) : (
<button
type="button"
onClick={() => {
onAdopt(finding);
}}
className="shrink-0 cursor-pointer rounded border border-border bg-transparent px-1.5 py-0.5 text-[10px] text-muted-foreground hover:border-primary/50 hover:text-foreground"
title="Add this finding as a review comment (posts to GitHub only via Mirror / Submit review)"
>
Add to review
</button>
))}
{hasLine ? (
<button
type="button"
Expand Down Expand Up @@ -307,6 +342,8 @@ export function FindingsPanel({
onToggle,
onDismiss,
onJumpTo,
onAdopt,
adoptedIds,
}: FindingsPanelProps) {
const visible = useMemo(
() =>
Expand Down Expand Up @@ -372,6 +409,8 @@ export function FindingsPanel({
finding={finding}
onDismiss={onDismiss}
onJumpTo={onJumpTo}
onAdopt={onAdopt}
adopted={adoptedIds?.has(finding.id) ?? false}
/>
))}
</ul>
Expand Down
Loading
Loading