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
5,286 changes: 3,939 additions & 1,347 deletions app/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@base-ui/react": "^1.6.0",
"@excalidraw/excalidraw": "^0.18.1",
"@fontsource-variable/archivo": "^5.2.8",
"@fontsource-variable/geist": "^5.2.9",
"@fontsource/ibm-plex-mono": "^5.2.7",
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ function App() {
if (currentView.kind === "plan") {
void fetchPlan(currentView.project);
}
// Refetch the Briefing recap for a completed review agent: the pre-review
// regenerates the recap without moving the head, so the head-keyed fetch
// in BriefingTab would otherwise miss it.
if (isReviewMode(mode)) {
void useAppStore.getState().fetchRecap(object_id);
}

// Await the active-review refresh so a Review-mode toast can report the
// freshly parsed findings count. Kept in an IIFE so the listener callback
Expand Down
152 changes: 152 additions & 0 deletions app/src/components/BriefingFindings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* The Briefing tab's findings section — every advisory finding for the review,
* severity-ranked, with full rationale visible.
*
* This is the read-forward companion to the diff gate's collapsible
* `FindingsPanel`: same row anatomy (severity dot + label, optional area chip,
* `path:line` mono, a Jump control) and the same shared severity conventions,
* but non-collapsible, non-dismissable, and with the rationale shown in full
* (the reviewer is here to read, not triage). "Jump" hands off to the Diff tab
* via the caller. Findings are advisory — never a verdict, never a gate.
*/

import type { ReviewFinding } from "../bindings/ReviewFinding";
import type { DiffSide } from "../bindings/DiffSide";
import { severityMeta, sortFindingsBySeverity } from "@/lib/finding-severity";
import { AgentMark } from "@/lib/agent-event";
import { Markdown } from "./Markdown";
import { cn } from "@/lib/utils";

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------

interface BriefingFindingsProps {
/** Every finding for the review; rendered highest-severity first. */
readonly findings: readonly ReviewFinding[];
/** Reveal a finding's line in the Diff tab, side-aware. */
readonly onJumpTo: (path: string, side: DiffSide, line: number) => void;
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** `path:start–end` (or just `path` for a file-level finding at line 0). */
function locationLabel(finding: ReviewFinding): string {
const [start, end] = finding.range;
if (start <= 0) return finding.path;
return start === end
? `${finding.path}:${String(start)}`
: `${finding.path}:${String(start)}–${String(end)}`;
}

// ---------------------------------------------------------------------------
// FindingRow
// ---------------------------------------------------------------------------

function FindingRow({
finding,
onJumpTo,
}: {
readonly finding: ReviewFinding;
readonly onJumpTo: (path: string, side: DiffSide, line: number) => void;
}) {
const meta = severityMeta(finding.severity);
const [start] = finding.range;
const hasLine = start > 0;

return (
<li className="rounded-md border border-border bg-background/40 px-3 py-2 text-xs">
<div className="flex items-center gap-2">
<span className="inline-flex shrink-0 items-center gap-1.5">
<span
className={cn("h-2 w-2 shrink-0 rounded-full", meta.dot)}
aria-hidden="true"
/>
<span
className={cn(
"text-[10px] font-semibold uppercase tracking-wide",
meta.text,
)}
>
{meta.label}
</span>
</span>
{finding.area !== null && (
<span
className="shrink-0 rounded border border-border/60 px-1 py-0.5 font-mono text-[9px] uppercase tracking-wide text-muted-foreground"
title={`Area: ${finding.area}`}
>
{finding.area}
</span>
)}
<span
className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground"
title={locationLabel(finding)}
>
{locationLabel(finding)}
</span>
{hasLine ? (
<button
type="button"
onClick={() => {
onJumpTo(finding.path, finding.side, start);
}}
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={`Jump to ${locationLabel(finding)} in the Diff tab`}
>
Jump
</button>
) : (
<span
className="shrink-0 rounded border border-border/60 px-1.5 py-0.5 text-[10px] text-muted-foreground/70"
title="This finding is file-level and has no specific line"
>
file-level
</span>
)}
</div>
<div className="mt-1 font-medium text-foreground">{finding.title}</div>
<div className="mt-0.5 leading-relaxed text-muted-foreground">
<Markdown text={finding.rationale} compact />
</div>
</li>
);
}

// ---------------------------------------------------------------------------
// BriefingFindings
// ---------------------------------------------------------------------------

/**
* The severity-ranked findings list for the Briefing tab. Renders nothing when
* there are no findings.
*/
export function BriefingFindings({ findings, onJumpTo }: BriefingFindingsProps) {
if (findings.length === 0) return null;
const ranked = sortFindingsBySeverity(findings);

return (
<section>
<div className="mb-2 flex items-center gap-1.5">
<AgentMark className="h-3 w-3 text-brand" />
<h2 className="font-mono text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
Findings ({String(ranked.length)})
</h2>
<span className="font-sans text-[10px] normal-case text-muted-foreground/60">
advisory — never blocks
</span>
</div>
<ul className="space-y-1.5">
{ranked.map((finding) => (
<FindingRow
key={finding.id}
finding={finding}
onJumpTo={onJumpTo}
/>
))}
</ul>
</section>
);
}
151 changes: 151 additions & 0 deletions app/src/components/BriefingTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* The Briefing tab — the review workspace's read-forward landing surface.
*
* Top to bottom: the agent's distilled intent (the "agent's read"), a curated
* instrument row of deterministic signals, the severity-ranked findings, and
* the visual recap (Excalidraw scenes + MDX). When a review has none of these —
* no intent, no findings, no recap, not stale — it shows a centered empty state
* offering to run the advisory pre-review.
*
* Findings and evidence come from the review + the existing evidence command;
* the recap comes from the store's `fetchRecap`. A finding's "Jump" hands off to
* the Diff tab via {@link BriefingTabProps.onJumpToDiff}.
*/

import { useCallback, useEffect, useMemo, useState } from "react";
import { Sparkles } from "lucide-react";
import type { Review } from "../bindings/Review";
import type { DiffSide } from "../bindings/DiffSide";
import type { EvidenceSummary } from "../bindings/EvidenceSummary";
import type { RecapPayload } from "../bindings/RecapPayload";
import { useAppStore } from "../store";
import { recapStale } from "@/lib/recap";
import { findingsBreakdown } from "./FindingsPanel";
import { IntentHero } from "./IntentHero";
import { InstrumentRow } from "./InstrumentRow";
import { BriefingFindings } from "./BriefingFindings";
import { RecapView } from "./RecapView";
import { EmptyState } from "./EmptyState";

/** No findings are dismissed on the Briefing; it is a read-forward surface. */
const NO_DISMISSED: ReadonlySet<string> = new Set<string>();

interface BriefingTabProps {
/** The review being briefed. */
readonly review: Review;
/** Whether this tab is currently visible (gates the lazy Excalidraw mount). */
readonly active: boolean;
/** Reveal a finding's line in the Diff tab, side-aware. */
readonly onJumpToDiff: (path: string, side: DiffSide, line: number) => void;
}

/** The Briefing tab body. */
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 recap = useAppStore((s) => s.recaps[review.pr] ?? null);
const appTheme = useAppStore((s) => s.config?.app_theme ?? "dark");
const dark = appTheme !== "light";

const reviewPr = review.pr;
const reviewHead = review.head_sha;

// -- Evidence for the instrument row, refetched on head change. --
const [evidence, setEvidence] = useState<EvidenceSummary | null>(null);
useEffect(() => {
let cancelled = false;
void fetchEvidence(reviewPr).then((e) => {
if (!cancelled) setEvidence(e);
});
return () => {
cancelled = true;
};
}, [reviewPr, reviewHead, fetchEvidence]);

// -- Recap payload, refetched on head change (also on `agent-completed`). --
useEffect(() => {
void fetchRecap(reviewPr);
}, [reviewPr, reviewHead, fetchRecap]);

// -- Regenerate / run-pre-review (advisory; refuses while an agent runs). --
const [preReviewing, setPreReviewing] = useState(false);
const handlePreReview = useCallback(async () => {
setPreReviewing(true);
try {
await preReview(reviewPr);
} finally {
setPreReviewing(false);
}
}, [preReview, reviewPr]);

const findingsCount = useMemo(
() => findingsBreakdown(review.review_findings, NO_DISMISSED),
[review.review_findings],
);

const intent = review.distilled_intent;
const hasFindings = review.review_findings.length > 0;
const stale = recapStale(review.recap_sha, review.head_sha);
const hasRecapContent =
recap !== null && (recap.mdx !== null || recap.scenes.length > 0);
const agentRunning = review.agent !== null;
const regenerateDisabled = preReviewing || agentRunning;

// The whole Briefing is empty only when there is no intent, no findings, no
// recap content, and nothing stale to nudge about.
const empty = intent === null && !hasFindings && !hasRecapContent && !stale;

if (empty) {
return (
<div className="flex h-full items-center justify-center overflow-y-auto p-6">
<EmptyState
icon={Sparkles}
title="No briefing yet"
description="Run the advisory pre-review to distill the agent's read, surface findings, and render a visual recap."
actionLabel={regenerateDisabled ? undefined : "Run pre-review"}
onAction={regenerateDisabled ? undefined : handlePreReview}
/>
</div>
);
}

// Show the recap block when there is content or a stale nudge to raise; feed
// an empty payload while the fetch is still in flight so the nudge is instant.
const effectiveRecap: RecapPayload = recap ?? {
mdx: null,
scenes: [],
sha: review.recap_sha,
};
const showRecap = hasRecapContent || stale;

return (
<div className="h-full overflow-y-auto">
<div className="mx-auto max-w-4xl space-y-5 px-6 py-6">
{intent !== null && (
<IntentHero intent={intent} recapSha={review.recap_sha} />
)}

<InstrumentRow evidence={evidence} findings={findingsCount} />

{hasFindings && (
<BriefingFindings
findings={review.review_findings}
onJumpTo={onJumpToDiff}
/>
)}

{showRecap && (
<RecapView
recap={effectiveRecap}
stale={stale}
onRegenerate={handlePreReview}
regenerateDisabled={regenerateDisabled}
dark={dark}
active={active}
/>
)}
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions app/src/components/DiffView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,24 @@ export function DiffView({
[],
);

// Cross-tab jump: consume a `pendingDiffJump` published by a sibling tab (e.g.
// a Briefing finding's "Jump"), reusing the same reveal machinery as the
// in-tab findings/evidence jumps. Keyed on the request nonce so an identical
// target still re-fires, and scoped to this review's PR.
const pendingDiffJump = useAppStore((s) => s.pendingDiffJump);
const consumedJumpNonceRef = useRef(-1);
useEffect(() => {
if (pendingDiffJump === null) return;
if (pendingDiffJump.pr !== review.pr) return;
if (pendingDiffJump.nonce === consumedJumpNonceRef.current) return;
consumedJumpNonceRef.current = pendingDiffJump.nonce;
handleJumpTo(
pendingDiffJump.path,
pendingDiffJump.side,
pendingDiffJump.line,
);
}, [pendingDiffJump, review.pr, handleJumpTo]);

// -- Findings panel: auto-expand on entry only when a Critical finding
// exists; otherwise leave the header collapsed-but-visible. Keyed on the PR
// so switching reviews recomputes the default without clobbering a manual
Expand Down
Loading
Loading