Files
kandev/apps/web/components/diff/use-review-finding-annotations.ts
T

84 lines
3.3 KiB
TypeScript

import { useMemo } from "react";
import type { AnnotationSide, DiffLineAnnotation } from "@pierre/diffs";
import { useOptionalAppStore } from "@/components/state-provider";
import { resolveFindingAnchor } from "@/lib/review/findings";
import { hashDiff } from "@/components/review/types";
import type { TaskReviewFinding } from "@/lib/types/review";
import type { AnnotationMetadata } from "./use-diff-annotation-renderer";
import { useTranslation } from "react-i18next";
/**
* Key for the message shown on a finding whose line moved but whose anchor text
* survived. Stored as a key, not resolved copy: a module-level `t()` would
* freeze at the boot locale.
*/
const RELOCATED_REASON_KEY = "diff:theDiffChangedAfterThisReviewRelocated";
export type ReviewFindingAnnotationsResult = {
/** Findings that could be anchored to a current line in this file. */
annotations: DiffLineAnnotation<AnnotationMetadata>[];
/**
* Findings for this file that could not be anchored — the diff moved and the
* anchor text is gone. The caller renders these above the diff so a stale
* finding is neither dropped nor placed against unrelated code.
*/
unanchored: TaskReviewFinding[];
};
const EMPTY_RESULT: ReviewFindingAnnotationsResult = { annotations: [], unanchored: [] };
/**
* Builds inline annotations for the review findings belonging to one file.
*
* Findings are matched by the same composite `<repo>\x00<path>` key the rest of
* the Review panel uses, so in a multi-repository task a finding renders only
* inside its own repository's file.
*/
export function useReviewFindingAnnotations(opts: {
filePath: string;
repo?: string;
diff?: string;
}): ReviewFindingAnnotationsResult {
const { filePath, repo, diff } = opts;
const { t } = useTranslation();
// The diff reaching the viewer is already normalized (see normalizeDiffContent
// in components/review/types.ts), so hashing it here yields the same value the
// backend stamped on the finding and the same one computeReviewSets uses for
// review-mark staleness.
const diffHash = useMemo(() => (diff ? hashDiff(diff) : undefined), [diff]);
const activeTaskId = useOptionalAppStore((s) => s.tasks.activeTaskId, null);
const findings = useOptionalAppStore(
(s) => (activeTaskId ? s.taskReview.findingsByTaskId[activeTaskId] : undefined),
undefined,
);
return useMemo(() => {
if (!findings?.length) return EMPTY_RESULT;
const forFile = findings.filter(
(f) => f.file_path === filePath && (f.repository_name || undefined) === (repo || undefined),
);
if (forFile.length === 0) return EMPTY_RESULT;
const annotations: DiffLineAnnotation<AnnotationMetadata>[] = [];
const unanchored: TaskReviewFinding[] = [];
for (const finding of forFile) {
const anchor = resolveFindingAnchor(finding, diff, diffHash);
if (!anchor) {
unanchored.push(finding);
continue;
}
annotations.push({
side: (finding.side === "deletions" ? "deletions" : "additions") as AnnotationSide,
lineNumber: Math.max(anchor.startLine, anchor.endLine),
metadata: {
type: "review-finding" as const,
finding,
findingStaleReason: anchor.relocated ? t(RELOCATED_REASON_KEY) : undefined,
},
});
}
return { annotations, unanchored };
}, [findings, filePath, repo, diff, diffHash, t]);
}