[W9-4] Add code-snippet viewer for revision findings (#532)

Backend: GET /git/file reads a file at a branch tip (read_file_at_branch)
and slices it to a line window — explicit start/end, a line+context center,
or the whole file capped at 2000 lines. _compute_file_range is the pure
helper (unit-tested).

Frontend: useGitFile hook + CodeSnippet (styled <pre>, line numbers, active-
line highlight — matches git-diff-viewer, no shiki). Wired into FindingCard
so each file:line finding shows the surrounding source. Fail-open: a missing
file renders a muted hint, never breaks the card.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-15 04:34:12 +02:00
committed by GitHub
co-authored by Renn F
parent 1054538d2f
commit f07e2420a8
10 changed files with 427 additions and 2 deletions
@@ -11,6 +11,12 @@ const { useTaskFindings } = vi.hoisted(() => ({ useTaskFindings: vi.fn() }));
vi.mock("@/hooks/use-tasks", () => ({ useTaskFindings }));
// CodeSnippet runs a real useQuery (needs a QueryClient); stub it so the
// findings test stays focused on grouping/rendering, not git fetching.
vi.mock("@/components/git/code-snippet", () => ({
CodeSnippet: () => <div data-testid="code-snippet" />,
}));
import { TabFindings } from "../tab-findings";
function buildTask(overrides: Partial<Task> = {}): Task {
@@ -7,6 +7,7 @@ import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { ListChecks } from "lucide-react";
import { CodeSnippet } from "@/components/git/code-snippet";
interface TabFindingsProps {
task: Task;
@@ -37,7 +38,13 @@ const ORIGIN_LABEL: Record<string, string> = {
ceo: "CEO",
};
function FindingCard({ finding }: { finding: TaskFinding }) {
function FindingCard({
finding,
branch,
}: {
finding: TaskFinding;
branch: string | null;
}) {
return (
<Card>
<CardContent className="pt-4 space-y-2">
@@ -65,6 +72,13 @@ function FindingCard({ finding }: { finding: TaskFinding }) {
</code>
)}
</div>
{finding.file && (
<CodeSnippet
branch={branch}
file={finding.file}
activeLine={finding.line}
/>
)}
<div className="space-y-1 text-sm">
<p>
<span className="text-muted-foreground">Expected:</span>{" "}
@@ -154,7 +168,7 @@ export function TabFindings({ task }: TabFindingsProps) {
</div>
<div className="space-y-3">
{group.items.map((f) => (
<FindingCard key={f.id} finding={f} />
<FindingCard key={f.id} finding={f} branch={task.branch_name} />
))}
</div>
</div>